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.IntegrationTests;

/// <summary>
///     #138 e2e coverage for the new addressing grammar (#136-#144) against a live pymodbus
///     simulator. Skips when the simulator is unreachable so the suite stays portable on dev
///     boxes without Docker. Coverage is breadth-first — one round-trip per major feature
///     (Modicon parse, suffix grammar, byte order, array, multi-unit) — rather than exhaustive,
///     because the unit tests already pin the per-call semantics; the integration tests prove
///     the wire glue agrees with the simulator end-to-end.
/// </summary>
[Trait("Category", "Integration")]
[Collection("ModbusSimulator")]
public sealed class AddressingGrammarTests
{
    private readonly ModbusSimulatorFixture _sim;

    /// <summary>Initializes a new instance of the AddressingGrammarTests class.</summary>
    /// <param name="sim">The Modbus simulator fixture.</param>
    public AddressingGrammarTests(ModbusSimulatorFixture sim) => _sim = sim;

    private async Task<ModbusDriver> NewDriverAsync(params ModbusTagDefinition[] tags)
    {
        var opts = new ModbusDriverOptions
        {
            Host = _sim.Host, Port = _sim.Port, RawTags = ModbusRawTags.Entries(tags),
            Probe = new ModbusProbeOptions { Enabled = false },
        };
        var drv = new ModbusDriver(opts, "addressing-e2e");
        await drv.InitializeAsync("{}", CancellationToken.None);
        return drv;
    }

    /// <summary>Verifies that 5-digit and 6-digit Modicon formats map to the same wire offset.</summary>
    [Fact]
    public async Task Modicon_5_And_6_Digit_Both_Map_To_Same_Wire_Offset()
    {
        if (_sim.SkipReason is not null) Assert.Skip(_sim.SkipReason);

        // Both tags target HR offset 0 via different grammar forms; reads should be equivalent.
        var t5 = new ModbusTagDefinition("Five",  ModbusRegion.HoldingRegisters, 0, ModbusDataType.Int16);
        var t6 = new ModbusTagDefinition("Six",   ModbusRegion.HoldingRegisters, 0, ModbusDataType.Int16);
        var drv = await NewDriverAsync(t5, t6);

        var values = await drv.ReadAsync(["Five", "Six"], CancellationToken.None);
        values[0].StatusCode.ShouldBe(values[1].StatusCode);
        values[0].Value.ShouldBe(values[1].Value);

        await drv.ShutdownAsync(CancellationToken.None);
    }

    /// <summary>Verifies that Float32 values roundtrip correctly with CDAB byte order.</summary>
    [Fact]
    public async Task Float32_With_CDAB_Roundtrips_Through_Wire()
    {
        if (_sim.SkipReason is not null) Assert.Skip(_sim.SkipReason);

        // HR 200 lives in pymodbus's scratch range (`write: [200, 209]` per standard.json),
        // which gives us two consecutive writable HRs for the Float32 round-trip. The earlier
        // version used HR 100 — but standard.json declares HR 100 as a single-cell auto-
        // incrementing register (`write: [100, 100]`) so the second register of the Float32
        // write was rejected with Illegal Data Address.
        var tag = new ModbusTagDefinition("Tank", ModbusRegion.HoldingRegisters, 200, ModbusDataType.Float32,
            ByteOrder: ModbusByteOrder.WordSwap);
        var drv = await NewDriverAsync(tag);

        await drv.WriteAsync([new WriteRequest("Tank", 12.5f)], CancellationToken.None);
        var read = await drv.ReadAsync(["Tank"], CancellationToken.None);
        read[0].Value.ShouldBe(12.5f);

        await drv.ShutdownAsync(CancellationToken.None);
    }

    /// <summary>Verifies that Int16 array reads surface as typed arrays.</summary>
    [Fact]
    public async Task Int16_Array_Reads_Surface_As_Typed_Array()
    {
        if (_sim.SkipReason is not null) Assert.Skip(_sim.SkipReason);

        var tag = new ModbusTagDefinition("Levels", ModbusRegion.HoldingRegisters, 200, ModbusDataType.Int16, ArrayCount: 5);
        var drv = await NewDriverAsync(tag);

        await drv.WriteAsync([new WriteRequest("Levels", new short[] { 10, 20, 30, 40, 50 })], CancellationToken.None);
        var read = await drv.ReadAsync(["Levels"], CancellationToken.None);
        var arr = read[0].Value.ShouldBeOfType<short[]>();
        arr.ShouldBe(new short[] { 10, 20, 30, 40, 50 });

        await drv.ShutdownAsync(CancellationToken.None);
    }

    /// <summary>Verifies that block read coalescing reduces PDU count end-to-end.</summary>
    [Fact]
    public async Task Block_Read_Coalescing_Reduces_PDU_Count_End_To_End()
    {
        if (_sim.SkipReason is not null) Assert.Skip(_sim.SkipReason);

        // Sanity check that the simulator accepts the larger PDU coalescing produces.
        // Using HR 200/202/204 in the scratch range (standard.json's `uint16: 200..209`),
        // not 300/302/304 — pymodbus rejects reads outside the seeded uint16 list with
        // Illegal Data Address (= BadOutOfRange). The coalescing semantic the test
        // exercises is identical with the scratch addresses.
        var t1 = new ModbusTagDefinition("T1", ModbusRegion.HoldingRegisters, 200, ModbusDataType.Int16);
        var t2 = new ModbusTagDefinition("T2", ModbusRegion.HoldingRegisters, 202, ModbusDataType.Int16);
        var t3 = new ModbusTagDefinition("T3", ModbusRegion.HoldingRegisters, 204, ModbusDataType.Int16);
        var opts = new ModbusDriverOptions
        {
            Host = _sim.Host, Port = _sim.Port, RawTags = ModbusRawTags.Entries([t1, t2, t3]), MaxReadGap = 5,
            Probe = new ModbusProbeOptions { Enabled = false },
        };
        var drv = new ModbusDriver(opts, "addressing-e2e");
        await drv.InitializeAsync("{}", CancellationToken.None);

        var read = await drv.ReadAsync(["T1", "T2", "T3"], CancellationToken.None);
        read.Count.ShouldBe(3);
        // All three should read Good (the simulator accepts a 5-register coalesced read).
        read.All(v => v.StatusCode == 0u).ShouldBeTrue("coalesced read must succeed against the simulator");

        await drv.ShutdownAsync(CancellationToken.None);
    }
}
