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

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

[Trait("Category", "Unit")]
public sealed class FocasPmcBitRmwTests
{
    /// <summary>
    ///     Fake client simulating PMC byte storage + exposing it as a sbyte so RMW callers can
    ///     observe the read-modify-write round-trip. ReadAsync for a Bit with bitIndex surfaces
    ///     the current bit; WriteAsync stores the full byte the driver issues.
    /// </summary>
    private sealed class PmcRmwFake : FakeFocasClient
    {
        /// <summary>Gets the simulated PMC byte storage.</summary>
        public byte[] PmcBytes { get; } = new byte[1024];

        /// <inheritdoc />
        public override Task<(object? value, uint status)> ReadAsync(
            FocasAddress address, FocasDataType type, CancellationToken ct)
        {
            if (address.Kind == FocasAreaKind.Pmc && type == FocasDataType.Byte)
                return Task.FromResult(((object?)(sbyte)PmcBytes[address.Number], FocasStatusMapper.Good));
            if (address.Kind == FocasAreaKind.Pmc && type == FocasDataType.Bit && address.BitIndex is int bit)
                return Task.FromResult(((object?)((PmcBytes[address.Number] & (1 << bit)) != 0), FocasStatusMapper.Good));
            return base.ReadAsync(address, type, ct);
        }

        /// <inheritdoc />
        public override Task<uint> WriteAsync(
            FocasAddress address, FocasDataType type, object? value, CancellationToken ct)
        {
            // Driver writes the full byte after RMW (type==Byte with full byte value), OR a raw
            // bit write (type==Bit, bitIndex non-null) — depending on how the driver routes it.
            if (address.Kind == FocasAreaKind.Pmc && type == FocasDataType.Byte)
            {
                PmcBytes[address.Number] = (byte)Convert.ToSByte(value);
                return Task.FromResult(FocasStatusMapper.Good);
            }
            if (address.Kind == FocasAreaKind.Pmc && type == FocasDataType.Bit && address.BitIndex is int bit)
            {
                var current = PmcBytes[address.Number];
                PmcBytes[address.Number] = Convert.ToBoolean(value)
                    ? (byte)(current | (1 << bit))
                    : (byte)(current & ~(1 << bit));
                return Task.FromResult(FocasStatusMapper.Good);
            }
            return base.WriteAsync(address, type, value, ct);
        }
    }

    private static (FocasDriver drv, PmcRmwFake fake) NewDriver(params FocasTagDefinition[] tags)
    {
        var fake = new PmcRmwFake();
        var factory = new FakeFocasClientFactory { Customise = () => fake };
        var drv = new FocasDriver(new FocasDriverOptions
        {
            Devices = [new FocasDeviceOptions("focas://10.0.0.5:8193")],
            RawTags = FocasRawTags.Entries(tags),
            Probe = new FocasProbeOptions { Enabled = false },
        }, "drv-1", factory);
        return (drv, fake);
    }

    /// <summary>Verifies that a bit set operation surfaces as Good status and flips the bit.</summary>
    [Fact]
    public async Task Bit_set_surfaces_as_Good_status_and_flips_bit()
    {
        var (drv, fake) = NewDriver(
            new FocasTagDefinition("Run", "focas://10.0.0.5:8193", "R100.3", FocasDataType.Bit));
        await drv.InitializeAsync("{}", CancellationToken.None);
        fake.PmcBytes[100] = 0b0000_0001;

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

        results.Single().StatusCode.ShouldBe(FocasStatusMapper.Good);
        fake.PmcBytes[100].ShouldBe((byte)0b0000_1001);
    }

    /// <summary>Verifies that clearing a bit preserves other bits.</summary>
    [Fact]
    public async Task Bit_clear_preserves_other_bits()
    {
        var (drv, fake) = NewDriver(
            new FocasTagDefinition("Flag", "focas://10.0.0.5:8193", "R100.3", FocasDataType.Bit));
        await drv.InitializeAsync("{}", CancellationToken.None);
        fake.PmcBytes[100] = 0xFF;

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

        fake.PmcBytes[100].ShouldBe((byte)0b1111_0111);
    }

    /// <summary>Verifies that subsequent bit sets in the same byte compose correctly.</summary>
    [Fact]
    public async Task Subsequent_bit_sets_in_same_byte_compose_correctly()
    {
        var tags = Enumerable.Range(0, 8)
            .Select(b => new FocasTagDefinition($"Bit{b}", "focas://10.0.0.5:8193", $"R100.{b}", FocasDataType.Bit))
            .ToArray();
        var (drv, fake) = NewDriver(tags);
        await drv.InitializeAsync("{}", CancellationToken.None);
        fake.PmcBytes[100] = 0;

        for (var b = 0; b < 8; b++)
            await drv.WriteAsync([new WriteRequest($"Bit{b}", true)], CancellationToken.None);

        fake.PmcBytes[100].ShouldBe((byte)0xFF);
    }

    /// <summary>Verifies that bit writes to different bytes do not contend.</summary>
    [Fact]
    public async Task Bit_write_to_different_bytes_does_not_contend()
    {
        var tags = Enumerable.Range(0, 4)
            .Select(i => new FocasTagDefinition($"Bit{i}", "focas://10.0.0.5:8193", $"R{50 + i}.0", FocasDataType.Bit))
            .ToArray();
        var (drv, fake) = NewDriver(tags);
        await drv.InitializeAsync("{}", CancellationToken.None);

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

        for (var i = 0; i < 4; i++)
            fake.PmcBytes[50 + i].ShouldBe((byte)0x01);
    }
}
