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

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

[Trait("Category", "Unit")]
public sealed class ModbusAddressParserTests
{
    // ----- Bare Modicon-only forms inherit #136 behaviour; one sanity row per region. -----

    /// <summary>Verifies that bare Modicon address forms inherit their data type from region defaults.</summary>
    /// <param name="addr">The Modicon address string to parse.</param>
    /// <param name="region">The expected Modbus region.</param>
    /// <param name="offset">The expected zero-based register offset.</param>
    /// <param name="type">The expected data type inferred from the region.</param>
    [Theory]
    [InlineData("40001", ModbusRegion.HoldingRegisters, 0,  ModbusDataType.Int16)]
    [InlineData("400001", ModbusRegion.HoldingRegisters, 0, ModbusDataType.Int16)]
    [InlineData("30001", ModbusRegion.InputRegisters,   0,  ModbusDataType.Int16)]
    [InlineData("00001", ModbusRegion.Coils,            0,  ModbusDataType.Bool)]
    [InlineData("10001", ModbusRegion.DiscreteInputs,   0,  ModbusDataType.Bool)]
    [InlineData("465536", ModbusRegion.HoldingRegisters, 65535, ModbusDataType.Int16)]
    public void Bare_Modicon_Defaults_DataType_From_Region(string addr, ModbusRegion region, int offset, ModbusDataType type)
    {
        var p = ModbusAddressParser.Parse(addr);
        p.Region.ShouldBe(region);
        p.Offset.ShouldBe((ushort)offset);
        p.DataType.ShouldBe(type);
        p.Bit.ShouldBeNull();
        p.ArrayCount.ShouldBeNull();
        p.ByteOrder.ShouldBe(ModbusByteOrder.BigEndian);
    }

    // ----- Mnemonic forms — HR / IR / C / DI -----

    /// <summary>Verifies that mnemonic region forms (HR, IR, C, DI) parse correctly.</summary>
    /// <param name="addr">The mnemonic address string to parse.</param>
    /// <param name="region">The expected Modbus region.</param>
    /// <param name="offset">The expected zero-based register offset.</param>
    [Theory]
    [InlineData("HR1",  ModbusRegion.HoldingRegisters, 0)]
    [InlineData("HR65536", ModbusRegion.HoldingRegisters, 65535)]
    [InlineData("IR1",  ModbusRegion.InputRegisters, 0)]
    [InlineData("C100", ModbusRegion.Coils, 99)]
    [InlineData("DI1",  ModbusRegion.DiscreteInputs, 0)]
    [InlineData("hr1",  ModbusRegion.HoldingRegisters, 0)]    // lowercase
    [InlineData("Ir50", ModbusRegion.InputRegisters, 49)]     // mixed case
    public void Mnemonic_Region_Forms_Parse(string addr, ModbusRegion region, int offset)
    {
        var p = ModbusAddressParser.Parse(addr);
        p.Region.ShouldBe(region);
        p.Offset.ShouldBe((ushort)offset);
    }

    // ----- Bit suffix .N -----

    /// <summary>Verifies that bit suffix notation sets the expected bit position and data type.</summary>
    /// <param name="addr">The address string with bit suffix to parse.</param>
    /// <param name="expectedBit">The expected bit index within the register.</param>
    [Theory]
    [InlineData("40001.0",  0)]
    [InlineData("40001.5",  5)]
    [InlineData("40001.15", 15)]
    [InlineData("HR1.7",    7)]
    public void Bit_Suffix_Implies_BitInRegister(string addr, int expectedBit)
    {
        var p = ModbusAddressParser.Parse(addr);
        p.Bit.ShouldBe((byte)expectedBit);
        p.DataType.ShouldBe(ModbusDataType.BitInRegister);
    }

    /// <summary>Verifies that bit suffix notation combined with explicit type specification is rejected.</summary>
    [Fact]
    public void Bit_Plus_Explicit_Type_Rejected()
    {
        Should.Throw<FormatException>(() => ModbusAddressParser.Parse("40001.5:F"))
            .Message.ShouldContain("Bit suffix");
    }

    /// <summary>Verifies that bit positions outside the valid 0-15 range are rejected.</summary>
    [Fact]
    public void Bit_Above_15_Rejected()
    {
        Should.Throw<FormatException>(() => ModbusAddressParser.Parse("40001.16"))
            .Message.ShouldContain("0..15");
    }

