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

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

[Trait("Category", "Unit")]
public sealed class ModbusCapTests
{
    /// <summary>
    ///     Records every PDU sent so tests can assert request-count and per-request quantity —
    ///     the only observable behaviour of the auto-chunking path.
    /// </summary>
    private sealed class RecordingTransport : IModbusTransport
    {
        /// <summary>Gets the simulated holding register storage.</summary>
        public readonly ushort[] HoldingRegisters = new ushort[1024];
        /// <summary>Gets the list of all FC03 read requests made to the transport.</summary>
        public readonly List<(ushort Address, ushort Quantity)> Fc03Requests = new();
        /// <summary>Gets the list of all FC16 write requests made to the transport.</summary>
        public readonly List<(ushort Address, ushort Quantity)> Fc16Requests = new();

        /// <inheritdoc />
        public Task ConnectAsync(CancellationToken ct) => Task.CompletedTask;

        /// <inheritdoc />
        public Task<byte[]> SendAsync(byte unitId, byte[] pdu, CancellationToken ct)
        {
            var fc = pdu[0];
            if (fc == 0x03)
            {
                var addr = (ushort)((pdu[1] << 8) | pdu[2]);
                var qty = (ushort)((pdu[3] << 8) | pdu[4]);
                Fc03Requests.Add((addr, qty));
                var byteCount = (byte)(qty * 2);
                var resp = new byte[2 + byteCount];
                resp[0] = 0x03;
                resp[1] = byteCount;
                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 (fc == 0x10)
            {
                var addr = (ushort)((pdu[1] << 8) | pdu[2]);
                var qty = (ushort)((pdu[3] << 8) | pdu[4]);
                Fc16Requests.Add((addr, qty));
                for (var i = 0; i < qty; i++)
                    HoldingRegisters[addr + i] = (ushort)((pdu[6 + i * 2] << 8) | pdu[7 + i * 2]);
                return Task.FromResult(new byte[] { 0x10, pdu[1], pdu[2], pdu[3], pdu[4] });
            }
            return Task.FromException<byte[]>(new ModbusException(fc, 0x01, $"fc={fc} unsupported"));
        }

        /// <inheritdoc />
        public ValueTask DisposeAsync() => ValueTask.CompletedTask;
    }

    /// <summary>Verifies that a read within the cap issues a single FC03 request.</summary>
    [Fact]
    public async Task Read_within_cap_issues_single_FC03_request()
    {
        var tag = new ModbusTagDefinition("S", ModbusRegion.HoldingRegisters, 0, ModbusDataType.String,
            StringLength: 40); // 20 regs — fits in default cap (125).
        var transport = new RecordingTransport();
        var opts = new ModbusDriverOptions { Host = "fake", RawTags = ModbusRawTags.Entries([tag]), Probe = new ModbusProbeOptions { Enabled = false } };
        await using var drv = new ModbusDriver(opts, "modbus-1", _ => transport);
        await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);

        _ = await drv.ReadAsync(["S"], TestContext.Current.CancellationToken);

        transport.Fc03Requests.Count.ShouldBe(1);
        transport.Fc03Requests[0].Quantity.ShouldBe((ushort)20);
    }

    /// <summary>Verifies that a read above cap splits into two FC03 requests.</summary>
    [Fact]
    public async Task Read_above_cap_splits_into_two_FC03_requests()
    {
        // 240-char string = 120 regs. Cap = 100 (a typical sub-spec device cap). Expect 100 + 20.
        var tag = new ModbusTagDefinition("LongString", ModbusRegion.HoldingRegisters, 100, ModbusDataType.String,
            StringLength: 240);
        var transport = new RecordingTransport();
        // Seed cells so the re-assembled payload is stable — confirms chunks are stitched in order.
        for (ushort i = 100; i < 100 + 120; i++)
            transport.HoldingRegisters[i] = (ushort)((('A' + (i - 100) % 26) << 8) | ('A' + (i - 100) % 26));

        var opts = new ModbusDriverOptions
        {
            Host = "fake",
            RawTags = ModbusRawTags.Entries([tag]),
            MaxRegistersPerRead = 100,
            Probe = new ModbusProbeOptions { Enabled = false },
        };
        await using var drv = new ModbusDriver(opts, "modbus-1", _ => transport);
        await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);

