using Mbproxy.Bcd;
using Mbproxy.Proxy;
using Shouldly;
using Xunit;

namespace Mbproxy.Tests.Proxy;

/// <summary>
/// Unit tests for <see cref="TagValueCapture"/> — the on-demand per-tag value store
/// behind the connection-detail debug view.
/// </summary>
[Trait("Category", "Unit")]
public sealed class TagValueCaptureTests
{
    private static TagValueCapture Make(params (ushort addr, byte width)[] tags)
        => new(tags.Select(t => BcdTag.Create(t.addr, t.width)));

    [Fact]
    public void Disarmed_Record_IsNoOp()
    {
        var capture = Make((100, 16));
        // No Arm() call — capture starts disarmed.
        capture.Record(100, 0x1234, 0, 1234, CaptureDirection.Read);

        capture.IsArmed.ShouldBeFalse();
        var slot = capture.Snapshot().ShouldHaveSingleItem();
        slot.UpdatedAtUtc.ShouldBeNull();
    }

    [Fact]
    public void Armed_Record_UpdatesMatchingSlot()
    {
        var capture = Make((100, 16));
        capture.Arm();
        capture.Record(100, 0x1234, 0, 1234, CaptureDirection.Read);

        var slot = capture.Snapshot().ShouldHaveSingleItem();
        slot.Address.ShouldBe((ushort)100);
        slot.Width.ShouldBe((byte)16);
        slot.RawLow.ShouldBe((ushort)0x1234);
        slot.DecodedValue.ShouldBe(1234);
        slot.Direction.ShouldBe(CaptureDirection.Read);
        slot.UpdatedAtUtc.ShouldNotBeNull();
    }

    [Fact]
    public void Armed_Record_UnknownAddress_IsIgnored()
    {
        var capture = Make((100, 16));
        capture.Arm();
        capture.Record(999, 0x1111, 0, 1111, CaptureDirection.Read);

        capture.Snapshot().ShouldAllBe(s => s.UpdatedAtUtc == null);
    }

    [Fact]
    public void Disarm_ClearsAllSlots()
    {
        var capture = Make((100, 16), (200, 16));
        capture.Arm();
        capture.Record(100, 0x0042, 0, 42, CaptureDirection.Read);
        capture.Record(200, 0x0099, 0, 99, CaptureDirection.Read);

        capture.Disarm();

        capture.IsArmed.ShouldBeFalse();
        capture.Snapshot().ShouldAllBe(s => s.UpdatedAtUtc == null);
    }

    [Fact]
    public void ReArm_AfterDisarm_StartsEmpty()
    {
        var capture = Make((100, 16));
        capture.Arm();
        capture.Record(100, 0x0042, 0, 42, CaptureDirection.Read);
        capture.Disarm();
        capture.Arm();

        // No new traffic since re-arm — slot must read as empty, not the pre-disarm value.
        capture.Snapshot().ShouldHaveSingleItem().UpdatedAtUtc.ShouldBeNull();
    }

    [Fact]
    public void ThirtyTwoBitTag_RecordsBothRawWords()
    {
        var capture = Make((100, 32));
        capture.Arm();
        capture.Record(100, 0x5678, 0x1234, 12345678, CaptureDirection.Read);

        var slot = capture.Snapshot().ShouldHaveSingleItem();
        slot.Width.ShouldBe((byte)32);
        slot.RawLow.ShouldBe((ushort)0x5678);
        slot.RawHigh.ShouldBe((ushort)0x1234);
        slot.DecodedValue.ShouldBe(12345678);
    }

    [Fact]
    public void Snapshot_ReturnsOneRowPerTag_OrderedByAddress()
    {
        var capture = Make((300, 16), (100, 32), (200, 16));
        capture.TagCount.ShouldBe(3);

        var snap = capture.Snapshot();
        snap.Select(s => s.Address).ShouldBe([(ushort)100, (ushort)200, (ushort)300]);
    }

    [Fact]
    public void WriteDirection_IsPreserved()
    {
        var capture = Make((100, 16));
        capture.Arm();
        capture.Record(100, 0x0500, 0, 500, CaptureDirection.Write);

        capture.Snapshot().ShouldHaveSingleItem().Direction.ShouldBe(CaptureDirection.Write);
    }

    [Fact]
    public async Task ConcurrentRecordAndSnapshot_NeverYieldsTornSlot()
    {
        // Invariant maintained by every Record: DecodedValue == RawLow + RawHigh.
        // A torn read (fields from two different Record calls) would break it.
        var capture = Make((100, 32));
        capture.Arm();

        var ct = TestContext.Current.CancellationToken;
        bool tornObserved = false;

        var writers = Enumerable.Range(0, 4).Select(seed => Task.Run(() =>
        {
            var rng = new Random(seed + 1);
            for (int i = 0; i < 200_000; i++)
            {
                ushort lo = (ushort)rng.Next(0, 60000);
                ushort hi = (ushort)rng.Next(0, 5000);
                capture.Record(100, lo, hi, lo + hi, CaptureDirection.Read);
            }
        }, ct)).ToArray();

        var reader = Task.Run(() =>
        {
            for (int i = 0; i < 200_000; i++)
            {
                foreach (var slot in capture.Snapshot())
                {
                    if (slot.UpdatedAtUtc is null)
                        continue;
                    if (slot.DecodedValue != slot.RawLow + slot.RawHigh)
                        tornObserved = true;
                }
            }
        }, ct);

        await Task.WhenAll([.. writers, reader]);
        tornObserved.ShouldBeFalse("Snapshot must never observe a torn (half-updated) slot");
    }
}
