using Mbproxy.Bcd;
using Shouldly;
using Xunit;

namespace Mbproxy.Tests.Bcd;

/// <summary>
/// Unit tests for <see cref="BcdCodec"/> — the allocation-free BCD nibble codec.
///
/// NOTE on allocation profile:
///   BcdCodec is a purely static class operating on value types (ushort, int, tuples).
///   It allocates only when constructing exception objects (the error path), never on
///   the success path. TryGet / hot-path decode callers are allocation-free for valid
///   BCD registers.
/// </summary>
[Trait("Category", "Unit")]
public sealed class BcdCodecTests
{
    // ── Encode16 ────────────────────────────────────────────────────────────

    [Fact]
    public void Encode16_1234_Returns_0x1234()
        => BcdCodec.Encode16(1234).ShouldBe((ushort)0x1234);

    [Fact]
    public void Encode16_0_Returns_0x0000()
        => BcdCodec.Encode16(0).ShouldBe((ushort)0x0000);

    [Fact]
    public void Encode16_9999_Returns_0x9999()
        => BcdCodec.Encode16(9999).ShouldBe((ushort)0x9999);

    [Fact]
    public void Encode16_10000_Throws_OutOfRange()
    {
        Should.Throw<ArgumentOutOfRangeException>(() => BcdCodec.Encode16(10_000))
              .ParamName.ShouldBe("value");
    }

    [Fact]
    public void Encode16_Negative_Throws_OutOfRange()
    {
        Should.Throw<ArgumentOutOfRangeException>(() => BcdCodec.Encode16(-1))
              .ParamName.ShouldBe("value");
    }

    /// <summary>
    /// Locks the boundary contract for the `(uint)value > Max16` range check.
    /// `int.MinValue` cast to `uint` becomes `0x80000000`, which is well above
    /// `Max16` (= 9999), so the throw fires cleanly without arithmetic surprise. Prevents
    /// regressions if the bounds check is ever rewritten with a two-sided int comparison
    /// that would underflow on extreme negatives.
    /// </summary>
    [Fact]
    public void Encode16_IntMinValue_Throws_OutOfRange_NoArithmeticSurprise()
    {
        Should.Throw<ArgumentOutOfRangeException>(() => BcdCodec.Encode16(int.MinValue))
              .ParamName.ShouldBe("value");
    }

    // ── Decode16 ────────────────────────────────────────────────────────────

    [Fact]
    public void Decode16_0x1234_Returns_1234()
        => BcdCodec.Decode16(0x1234).ShouldBe(1234);

    [Fact]
    public void Decode16_0x0000_Returns_0()
        => BcdCodec.Decode16(0x0000).ShouldBe(0);

    [Fact]
    public void Decode16_0x9999_Returns_9999()
        => BcdCodec.Decode16(0x9999).ShouldBe(9999);

    [Fact]
    public void Decode16_0x123A_Throws_Format()
    {
        // Nibble 'A' (10) is not a valid BCD digit; message must contain the raw hex value.
        var ex = Should.Throw<FormatException>(() => BcdCodec.Decode16(0x123A));
        ex.Message.ShouldContain("0x123A", Case.Insensitive);
    }

    [Fact]
    public void Decode16_0x12FA_TwoBadNibbles_Throws_Format()
    {
        // Two bad nibbles in one register — still throws once with the raw value.
        var ex = Should.Throw<FormatException>(() => BcdCodec.Decode16(0x12FA));
        ex.Message.ShouldContain("0x12FA", Case.Insensitive);
    }

    // ── Encode32 ────────────────────────────────────────────────────────────

    [Fact]
    public void Encode32_12345678_Returns_LowHigh_5678_1234()
    {
        var (low, high) = BcdCodec.Encode32(12_345_678);
        low.ShouldBe((ushort)0x5678);
        high.ShouldBe((ushort)0x1234);
    }

    [Fact]
    public void Encode32_0_Returns_LowHigh_0_0()
    {
        var (low, high) = BcdCodec.Encode32(0);
        low.ShouldBe((ushort)0x0000);
        high.ShouldBe((ushort)0x0000);
    }

    [Fact]
    public void Encode32_99999999_Returns_LowHigh_9999_9999()
    {
        var (low, high) = BcdCodec.Encode32(99_999_999);
        low.ShouldBe((ushort)0x9999);
        high.ShouldBe((ushort)0x9999);
    }

    [Fact]
    public void Encode32_100000000_Throws_OutOfRange()
    {
        Should.Throw<ArgumentOutOfRangeException>(() => BcdCodec.Encode32(100_000_000))
              .ParamName.ShouldBe("value");
    }

    // ── Decode32 ────────────────────────────────────────────────────────────

    [Fact]
    public void Decode32_LowHigh_5678_1234_Returns_12345678()
        => BcdCodec.Decode32(0x5678, 0x1234).ShouldBe(12_345_678);

    [Fact]
    public void Decode32_BadNibble_InLow_Throws()
    {
        // Low word has a bad nibble; Decode32 must propagate the FormatException.
        Should.Throw<FormatException>(() => BcdCodec.Decode32(0xABCD, 0x1234));
    }

    [Fact]
    public void Decode32_BadNibble_InHigh_Throws()
    {
        Should.Throw<FormatException>(() => BcdCodec.Decode32(0x5678, 0xABCD));
    }

    // ── Round-trip 16-bit ────────────────────────────────────────────────────

    /// <summary>
    /// Dense round-trip: boundary values plus every 100th value in [0, 9999].
    /// Ensures Decode16(Encode16(v)) == v for all practical inputs.
    /// </summary>
    [Theory]
    [MemberData(nameof(RoundTrip16Values))]
    public void RoundTrip16_AllValuesUnder10000(int value)
        => BcdCodec.Decode16(BcdCodec.Encode16(value)).ShouldBe(value);

    public static IEnumerable<object[]> RoundTrip16Values()
    {
        // Every 100th value (0, 100, 200, … 9900) — covers 0 as boundary automatically
        for (int v = 0; v <= 9999; v += 100)
            yield return [v];

        // Additional boundary values not already hit by the stride-100 loop
        yield return [1];
        yield return [9];
        yield return [99];
        yield return [999];
        yield return [9999];

        // Some spot-check midpoints
        yield return [1234];
        yield return [5678];
        yield return [4321];
    }

    // ── Round-trip 32-bit ────────────────────────────────────────────────────

    [Theory]
    [InlineData(0)]
    [InlineData(1)]
    [InlineData(9999)]
    [InlineData(10_000)]
    [InlineData(99_999_999)]
    [InlineData(12_345_678)]
    [InlineData(5_000_000)]
    public void RoundTrip32_RepresentativeValues(int value)
    {
        var (low, high) = BcdCodec.Encode32(value);
        BcdCodec.Decode32(low, high).ShouldBe(value);
    }
}
