using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy;

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

/// <summary>
///     Regression coverage for Driver.AbLegacy-001 — a PCCC bit index must be range-checked
///     against the parent word width: 0..15 for 16-bit element files (N/B/I/O/S/A), 0..31 for
///     the 32-bit L file. Float files are not bit-addressable.
/// </summary>
[Trait("Category", "Unit")]
public sealed class AbLegacyBitIndexRangeTests
{
    /// <summary>Verifies that bit index 0 to 15 is accepted on 16-bit files.</summary>
    /// <param name="input">The PCCC address string to test.</param>
    [Theory]
    [InlineData("N7:0/15")]
    [InlineData("B3:0/15")]
    [InlineData("I:0/15")]
    [InlineData("O:1/15")]
    [InlineData("S:1/15")]
    [InlineData("A10:0/15")]
    public void Bit_index_0_to_15_accepted_on_16bit_files(string input) =>
        AbLegacyAddress.TryParse(input).ShouldNotBeNull();

    /// <summary>Verifies that bit index above 15 is rejected on 16-bit files.</summary>
    /// <param name="input">The PCCC address string to test.</param>
    [Theory]
    [InlineData("N7:0/16")]   // first bit past a 16-bit word
    [InlineData("N7:0/20")]
    [InlineData("N7:0/31")]
    [InlineData("B3:0/16")]
    [InlineData("I:0/16")]
    [InlineData("O:1/16")]
    [InlineData("S:1/16")]
    [InlineData("A10:0/16")]
    public void Bit_index_above_15_rejected_on_16bit_files(string input) =>
        AbLegacyAddress.TryParse(input).ShouldBeNull();

    /// <summary>Verifies that bit index 0 to 31 is accepted on L file.</summary>
    /// <param name="input">The PCCC address string to test.</param>
    [Theory]
    [InlineData("L9:0/0")]
    [InlineData("L9:0/15")]
    [InlineData("L9:0/16")]   // L-file words are 32-bit, so 16..31 are valid
    [InlineData("L9:0/31")]
    public void Bit_index_0_to_31_accepted_on_L_file(string input) =>
        AbLegacyAddress.TryParse(input).ShouldNotBeNull();

    /// <summary>Verifies that bit index above 31 is rejected on L file.</summary>
    [Fact]
    public void Bit_index_above_31_rejected_on_L_file() =>
        AbLegacyAddress.TryParse("L9:0/32").ShouldBeNull();

    /// <summary>Verifies that bit index is rejected on float file.</summary>
    /// <param name="input">The PCCC address string to test.</param>
    [Theory]
    [InlineData("F8:0/0")]    // float files are not bit-addressable at all
    [InlineData("F8:0/3")]
    public void Bit_index_rejected_on_float_file(string input) =>
        AbLegacyAddress.TryParse(input).ShouldBeNull();

    /// <summary>Verifies that negative bit index is still rejected.</summary>
    [Fact]
    public void Negative_bit_index_still_rejected() =>
        AbLegacyAddress.TryParse("N7:0/-1").ShouldBeNull();

    /// <summary>Verifies that bit in word RMW against L file uses 32-bit parent and high bit.</summary>
    [Fact]
    public async Task Bit_in_word_RMW_against_L_file_uses_32bit_parent_and_high_bit()
    {
        // L9:0/20 — bit 20 of a 32-bit L-file word. The parent must be read/written as a
        // 32-bit Long so the high bits are addressable; a 16-bit (short)cast would truncate.
        var factory = new FakeAbLegacyTagFactory
        {
            Customise = p => new FakeAbLegacyTag(p) { Value = 0 },
        };
        var drv = new AbLegacyDriver(new AbLegacyDriverOptions
        {
            Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
            RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("LBit20", "ab://10.0.0.5/1,0", "L9:0/20", AbLegacyDataType.Bit)]),
            Probe = new AbLegacyProbeOptions { Enabled = false },
        }, "drv-1", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

        var results = await drv.WriteAsync([new WriteRequest("LBit20", true)], CancellationToken.None);

        results.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.Good);
        factory.Tags.ShouldContainKey("L9:0");
        Convert.ToInt32(factory.Tags["L9:0"].Value).ShouldBe(1 << 20);
    }

    /// <summary>Verifies that bit in word RMW high bit 15 does not corrupt via sign extension.</summary>
    [Fact]
    public async Task Bit_in_word_RMW_high_bit_15_does_not_corrupt_via_sign_extension()
    {
        // Parent word has bit 15 set (0x8000) — DecodeValue returns a sign-extended negative
        // int. Setting bit 0 must yield exactly 0x8001, not a sign-extended value.
        var factory = new FakeAbLegacyTagFactory
        {
            Customise = p => new FakeAbLegacyTag(p) { Value = unchecked((short)0x8000) },
        };
        var drv = new AbLegacyDriver(new AbLegacyDriverOptions
        {
            Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
            RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("Bit0", "ab://10.0.0.5/1,0", "N7:0/0", AbLegacyDataType.Bit)]),
            Probe = new AbLegacyProbeOptions { Enabled = false },
        }, "drv-1", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

        await drv.WriteAsync([new WriteRequest("Bit0", true)], CancellationToken.None);

        // (short)0x8001 round-trips through the fake as -32767.
        Convert.ToInt32(factory.Tags["N7:0"].Value).ShouldBe(unchecked((short)0x8001));
    }
}
