using System.Collections.Frozen;
using Mbproxy.Bcd;
using Mbproxy.Proxy;
using Mbproxy.Proxy.Multiplexing;
using Microsoft.Extensions.Logging.Abstractions;
using Shouldly;
using Xunit;

namespace Mbproxy.Tests.Proxy.Multiplexing;

/// <summary>
/// Verifies that <see cref="BcdPduPipeline"/> correlates FC03/FC04 responses through
/// <see cref="PerPlcContext.CurrentRequest"/>. Concurrent in-flight requests from
/// different upstream clients must decode against their own request range without
/// cross-talk.
/// </summary>
[Trait("Category", "Unit")]
public sealed class RewriterCorrelationTests
{
    private static readonly BcdPduPipeline Pipeline = new();

    private static PerPlcContext MakeContext(params BcdTag[] tags)
    {
        var frozen = tags.ToDictionary(t => t.Address).ToFrozenDictionary();
        var map = frozen.Count > 0 ? new BcdTagMap(frozen) : BcdTagMap.Empty;
        return new PerPlcContext
        {
            PlcName  = "MuxTest",
            TagMap   = map,
            Counters = new ProxyCounters(),
            Logger   = NullLogger.Instance,
        };
    }

    private static InFlightRequest MakeReq(byte fc, ushort start, ushort qty)
        => new(
            UnitId: 1, Fc: fc, StartAddress: start, Qty: qty,
            InterestedParties: Array.Empty<InterestedParty>(),
            SentAtUtc: DateTimeOffset.UtcNow);

    private static byte[] Fc03Response(params ushort[] registers)
    {
        var pdu = new byte[2 + registers.Length * 2];
        pdu[0] = 0x03;
        pdu[1] = (byte)(registers.Length * 2);
        for (int i = 0; i < registers.Length; i++)
        {
            pdu[2 + i * 2]     = (byte)(registers[i] >> 8);
            pdu[2 + i * 2 + 1] = (byte)(registers[i] & 0xFF);
        }
        return pdu;
    }

    private static ushort ReadReg(byte[] pdu, int offsetWords)
        => (ushort)((pdu[2 + offsetWords * 2] << 8) | pdu[2 + offsetWords * 2 + 1]);

    /// <summary>
    /// Confirms the rewriter reads address+qty from <see cref="PerPlcContext.CurrentRequest"/>
    /// (not from any per-pair slot) when processing an FC03 response.
    /// </summary>
    [Fact]
    public void FC03Response_DecodedViaInFlightRequest_NotPerPairSlot()
    {
        var ctx = MakeContext(BcdTag.Create(100, 16));

        // Build a response with raw BCD nibbles at address 100; no prior request was sent
        // on this context. Without CurrentRequest, the rewriter must NOT touch the bytes.
        var pdu = Fc03Response(0x1234);
        byte[] original = [.. pdu];
        Pipeline.Process(MbapDirection.ResponseToClient, ReadOnlySpan<byte>.Empty, pdu.AsSpan(), ctx);
        pdu.ShouldBe(original, "without CurrentRequest the rewriter has no correlation; bytes must pass through");

        // Now attach a CurrentRequest that points at address 100 / qty 1.
        var withReq = ctx.WithCurrentRequest(MakeReq(fc: 0x03, start: 100, qty: 1));
        pdu = Fc03Response(0x1234);
        Pipeline.Process(MbapDirection.ResponseToClient, ReadOnlySpan<byte>.Empty, pdu.AsSpan(), withReq);
        ReadReg(pdu, 0).ShouldBe((ushort)1234);
    }