        var results = await drv.ReadAsync(["LongString"], TestContext.Current.CancellationToken);
        results[0].StatusCode.ShouldBe(0u);

        transport.Fc03Requests.Count.ShouldBe(2, "120 regs / cap 100 → 2 requests");
        transport.Fc03Requests[0].ShouldBe(((ushort)100, (ushort)100));
        transport.Fc03Requests[1].ShouldBe(((ushort)200, (ushort)20));

        // Payload continuity: re-assembled string starts where register 100 does and keeps going.
        var s = (string)results[0].Value!;
        s.Length.ShouldBeGreaterThan(0);
        s[0].ShouldBe('A'); // register[100] high byte
    }

    /// <summary>Verifies that read cap honors Mitsubishi lower cap of 64 registers.</summary>
    [Fact]
    public async Task Read_cap_honors_Mitsubishi_lower_cap_of_64()
    {
        // 200-char string = 100 regs. Mitsubishi Q cap = 64. Expect: 64, 36.
        var tag = new ModbusTagDefinition("MitString", ModbusRegion.HoldingRegisters, 0, ModbusDataType.String,
            StringLength: 200);
        var transport = new RecordingTransport();
        var opts = new ModbusDriverOptions { Host = "fake", RawTags = ModbusRawTags.Entries([tag]), MaxRegistersPerRead = 64, Probe = new ModbusProbeOptions { Enabled = false } };
        await using var drv = new ModbusDriver(opts, "modbus-1", _ => transport);
        await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);

        _ = await drv.ReadAsync(["MitString"], TestContext.Current.CancellationToken);

        transport.Fc03Requests.Count.ShouldBe(2);
        transport.Fc03Requests[0].Quantity.ShouldBe((ushort)64);
        transport.Fc03Requests[1].Quantity.ShouldBe((ushort)36);
    }

    /// <summary>Verifies that write exceeding cap throws instead of splitting.</summary>
    [Fact]
    public async Task Write_exceeding_cap_throws_instead_of_splitting()
    {
        // Partial FC16 across two transactions is not atomic. Forcing an explicit exception so the
        // caller knows their tag definition is incompatible with the device cap rather than silently
        // writing half a string and crashing between chunks.
        var tag = new ModbusTagDefinition("LongStringWrite", ModbusRegion.HoldingRegisters, 0, ModbusDataType.String,
            StringLength: 220); // 110 regs.
        var transport = new RecordingTransport();
        var opts = new ModbusDriverOptions { Host = "fake", RawTags = ModbusRawTags.Entries([tag]), MaxRegistersPerWrite = 100, Probe = new ModbusProbeOptions { Enabled = false } };
        await using var drv = new ModbusDriver(opts, "modbus-1", _ => transport);
        await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);

        var results = await drv.WriteAsync(
            [new WriteRequest("LongStringWrite", new string('A', 220))],
            TestContext.Current.CancellationToken);

        // Driver catches the internal exception and surfaces BadInternalError — the Fc16Requests
        // list must still be empty because nothing was sent.
        results[0].StatusCode.ShouldNotBe(0u);
        transport.Fc16Requests.Count.ShouldBe(0);
    }

    /// <summary>Verifies that write within cap proceeds normally.</summary>
    [Fact]
    public async Task Write_within_cap_proceeds_normally()
    {
        var tag = new ModbusTagDefinition("ShortStringWrite", ModbusRegion.HoldingRegisters, 0, ModbusDataType.String,
            StringLength: 40); // 20 regs.
        var transport = new RecordingTransport();
        var opts = new ModbusDriverOptions { Host = "fake", RawTags = ModbusRawTags.Entries([tag]), MaxRegistersPerWrite = 100, Probe = new ModbusProbeOptions { Enabled = false } };
        await using var drv = new ModbusDriver(opts, "modbus-1", _ => transport);
        await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);

        var results = await drv.WriteAsync(
            [new WriteRequest("ShortStringWrite", "HELLO")],
            TestContext.Current.CancellationToken);

        results[0].StatusCode.ShouldBe(0u);
        transport.Fc16Requests.Count.ShouldBe(1);
        transport.Fc16Requests[0].Quantity.ShouldBe((ushort)20);
    }
}
