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

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

/// <summary>
///     Driver.Modbus.Addressing-008: boundary and overflow edge cases for the address-arithmetic
///     helpers and the parser input-validation paths. These cover the risk surface cited in the
///     code review: overflow in DL205 / MELSEC helpers, empty trailing parser fields (finding
///     -002), and coverage of <see cref="DirectLogicAddress.SystemVMemoryToPdu"/> (finding -001
///     regression guard), which was previously unreachable from the parser.
/// </summary>
[Trait("Category", "Unit")]
public sealed class ModbusAddressEdgeCaseTests
{
    // ── Parser: empty trailing-field rejection (Driver.Modbus.Addressing-002) ──────────────

    /// <summary>Verifies that a 3-field address with an empty third field is rejected.</summary>
    [Fact]
    public void Parser_3field_empty_third_field_rejected()
    {
        // "40001:F:" — trailing colon with nothing after it must produce a diagnostic, not
        // silently parse as a scalar (Enumerable.All returns true for an empty sequence).
        var ok = ModbusAddressParser.TryParse("40001:F:", out _, out var error);
        ok.ShouldBeFalse();
        error.ShouldNotBeNullOrEmpty();
        error!.ShouldContain("empty");
    }

    /// <summary>Verifies that a 4-field address with an empty third field (byte order) accepts the default.</summary>
    [Fact]
    public void Parser_4field_empty_third_field_accepted_as_default_order()
    {
        // "40001:F::5" — empty order field in 4-field form IS valid (means default byte order).
        // This is a different case from the 3-field "40001:F:" empty trailing colon.
        var ok = ModbusAddressParser.TryParse("40001:F::5", out var result, out _);
        ok.ShouldBeTrue();
        result!.ByteOrder.ShouldBe(ModbusByteOrder.BigEndian);
        result.ArrayCount.ShouldBe(5);
    }

    // ── Parser: misplaced type code gives better diagnostic (Driver.Modbus.Addressing-003) ─

    /// <summary>Verifies that a misplaced type code in the third field produces a helpful error.</summary>
    [Fact]
    public void Parser_3field_misplaced_type_in_third_field_gives_helpful_error()
    {
        // "40001:S:BOOL" — BOOL is a 4-letter type code typed in the byte-order field.
        // The parser should mention that field 3 is a byte order, not a type.
        var ok = ModbusAddressParser.TryParse("40001:S:BOOL", out _, out var error);
        ok.ShouldBeFalse();
        error.ShouldNotBeNullOrEmpty();
        // The error should guide the user toward the correct field (field 2 for type).
        error!.ShouldContain("field 2", Case.Insensitive);
    }

    // ── Parser: multi-dot input (Driver.Modbus.Addressing-004) ──────────────────────────────

    /// <summary>Verifies that addresses with multiple dots are rejected with a clear error.</summary>
    [Fact]
    public void Parser_multi_dot_input_rejected_with_clear_error()
    {
        // "40001.5.3" — multiple dots should not silently parse bit as "5.3".
        var ok = ModbusAddressParser.TryParse("40001.5.3", out _, out var error);
        ok.ShouldBeFalse();
        error.ShouldNotBeNullOrEmpty();
    }

    /// <summary>Verifies that a decimal point typo produces a precise error.</summary>
    [Fact]
    public void Parser_decimal_point_typo_400_01_gives_precise_error()
    {
        // "400.01" — looks like a Modicon decimal typo. The bit suffix "01" is valid (bit index
        // 1), but "400" fails Modicon validation with a length error — NOT a bit-index error.
        // Verify the parser fails (any diagnostic is acceptable; we just check it fails).
        var ok = ModbusAddressParser.TryParse("400.01", out _, out var error);
        ok.ShouldBeFalse();
        error.ShouldNotBeNullOrEmpty();
    }

    // ── DirectLogicAddress overflow and boundary (Driver.Modbus.Addressing-008) ────────────

    /// <summary>Verifies that user V-memory overflow throws an OverflowException.</summary>
    [Fact]
    public void UserVMemoryToPdu_overflow_throws_OverflowException()
    {
        // V200000 in octal = 65536 decimal — just beyond ushort.MaxValue.
        Should.Throw<OverflowException>(() => DirectLogicAddress.UserVMemoryToPdu("V200000"));
    }