    // ----- Type codes -----

    // #146 — codes verified against current Wonderware DASMBTCP + Ignition Modbus docs
    // (2026-04-25). `:I` = Int32 (was Int16) per Wonderware; `:S` is the explicit Int16 code.
    // `:US` for UInt16 (Ignition HRUS). `:I_64` / `:UI_64` for 64-bit (Ignition HRI_64).
    // `:BCD_32` for 32-bit BCD (Ignition HRBCD_32). The pre-#146 `:DI`/`:L`/`:UDI`/`:UL`/
    // `:LI`/`:ULI`/`:LBCD` aliases are removed — they conflict with the Wonderware mapping
    // and have no clear vendor precedent.
    /// <summary>Verifies that type code suffixes parse to the expected data type.</summary>
    /// <param name="addr">The address string with type code suffix to parse.</param>
    /// <param name="expected">The expected data type.</param>
    [Theory]
    [InlineData("40001:BOOL",   ModbusDataType.Bool)]
    [InlineData("40001:S",      ModbusDataType.Int16)]
    [InlineData("40001:US",     ModbusDataType.UInt16)]
    [InlineData("40001:I",      ModbusDataType.Int32)]
    [InlineData("40001:UI",     ModbusDataType.UInt32)]
    [InlineData("40001:I_64",   ModbusDataType.Int64)]
    [InlineData("40001:UI_64",  ModbusDataType.UInt64)]
    [InlineData("40001:F",      ModbusDataType.Float32)]
    [InlineData("40001:D",      ModbusDataType.Float64)]
    [InlineData("40001:BCD",    ModbusDataType.Bcd16)]
    [InlineData("40001:BCD_32", ModbusDataType.Bcd32)]
    [InlineData("40001:f",      ModbusDataType.Float32)]    // lowercase
    [InlineData("40001:i_64",   ModbusDataType.Int64)]      // lowercase + underscore form
    public void Type_Codes_Parse(string addr, ModbusDataType expected)
    {
        ModbusAddressParser.Parse(addr).DataType.ShouldBe(expected);
    }

    /// <summary>Verifies that pre-#146 type code aliases are rejected with a clear diagnostic.</summary>
    /// <param name="addr">The address string containing a removed alias type code.</param>
    [Theory]
    [InlineData("40001:DI")]    // pre-#146 alias removed
    [InlineData("40001:L")]
    [InlineData("40001:UDI")]
    [InlineData("40001:UL")]
    [InlineData("40001:LI")]
    [InlineData("40001:ULI")]
    [InlineData("40001:LBCD")]
    public void Removed_Aliases_Are_Rejected(string addr)
    {
        // Defensive — these used to be accepted; now they fail with a clear "Unknown type code"
        // diagnostic so users with legacy spreadsheets get a fast surface-level error rather
        // than silent wrong-typed reads.
        Should.Throw<FormatException>(() => ModbusAddressParser.Parse(addr))
            .Message.ShouldContain("Unknown type code");
    }

    /// <summary>Verifies that string type notation carries the expected string length.</summary>
    /// <param name="addr">The address string with STR type and length to parse.</param>
    /// <param name="expectedLen">The expected string length.</param>
    [Theory]
    [InlineData("40001:STR1",  1)]
    [InlineData("40001:STR20", 20)]
    [InlineData("40001:STR255", 255)]
    public void STR_Type_Carries_Length(string addr, int expectedLen)
    {
        var p = ModbusAddressParser.Parse(addr);
        p.DataType.ShouldBe(ModbusDataType.String);
        p.StringLength.ShouldBe((ushort)expectedLen);
    }

    /// <summary>Verifies that string type notation without length is rejected.</summary>
    [Fact]
    public void STR_Without_Length_Rejected()
    {
        Should.Throw<FormatException>(() => ModbusAddressParser.Parse("40001:STR"))
            .Message.ShouldContain("STR");
    }

    /// <summary>Verifies that string length of zero is rejected.</summary>
    [Fact]
    public void STR_Length_Zero_Rejected()
    {
        Should.Throw<FormatException>(() => ModbusAddressParser.Parse("40001:STR0"))
            .Message.ShouldContain("positive");
    }

