using Shouldly;
using Xunit;

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

[Trait("Category", "Unit")]
public sealed class AbCipUdtMemberLayoutTests
{
    /// <summary>Verifies that packed atomic types get natural alignment offsets.</summary>
    [Fact]
    public void Packed_Atomics_Get_Natural_Alignment_Offsets()
    {
        // DInt (4 align) + Real (4) + Int (2) + LInt (8 — forces 2-byte pad before it)
        var members = new[]
        {
            new AbCipStructureMember("A", AbCipDataType.DInt),
            new AbCipStructureMember("B", AbCipDataType.Real),
            new AbCipStructureMember("C", AbCipDataType.Int),
            new AbCipStructureMember("D", AbCipDataType.LInt),
        };

        var offsets = AbCipUdtMemberLayout.TryBuild(members);
        offsets.ShouldNotBeNull();
        offsets!["A"].ShouldBe(0);
        offsets["B"].ShouldBe(4);
        offsets["C"].ShouldBe(8);
        // cursor at 10 after Int; LInt needs 8-byte alignment → pad to 16
        offsets["D"].ShouldBe(16);
    }

    /// <summary>Verifies that signed integer types are packed without padding.</summary>
    [Fact]
    public void SInt_Packed_Without_Padding()
    {
        var members = new[]
        {
            new AbCipStructureMember("X", AbCipDataType.SInt),
            new AbCipStructureMember("Y", AbCipDataType.SInt),
            new AbCipStructureMember("Z", AbCipDataType.SInt),
        };
        var offsets = AbCipUdtMemberLayout.TryBuild(members);
        offsets!["X"].ShouldBe(0);
        offsets["Y"].ShouldBe(1);
        offsets["Z"].ShouldBe(2);
    }

    /// <summary>Verifies that layout returns null when a member is a Bool type.</summary>
    [Fact]
    public void Returns_Null_When_Member_Is_Bool()
    {
        // BOOL storage in Logix UDTs is packed into a hidden host byte; declaration-only
        // layout can't place it. Grouping opts out; per-tag read path handles the member.
        var members = new[]
        {
            new AbCipStructureMember("A", AbCipDataType.DInt),
            new AbCipStructureMember("Flag", AbCipDataType.Bool),
        };
        AbCipUdtMemberLayout.TryBuild(members).ShouldBeNull();
    }

    /// <summary>Verifies that layout returns null when a member is a String or Structure type.</summary>
    [Fact]
    public void Returns_Null_When_Member_Is_String_Or_Structure()
    {
        AbCipUdtMemberLayout.TryBuild(
            new[] { new AbCipStructureMember("Name", AbCipDataType.String) }).ShouldBeNull();
        AbCipUdtMemberLayout.TryBuild(
            new[] { new AbCipStructureMember("Nested", AbCipDataType.Structure) }).ShouldBeNull();
    }

    /// <summary>Verifies that layout returns null when the member list is empty.</summary>
    [Fact]
    public void Returns_Null_On_Empty_Members()
    {
        AbCipUdtMemberLayout.TryBuild(Array.Empty<AbCipStructureMember>()).ShouldBeNull();
    }

    /// <summary>
    ///     Driver.AbCip-016 — declaration-only layout returns null when any member is a 1-D array.
    ///     The whole-UDT grouped read path decodes one scalar per member at its offset
    ///     (<c>DecodeValueAt</c>) and cannot return an array, and the scalar-size cursor advance
    ///     would mis-place every member after the array. Opting the whole group out sends array
    ///     members through the per-tag read path, which reads them as typed arrays.
    /// </summary>
    [Fact]
    public void Returns_Null_When_A_Member_Is_An_Array()
    {
        // Explicit IsArray flag (even a 1-element array).
        AbCipUdtMemberLayout.TryBuild(new[]
        {
            new AbCipStructureMember("A", AbCipDataType.DInt),
            new AbCipStructureMember("Buf", AbCipDataType.Real, IsArray: true, ElementCount: 1),
        }).ShouldBeNull();

        // Legacy ElementCount > 1 with the flag unset.
        AbCipUdtMemberLayout.TryBuild(new[]
        {
            new AbCipStructureMember("A", AbCipDataType.DInt),
            new AbCipStructureMember("Setpoints", AbCipDataType.Real, ElementCount: 4),
        }).ShouldBeNull();
    }
}