    /// <summary>Verifies that octal offset overflow via Y throws an OverflowException.</summary>
    [Fact]
    public void AddOctalOffset_overflow_via_Y_throws_OverflowException()
    {
        // Y prefix with an octal value that pushes YOutputBaseCoil (2048) past 0xFFFF.
        // YOutputBaseCoil = 2048; we need octal digits that decode to > 65535 - 2048 = 63487.
        // 63488 in octal = 174000; "Y174000" should overflow.
        Should.Throw<OverflowException>(() => DirectLogicAddress.YOutputToCoil("Y174000"));
    }

    /// <summary>Verifies that octal offset overflow via C throws an OverflowException.</summary>
    [Fact]
    public void AddOctalOffset_overflow_via_C_throws_OverflowException()
    {
        // CRelayBaseCoil = 3072; we need offset > 65535 - 3072 = 62463.
        // 62464 in octal = 172000; "C172000" should overflow.
        Should.Throw<OverflowException>(() => DirectLogicAddress.CRelayToCoil("C172000"));
    }

    /// <summary>Verifies that SystemVMemoryToPdu is exercised and returns the correct base.</summary>
    [Fact]
    public void SystemVMemoryToPdu_is_exercised_and_returns_correct_base()
    {
        // Direct coverage of SystemVMemoryToPdu — previously unreachable from the parser
        // before Driver.Modbus.Addressing-001 was fixed (VMemoryToPdu now calls it).
        DirectLogicAddress.SystemVMemoryToPdu(0).ShouldBe(DirectLogicAddress.SystemVMemoryBasePdu);
        DirectLogicAddress.SystemVMemoryToPdu(1).ShouldBe((ushort)(DirectLogicAddress.SystemVMemoryBasePdu + 1));
    }

    /// <summary>Verifies that system V-memory overflow throws an OverflowException.</summary>
    [Fact]
    public void SystemVMemoryToPdu_overflow_throws_OverflowException()
    {
        // An offset that pushes SystemVMemoryBasePdu (0x2100 = 8448) past 0xFFFF.
        // 0xFFFF - 0x2100 + 1 = 57088 (0xDF00) should overflow.
        Should.Throw<OverflowException>(() => DirectLogicAddress.SystemVMemoryToPdu(0xDF00));
    }

    // ── MelsecAddress overflow / boundary (Driver.Modbus.Addressing-008) ─────────────────

    /// <summary>Verifies that MELSEC hex address parsing overflow throws an OverflowException.</summary>
    [Fact]
    public void MelsecAddress_ParseHex_overflow_throws_OverflowException()
    {
        // X address in Q-family (hex): "X10000" = 0x10000 = 65536, overflows ushort.
        Should.Throw<OverflowException>(() => MelsecAddress.XInputToDiscrete("X10000", MelsecFamily.Q_L_iQR));
    }

    /// <summary>Verifies that MELSEC D-register to holding overflow throws an exception.</summary>
    [Fact]
    public void MelsecAddress_DRegisterToHolding_overflow_throws_OverflowException()
    {
        // D65536 + base 0 = 65536, overflows ushort.MaxValue.
        Should.Throw<ArgumentException>(() => MelsecAddress.DRegisterToHolding("D65536"));
    }

    /// <summary>Verifies that MELSEC M-relay to coil overflow throws an OverflowException.</summary>
    [Fact]
    public void MelsecAddress_MRelayToCoil_overflow_throws_OverflowException()
    {
        // M65535 with base 1 = 65536, overflows.
        Should.Throw<OverflowException>(() => MelsecAddress.MRelayToCoil("M65535", mBankBase: 1));
    }

    /// <summary>Verifies that MELSEC D-register bank base overflow throws an OverflowException.</summary>
    [Fact]
    public void MelsecAddress_DRegisterToHolding_bank_base_overflow_throws_OverflowException()
    {
        // D0 with a bank base that itself overflows: base 65535 + D1 = 65536.
        Should.Throw<OverflowException>(() => MelsecAddress.DRegisterToHolding("D1", dBankBase: 65535));
    }

    // ── TryParse never throws (Driver.Modbus.Addressing-006) ─────────────────────────────────
    //
    // The TryParse contract is that it converts every parse failure into a structured (false,
    // error) return — config-bind hot paths depend on this. The family-native catch was previously
    // narrow (ArgumentException / OverflowException only); any future helper change that threw a
    // different exception type (e.g. FormatException from a ushort.Parse swap) would escape as an
    // unhandled exception out of a TryParse method. These tests assert the defensive contract
    // across a broad set of pathological inputs.

