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 CipSymbolObjectDecoderTests
{
    /// <summary>
    ///     Build one Symbol Object entry in the byte layout
    ///     <c>instance_id(u32) symbol_type(u16) element_length(u16) array_dims(u32×3) name_len(u16) name[len] pad</c>.
    /// </summary>
    private static byte[] BuildEntry(
        uint instanceId,
        ushort symbolType,
        ushort elementLength,
        (uint, uint, uint) arrayDims,
        string name)
    {
        var nameBytes = Encoding.ASCII.GetBytes(name);
        var nameLen = nameBytes.Length;
        var totalLen = 22 + nameLen;
        if ((totalLen & 1) != 0) totalLen++; // pad to even

        var buf = new byte[totalLen];
        BinaryPrimitives.WriteUInt32LittleEndian(buf.AsSpan(0), instanceId);
        BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(4), symbolType);
        BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(6), elementLength);
        BinaryPrimitives.WriteUInt32LittleEndian(buf.AsSpan(8), arrayDims.Item1);
        BinaryPrimitives.WriteUInt32LittleEndian(buf.AsSpan(12), arrayDims.Item2);
        BinaryPrimitives.WriteUInt32LittleEndian(buf.AsSpan(16), arrayDims.Item3);
        BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(20), (ushort)nameLen);
        Buffer.BlockCopy(nameBytes, 0, buf, 22, nameLen);
        return buf;
    }

    private static byte[] Concat(params byte[][] chunks)
    {
        var total = chunks.Sum(c => c.Length);
        var result = new byte[total];
        var pos = 0;
        foreach (var c in chunks)
        {
            Buffer.BlockCopy(c, 0, result, pos, c.Length);
            pos += c.Length;
        }
        return result;
    }

    /// <summary>Verifies that a single DInt entry decodes correctly.</summary>
    [Fact]
    public void Single_DInt_entry_decodes_to_scalar_DInt_tag()
    {
        var bytes = BuildEntry(
            instanceId: 42,
            symbolType: 0xC4,
            elementLength: 4,
            arrayDims: (0, 0, 0),
            name: "Counter");

        var tags = CipSymbolObjectDecoder.Decode(bytes).ToList();

        tags.Count.ShouldBe(1);
        tags[0].Name.ShouldBe("Counter");
        tags[0].ProgramScope.ShouldBeNull();
        tags[0].DataType.ShouldBe(AbCipDataType.DInt);
        tags[0].IsSystemTag.ShouldBeFalse();
    }

    /// <summary>Verifies that all known atomic type codes map to correct data types.</summary>
    /// <param name="typeCode">The CIP type code to map.</param>
    /// <param name="expected">The expected AbCipDataType result.</param>
    [Theory]
    [InlineData((byte)0xC1, AbCipDataType.Bool)]
    [InlineData((byte)0xC2, AbCipDataType.SInt)]
    [InlineData((byte)0xC3, AbCipDataType.Int)]
    [InlineData((byte)0xC4, AbCipDataType.DInt)]
    [InlineData((byte)0xC5, AbCipDataType.LInt)]
    [InlineData((byte)0xC6, AbCipDataType.USInt)]
    [InlineData((byte)0xC7, AbCipDataType.UInt)]
    [InlineData((byte)0xC8, AbCipDataType.UDInt)]
    [InlineData((byte)0xC9, AbCipDataType.ULInt)]
    [InlineData((byte)0xCA, AbCipDataType.Real)]
    [InlineData((byte)0xCB, AbCipDataType.LReal)]
    [InlineData((byte)0xD0, AbCipDataType.String)]
    public void Every_known_atomic_type_code_maps_to_correct_AbCipDataType(byte typeCode, AbCipDataType expected)
    {
        CipSymbolObjectDecoder.MapTypeCode(typeCode).ShouldBe(expected);
    }

    /// <summary>Verifies that unknown type codes return null for opaque handling.</summary>
    [Fact]
    public void Unknown_type_code_returns_null_so_caller_treats_as_opaque()
    {
        CipSymbolObjectDecoder.MapTypeCode(0xFF).ShouldBeNull();
    }

    /// <summary>
    ///     Verifies that the struct flag overrides the type code AND that the lower 12 bits are
    ///     surfaced as the CIP template instance id (used by discovery to fetch the UDT's Template
    ///     Object). This is the plumbing that makes top-level discovered-UDT expansion functional.
    /// </summary>
    [Fact]
    public void Struct_flag_yields_Structure_and_surfaces_template_instance_id()
    {
        // 0x8000 (struct flag) | 0x0234 (template instance id in the lower 12 bits)
        var bytes = BuildEntry(
            instanceId: 5,
            symbolType: 0x8000 | 0x0234,
            elementLength: 16,
            arrayDims: (0, 0, 0),
            name: "Motor1");

        var tag = CipSymbolObjectDecoder.Decode(bytes).Single();
        tag.DataType.ShouldBe(AbCipDataType.Structure);
        tag.TemplateInstanceId.ShouldBe(0x0234u);
    }

    /// <summary>
    ///     Verifies that an atomic (non-struct) symbol carries no template instance id — only struct
    ///     symbols repurpose the lower 12 bits as a template id.
    /// </summary>
    [Fact]
    public void Atomic_symbol_has_no_template_instance_id()
    {
        var bytes = BuildEntry(
            instanceId: 7,
            symbolType: 0xC4, // DINT — atomic, not a struct
            elementLength: 4,
            arrayDims: (0, 0, 0),
            name: "Counter");

        var tag = CipSymbolObjectDecoder.Decode(bytes).Single();
        tag.TemplateInstanceId.ShouldBeNull();
    }

    /// <summary>Verifies that system flag surfaces as IsSystemTag true.</summary>
    [Fact]
    public void System_flag_surfaces_as_IsSystemTag_true()
    {
        var bytes = BuildEntry(
            instanceId: 99,
            symbolType: 0x1000 | 0xC4, // system flag + DINT
            elementLength: 4,
            arrayDims: (0, 0, 0),
            name: "__Reserved_1");

        var tag = CipSymbolObjectDecoder.Decode(bytes).Single();
        tag.IsSystemTag.ShouldBeTrue();
        tag.DataType.ShouldBe(AbCipDataType.DInt);
    }

    /// <summary>Verifies that program scope names split correctly into prefix and name.</summary>
    [Fact]
    public void Program_scope_name_splits_prefix_into_ProgramScope()
    {
        var bytes = BuildEntry(
            instanceId: 1,
            symbolType: 0xC4,
            elementLength: 4,
            arrayDims: (0, 0, 0),
            name: "Program:MainProgram.StepIndex");

        var tag = CipSymbolObjectDecoder.Decode(bytes).Single();
        tag.ProgramScope.ShouldBe("MainProgram");
        tag.Name.ShouldBe("StepIndex");
    }

    /// <summary>Verifies that multiple entries decode in wire order with proper padding.</summary>
    [Fact]
    public void Multiple_entries_decode_in_wire_order_with_even_padding()
    {
        // Name "Abc" is 3 bytes — triggers the even-pad branch between entries.
        var bytes = Concat(
            BuildEntry(1, 0xC4, 4, (0, 0, 0), "Abc"),       // DINT named "Abc" (3-byte name, pads to 4)
            BuildEntry(2, 0xCA, 4, (0, 0, 0), "Pi"));        // REAL named "Pi"

        var tags = CipSymbolObjectDecoder.Decode(bytes).ToList();
        tags.Count.ShouldBe(2);
        tags[0].Name.ShouldBe("Abc");
        tags[0].DataType.ShouldBe(AbCipDataType.DInt);
        tags[1].Name.ShouldBe("Pi");
        tags[1].DataType.ShouldBe(AbCipDataType.Real);
    }

    /// <summary>Verifies that truncated buffers stop decoding gracefully.</summary>
    [Fact]
    public void Truncated_buffer_stops_decoding_gracefully()
    {
        var full = BuildEntry(7, 0xC4, 4, (0, 0, 0), "Counter");
        // Deliberately chop off the last 5 bytes — decoder should bail cleanly, not throw.
        var truncated = full.Take(full.Length - 5).ToArray();

        CipSymbolObjectDecoder.Decode(truncated).ToList().Count.ShouldBeLessThan(1); // 0 — didn't parse the broken entry
    }

    /// <summary>Verifies that empty buffers yield no tags.</summary>
    [Fact]
    public void Empty_buffer_yields_no_tags()
    {
        CipSymbolObjectDecoder.Decode([]).ShouldBeEmpty();
    }

    /// <summary>Verifies that SplitProgramScope handles all valid name shapes.</summary>
    /// <param name="input">The name string to split.</param>
    /// <param name="expectedScope">The expected program scope prefix, if any.</param>
    /// <param name="expectedName">The expected name after splitting.</param>
    [Theory]
    [InlineData("Counter", null, "Counter")]
    [InlineData("Program:MainProgram.Step", "MainProgram", "Step")]
    [InlineData("Program:MyProg.a.b.c", "MyProg", "a.b.c")]
    [InlineData("Program:", null, "Program:")]          // malformed — no dot
    [InlineData("Program:OnlyProg", null, "Program:OnlyProg")]
    [InlineData("Motor.Status.Running", null, "Motor.Status.Running")]
    public void SplitProgramScope_handles_every_shape(string input, string? expectedScope, string expectedName)
    {
        var (scope, name) = CipSymbolObjectDecoder.SplitProgramScope(input);
        scope.ShouldBe(expectedScope);
        name.ShouldBe(expectedName);
    }
}
