using System.Buffers.Binary;
using System.Text;
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 CipTemplateObjectDecoderTests
{
    /// <summary>
    ///     Construct a Template Object blob — header + member blocks + semicolon-delimited
    ///     strings (UDT name first, then member names).
    /// </summary>
    private static byte[] BuildTemplate(
        string udtName,
        uint instanceSize,
        params (string name, ushort info, ushort arraySize, uint offset)[] members)
    {
        var memberCount = (ushort)members.Length;
        var headerSize = 12;
        var memberBlockSize = 8;
        var blocksSize = memberBlockSize * members.Length;

        var stringsBuf = new MemoryStream();
        void AppendString(string s)
        {
            var bytes = Encoding.ASCII.GetBytes(s + ";\0");
            stringsBuf.Write(bytes, 0, bytes.Length);
        }
        AppendString(udtName);
        foreach (var m in members) AppendString(m.name);
        var strings = stringsBuf.ToArray();

        var buf = new byte[headerSize + blocksSize + strings.Length];
        BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(0), memberCount);
        BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(2), 0x1234);
        BinaryPrimitives.WriteUInt32LittleEndian(buf.AsSpan(4), instanceSize);
        BinaryPrimitives.WriteUInt32LittleEndian(buf.AsSpan(8), 0);

        for (var i = 0; i < members.Length; i++)
        {
            var o = headerSize + (i * memberBlockSize);
            BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(o), members[i].info);
            BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(o + 2), members[i].arraySize);
            BinaryPrimitives.WriteUInt32LittleEndian(buf.AsSpan(o + 4), members[i].offset);
        }
        Buffer.BlockCopy(strings, 0, buf, headerSize + blocksSize, strings.Length);
        return buf;
    }

    /// <summary>Verifies that a simple UDT with two members decodes correctly.</summary>
    [Fact]
    public void Simple_two_member_UDT_decodes_correctly()
    {
        var bytes = BuildTemplate("MotorUdt", instanceSize: 8,
            ("Speed", info: 0xC4, arraySize: 0, offset: 0),  // DINT at offset 0
            ("Enabled", info: 0xC1, arraySize: 0, offset: 4)); // BOOL at offset 4

        var shape = CipTemplateObjectDecoder.Decode(bytes);

        shape.ShouldNotBeNull();
        shape.TypeName.ShouldBe("MotorUdt");
        shape.TotalSize.ShouldBe(8);
        shape.Members.Count.ShouldBe(2);
        shape.Members[0].Name.ShouldBe("Speed");
        shape.Members[0].DataType.ShouldBe(AbCipDataType.DInt);
        shape.Members[0].Offset.ShouldBe(0);
        shape.Members[0].ArrayLength.ShouldBe(1);
        shape.Members[1].Name.ShouldBe("Enabled");
        shape.Members[1].DataType.ShouldBe(AbCipDataType.Bool);
        shape.Members[1].Offset.ShouldBe(4);
    }

    /// <summary>Verifies that the struct member flag is decoded as Structure type.</summary>
    [Fact]
    public void Struct_member_flag_surfaces_Structure_type()
    {
        var bytes = BuildTemplate("ContainerUdt", instanceSize: 32,
            ("InnerStruct", info: 0x8042, arraySize: 0, offset: 0)); // struct flag + template-id 0x42

        var shape = CipTemplateObjectDecoder.Decode(bytes);

        shape.ShouldNotBeNull();
        shape.Members.Single().DataType.ShouldBe(AbCipDataType.Structure);
    }

    /// <summary>
    ///     A struct member's lower 12 bits carry the nested UDT's template instance id (same
    ///     encoding as the Symbol Object); the decoder captures it as <c>NestedTemplateId</c> so the
    ///     nested shape can be fetched. A scalar member carries no nested id. Uses the FULL 12-bit
    ///     mask (0x123 here exceeds a byte) to prove the id is not byte-truncated.
    /// </summary>
    [Fact]
    public void Struct_member_captures_nested_template_id_scalar_member_does_not()
    {
        var bytes = BuildTemplate("ContainerUdt", instanceSize: 8,
            ("Inner", info: (ushort)(0x8000 | 0x123), arraySize: 0, offset: 0), // struct + nested id 0x123
            ("Count", info: 0xC4, arraySize: 0, offset: 4));                    // scalar DINT, no nested id

        var shape = CipTemplateObjectDecoder.Decode(bytes);

        shape.ShouldNotBeNull();
        shape.Members.Count.ShouldBe(2);
        shape.Members[0].Name.ShouldBe("Inner");
        shape.Members[0].DataType.ShouldBe(AbCipDataType.Structure);
        shape.Members[0].NestedTemplateId.ShouldBe(0x123u); // full 12-bit id, not byte-truncated
        shape.Members[1].Name.ShouldBe("Count");
        shape.Members[1].DataType.ShouldBe(AbCipDataType.DInt);
        shape.Members[1].NestedTemplateId.ShouldBeNull();
    }

    /// <summary>Verifies that array members carry correct non-one array lengths.</summary>
    [Fact]
    public void Array_member_carries_non_one_ArrayLength()
    {
        var bytes = BuildTemplate("ArrayUdt", instanceSize: 40,
            ("Values", info: 0xC4, arraySize: 10, offset: 0));

        var shape = CipTemplateObjectDecoder.Decode(bytes);
        shape.ShouldNotBeNull();
        shape.Members.Single().ArrayLength.ShouldBe(10);
    }

    /// <summary>Verifies that multiple atomic types preserve their offsets and type information.</summary>
    [Fact]
    public void Multiple_atomic_types_preserve_offsets_and_types()
    {
        var bytes = BuildTemplate("MixedUdt", instanceSize: 24,
            ("A", 0xC1, 0, 0),    // BOOL
            ("B", 0xC2, 0, 1),    // SINT
            ("C", 0xC3, 0, 2),    // INT
            ("D", 0xC4, 0, 4),    // DINT
            ("E", 0xCA, 0, 8),    // REAL
            ("F", 0xCB, 0, 16));  // LREAL

        var shape = CipTemplateObjectDecoder.Decode(bytes);

        shape.ShouldNotBeNull();
        shape.Members.Count.ShouldBe(6);
        shape.Members.Select(m => m.DataType).ShouldBe(
            [AbCipDataType.Bool, AbCipDataType.SInt, AbCipDataType.Int,
             AbCipDataType.DInt, AbCipDataType.Real, AbCipDataType.LReal]);
        shape.Members.Select(m => m.Offset).ShouldBe([0, 1, 2, 4, 8, 16]);
    }

    /// <summary>Verifies that unknown atomic type codes fall back to Structure type.</summary>
    [Fact]
    public void Unknown_atomic_type_code_falls_back_to_Structure()
    {
        var bytes = BuildTemplate("WeirdUdt", instanceSize: 4,
            ("Unknown", info: 0xFF, 0, 0));

        var shape = CipTemplateObjectDecoder.Decode(bytes);
        shape.ShouldNotBeNull();
        shape.Members.Single().DataType.ShouldBe(AbCipDataType.Structure);
    }

    /// <summary>Verifies that zero member count returns null.</summary>
    [Fact]
    public void Zero_member_count_returns_null()
    {
        var buf = new byte[12];
        BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(0), 0);
        CipTemplateObjectDecoder.Decode(buf).ShouldBeNull();
    }

    /// <summary>Verifies that a short buffer returns null.</summary>
    [Fact]
    public void Short_buffer_returns_null()
    {
        CipTemplateObjectDecoder.Decode([0x01, 0x00]).ShouldBeNull(); // only 2 bytes — less than header
    }

    /// <summary>Verifies that missing member names surface a placeholder.</summary>
    [Fact]
    public void Missing_member_name_surfaces_placeholder()
    {
        // Header says 3 members but strings list has only UDT name + 2 member names.
        var memberCount = (ushort)3;
        var buf = new byte[12 + 8 * 3];
        BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(0), memberCount);
        BinaryPrimitives.WriteUInt32LittleEndian(buf.AsSpan(4), 12);
        for (var i = 0; i < 3; i++)
        {
            var o = 12 + i * 8;
            BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(o), 0xC4);
            BinaryPrimitives.WriteUInt32LittleEndian(buf.AsSpan(o + 4), (uint)(i * 4));
        }
        // strings: only UDT + 2 members, missing the third.
        var strings = Encoding.ASCII.GetBytes("MyUdt;\0A;\0B;\0");
        var combined = buf.Concat(strings).ToArray();

        var shape = CipTemplateObjectDecoder.Decode(combined);
        shape.ShouldNotBeNull();
        shape.Members.Count.ShouldBe(3);
        shape.Members[2].Name.ShouldBe("<member_2>");
    }

    /// <summary>Verifies that semicolon-terminated string parsing handles various input shapes.</summary>
    /// <param name="input">The raw semicolon-terminated string input.</param>
    /// <param name="expected">The expected array of parsed strings.</param>
    [Theory]
    [InlineData("Foo;\0Bar;\0", new[] { "Foo", "Bar" })]
    [InlineData("Foo;Bar;", new[] { "Foo", "Bar" })]          // no nulls
    [InlineData("Only;\0", new[] { "Only" })]
    [InlineData(";\0", new string[] { })]                     // empty
    [InlineData("", new string[] { })]
    public void ParseSemicolonTerminatedStrings_handles_shapes(string input, string[] expected)
    {
        var bytes = Encoding.ASCII.GetBytes(input);
        var result = CipTemplateObjectDecoder.ParseSemicolonTerminatedStrings(bytes);
        result.ShouldBe(expected);
    }
}
