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

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

/// <summary>
///     #144 family-native parser branch — DL205 + MELSEC. Family flag drives the parser to
///     try the family's native syntax (V2000, D100, X20 hex/octal) before falling back to
///     Modicon / mnemonic.
/// </summary>
[Trait("Category", "Unit")]
public sealed class ModbusFamilyParserTests
{
    // ----- DL205 native: V-memory (octal), Y/C/X/SP coils + discrete -----

    /// <summary>Verifies DL205 user V-memory addresses map to holding registers.</summary>
    /// <param name="addr">The V-memory address.</param>
    /// <param name="expectedOffset">The expected holding register offset.</param>
    [Theory]
    [InlineData("V0",     0)]
    [InlineData("V2000",  1024)]   // octal 2000 = decimal 1024
    [InlineData("V7777",  4095)]   // octal 7777 = decimal 4095, top of the user bank example
    public void DL205_UserVMemory_To_HoldingRegisters(string addr, int expectedOffset)
    {
        var p = ModbusAddressParser.Parse(addr, ModbusFamily.DL205);
        p.Region.ShouldBe(ModbusRegion.HoldingRegisters);
        p.Offset.ShouldBe((ushort)expectedOffset);
        p.DataType.ShouldBe(ModbusDataType.Int16);
    }

    // Regression: Driver.Modbus.Addressing-001. DL205/DL260 system V-memory (V40400+) is NOT a
    // plain octal decode — the CPU relocates the system bank to Modbus PDU 0x2100. Octal-decoding
    // V40400 yields 16640 (0x4100), the WRONG register. Per docs/v2/dl205.md §V-Memory Addressing,
    // V40400 must map to PDU 0x2100 (decimal 8448) and the bank is contiguous from there.
    /// <summary>Verifies DL205 system V-memory addresses map to the system bank in holding registers.</summary>
    /// <param name="addr">The V-memory address.</param>
    /// <param name="expectedOffset">The expected holding register offset.</param>
    [Theory]
    [InlineData("V40400", 0x2100)]  // system base
    [InlineData("V40401", 0x2101)]  // next register — contiguous, +1 decimal
    [InlineData("V40410", 0x2108)]  // octal 40410 = base + octal(10) = base + 8 decimal
    public void DL205_SystemVMemory_To_HoldingRegisters_SystemBank(string addr, int expectedOffset)
    {
        var p = ModbusAddressParser.Parse(addr, ModbusFamily.DL205);
        p.Region.ShouldBe(ModbusRegion.HoldingRegisters);
        p.Offset.ShouldBe((ushort)expectedOffset);
        p.DataType.ShouldBe(ModbusDataType.Int16);
    }

    /// <summary>Verifies system V-memory helper correctly routes through system bank.</summary>
    [Fact]
    public void DL205_SystemVMemory_Helper_Routes_Through_SystemBank()
    {
        // Direct helper coverage: VMemoryToPdu routes user vs system; the legacy
        // UserVMemoryToPdu is the plain octal decoder used only for the user bank.
        DirectLogicAddress.VMemoryToPdu("V40400").ShouldBe((ushort)DirectLogicAddress.SystemVMemoryBasePdu);
        DirectLogicAddress.VMemoryToPdu("V2000").ShouldBe((ushort)1024);
        DirectLogicAddress.UserVMemoryToPdu("V40400").ShouldBe((ushort)16640); // plain octal decode — user-bank only
    }

    /// <summary>Verifies DL205 Y output addresses map to coils bank.</summary>
    [Fact]
    public void DL205_Y_Output_Maps_To_Coils_Bank()
    {
        var p = ModbusAddressParser.Parse("Y0", ModbusFamily.DL205);
        p.Region.ShouldBe(ModbusRegion.Coils);
        p.Offset.ShouldBe((ushort)2048);   // YOutputBaseCoil
        p.DataType.ShouldBe(ModbusDataType.Bool);
    }

    /// <summary>Verifies DL205 C control relay addresses map to coils bank, not Modicon coil.</summary>
    [Fact]
    public void DL205_C_Relay_Maps_To_Coils_Bank_NotModiconCoil()
    {
        // Cross-family ambiguity check: under Generic, "C100" is mnemonic Coils[99].
        // Under DL205 family, "C100" is a control relay → CRelayBaseCoil + octal(100) = 3072 + 64.
        var p = ModbusAddressParser.Parse("C100", ModbusFamily.DL205);
        p.Region.ShouldBe(ModbusRegion.Coils);
        p.Offset.ShouldBe((ushort)(3072 + 64));
    }

    /// <summary>Verifies DL205 X input addresses map to discrete inputs.</summary>
    [Fact]
    public void DL205_X_Input_Maps_To_DiscreteInputs()
    {
        var p = ModbusAddressParser.Parse("X17", ModbusFamily.DL205);
        p.Region.ShouldBe(ModbusRegion.DiscreteInputs);
        p.Offset.ShouldBe((ushort)15);  // octal 17 = decimal 15
    }