    /// <summary>Verifies that unknown type codes are rejected with a catalog of valid codes.</summary>
    [Fact]
    public void Unknown_Type_Code_Rejected_With_Catalog()
    {
        Should.Throw<FormatException>(() => ModbusAddressParser.Parse("40001:WIDGET"))
            .Message.ShouldContain("Valid: BOOL, S, US, I,");
    }

    // ----- Region-type compatibility -----

    /// <summary>Verifies that float types are rejected for coil region.</summary>
    [Fact]
    public void Coils_With_Float_Type_Rejected()
    {
        Should.Throw<FormatException>(() => ModbusAddressParser.Parse("00001:F"))
            .Message.ShouldContain("only supports Bool");
    }

    /// <summary>Verifies that integer types are rejected for discrete input region.</summary>
    [Fact]
    public void DiscreteInputs_With_Int_Type_Rejected()
    {
        Should.Throw<FormatException>(() => ModbusAddressParser.Parse("10001:I"))
            .Message.ShouldContain("only supports Bool");
    }

    // ----- Byte order modifiers — all four -----

    /// <summary>Verifies that byte order modifiers parse to the expected ordering.</summary>
    /// <param name="addr">The address string with byte order modifier to parse.</param>
    /// <param name="expected">The expected byte order.</param>
    [Theory]
    [InlineData("40001:F:ABCD", ModbusByteOrder.BigEndian)]
    [InlineData("40001:F:CDAB", ModbusByteOrder.WordSwap)]
    [InlineData("40001:F:BADC", ModbusByteOrder.ByteSwap)]
    [InlineData("40001:F:DCBA", ModbusByteOrder.FullReverse)]
    [InlineData("40001:F:cdab", ModbusByteOrder.WordSwap)]    // lowercase
    public void Byte_Order_Modifiers_Parse(string addr, ModbusByteOrder expected)
    {
        ModbusAddressParser.Parse(addr).ByteOrder.ShouldBe(expected);
    }

    /// <summary>Verifies that unknown byte order codes are rejected with a catalog of valid codes.</summary>
    [Fact]
    public void Unknown_Byte_Order_Rejected_With_Catalog()
    {
        Should.Throw<FormatException>(() => ModbusAddressParser.Parse("40001:F:WXYZ"))
            .Message.ShouldContain("Valid: ABCD, CDAB, BADC, DCBA");
    }

    /// <summary>Verifies that empty byte order field defaults to big-endian.</summary>
    [Fact]
    public void Empty_Order_Field_Means_Default()
    {
        // 40001:I::5 → Int32 array (5 elements), no order override, default (BigEndian).
        // The empty middle field is the strict-mode marker that "third position is array count,
        // not byte order" — exercises the 4-field positional parse without a byte-order value.
        var p = ModbusAddressParser.Parse("40001:I::5");
        p.ByteOrder.ShouldBe(ModbusByteOrder.BigEndian);
        p.ArrayCount.ShouldBe(5);
        p.DataType.ShouldBe(ModbusDataType.Int32);
    }

    // ----- Array count -----

    /// <summary>Verifies that array count notation parses to the expected count.</summary>
    /// <param name="addr">The address string with array count to parse.</param>
    /// <param name="expectedCount">The expected array element count.</param>
    [Theory]
    [InlineData("40001:I:ABCD:1",  1)]
    [InlineData("40001:F:5",       5)]
    [InlineData("40001:F:CDAB:10", 10)]
    [InlineData("40001:S:100",     100)]    // Int16[100] via the post-#146 :S code
    public void Array_Count_Parses(string addr, int expectedCount)
    {
        ModbusAddressParser.Parse(addr).ArrayCount.ShouldBe(expectedCount);
    }

    /// <summary>Verifies that array count of zero is rejected.</summary>
    [Fact]
    public void Array_Count_Zero_Rejected()
    {
        Should.Throw<FormatException>(() => ModbusAddressParser.Parse("40001:F:ABCD:0"))
            .Message.ShouldContain("positive");
    }

    /// <summary>Verifies that non-numeric array count is rejected.</summary>
    [Fact]
    public void Array_Count_NonNumeric_Rejected()
    {
        Should.Throw<FormatException>(() => ModbusAddressParser.Parse("40001:F:ABCD:five"))
            .Message.ShouldContain("positive");
    }

    /// <summary>Verifies that bit suffix combined with array count is rejected.</summary>
    [Fact]
    public void Bit_Plus_Array_Rejected()
    {
        Should.Throw<FormatException>(() => ModbusAddressParser.Parse("40001.5:::5"))
            .Message.ShouldContain("Bit suffix and array count");
    }

