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

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

[Trait("Category", "Unit")]
public sealed class AbCipBoolInDIntRmwTests
{
    /// <summary>
    ///     Fake tag runtime that stores a DINT value + exposes Read/Write/EncodeValue/DecodeValue
    ///     for DInt. RMW tests use one instance as the "parent" runtime (tag name "Motor.Flags")
    ///     which the driver's WriteBitInDIntAsync reads + writes.
    /// </summary>
    private sealed class ParentDintFake(AbCipTagCreateParams p) : FakeAbCipTag(p)
    {
        // Uses the base FakeAbCipTag's Value + ReadCount + WriteCount.
    }

    /// <summary>Verifies that bit set reads parent, ORs bit, and writes back.</summary>
    [Fact]
    public async Task Bit_set_reads_parent_ORs_bit_writes_back()
    {
        var factory = new FakeAbCipTagFactory
        {
            Customise = p => new ParentDintFake(p) { Value = 0b0001 },
        };
        var drv = new AbCipDriver(new AbCipDriverOptions
        {
            Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
            RawTags = AbCipRawTags.From(
                new AbCipTagDefinition("Flag3", "ab://10.0.0.5/1,0", "Motor.Flags.3", AbCipDataType.Bool))
            ,
            Probe = new AbCipProbeOptions { 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(AbCipStatusMapper.Good);

        // Parent runtime created under name "Motor.Flags" — distinct from the bit-selector tag.
        factory.Tags.ShouldContainKey("Motor.Flags");
        factory.Tags["Motor.Flags"].Value.ShouldBe(0b1001); // bit 3 set, bit 0 preserved
        factory.Tags["Motor.Flags"].ReadCount.ShouldBe(1);
        factory.Tags["Motor.Flags"].WriteCount.ShouldBe(1);
    }

    /// <summary>Verifies that bit clear preserves other bits.</summary>
    [Fact]
    public async Task Bit_clear_preserves_other_bits()
    {
        var factory = new FakeAbCipTagFactory
        {
            Customise = p => new ParentDintFake(p) { Value = unchecked((int)0xFFFFFFFF) },
        };
        var drv = new AbCipDriver(new AbCipDriverOptions
        {
            Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
            RawTags = AbCipRawTags.From(new AbCipTagDefinition("F", "ab://10.0.0.5/1,0", "Motor.Flags.3", AbCipDataType.Bool)),
            Probe = new AbCipProbeOptions { Enabled = false },
        }, "drv-1", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

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

        var updated = Convert.ToInt32(factory.Tags["Motor.Flags"].Value);
        (updated & (1 << 3)).ShouldBe(0);                 // bit 3 cleared
        (updated & ~(1 << 3)).ShouldBe(unchecked((int)0xFFFFFFF7)); // every other bit preserved
    }

    /// <summary>Verifies that concurrent bit writes to same parent compose correctly.</summary>
    [Fact]
    public async Task Concurrent_bit_writes_to_same_parent_compose_correctly()
    {
        var factory = new FakeAbCipTagFactory
        {
            Customise = p => new ParentDintFake(p) { Value = 0 },
        };
        var tags = Enumerable.Range(0, 8)
            .Select(b => new AbCipTagDefinition($"Bit{b}", "ab://10.0.0.5/1,0", $"Flags.{b}", AbCipDataType.Bool))
            .ToArray();
        var drv = new AbCipDriver(new AbCipDriverOptions
        {
            Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
            RawTags = AbCipRawTags.From(tags),
            Probe = new AbCipProbeOptions { 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["Flags"].Value).ShouldBe(0xFF);
    }

    /// <summary>Verifies that bit writes to different parents each get their own runtime.</summary>
    [Fact]
    public async Task Bit_writes_to_different_parents_each_get_own_runtime()
    {
        var factory = new FakeAbCipTagFactory
        {
            Customise = p => new ParentDintFake(p) { Value = 0 },
        };
        var drv = new AbCipDriver(new AbCipDriverOptions
        {
            Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
            RawTags = AbCipRawTags.From(
                new AbCipTagDefinition("A", "ab://10.0.0.5/1,0", "Motor1.Flags.0", AbCipDataType.Bool),
                new AbCipTagDefinition("B", "ab://10.0.0.5/1,0", "Motor2.Flags.0", AbCipDataType.Bool))
            ,
            Probe = new AbCipProbeOptions { Enabled = false },
        }, "drv-1", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

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

        factory.Tags.ShouldContainKey("Motor1.Flags");
        factory.Tags.ShouldContainKey("Motor2.Flags");
    }

    /// <summary>Verifies that repeat bit writes reuse one parent runtime.</summary>
    [Fact]
    public async Task Repeat_bit_writes_reuse_one_parent_runtime()
    {
        var factory = new FakeAbCipTagFactory
        {
            Customise = p => new ParentDintFake(p) { Value = 0 },
        };
        var drv = new AbCipDriver(new AbCipDriverOptions
        {
            Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
            RawTags = AbCipRawTags.From(
                new AbCipTagDefinition("Bit0", "ab://10.0.0.5/1,0", "Flags.0", AbCipDataType.Bool),
                new AbCipTagDefinition("Bit5", "ab://10.0.0.5/1,0", "Flags.5", AbCipDataType.Bool))
            ,
            Probe = new AbCipProbeOptions { 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);

        // Three factory invocations: two bit-selector tags (never used for writes, but the
        // driver may create them opportunistically) + one shared parent. Assert the parent was
        // init'd exactly once + used for both writes.
        factory.Tags["Flags"].InitializeCount.ShouldBe(1);
        factory.Tags["Flags"].WriteCount.ShouldBe(2);
        Convert.ToInt32(factory.Tags["Flags"].Value).ShouldBe(0x21); // bits 0 + 5
    }
}
