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;

[Trait("Category", "Unit")]
public sealed class AbLegacyBitRmwTests
{
    /// <summary>Verifies that setting a bit reads the parent word, ORs the bit, and writes back.</summary>
    [Fact]
    public async Task Bit_set_reads_parent_word_ORs_bit_writes_back()
    {
        var factory = new FakeAbLegacyTagFactory
        {
            Customise = p => new FakeAbLegacyTag(p) { Value = (short)0b0001 },
        };
        var drv = new AbLegacyDriver(new AbLegacyDriverOptions
        {
            Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
            RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("Flag3", "ab://10.0.0.5/1,0", "N7:0/3", AbLegacyDataType.Bit)]),
            Probe = new AbLegacyProbeOptions { Enabled = false },
        }, "drv-1", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

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

        results.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.Good);
        factory.Tags.ShouldContainKey("N7:0"); // parent word runtime created
        Convert.ToInt32(factory.Tags["N7:0"].Value).ShouldBe(0b1001);
    }

    /// <summary>Verifies that clearing a bit preserves other bits in the word.</summary>
    [Fact]
    public async Task Bit_clear_preserves_other_bits_in_N_file_word()
    {
        var factory = new FakeAbLegacyTagFactory
        {
            Customise = p => new FakeAbLegacyTag(p) { Value = unchecked((short)0xFFFF) },
        };
        var drv = new AbLegacyDriver(new AbLegacyDriverOptions
        {
            Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
            RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("F", "ab://10.0.0.5/1,0", "N7:0/3", AbLegacyDataType.Bit)]),
            Probe = new AbLegacyProbeOptions { Enabled = false },
        }, "drv-1", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

        await drv.WriteAsync([new WriteRequest("F", false)], CancellationToken.None);

        Convert.ToInt32(factory.Tags["N7:0"].Value).ShouldBe(unchecked((short)0xFFF7));
    }

    /// <summary>Verifies that concurrent bit writes to the same word compose correctly.</summary>
    [Fact]
    public async Task Concurrent_bit_writes_to_same_word_compose_correctly()
    {
        var factory = new FakeAbLegacyTagFactory
        {
            Customise = p => new FakeAbLegacyTag(p) { Value = (short)0 },
        };
        var tags = Enumerable.Range(0, 8)
            .Select(b => new AbLegacyTagDefinition($"Bit{b}", "ab://10.0.0.5/1,0", $"N7:0/{b}", AbLegacyDataType.Bit))
            .ToArray();
        var drv = new AbLegacyDriver(new AbLegacyDriverOptions
        {
            Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
            RawTags = AbLegacyRawTags.Entries(tags),
            Probe = new AbLegacyProbeOptions { Enabled = false },
        }, "drv-1", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

        await Task.WhenAll(Enumerable.Range(0, 8).Select(b =>
            drv.WriteAsync([new WriteRequest($"Bit{b}", true)], CancellationToken.None)));

        Convert.ToInt32(factory.Tags["N7:0"].Value).ShouldBe(0xFF);
    }

    /// <summary>Verifies that repeated bit writes reuse the parent word runtime.</summary>
    [Fact]
    public async Task Repeat_bit_writes_reuse_parent_runtime()
    {
        var factory = new FakeAbLegacyTagFactory
        {
            Customise = p => new FakeAbLegacyTag(p) { Value = (short)0 },
        };
        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),
                new AbLegacyTagDefinition("Bit5", "ab://10.0.0.5/1,0", "N7:0/5", AbLegacyDataType.Bit),
            ]),
            Probe = new AbLegacyProbeOptions { Enabled = false },
        }, "drv-1", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

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

        factory.Tags["N7:0"].InitializeCount.ShouldBe(1);
        factory.Tags["N7:0"].WriteCount.ShouldBe(2);
        Convert.ToInt32(factory.Tags["N7:0"].Value).ShouldBe(0x21); // bits 0 + 5
    }

    /// <summary>B/I/O-file bit set reads the parent word, ORs the bit, writes it back (was BadNotSupported).</summary>
    [Theory]
    [InlineData("B3:0/3", "B3:0")]
    [InlineData("I:0/3", "I:0")]
    [InlineData("O:1/3", "O:1")]
    public async Task Bit_set_on_B_I_O_file_RMWs_parent_word(string bitAddr, string parentName)
    {
        var factory = new FakeAbLegacyTagFactory
        {
            Customise = p => new FakeAbLegacyTag(p) { Value = (short)0b0001 },
        };
        var drv = new AbLegacyDriver(new AbLegacyDriverOptions
        {
            Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
            RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("F", "ab://10.0.0.5/1,0", bitAddr, AbLegacyDataType.Bit)]),
            Probe = new AbLegacyProbeOptions { Enabled = false },
        }, "drv-1", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

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

        results.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.Good);
        factory.Tags.ShouldContainKey(parentName);
        Convert.ToInt32(factory.Tags[parentName].Value).ShouldBe(0b1001);
    }

    /// <summary>B-file bit clear preserves the other bits in the parent word.</summary>
    [Fact]
    public async Task Bit_clear_preserves_other_bits_in_B_file_word()
    {
        var factory = new FakeAbLegacyTagFactory
        {
            Customise = p => new FakeAbLegacyTag(p) { Value = unchecked((short)0xFFFF) },
        };
        var drv = new AbLegacyDriver(new AbLegacyDriverOptions
        {
            Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
            RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("F", "ab://10.0.0.5/1,0", "B3:0/3", AbLegacyDataType.Bit)]),
            Probe = new AbLegacyProbeOptions { Enabled = false },
        }, "drv-1", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

        await drv.WriteAsync([new WriteRequest("F", false)], CancellationToken.None);

        Convert.ToInt32(factory.Tags["B3:0"].Value).ShouldBe(unchecked((short)0xFFF7));
    }

    /// <summary>Concurrent bit writes to the same B-file word compose correctly (per-parent lock).</summary>
    [Fact]
    public async Task Concurrent_bit_writes_to_same_B_file_word_compose_correctly()
    {
        var factory = new FakeAbLegacyTagFactory
        {
            Customise = p => new FakeAbLegacyTag(p) { Value = (short)0 },
        };
        var tags = Enumerable.Range(0, 8)
            .Select(b => new AbLegacyTagDefinition($"Bit{b}", "ab://10.0.0.5/1,0", $"B3:0/{b}", AbLegacyDataType.Bit))
            .ToArray();
        var drv = new AbLegacyDriver(new AbLegacyDriverOptions
        {
            Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
            RawTags = AbLegacyRawTags.Entries(tags),
            Probe = new AbLegacyProbeOptions { Enabled = false },
        }, "drv-1", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

        await Task.WhenAll(Enumerable.Range(0, 8).Select(b =>
            drv.WriteAsync([new WriteRequest($"Bit{b}", true)], CancellationToken.None)));

        Convert.ToInt32(factory.Tags["B3:0"].Value).ShouldBe(0xFF);
    }

    /// <summary>
    ///     Verifies that disposing (teardown) and re-initializing the driver does not orphan the
    ///     per-parent RMW-lock semaphores from the previous session. A bit write after the
    ///     reinit cycle must succeed, proving that <see cref="DeviceState.DisposeRuntimes"/> clears
    ///     the lock map so fresh semaphores are created for the new session rather than re-using
    ///     (already-disposed) handles from the old one.
    /// </summary>
    [Fact]
    public async Task RMW_locks_are_disposed_and_fresh_after_driver_reinitialise()
    {
        var factory = new FakeAbLegacyTagFactory
        {
            Customise = p => new FakeAbLegacyTag(p) { Value = (short)0 },
        };
        var opts = new AbLegacyDriverOptions
        {
            Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
            RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("Bit2", "ab://10.0.0.5/1,0", "N7:0/2", AbLegacyDataType.Bit)]),
            Probe = new AbLegacyProbeOptions { Enabled = false },
        };

        // First session: init + bit write — seeds an RMW lock entry for "N7:0".
        var drv = new AbLegacyDriver(opts, "drv-1", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);
        var firstResult = await drv.WriteAsync([new WriteRequest("Bit2", true)], CancellationToken.None);
        firstResult.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.Good);

        // Teardown — DisposeRuntimes must dispose + clear the RMW lock map.
        await drv.DisposeAsync();

        // Second session: re-init on a fresh driver instance (mirrors ReinitializeAsync behavior).
        factory.Tags.Clear();
        var drv2 = new AbLegacyDriver(opts, "drv-1", factory);
        await drv2.InitializeAsync("{}", CancellationToken.None);
        var secondResult = await drv2.WriteAsync([new WriteRequest("Bit2", false)], CancellationToken.None);

        // Must succeed — a disposed/orphaned semaphore would throw ObjectDisposedException.
        secondResult.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.Good);
        Convert.ToInt32(factory.Tags["N7:0"].Value).ShouldBe(0);

        await drv2.DisposeAsync();
    }

    /// <summary>
    ///     A non-zero libplctag status returned by the parent-word write is surfaced as a non-Good
    ///     OPC UA StatusCode — the PCCC device rejection propagates to the caller.
    /// </summary>
    [Fact]
    public async Task Bit_write_surfaces_device_rejection_status()
    {
        // Arrange: the parent tag reads OK (Status = 0) but write returns a timeout error.
        const int errorTimeout = (int)libplctag.Status.ErrorTimeout;
        var factory = new FakeAbLegacyTagFactory
        {
            Customise = p => new FakeAbLegacyTag(p)
            {
                Value = (short)0b0001,
                WriteStatusOverride = errorTimeout,
            },
        };
        var drv = new AbLegacyDriver(new AbLegacyDriverOptions
        {
            Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
            RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("F", "ab://10.0.0.5/1,0", "I:0/3", AbLegacyDataType.Bit)]),
            Probe = new AbLegacyProbeOptions { Enabled = false },
        }, "drv-1", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

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

        // Assert: the write rejection must NOT be silently swallowed as Good.
        var statusCode = results.Single().StatusCode;
        statusCode.ShouldNotBe(AbLegacyStatusMapper.Good);
        statusCode.ShouldBe(AbLegacyStatusMapper.MapLibplctagStatus(errorTimeout));
    }
}