    /// <summary>
    /// Two concurrent in-flight responses with different start addresses must each decode
    /// against their own request range — proves no shared-mutable-state cross-talk.
    /// Delivers them out of order to make sure ordering doesn't accidentally mask the bug.
    /// </summary>
    [Fact]
    public void ConcurrentFC03_FromTwoUpstreams_DecodeCorrectly_NoCrossTalk()
    {
        // Tags at address 100 and 200, both 16-bit.
        var ctx = MakeContext(BcdTag.Create(100, 16), BcdTag.Create(200, 16));

        // Request A reads addr 100 / qty 1. Response has BCD nibbles 0x1234 (decimal 1234).
        var ctxA = ctx.WithCurrentRequest(MakeReq(0x03, 100, 1));
        var rspA = Fc03Response(0x1234);

        // Request B reads addr 200 / qty 1. Response has BCD nibbles 0x9876 (decimal 9876).
        var ctxB = ctx.WithCurrentRequest(MakeReq(0x03, 200, 1));
        var rspB = Fc03Response(0x9876);

        // Deliver B first, then A.
        Pipeline.Process(MbapDirection.ResponseToClient, ReadOnlySpan<byte>.Empty, rspB.AsSpan(), ctxB);
        Pipeline.Process(MbapDirection.ResponseToClient, ReadOnlySpan<byte>.Empty, rspA.AsSpan(), ctxA);

        ReadReg(rspB, 0).ShouldBe((ushort)9876, "B must decode against its own start address (200)");
        ReadReg(rspA, 0).ShouldBe((ushort)1234, "A must decode against its own start address (100)");
    }

    /// <summary>
    /// FC06 responses are correlated via the address embedded in the echo, not via
    /// CurrentRequest. This test verifies two concurrent FC06 echoes from different
    /// upstreams each decode correctly when the rewriter ran their requests first.
    /// </summary>
    [Fact]
    public void ConcurrentFC06_FromTwoUpstreams_EncodeCorrectly()
    {
        var ctx = MakeContext(BcdTag.Create(300, 16), BcdTag.Create(400, 16));

        // Client A writes binary 1234 to address 300.
        var reqA = new byte[] { 0x06, 0x01, 0x2C, 0x04, 0xD2 };       // addr=300, value=1234
        var ctxA = ctx.WithCurrentRequest(MakeReq(0x06, 300, 1));
        Pipeline.Process(MbapDirection.RequestToBackend, ReadOnlySpan<byte>.Empty, reqA.AsSpan(), ctxA);
        ((reqA[3] << 8) | reqA[4]).ShouldBe(0x1234, "client A request must be BCD-encoded to 0x1234");

        // Client B writes binary 5678 to address 400.
        var reqB = new byte[] { 0x06, 0x01, 0x90, 0x16, 0x2E };       // addr=400, value=5678
        var ctxB = ctx.WithCurrentRequest(MakeReq(0x06, 400, 1));
        Pipeline.Process(MbapDirection.RequestToBackend, ReadOnlySpan<byte>.Empty, reqB.AsSpan(), ctxB);
        ((reqB[3] << 8) | reqB[4]).ShouldBe(0x5678, "client B request must be BCD-encoded to 0x5678");

        // Now both responses echo the BCD nibbles. The rewriter must decode them.
        var rspA = new byte[] { 0x06, 0x01, 0x2C, 0x12, 0x34 };
        var rspB = new byte[] { 0x06, 0x01, 0x90, 0x56, 0x78 };
        Pipeline.Process(MbapDirection.ResponseToClient, ReadOnlySpan<byte>.Empty, rspA.AsSpan(), ctxA);
        Pipeline.Process(MbapDirection.ResponseToClient, ReadOnlySpan<byte>.Empty, rspB.AsSpan(), ctxB);

        ((rspA[3] << 8) | rspA[4]).ShouldBe(1234);
        ((rspB[3] << 8) | rspB[4]).ShouldBe(5678);
    }

    /// <summary>
    /// The rewriter must not throw if the response arrives after the upstream has gone
    /// away. The multiplexer drops responses for dead pipes silently — but the rewriter
    /// runs on the response regardless, so a dropped party should produce no exception.
    /// </summary>
    [Fact]
    public void ResponseForDeadUpstream_IsDropped_NoExceptionPropagates()
    {
        // Dead upstream is modeled by an empty InterestedParties list (the multiplexer
        // discovered on cascade walk that the pipe was no longer alive).
        var ctx = MakeContext(BcdTag.Create(100, 16));
        var ctxWithReq = ctx.WithCurrentRequest(MakeReq(0x03, 100, 1));

        var rsp = Fc03Response(0x1234);
        // No assertion needed beyond "does not throw"; the rewriter is purely a bytes
        // operation and is unaware of upstream liveness.
        Should.NotThrow(() =>
            Pipeline.Process(MbapDirection.ResponseToClient, ReadOnlySpan<byte>.Empty, rsp.AsSpan(), ctxWithReq));
        ReadReg(rsp, 0).ShouldBe((ushort)1234, "the bytes were still rewritten in place");
    }
}
