using Shouldly;
using Xunit;

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

[Trait("Category", "Unit")]
public sealed class DirectLogicAddressTests
{
    /// <summary>Tests that octal V-prefixed addresses are correctly converted to PDU values.</summary>
    /// <param name="v">V-prefixed octal address string.</param>
    /// <param name="expected">Expected PDU address value.</param>
    [Theory]
    [InlineData("V0", (ushort)0x0000)]
    [InlineData("V1", (ushort)0x0001)]
    [InlineData("V7", (ushort)0x0007)]
    [InlineData("V10", (ushort)0x0008)]
    [InlineData("V2000", (ushort)0x0400)]  // canonical DL205/DL260 user-memory start
    [InlineData("V7777", (ushort)0x0FFF)]
    [InlineData("V10000", (ushort)0x1000)]
    [InlineData("V17777", (ushort)0x1FFF)]
    public void UserVMemoryToPdu_converts_octal_V_prefix(string v, ushort expected)
        => DirectLogicAddress.UserVMemoryToPdu(v).ShouldBe(expected);

    /// <summary>Tests that user memory addresses accept bare, prefixed, and padded forms.</summary>
    /// <param name="v">Address string in various formats.</param>
    /// <param name="expected">Expected PDU address value.</param>
    [Theory]
    [InlineData("0", (ushort)0)]
    [InlineData("2000", (ushort)0x0400)]
    [InlineData("v2000", (ushort)0x0400)]   // lowercase v
    [InlineData(" V2000 ", (ushort)0x0400)] // surrounding whitespace
    public void UserVMemoryToPdu_accepts_bare_or_prefixed_or_padded(string v, ushort expected)
        => DirectLogicAddress.UserVMemoryToPdu(v).ShouldBe(expected);

    /// <summary>Tests that non-octal digits in user memory addresses are rejected.</summary>
    /// <param name="v">Address string with invalid octal digits.</param>
    [Theory]
    [InlineData("V8")]     // 8 is not a valid octal digit
    [InlineData("V19")]
    [InlineData("V2009")]
    public void UserVMemoryToPdu_rejects_non_octal_digits(string v)
    {
        Should.Throw<ArgumentException>(() => DirectLogicAddress.UserVMemoryToPdu(v))
            .Message.ShouldContain("octal");
    }

    /// <summary>Tests that empty input for user memory addresses is rejected.</summary>
    /// <param name="v">Empty or whitespace-only address string.</param>
    [Theory]
    [InlineData(null)]
    [InlineData("")]
    [InlineData("   ")]
    [InlineData("V")]
    public void UserVMemoryToPdu_rejects_empty_input(string? v)
        => Should.Throw<ArgumentException>(() => DirectLogicAddress.UserVMemoryToPdu(v!));

    /// <summary>Tests that user memory overflow is rejected.</summary>
    [Fact]
    public void UserVMemoryToPdu_overflow_rejected()
    {
        // 200000 octal = 0x10000 — one past ushort range.
        Should.Throw<OverflowException>(() => DirectLogicAddress.UserVMemoryToPdu("V200000"));
    }

    /// <summary>Tests that system memory base PDU is correctly set to 0x2100.</summary>
    [Fact]
    public void SystemVMemoryBasePdu_is_0x2100_for_V40400()
    {
        // V40400 on DL260 / H2-ECOM100 absolute mode → PDU 0x2100 (decimal 8448), NOT 0x4100
        // which a naive octal-to-decimal of 40400 octal would give (= 16640).
        DirectLogicAddress.SystemVMemoryBasePdu.ShouldBe((ushort)0x2100);
        DirectLogicAddress.SystemVMemoryToPdu(0).ShouldBe((ushort)0x2100);
    }

    /// <summary>Tests that system memory addresses correctly offset within bank.</summary>
    [Fact]
    public void SystemVMemoryToPdu_offsets_within_bank()
    {
        DirectLogicAddress.SystemVMemoryToPdu(1).ShouldBe((ushort)0x2101);
        DirectLogicAddress.SystemVMemoryToPdu(0x100).ShouldBe((ushort)0x2200);
    }

    /// <summary>Tests that system memory overflow is rejected.</summary>
    [Fact]
    public void SystemVMemoryToPdu_rejects_overflow()
    {
        // ushort wrap: 0xFFFF - 0x2100 = 0xDEFF; anything above should throw.
        Should.NotThrow(() => DirectLogicAddress.SystemVMemoryToPdu(0xDEFF));
        Should.Throw<OverflowException>(() => DirectLogicAddress.SystemVMemoryToPdu(0xDF00));
    }

