using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.AbCip;

namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests;

[Trait("Category", "Unit")]
public sealed class AbCipTagPathTests
{
    /// <summary>Verifies that a controller-scope single-segment tag path parses correctly.</summary>
    [Fact]
    public void Controller_scope_single_segment()
    {
        var p = AbCipTagPath.TryParse("Motor1_Speed");
        p.ShouldNotBeNull();
        p.ProgramScope.ShouldBeNull();
        p.Segments.Count.ShouldBe(1);
        p.Segments[0].Name.ShouldBe("Motor1_Speed");
        p.Segments[0].Subscripts.ShouldBeEmpty();
        p.BitIndex.ShouldBeNull();
        p.ToLibplctagName().ShouldBe("Motor1_Speed");
    }

    /// <summary>Verifies that a program-scope tag path parses correctly.</summary>
    [Fact]
    public void Program_scope_parses()
    {
        var p = AbCipTagPath.TryParse("Program:MainProgram.StepIndex");
        p.ShouldNotBeNull();
        p.ProgramScope.ShouldBe("MainProgram");
        p.Segments.Single().Name.ShouldBe("StepIndex");
        p.ToLibplctagName().ShouldBe("Program:MainProgram.StepIndex");
    }

    /// <summary>Verifies that structured member access splits into multiple segments.</summary>
    [Fact]
    public void Structured_member_access_splits_segments()
    {
        var p = AbCipTagPath.TryParse("Motor1.Speed.Setpoint");
        p.ShouldNotBeNull();
        p.Segments.Select(s => s.Name).ShouldBe(["Motor1", "Speed", "Setpoint"]);
        p.ToLibplctagName().ShouldBe("Motor1.Speed.Setpoint");
    }

    /// <summary>Verifies that single-dimensional array subscript is parsed correctly.</summary>
    [Fact]
    public void Single_dim_array_subscript()
    {
        var p = AbCipTagPath.TryParse("Data[7]");
        p.ShouldNotBeNull();
        p.Segments.Single().Name.ShouldBe("Data");
        p.Segments.Single().Subscripts.ShouldBe([7]);
        p.ToLibplctagName().ShouldBe("Data[7]");
    }

    /// <summary>Verifies that multi-dimensional array subscript is parsed correctly.</summary>
    [Fact]
    public void Multi_dim_array_subscript()
    {
        var p = AbCipTagPath.TryParse("Matrix[1,2,3]");
        p.ShouldNotBeNull();
        p.Segments.Single().Subscripts.ShouldBe([1, 2, 3]);
        p.ToLibplctagName().ShouldBe("Matrix[1,2,3]");
    }

    /// <summary>Verifies that bit index in DINT is captured correctly.</summary>
    [Fact]
    public void Bit_in_dint_captured_as_bit_index()
    {
        var p = AbCipTagPath.TryParse("Flags.3");
        p.ShouldNotBeNull();
        p.Segments.Single().Name.ShouldBe("Flags");
        p.BitIndex.ShouldBe(3);
        p.ToLibplctagName().ShouldBe("Flags.3");
    }

    /// <summary>Verifies that bit index after member access is captured correctly.</summary>
    [Fact]
    public void Bit_in_dint_after_member()
    {
        var p = AbCipTagPath.TryParse("Motor.Status.12");
        p.ShouldNotBeNull();
        p.Segments.Select(s => s.Name).ShouldBe(["Motor", "Status"]);
        p.BitIndex.ShouldBe(12);
        p.ToLibplctagName().ShouldBe("Motor.Status.12");
    }

    /// <summary>Verifies that bit index 32 is rejected as out of range.</summary>
    [Fact]
    public void Bit_index_32_rejected_out_of_range()
    {
        // 32 exceeds the DINT bit width — treated as a member name rather than bit selector,
        // which fails ident validation and returns null.
        AbCipTagPath.TryParse("Flags.32").ShouldBeNull();
    }

    /// <summary>Verifies that program scope with members, subscript, and bit index parses correctly.</summary>
    [Fact]
    public void Program_scope_with_members_and_subscript_and_bit()
    {
        var p = AbCipTagPath.TryParse("Program:MainProgram.Motors[0].Status.5");
        p.ShouldNotBeNull();
        p.ProgramScope.ShouldBe("MainProgram");
        p.Segments.Select(s => s.Name).ShouldBe(["Motors", "Status"]);
        p.Segments[0].Subscripts.ShouldBe([0]);
        p.BitIndex.ShouldBe(5);
        p.ToLibplctagName().ShouldBe("Program:MainProgram.Motors[0].Status.5");
    }

    /// <summary>Verifies that invalid tag path shapes return null.</summary>
    /// <param name="input">The input string to test for invalid shapes.</param>
    [Theory]
    [InlineData(null)]
    [InlineData("")]
    [InlineData("   ")]
    [InlineData("Program:")]             // empty scope
    [InlineData("Program:MP")]            // no body after scope
    [InlineData("1InvalidStart")]         // ident starts with digit
    [InlineData("Bad Name")]              // space in ident
    [InlineData("Motor[]")]               // empty subscript
    [InlineData("Motor[-1]")]             // negative subscript
    [InlineData("Motor[a]")]              // non-numeric subscript
    [InlineData("Motor[")]                // unbalanced bracket
    [InlineData("Motor.")]                // trailing dot
    [InlineData(".Motor")]                // leading dot
    public void Invalid_shapes_return_null(string? input)
    {
        AbCipTagPath.TryParse(input).ShouldBeNull();
    }

    /// <summary>Verifies that identifiers with underscores are accepted.</summary>
    [Fact]
    public void Ident_with_underscore_accepted()
    {
        AbCipTagPath.TryParse("_private_tag")!.Segments.Single().Name.ShouldBe("_private_tag");
    }

    /// <summary>Verifies that ToLibplctagName recomposes a round-trip correctly.</summary>
    [Fact]
    public void ToLibplctagName_recomposes_round_trip()
    {
        var cases = new[]
        {
            "Motor1_Speed",
            "Program:Main.Counter",
            "Array[5]",
            "Matrix[1,2]",
            "Obj.Member.Sub",
            "Flags.0",
            "Program:P.Obj[2].Flags.15",
        };
        foreach (var c in cases)
        {
            var parsed = AbCipTagPath.TryParse(c);
            parsed.ShouldNotBeNull(c);
            parsed.ToLibplctagName().ShouldBe(c);
        }
    }
}