    /// <summary>Verifies that DL205 address parsing never throws and returns a structured error.</summary>
    /// <param name="addr">The address string to parse.</param>
    [Theory]
    [InlineData("V")]                  // V prefix with no digits
    [InlineData("V99999999999999")]    // overflow in user V-memory octal decode
    [InlineData("V200000")]            // overflow in user V-memory octal decode
    [InlineData("V77777777")]          // octal way past 0xFFFF in system bank
    [InlineData("Y")]                  // Y prefix with no digits
    [InlineData("Y8888")]              // non-octal digit
    [InlineData("Y174000")]            // octal offset overflows YOutputBaseCoil + value
    [InlineData("C")]                  // C prefix alone
    [InlineData("C99999999")]          // overflow in C-relay
    [InlineData("X")]                  // X prefix alone
    [InlineData("X8")]                 // non-octal digit
    [InlineData("SP")]                 // SP prefix alone
    [InlineData("SP9")]                // non-octal digit
    [InlineData("Z123")]               // unknown DL205 prefix
    public void DL205_TryParse_NeverThrows_ReturnsStructuredError(string addr)
    {
        // Defensive contract: any helper failure must surface as (false, non-null error), never
        // as an unhandled exception out of TryParse.
        var ok = ModbusAddressParser.TryParse(addr, ModbusFamily.DL205, MelsecFamily.Q_L_iQR, out var result, out var error);
        ok.ShouldBeFalse();
        result.ShouldBeNull();
        error.ShouldNotBeNullOrEmpty();
    }

    /// <summary>Verifies that MELSEC address parsing never throws and returns a structured error.</summary>
    /// <param name="addr">The address string to parse.</param>
    [Theory]
    [InlineData("D")]                          // D prefix alone — no digits
    [InlineData("D-1")]                        // negative — would fail ushort.TryParse, must not throw
    [InlineData("D65536")]                     // overflow
    [InlineData("DABC")]                       // non-decimal digits in D
    [InlineData("MABC")]                       // non-decimal digits in M
    [InlineData("X10000")]                     // hex overflow (Q-family)
    [InlineData("XZZZZ")]                      // non-hex digit (Q-family)
    [InlineData("Y10000")]                     // hex overflow (Q-family)
    public void MELSEC_TryParse_NeverThrows_ReturnsStructuredError(string addr)
    {
        var ok = ModbusAddressParser.TryParse(addr, ModbusFamily.MELSEC, MelsecFamily.Q_L_iQR, out var result, out var error);
        ok.ShouldBeFalse();
        result.ShouldBeNull();
        error.ShouldNotBeNullOrEmpty();
    }

    // ── ModbusStringByteOrder is grammar-out-of-scope (Driver.Modbus.Addressing-007) ────────
    //
    // ModbusStringByteOrder (HighByteFirst / LowByteFirst) is the DL205 low-byte-first packing
    // knob. It is intentionally NOT expressible through the address grammar — there is no token
    // form to set it and ParsedModbusAddress has no field for it. The string byte order is
    // configurable only via the structured tag form (ModbusTagDefinition.StringByteOrder), which
    // is the canonical config path. These tests pin that contract so a future grammar change
    // can't quietly add a token that conflicts with the array-count slot.

    /// <summary>Verifies that the parser's STR grammar does not carry StringByteOrder.</summary>
    [Fact]
    public void Parser_STR_grammar_does_not_carry_StringByteOrder()
    {
        // STR20 parses fine — but the result has no StringByteOrder field (the property does
        // not exist on ParsedModbusAddress). The string byte order must be set on the structured
        // tag definition, not the grammar string.
        var ok = ModbusAddressParser.TryParse("40001:STR20", out var result, out _);
        ok.ShouldBeTrue();
        result!.DataType.ShouldBe(ModbusDataType.String);
        result.StringLength.ShouldBe((ushort)20);
        // Compile-time assertion: ParsedModbusAddress does not expose StringByteOrder.
        // Searching for a property by reflection would let us assert "no such field":
        typeof(ParsedModbusAddress)
            .GetProperty("StringByteOrder")
            .ShouldBeNull();
    }

    /// <summary>Verifies that the parser rejects unknown string byte order tokens in the grammar.</summary>
    [Fact]
    public void Parser_rejects_unknown_string_byte_order_token_in_grammar()
    {
        // A user trying to express low-byte-first via a grammar suffix like "LOWB" or "HIGH" in
        // the byte-order slot gets the standard "Unknown byte order" diagnostic — the parser is
        // explicit that field 3 is the multi-register word/byte order, not the per-string byte
        // order. The structured tag form is the only configuration path for ModbusStringByteOrder.
        var ok = ModbusAddressParser.TryParse("40001:STR20:LOWB", out _, out var error);
        ok.ShouldBeFalse();
        error.ShouldNotBeNullOrEmpty();
        error!.ShouldContain("byte order", Case.Insensitive);
    }