    // ----- Composition / examples -----

    /// <summary>Verifies parsing of float with word-swap byte ordering.</summary>
    [Fact]
    public void Worked_Example_Float_With_Word_Swap()
    {
        var p = ModbusAddressParser.Parse("40001:F:CDAB");
        p.Region.ShouldBe(ModbusRegion.HoldingRegisters);
        p.Offset.ShouldBe((ushort)0);
        p.DataType.ShouldBe(ModbusDataType.Float32);
        p.ByteOrder.ShouldBe(ModbusByteOrder.WordSwap);
        p.ArrayCount.ShouldBeNull();
    }

    /// <summary>Verifies parsing of Int16 array with explicit type code.</summary>
    [Fact]
    public void Worked_Example_Int16_Array()
    {
        // Post-#146: :S is the explicit Int16 code. (`:I` is now Int32 per Wonderware.)
        var p = ModbusAddressParser.Parse("40001:S::10");
        p.Region.ShouldBe(ModbusRegion.HoldingRegisters);
        p.DataType.ShouldBe(ModbusDataType.Int16);
        p.ArrayCount.ShouldBe(10);
        p.ByteOrder.ShouldBe(ModbusByteOrder.BigEndian);
    }

    /// <summary>Verifies parsing of Int32 array using the :I type code.</summary>
    [Fact]
    public void Worked_Example_Int32_Array_Via_I_Code()
    {
        // Companion to the above — `:I` now means Int32 (matches Wonderware DASMBTCP and
        // Ignition HRI). Anyone with `:I` in legacy spreadsheets gets a different type than
        // pre-#146; that's the intended semantic alignment.
        var p = ModbusAddressParser.Parse("40001:I::10");
        p.DataType.ShouldBe(ModbusDataType.Int32);
        p.ArrayCount.ShouldBe(10);
    }

    /// <summary>Verifies parsing of float array with word-swap and 6-digit address.</summary>
    [Fact]
    public void Worked_Example_Float_Array_Word_Swap_6_Digit()
    {
        var p = ModbusAddressParser.Parse("465500:F:CDAB:5");
        p.Region.ShouldBe(ModbusRegion.HoldingRegisters);
        p.Offset.ShouldBe((ushort)65499);
        p.DataType.ShouldBe(ModbusDataType.Float32);
        p.ByteOrder.ShouldBe(ModbusByteOrder.WordSwap);
        p.ArrayCount.ShouldBe(5);
    }

    /// <summary>Verifies parsing of string type with specified length.</summary>
    [Fact]
    public void Worked_Example_String_With_Length()
    {
        var p = ModbusAddressParser.Parse("40001:STR20");
        p.DataType.ShouldBe(ModbusDataType.String);
        p.StringLength.ShouldBe((ushort)20);
        p.ArrayCount.ShouldBeNull();   // strings ARE multi-register but they are not "array of string"
    }

    /// <summary>Verifies that TryParse returns a diagnostic on parse failure.</summary>
    [Fact]
    public void TryParse_Returns_Diagnostic_On_Failure()
    {
        ModbusAddressParser.TryParse("garbage", out var p, out var err).ShouldBeFalse();
        p.ShouldBeNull();
        err.ShouldNotBeNull();
    }

    /// <summary>Verifies that TryParse returns a result on successful parse.</summary>
    [Fact]
    public void TryParse_Returns_Result_On_Success()
    {
        ModbusAddressParser.TryParse("HR1:F:CDAB:3", out var p, out var err).ShouldBeTrue();
        p.ShouldNotBeNull();
        err.ShouldBeNull();
        p!.Region.ShouldBe(ModbusRegion.HoldingRegisters);
        p.DataType.ShouldBe(ModbusDataType.Float32);
        p.ByteOrder.ShouldBe(ModbusByteOrder.WordSwap);
        p.ArrayCount.ShouldBe(3);
    }

    /// <summary>Verifies that addresses with too many colon-separated fields are rejected.</summary>
    [Fact]
    public void Too_Many_Colons_Rejected()
    {
        Should.Throw<FormatException>(() => ModbusAddressParser.Parse("40001:F:CDAB:5:extra"))
            .Message.ShouldContain("too many");
    }
}
