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

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

[Trait("Category", "Unit")]
public sealed class ModbusBitRmwTests
{
    /// <summary>Fake transport capturing each PDU so tests can assert on the read + write sequence.</summary>
    private sealed class RmwTransport : IModbusTransport
    {
        public readonly ushort[] HoldingRegisters = new ushort[256];
        public readonly List<byte[]> Pdus = new();

        /// <summary>Connects asynchronously (no-op for fake).</summary>
        /// <param name="ct">Cancellation token (unused).</param>
        /// <returns>A completed task.</returns>
        public Task ConnectAsync(CancellationToken ct) => Task.CompletedTask;

        /// <summary>Sends a Modbus PDU and returns a response.</summary>
        /// <param name="unitId">The Modbus unit ID (unused).</param>
        /// <param name="pdu">The protocol data unit to send.</param>
        /// <param name="ct">Cancellation token (unused).</param>
        /// <returns>A task containing the response PDU.</returns>
        public Task<byte[]> SendAsync(byte unitId, byte[] pdu, CancellationToken ct)
        {
            Pdus.Add(pdu);
            if (pdu[0] == 0x03)
            {
                // FC03 Read Holding Registers.
                var addr = (ushort)((pdu[1] << 8) | pdu[2]);
                var qty = (ushort)((pdu[3] << 8) | pdu[4]);
                var resp = new byte[2 + qty * 2];
                resp[0] = 0x03;
                resp[1] = (byte)(qty * 2);
                for (var i = 0; i < qty; i++)
                {
                    resp[2 + i * 2] = (byte)(HoldingRegisters[addr + i] >> 8);
                    resp[3 + i * 2] = (byte)(HoldingRegisters[addr + i] & 0xFF);
                }
                return Task.FromResult(resp);
            }
            if (pdu[0] == 0x06)
            {
                // FC06 Write Single Register.
                var addr = (ushort)((pdu[1] << 8) | pdu[2]);
                var v = (ushort)((pdu[3] << 8) | pdu[4]);
                HoldingRegisters[addr] = v;
                return Task.FromResult(new byte[] { 0x06, pdu[1], pdu[2], pdu[3], pdu[4] });
            }
            return Task.FromException<byte[]>(new NotSupportedException($"FC 0x{pdu[0]:X2} not supported by fake"));
        }

        /// <summary>Disposes asynchronously (no-op for fake).</summary>
        /// <returns>A completed task.</returns>
        public ValueTask DisposeAsync() => ValueTask.CompletedTask;
    }

    private static (ModbusDriver drv, RmwTransport fake) NewDriver(params ModbusTagDefinition[] tags)
    {
        var fake = new RmwTransport();
        var opts = new ModbusDriverOptions
        {
            Host = "fake",
            RawTags = ModbusRawTags.Entries(tags),
            Probe = new ModbusProbeOptions { Enabled = false },
        };
        return (new ModbusDriver(opts, "modbus-1", _ => fake), fake);
    }

    /// <summary>Verifies that setting a bit reads the current register, ORs the bit, and writes back.</summary>
    [Fact]
    public async Task Bit_set_reads_current_register_ORs_bit_writes_back()
    {
        var (drv, fake) = NewDriver(
            new ModbusTagDefinition("Flag3", ModbusRegion.HoldingRegisters, 10, ModbusDataType.BitInRegister, BitIndex: 3));
        await drv.InitializeAsync("{}", CancellationToken.None);
        fake.HoldingRegisters[10] = 0b0000_0001; // bit 0 already set

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

        results.Single().StatusCode.ShouldBe(0u);
        fake.HoldingRegisters[10].ShouldBe((ushort)0b0000_1001); // bit 3 now set, bit 0 preserved
        // Two PDUs: FC03 read then FC06 write.
        fake.Pdus.Count.ShouldBe(2);
        fake.Pdus[0][0].ShouldBe((byte)0x03);
        fake.Pdus[1][0].ShouldBe((byte)0x06);
    }

    /// <summary>Verifies that clearing a bit reads the current register, ANDs the bit off, and writes back.</summary>
    [Fact]
    public async Task Bit_clear_reads_current_register_ANDs_bit_off_writes_back()
    {
        var (drv, fake) = NewDriver(
            new ModbusTagDefinition("Flag3", ModbusRegion.HoldingRegisters, 10, ModbusDataType.BitInRegister, BitIndex: 3));
        await drv.InitializeAsync("{}", CancellationToken.None);
        fake.HoldingRegisters[10] = 0xFFFF; // all bits set

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

        fake.HoldingRegisters[10].ShouldBe((ushort)0b1111_1111_1111_0111); // bit 3 cleared, rest preserved
    }

