using Shouldly;
using Xunit;

namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests.Mitsubishi;

/// <summary>
///     Verifies the MELSEC-family Modbus quirks against the <c>mitsubishi.json</c> pymodbus
///     profile: CDAB word order default, binary-not-BCD D-register encoding, hex X-input
///     parsing (Q/L/iQ-R), D0 fingerprint, M-relay coil mapping with bank base.
/// </summary>
/// <remarks>
///     Groups all quirks in one test class instead of per-behavior classes (unlike the DL205
///     set) because MELSEC's per-model differentiation is handled by the
///     <see cref="MelsecFamily"/> enum on the helper + <c>MODBUS_SIM_PROFILE</c> env var on
///     the fixture, rather than per-PR test classes.
/// </remarks>
[Collection(ModbusSimulatorCollection.Name)]
[Trait("Category", "Integration")]
[Trait("Device", "Mitsubishi")]
public sealed class MitsubishiQuirkTests(ModbusSimulatorFixture sim)
{
    /// <summary>Verifies that Mitsubishi D0 register fingerprint reads the expected value 0x1234.</summary>
    [Fact]
    public async Task Mitsubishi_D0_fingerprint_reads_0x1234()
    {
        if (!ShouldRun()) return;
        await using var driver = await NewDriverAsync(
            new ModbusTagDefinition("D0_Fingerprint",
                ModbusRegion.HoldingRegisters,
                Address: MelsecAddress.DRegisterToHolding("D0"),
                DataType: ModbusDataType.UInt16, Writable: false));

        var r = await driver.ReadAsync(["D0_Fingerprint"], TestContext.Current.CancellationToken);
        r[0].StatusCode.ShouldBe(0u);
        r[0].Value.ShouldBe((ushort)0x1234);
    }

    /// <summary>Verifies that Mitsubishi Float32 with CDAB word order decodes correctly to 1.5 from D100.</summary>
    [Fact]
    public async Task Mitsubishi_Float32_CDAB_decodes_1_5f_from_D100()
    {
        if (!ShouldRun()) return;
        // MELSEC Q/L/iQ-R/iQ-F all store 32-bit values with CDAB word order (low word at
        // lower D-register address). HR[100..101] = [0, 0x3FC0] decodes as 1.5f under
        // WordSwap but as a denormal under BigEndian.
        var addr = MelsecAddress.DRegisterToHolding("D100");
        await using var driver = await NewDriverAsync(
            new ModbusTagDefinition("D100_Float_CDAB",
                ModbusRegion.HoldingRegisters, Address: addr,
                DataType: ModbusDataType.Float32, Writable: false,
                ByteOrder: ModbusByteOrder.WordSwap),
            new ModbusTagDefinition("D100_Float_ABCD_control",
                ModbusRegion.HoldingRegisters, Address: addr,
                DataType: ModbusDataType.Float32, Writable: false,
                ByteOrder: ModbusByteOrder.BigEndian));

        var r = await driver.ReadAsync(
            ["D100_Float_CDAB", "D100_Float_ABCD_control"],
            TestContext.Current.CancellationToken);
        r[0].Value.ShouldBe(1.5f, "MELSEC stores Float32 CDAB; WordSwap decode returns 1.5f");
        r[1].Value.ShouldNotBe(1.5f, "same wire with BigEndian must decode to a different value");
    }

    /// <summary>Verifies that Mitsubishi D registers store binary values, not BCD.</summary>
    [Fact]
    public async Task Mitsubishi_D10_is_binary_not_BCD()
    {
        if (!ShouldRun()) return;
        // Counter-to-DL205: MELSEC D-registers are binary by default. D10 = 1234 decimal =
        // 0x04D2. Reading as Int16 returns 1234; reading as Bcd16 would throw (nibble 0xD is
        // non-BCD) — the integration test proves the Int16 decode wins.
        await using var driver = await NewDriverAsync(
            new ModbusTagDefinition("D10_Binary",
                ModbusRegion.HoldingRegisters,
                Address: MelsecAddress.DRegisterToHolding("D10"),
                DataType: ModbusDataType.Int16, Writable: false));

        var r = await driver.ReadAsync(["D10_Binary"], TestContext.Current.CancellationToken);
        r[0].StatusCode.ShouldBe(0u);
        r[0].Value.ShouldBe((short)1234, "MELSEC stores numeric D-register values in binary; 0x04D2 = 1234");
    }

