using Shouldly;
using Xunit;

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

[Trait("Category", "Unit")]
public sealed class MelsecAddressTests
{
    // --- X / Y hex vs octal family trap ---

    /// <summary>Verifies that Q-series and iQR family X inputs parse as hexadecimal.</summary>
    [Theory]
    [InlineData("X0", (ushort)0)]
    [InlineData("X9", (ushort)9)]
    [InlineData("XA", (ushort)10)]      // hex
    [InlineData("XF", (ushort)15)]
    [InlineData("X10", (ushort)16)]     // hex 0x10 = decimal 16
    [InlineData("X20", (ushort)32)]     // hex 0x20 = decimal 32 — the classic MELSEC-Q trap
    [InlineData("X1FF", (ushort)511)]
    [InlineData("x10", (ushort)16)]     // lowercase prefix
    public void XInputToDiscrete_QLiQR_parses_hex(string x, ushort expected)
        => MelsecAddress.XInputToDiscrete(x, MelsecFamily.Q_L_iQR).ShouldBe(expected);

    /// <summary>Verifies that F-series and iQF family X inputs parse as octal.</summary>
    [Theory]
    [InlineData("X0", (ushort)0)]
    [InlineData("X7", (ushort)7)]
    [InlineData("X10", (ushort)8)]      // octal 10 = decimal 8
    [InlineData("X20", (ushort)16)]     // octal 20 = decimal 16 — SAME string, DIFFERENT value on FX
    [InlineData("X777", (ushort)511)]
    public void XInputToDiscrete_FiQF_parses_octal(string x, ushort expected)
        => MelsecAddress.XInputToDiscrete(x, MelsecFamily.F_iQF).ShouldBe(expected);

    /// <summary>Verifies that Q-series and iQR family Y outputs parse as hexadecimal.</summary>
    [Theory]
    [InlineData("Y0", (ushort)0)]
    [InlineData("Y1F", (ushort)31)]
    public void YOutputToCoil_QLiQR_parses_hex(string y, ushort expected)
        => MelsecAddress.YOutputToCoil(y, MelsecFamily.Q_L_iQR).ShouldBe(expected);

    /// <summary>Verifies that F-series and iQF family Y outputs parse as octal.</summary>
    [Theory]
    [InlineData("Y0", (ushort)0)]
    [InlineData("Y17", (ushort)15)]
    public void YOutputToCoil_FiQF_parses_octal(string y, ushort expected)
        => MelsecAddress.YOutputToCoil(y, MelsecFamily.F_iQF).ShouldBe(expected);

    /// <summary>
    ///     Verifies that the same address string decodes to different values depending on the family.
    /// </summary>
    [Fact]
    public void Same_address_string_decodes_differently_between_families()
    {
        // This is the headline quirk: "X20" in GX Works means one thing on Q-series and
        // another on FX-series. The driver's family selector is the only defence.
        MelsecAddress.XInputToDiscrete("X20", MelsecFamily.Q_L_iQR).ShouldBe((ushort)32);
        MelsecAddress.XInputToDiscrete("X20", MelsecFamily.F_iQF).ShouldBe((ushort)16);
    }

    /// <summary>Verifies that non-octal X input addresses are rejected for F-series and iQF families.</summary>
    [Theory]
    [InlineData("X8")]     // 8 is non-octal
    [InlineData("X12G")]   // G is non-hex
    public void XInputToDiscrete_FiQF_rejects_non_octal(string bad)
        => Should.Throw<ArgumentException>(() => MelsecAddress.XInputToDiscrete(bad, MelsecFamily.F_iQF));

    /// <summary>Verifies that non-hexadecimal X input addresses are rejected for Q-series and iQR families.</summary>
    [Theory]
    [InlineData("X12G")]
    public void XInputToDiscrete_QLiQR_rejects_non_hex(string bad)
        => Should.Throw<ArgumentException>(() => MelsecAddress.XInputToDiscrete(bad, MelsecFamily.Q_L_iQR));

    /// <summary>
    ///     Verifies that the bank base from assignment blocks is honored for X input addresses.
    /// </summary>
    [Fact]
    public void XInputToDiscrete_honors_bank_base_from_assignment_block()
    {
        // Real-world QJ71MT91 assignment blocks commonly place X at DI 8192+ when other
        // ranges take the low Modbus addresses. Helper must add the base cleanly.
        MelsecAddress.XInputToDiscrete("X10", MelsecFamily.Q_L_iQR, xBankBase: 8192).ShouldBe((ushort)(8192 + 16));
    }

    // --- M-relay (decimal, both families) ---

    /// <summary>Verifies that M relay addresses parse as decimal.</summary>
    [Theory]
    [InlineData("M0", (ushort)0)]
    [InlineData("M10", (ushort)10)]     // M addresses are DECIMAL, not hex or octal
    [InlineData("M511", (ushort)511)]
    [InlineData("m99", (ushort)99)]     // lowercase
    public void MRelayToCoil_parses_decimal(string m, ushort expected)
        => MelsecAddress.MRelayToCoil(m).ShouldBe(expected);

    /// <summary>
    ///     Verifies that the bank base is honored for M relay addresses.
    /// </summary>
    [Fact]
    public void MRelayToCoil_honors_bank_base()
        => MelsecAddress.MRelayToCoil("M0", mBankBase: 512).ShouldBe((ushort)512);

    /// <summary>
    ///     Verifies that non-numeric M relay addresses are rejected.
    /// </summary>
    [Fact]
    public void MRelayToCoil_rejects_non_numeric()
        => Should.Throw<ArgumentException>(() => MelsecAddress.MRelayToCoil("M1F"));

    // --- D-register (decimal, both families) ---

    /// <summary>Verifies that D register addresses parse as decimal.</summary>
    [Theory]
    [InlineData("D0", (ushort)0)]
    [InlineData("D100", (ushort)100)]
    [InlineData("d1023", (ushort)1023)]
    public void DRegisterToHolding_parses_decimal(string d, ushort expected)
        => MelsecAddress.DRegisterToHolding(d).ShouldBe(expected);

    /// <summary>
    ///     Verifies that the bank base is honored for D register addresses.
    /// </summary>
    [Fact]
    public void DRegisterToHolding_honors_bank_base()
        => MelsecAddress.DRegisterToHolding("D10", dBankBase: 4096).ShouldBe((ushort)4106);

    /// <summary>
    ///     Verifies that empty D register addresses are rejected.
    /// </summary>
    [Fact]
    public void DRegisterToHolding_rejects_empty()
        => Should.Throw<ArgumentException>(() => MelsecAddress.DRegisterToHolding("D"));

    // --- overflow ---

    /// <summary>
    ///     Verifies that X input addresses with overflow are rejected.
    /// </summary>
    [Fact]
    public void XInputToDiscrete_overflow_throws()
    {
        // 0xFFFF + base 1 = 0x10000 — past ushort.
        Should.Throw<OverflowException>(() =>
            MelsecAddress.XInputToDiscrete("XFFFF", MelsecFamily.Q_L_iQR, xBankBase: 1));
    }
}