    /// <summary>Verifies DL205 SP special relay addresses map to discrete inputs.</summary>
    [Fact]
    public void DL205_SP_Special_Relay_Maps_To_DiscreteInputs()
    {
        var p = ModbusAddressParser.Parse("SP10", ModbusFamily.DL205);
        p.Region.ShouldBe(ModbusRegion.DiscreteInputs);
        p.Offset.ShouldBe((ushort)(1024 + 8));  // SpecialBaseDiscrete + octal(10)
    }

    /// <summary>Verifies DL205 parser falls back to Modicon when native parsing fails.</summary>
    [Fact]
    public void DL205_Falls_Back_To_Modicon_When_Native_Misses()
    {
        // 40001 isn't a DL205 native form — falls through to the Modicon parser, returns
        // HoldingRegisters[0]. Important for users mixing legacy Modicon entries with native.
        var p = ModbusAddressParser.Parse("40001", ModbusFamily.DL205);
        p.Region.ShouldBe(ModbusRegion.HoldingRegisters);
        p.Offset.ShouldBe((ushort)0);
    }

    // ----- MELSEC native: D / X / Y / M with sub-family-aware X/Y parsing -----

    /// <summary>Verifies MELSEC D register addresses map to holding registers.</summary>
    [Fact]
    public void MELSEC_D_Register_Maps_To_HoldingRegisters()
    {
        var p = ModbusAddressParser.Parse("D100", ModbusFamily.MELSEC);
        p.Region.ShouldBe(ModbusRegion.HoldingRegisters);
        p.Offset.ShouldBe((ushort)100);   // base 0 + decimal 100
    }

    /// <summary>Verifies MELSEC M relay addresses map to coils with decimal format.</summary>
    [Fact]
    public void MELSEC_M_Relay_Maps_To_Coils_Decimal()
    {
        var p = ModbusAddressParser.Parse("M50", ModbusFamily.MELSEC);
        p.Region.ShouldBe(ModbusRegion.Coils);
        p.Offset.ShouldBe((ushort)50);
    }

    /// <summary>Verifies MELSEC Q family treats X addresses as hexadecimal.</summary>
    [Fact]
    public void MELSEC_Q_Family_Treats_X20_As_Hex()
    {
        var p = ModbusAddressParser.Parse("X20", ModbusFamily.MELSEC, MelsecFamily.Q_L_iQR);
        p.Region.ShouldBe(ModbusRegion.DiscreteInputs);
        p.Offset.ShouldBe((ushort)0x20);  // hex 20 = decimal 32
    }

    /// <summary>Verifies MELSEC F family treats X addresses as octal.</summary>
    [Fact]
    public void MELSEC_F_Family_Treats_X20_As_Octal()
    {
        var p = ModbusAddressParser.Parse("X20", ModbusFamily.MELSEC, MelsecFamily.F_iQF);
        p.Region.ShouldBe(ModbusRegion.DiscreteInputs);
        p.Offset.ShouldBe((ushort)16);    // octal 20 = decimal 16
    }

    // ----- Cross-family safety / Generic regression -----

    /// <summary>Verifies Generic family does not try DL205 native parsing.</summary>
    [Fact]
    public void Generic_Family_Does_Not_Try_DL205_Branch()
    {
        // "V2000" under Generic isn't a known mnemonic OR a Modicon address → parse fails.
        // (Only DL205 / MELSEC families know V-memory.)
        ModbusAddressParser.TryParse("V2000", ModbusFamily.Generic, MelsecFamily.Q_L_iQR, out _, out var error)
            .ShouldBeFalse();
        error.ShouldNotBeNull();
    }

    /// <summary>Verifies C100 under Generic family means Modicon coil 99.</summary>
    [Fact]
    public void C100_Under_Generic_Means_Modicon_Coil_99()
    {
        // Regression guard against the cross-family ambiguity: Generic must keep mnemonic "C"
        // mapping (Coil at offset = decimal-100 - 1).
        var p = ModbusAddressParser.Parse("C100", ModbusFamily.Generic);
        p.Region.ShouldBe(ModbusRegion.Coils);
        p.Offset.ShouldBe((ushort)99);
    }

    /// <summary>Verifies suffix grammar composes correctly with native addresses.</summary>
    [Fact]
    public void Suffix_Grammar_Composes_With_Native_Address()
    {
        // V2000:F:CDAB:5 should parse end-to-end: DL205 V2000 → HR[1024], Float32, word-swap, array of 5.
        var p = ModbusAddressParser.Parse("V2000:F:CDAB:5", ModbusFamily.DL205);
        p.Region.ShouldBe(ModbusRegion.HoldingRegisters);
        p.Offset.ShouldBe((ushort)1024);
        p.DataType.ShouldBe(ModbusDataType.Float32);
        p.ByteOrder.ShouldBe(ModbusByteOrder.WordSwap);
        p.ArrayCount.ShouldBe(5);
    }

    /// <summary>Verifies DL205 bit suffix works on V-memory addresses.</summary>
    [Fact]
    public void DL205_Bit_Suffix_On_VMemory()
    {
        var p = ModbusAddressParser.Parse("V2000.7", ModbusFamily.DL205);
        p.Region.ShouldBe(ModbusRegion.HoldingRegisters);
        p.Offset.ShouldBe((ushort)1024);
        p.Bit.ShouldBe((byte)7);
        p.DataType.ShouldBe(ModbusDataType.BitInRegister);
    }
}