    // --- Bit memory: Y-output, C-relay, X-input, SP-special ---

    /// <summary>Tests that Y-output addresses correctly add octal offset to 2048.</summary>
    /// <param name="y">Y-prefixed octal address string.</param>
    /// <param name="expected">Expected coil address value.</param>
    [Theory]
    [InlineData("Y0", (ushort)2048)]
    [InlineData("Y1", (ushort)2049)]
    [InlineData("Y7", (ushort)2055)]
    [InlineData("Y10", (ushort)2056)]    // octal 10 = decimal 8
    [InlineData("Y17", (ushort)2063)]    // octal 17 = decimal 15
    [InlineData("Y777", (ushort)2559)]   // top of DL260 Y range per doc table
    public void YOutputToCoil_adds_octal_offset_to_2048(string y, ushort expected)
        => DirectLogicAddress.YOutputToCoil(y).ShouldBe(expected);

    /// <summary>Tests that C-relay addresses correctly add octal offset to 3072.</summary>
    /// <param name="c">C-prefixed octal address string.</param>
    /// <param name="expected">Expected coil address value.</param>
    [Theory]
    [InlineData("C0", (ushort)3072)]
    [InlineData("C1", (ushort)3073)]
    [InlineData("C10", (ushort)3080)]
    [InlineData("C1777", (ushort)4095)]  // top of DL260 C range
    public void CRelayToCoil_adds_octal_offset_to_3072(string c, ushort expected)
        => DirectLogicAddress.CRelayToCoil(c).ShouldBe(expected);

    /// <summary>Tests that X-input addresses correctly add octal offset to 0.</summary>
    /// <param name="x">X-prefixed octal address string.</param>
    /// <param name="expected">Expected discrete address value.</param>
    [Theory]
    [InlineData("X0", (ushort)0)]
    [InlineData("X17", (ushort)15)]
    [InlineData("X777", (ushort)511)]    // top of DL260 X range
    public void XInputToDiscrete_adds_octal_offset_to_0(string x, ushort expected)
        => DirectLogicAddress.XInputToDiscrete(x).ShouldBe(expected);

    /// <summary>Tests that special-purpose bit addresses correctly add octal offset to 1024.</summary>
    /// <param name="sp">SP-prefixed octal address string.</param>
    /// <param name="expected">Expected discrete address value.</param>
    [Theory]
    [InlineData("SP0", (ushort)1024)]
    [InlineData("SP7", (ushort)1031)]
    [InlineData("sp0", (ushort)1024)]     // lowercase prefix
    [InlineData("SP777", (ushort)1535)]
    public void SpecialToDiscrete_adds_octal_offset_to_1024(string sp, ushort expected)
        => DirectLogicAddress.SpecialToDiscrete(sp).ShouldBe(expected);

    /// <summary>Tests that non-octal digits in bit addresses are rejected.</summary>
    /// <param name="bad">Bit address with invalid octal digits.</param>
    [Theory]
    [InlineData("Y8")]
    [InlineData("C9")]
    [InlineData("X18")]
    public void Bit_address_rejects_non_octal_digits(string bad)
        => Should.Throw<ArgumentException>(() =>
        {
            if (bad[0] == 'Y') DirectLogicAddress.YOutputToCoil(bad);
            else if (bad[0] == 'C') DirectLogicAddress.CRelayToCoil(bad);
            else DirectLogicAddress.XInputToDiscrete(bad);
        });

    /// <summary>Tests that empty input for bit addresses is rejected.</summary>
    /// <param name="bad">Empty or incomplete bit address.</param>
    [Theory]
    [InlineData("Y")]
    [InlineData("C")]
    [InlineData("")]
    public void Bit_address_rejects_empty(string bad)
        => Should.Throw<ArgumentException>(() => DirectLogicAddress.YOutputToCoil(bad));

    /// <summary>Tests that Y-output addresses accept lowercase prefix.</summary>
    [Fact]
    public void YOutputToCoil_accepts_lowercase_prefix()
        => DirectLogicAddress.YOutputToCoil("y0").ShouldBe((ushort)2048);

    /// <summary>Tests that C-relay addresses accept bare octal without C prefix.</summary>
    [Fact]
    public void CRelayToCoil_accepts_bare_octal_without_C_prefix()
        => DirectLogicAddress.CRelayToCoil("0").ShouldBe((ushort)3072);
}