    /// <summary>Verifies that reading a binary D register as BCD throws when the value contains non-decimal nibbles.</summary>
    [Fact]
    public async Task Mitsubishi_D10_as_BCD_throws_because_nibble_is_non_decimal()
    {
        if (!ShouldRun()) return;
        // If a site configured D10 with Bcd16 data type but the ladder writes binary, the
        // BCD decoder MUST reject the garbage rather than silently returning wrong decimal.
        // 0x04D2 contains nibble 0xD which fails BCD validation.
        await using var driver = await NewDriverAsync(
            new ModbusTagDefinition("D10_WrongBcd",
                ModbusRegion.HoldingRegisters,
                Address: MelsecAddress.DRegisterToHolding("D10"),
                DataType: ModbusDataType.Bcd16, Writable: false));

        var r = await driver.ReadAsync(["D10_WrongBcd"], TestContext.Current.CancellationToken);
        // ReadAsync catches the InvalidDataException from DecodeBcd and surfaces it as
        // BadCommunicationError (PR 52 mapping). Non-zero status = caller sees a real
        // problem and can check their tag config instead of getting silently-wrong numbers.
        r[0].StatusCode.ShouldNotBe(0u, "BCD decode of binary 0x04D2 must fail loudly because nibble D is non-BCD");
    }

    /// <summary>Verifies that Mitsubishi Q/L/iQ-R X inputs use hex addressing and X210 maps correctly.</summary>
    [Fact]
    public async Task Mitsubishi_QLiQR_X210_hex_maps_to_DI_528_reads_ON()
    {
        if (!ShouldRun()) return;
        // MELSEC-Q / L / iQ-R: X addresses are hex. X210 = 0x210 = 528 decimal.
        // mitsubishi.json seeds cell 33 (DI 528..543) with value 9 = bit 0 + bit 3 set.
        // X210 → DI 528 → cell 33 bit 0 = 1 (ON).
        var addr = MelsecAddress.XInputToDiscrete("X210", MelsecFamily.Q_L_iQR);
        addr.ShouldBe((ushort)528);

        await using var driver = await NewDriverAsync(
            new ModbusTagDefinition("X210_hex",
                ModbusRegion.DiscreteInputs, Address: addr,
                DataType: ModbusDataType.Bool, Writable: false));

        var r = await driver.ReadAsync(["X210_hex"], TestContext.Current.CancellationToken);
        r[0].StatusCode.ShouldBe(0u);
        r[0].Value.ShouldBe(true);
    }

    /// <summary>Verifies that X input address mapping differs between Mitsubishi MELSEC families (Q vs FX).</summary>
    [Fact]
    public void Mitsubishi_family_trap_X20_differs_on_Q_vs_FX()
    {
        // Not a live-sim test — a unit-level proof that the MELSEC family selector gates the
        // address correctly. Included in the integration suite so anyone running the MELSEC
        // tests sees the trap called out explicitly.
        MelsecAddress.XInputToDiscrete("X20", MelsecFamily.Q_L_iQR).ShouldBe((ushort)32);
        MelsecAddress.XInputToDiscrete("X20", MelsecFamily.F_iQF).ShouldBe((ushort)16);
    }

    /// <summary>Verifies that Mitsubishi M512 relay maps to coil address 512 and reads ON.</summary>
    [Fact]
    public async Task Mitsubishi_M512_maps_to_coil_512_reads_ON()
    {
        if (!ShouldRun()) return;
        // mitsubishi.json seeds cell 32 (coil 512..527) with value 5 = bit 0 + bit 2 set.
        // M512 → coil 512 → cell 32 bit 0 = 1 (ON).
        var addr = MelsecAddress.MRelayToCoil("M512");
        addr.ShouldBe((ushort)512);

        await using var driver = await NewDriverAsync(
            new ModbusTagDefinition("M512",
                ModbusRegion.Coils, Address: addr,
                DataType: ModbusDataType.Bool, Writable: false));

        var r = await driver.ReadAsync(["M512"], TestContext.Current.CancellationToken);
        r[0].StatusCode.ShouldBe(0u);
        r[0].Value.ShouldBe(true);
    }

    // --- helpers ---

    private bool ShouldRun()
    {
        if (sim.SkipReason is not null) { Assert.Skip(sim.SkipReason); return false; }
        if (!string.Equals(Environment.GetEnvironmentVariable("MODBUS_SIM_PROFILE"), "mitsubishi",
                StringComparison.OrdinalIgnoreCase))
        {
            Assert.Skip("MODBUS_SIM_PROFILE != mitsubishi — skipping.");
            return false;
        }
        return true;
    }

    private async Task<ModbusDriver> NewDriverAsync(params ModbusTagDefinition[] tags)
    {
        var drv = new ModbusDriver(
            new ModbusDriverOptions
            {
                Host = sim.Host,
                Port = sim.Port,
                UnitId = 1,
                Timeout = TimeSpan.FromSeconds(2),
                RawTags = ModbusRawTags.Entries(tags),
                Probe = new ModbusProbeOptions { Enabled = false },
            },
            driverInstanceId: "melsec-quirk");
        await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);
        return drv;
    }
}