    // ── Driver.Modbus.Addressing-010: isKnownTypeCode lists non-type-codes (REAL/DINT/UINT) ─
    //
    // TryParseByteOrder's isKnownTypeCode heuristic listed "REAL", "DINT", and "UINT" as
    // "known type codes" but those strings are NOT valid type codes in TryParseType. Following
    // the advice "type belongs in field 2 (e.g. '40001:REAL')" would lead to a second error
    // "Unknown type code 'REAL'", misdirecting the user. The list must be restricted to the
    // actual 4-letter valid type code: BOOL.

    /// <summary>Verifies that REAL in the byte-order slot gives a generic byte-order error, not false type-code advice.</summary>
    [Theory]
    [InlineData("40001:F:REAL")]
    [InlineData("40001:F:DINT")]
    [InlineData("40001:F:UINT")]
    public void ByteOrderSlot_NonTypeCode_Strings_Give_Generic_ByteOrder_Error(string addr)
    {
        // "REAL", "DINT", "UINT" look like PLC type names but are NOT valid type codes in this
        // parser. They should produce the generic "Unknown byte order" message, not a misleading
        // "type belongs in field 2" hint that would lead the user to another failure.
        var ok = ModbusAddressParser.TryParse(addr, out _, out var error);
        ok.ShouldBeFalse();
        error.ShouldNotBeNullOrEmpty();
        // Must NOT claim these are type codes — following that advice produces another error.
        error!.ShouldNotContain("type code", Case.Insensitive);
        // Must mention the valid byte orders so the user knows what field 3 accepts.
        error.ShouldContain("ABCD", Case.Insensitive);
    }

    /// <summary>Verifies that BOOL in the byte-order slot still gives the helpful type-code hint.</summary>
    [Fact]
    public void ByteOrderSlot_BOOL_Gives_TypeCode_Hint()
    {
        // BOOL IS a valid type code — the hint "type belongs in field 2 (e.g. '40001:BOOL')"
        // is correct advice since '40001:BOOL' does parse successfully.
        var ok = ModbusAddressParser.TryParse("40001:F:BOOL", out _, out var error);
        ok.ShouldBeFalse();
        error.ShouldNotBeNullOrEmpty();
        error!.ShouldContain("field 2", Case.Insensitive);
    }

    // ── Driver.Modbus.Addressing-011: dead overflow check in VMemoryToPdu ──────────────────
    //
    // VMemoryToPdu's "offsetWithinBank > ushort.MaxValue" guard is unreachable: DecodeOctalVAddress
    // already caps octalValue at ushort.MaxValue (0xFFFF), so offsetWithinBank can never exceed
    // 0xFFFF - SystemVMemoryOctalBase (0x4100) = 0xBEFF = 48895, which is always < ushort.MaxValue.
    // The real overflow guard lives in SystemVMemoryToPdu (pdu > ushort.MaxValue) and is reachable
    // when SystemVMemoryToPdu is called directly with a large offset. These tests pin the boundary
    // and confirm the overflow is caught by SystemVMemoryToPdu, not the outer check.

    /// <summary>Verifies that VMemoryToPdu correctly maps the last valid system-bank V-address.</summary>
    [Fact]
    public void VMemoryToPdu_max_system_bank_address_maps_correctly()
    {
        // The largest V-address whose octal-decoded value fits in ushort (0xFFFF = 65535 octal) is
        // V177777 (octal). octalValue = 0xFFFF. offsetWithinBank = 0xFFFF - 0x4100 = 0xBEFF.
        // pdu = 0x2100 + 0xBEFF = 0xDFFF = 57343 which is < 0xFFFF — does not overflow.
        // The "offsetWithinBank > ushort.MaxValue" check in VMemoryToPdu never fires for V-addresses
        // reachable through the parser (DecodeOctalVAddress caps at 0xFFFF).
        var result = DirectLogicAddress.VMemoryToPdu("V177777");
        result.ShouldBe((ushort)(DirectLogicAddress.SystemVMemoryBasePdu + (0xFFFF - DirectLogicAddress.SystemVMemoryOctalBase)));
    }
}