    /// <summary>Verifies that concurrent bit writes to the same register preserve all updates via serialization.</summary>
    [Fact]
    public async Task Concurrent_bit_writes_to_same_register_preserve_all_updates()
    {
        // Serialization test — 8 writers target different bits in register 20. Without the RMW
        // lock, concurrent reads interleave + last-to-commit wins so some bits get lost.
        var tags = Enumerable.Range(0, 8)
            .Select(b => new ModbusTagDefinition($"Bit{b}", ModbusRegion.HoldingRegisters, 20, ModbusDataType.BitInRegister, BitIndex: (byte)b))
            .ToArray();
        var (drv, fake) = NewDriver(tags);
        await drv.InitializeAsync("{}", CancellationToken.None);
        fake.HoldingRegisters[20] = 0;

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

        fake.HoldingRegisters[20].ShouldBe((ushort)0xFF); // all 8 bits set
    }

    /// <summary>Verifies that bit writes to different registers proceed in parallel without contention.</summary>
    [Fact]
    public async Task Bit_write_on_different_registers_proceeds_in_parallel_without_contention()
    {
        var tags = Enumerable.Range(0, 4)
            .Select(i => new ModbusTagDefinition($"Bit{i}", ModbusRegion.HoldingRegisters, (ushort)(50 + i), ModbusDataType.BitInRegister, BitIndex: 0))
            .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.HoldingRegisters[50 + i].ShouldBe((ushort)0x01);
    }

    /// <summary>Verifies that bit writes preserve other bits in the same register.</summary>
    [Fact]
    public async Task Bit_write_preserves_other_bits_in_the_same_register()
    {
        var (drv, fake) = NewDriver(
            new ModbusTagDefinition("BitA", ModbusRegion.HoldingRegisters, 30, ModbusDataType.BitInRegister, BitIndex: 5),
            new ModbusTagDefinition("BitB", ModbusRegion.HoldingRegisters, 30, ModbusDataType.BitInRegister, BitIndex: 10));
        await drv.InitializeAsync("{}", CancellationToken.None);

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

        fake.HoldingRegisters[30].ShouldBe((ushort)((1 << 5) | (1 << 10)));
    }

    // ---- Driver.Modbus-013: RMW read-response not validated before indexing ----

    /// <summary>
    ///     Transport that returns a too-short PDU for FC03 reads — simulates a buggy device
    ///     returning a malformed response during a BitInRegister read-modify-write. Pre-fix,
    ///     WriteBitInRegisterAsync indexed readResp[2]/[3] without checking resp.Length, causing
    ///     IndexOutOfRangeException. Post-fix it throws InvalidDataException which WriteAsync's
    ///     catch-all maps to BadInternalError.
    /// </summary>
    private sealed class TruncatedRmwTransport : IModbusTransport
    {
        /// <summary>Connects asynchronously (no-op for fake).</summary>
        /// <param name="ct">Cancellation token (unused).</param>
        public Task ConnectAsync(CancellationToken ct) => Task.CompletedTask;

        /// <summary>Returns a truncated FC03 response to trigger the validation path.</summary>
        /// <param name="unitId">The Modbus unit ID (unused).</param>
        /// <param name="pdu">The PDU to process.</param>
        /// <param name="ct">Cancellation token (unused).</param>
        public Task<byte[]> SendAsync(byte unitId, byte[] pdu, CancellationToken ct)
        {
            if (pdu[0] == 0x03)
                // Return only [fc][byte-count=2] — missing the actual register data bytes.
                return Task.FromResult(new byte[] { 0x03, 0x02 });
            // FC06 write echoes the PDU
            return Task.FromResult(pdu);
        }

        /// <summary>Disposes asynchronously (no-op for fake).</summary>
        public ValueTask DisposeAsync() => ValueTask.CompletedTask;
    }

    /// <summary>
    ///     Driver.Modbus-013/014: a truncated FC03 read response during a BitInRegister RMW must
    ///     surface as BadCommunicationError, not BadInternalError (a structural/driver-code
    ///     problem) — and not as an IndexOutOfRangeException that escapes WriteAsync uncaught.
    /// </summary>
    [Fact]
    public async Task BitInRegister_write_with_truncated_FC03_response_returns_BadCommunicationError()
    {
        var truncated = new TruncatedRmwTransport();
        var opts = new ModbusDriverOptions
        {
            Host = "fake",
            RawTags = ModbusRawTags.Entries([new ModbusTagDefinition("Bit5", ModbusRegion.HoldingRegisters, 10, ModbusDataType.BitInRegister, BitIndex: 5)]),
            Probe = new ModbusProbeOptions { Enabled = false },
        };
        var drv = new ModbusDriver(opts, "modbus-rmw-trunc", _ => truncated);
        await drv.InitializeAsync("{}", CancellationToken.None);

        // WriteAsync must complete (not throw) and return BadCommunicationError — the same
        // status the ReadAsync path returns for a truncated response. BadInternalError (0x80020000)
        // is wrong because the failure is a communication/protocol error, not a driver bug.
        const uint BadCommunicationError = 0x80050000u;
        var results = await drv.WriteAsync([new WriteRequest("Bit5", true)], CancellationToken.None);

        results.Single().StatusCode.ShouldBe(BadCommunicationError,
            "A truncated FC03 RMW response is a communication error, not an internal driver error");
    }
}
