using Mbproxy.Proxy;
using Xunit;

namespace Mbproxy.Tests.Proxy;

/// <summary>
/// Unit tests for <see cref="MbapFrame"/> header parsing and frame-length helpers.
/// All tests are pure in-memory; no network, no simulator required.
/// </summary>
[Trait("Category", "Unit")]
public sealed class MbapFrameTests
{
    // ── 1. TryParseHeader — too-short buffers ────────────────────────────────────────────

    [Fact]
    public void TryParseHeader_TooShort_ReturnsFalse()
    {
        // A buffer of only 6 bytes is one byte short of the 7-byte header.
        byte[] buf = [0x00, 0x01, 0x00, 0x00, 0x00, 0x06];
        bool result = MbapFrame.TryParseHeader(buf, out _, out _, out _, out _);
        Assert.False(result, "Buffer shorter than 7 bytes must return false.");
    }

    [Fact]
    public void TryParseHeader_EmptyBuffer_ReturnsFalse()
    {
        bool result = MbapFrame.TryParseHeader(ReadOnlySpan<byte>.Empty, out _, out _, out _, out _);
        Assert.False(result);
    }

    // ── 2. TryParseHeader — valid frame parses all fields ──────────────────────────────

    [Fact]
    public void TryParseHeader_ValidFrame_ParsesAllFields()
    {
        // TxId=0x0042, ProtocolId=0x0000, Length=0x0006, UnitId=0x01
        byte[] header = [0x00, 0x42, 0x00, 0x00, 0x00, 0x06, 0x01];

        bool ok = MbapFrame.TryParseHeader(header, out ushort txId, out ushort protocolId,
            out ushort length, out byte unitId);

        Assert.True(ok);
        Assert.Equal(0x0042, txId);
        Assert.Equal(0x0000, protocolId);
        Assert.Equal(6, length);
        Assert.Equal(1, unitId);
    }

    // ── 3. Non-zero ProtocolId still parses (PLC's job to reject it) ─────────────────

    [Fact]
    public void TryParseHeader_ProtocolId_NotZero_StillParses()
    {
        // ProtocolId = 0x0001 (non-standard but we don't filter it).
        byte[] header = [0x00, 0x01, 0x00, 0x01, 0x00, 0x06, 0xFF];

        bool ok = MbapFrame.TryParseHeader(header, out _, out ushort protocolId, out _, out _);

        Assert.True(ok);
        Assert.Equal(0x0001, protocolId);
    }

    // ── 4. TotalFrameLength — known good values ──────────────────────────────────────

    [Fact]
    public void TotalFrameLength_LengthField7_Returns13()
    {
        // 6 fixed prefix bytes + 7 = 13
        Assert.Equal(13, MbapFrame.TotalFrameLength(7));
    }

    [Fact]
    public void TotalFrameLength_LengthFieldMax_Returns_LengthFieldPlus6()
    {
        // The formula is always lengthField + 6.
        ushort max = ushort.MaxValue; // 65535
        Assert.Equal(max + 6, MbapFrame.TotalFrameLength(max));
    }

    // ── 5. Round-trip: FC03 read-holding-registers request ───────────────────────────

    [Fact]
    public void RoundTrip_FC03_ReadHoldingRegisters_Request_ParsesCorrectly()
    {
        // FC03 request: TxId=1, ProtocolId=0, Length=6, UnitId=1, FC=0x03, Start=0x0430, Qty=0x0001
        byte[] frame =
        [
            0x00, 0x01,  // TxId = 1
            0x00, 0x00,  // ProtocolId = 0
            0x00, 0x06,  // Length = 6
            0x01,        // UnitId = 1
            0x03,        // FC 03
            0x04, 0x30,  // Start address = 0x0430 (decimal 1072)
            0x00, 0x01,  // Quantity = 1
        ];

        bool ok = MbapFrame.TryParseHeader(frame.AsSpan(0, 7),
            out ushort txId, out ushort protocolId, out ushort length, out byte unitId);

        Assert.True(ok);
        Assert.Equal(1, txId);
        Assert.Equal(0, protocolId);
        Assert.Equal(6, length);
        Assert.Equal(1, unitId);

        // Total frame = 6 + length = 12 bytes
        Assert.Equal(12, MbapFrame.TotalFrameLength(length));
        Assert.Equal(frame.Length, MbapFrame.TotalFrameLength(length));
    }

    // ── 6. Round-trip: FC16 write-multiple-registers request ─────────────────────────

    [Fact]
    public void RoundTrip_FC16_WriteMultipleRegisters_ParsesCorrectly()
    {
        // FC16 request: TxId=5, ProtocolId=0, Length=11, UnitId=1
        // FC=0x10, Start=0x00C8 (200), Qty=2, ByteCount=4, Data=[0x00,0x0A, 0x00,0x14]
        byte[] frame =
        [
            0x00, 0x05,  // TxId = 5
            0x00, 0x00,  // ProtocolId = 0
            0x00, 0x0B,  // Length = 11
            0x01,        // UnitId = 1
            0x10,        // FC 16
            0x00, 0xC8,  // Start address = 200
            0x00, 0x02,  // Quantity = 2
            0x04,        // Byte count = 4
            0x00, 0x0A,  // Register 200 = 10
            0x00, 0x14,  // Register 201 = 20
        ];

        bool ok = MbapFrame.TryParseHeader(frame.AsSpan(0, 7),
            out ushort txId, out _, out ushort length, out byte unitId);

        Assert.True(ok);
        Assert.Equal(5, txId);
        Assert.Equal(11, length);
        Assert.Equal(1, unitId);

        // Total frame = 6 + 11 = 17
        Assert.Equal(17, MbapFrame.TotalFrameLength(length));
        Assert.Equal(frame.Length, MbapFrame.TotalFrameLength(length));
    }

    // ── 7. Length < 2 — parsed but unusual (callers' responsibility) ───────────────────

    [Fact]
    public void TryParseHeader_LengthLessThan2_ParsedButUnusual()
    {
        // length=1 means only a UnitId byte follows the 6-byte prefix; PDU body = 0 bytes.
        // The proxy does not reject this — that is the PLC's job. We parse and pass through.
        byte[] header = [0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01];

        bool ok = MbapFrame.TryParseHeader(header, out _, out _, out ushort length, out _);

        Assert.True(ok, "Header with length=1 should still parse; the proxy does not validate length semantics.");
        Assert.Equal(1, length);

        // TotalFrameLength still returns 6 + length = 7 (header only, no PDU body).
        Assert.Equal(7, MbapFrame.TotalFrameLength(length));
    }

    // ── 8. Exactly 7 bytes — boundary case ─────────────────────────────────────────────

    [Fact]
    public void TryParseHeader_ExactlySevenBytes_ParsesOk()
    {
        byte[] header = [0xFF, 0xFE, 0x00, 0x00, 0x00, 0x06, 0x02];
        bool ok = MbapFrame.TryParseHeader(header, out ushort txId, out _, out _, out byte unitId);
        Assert.True(ok);
        Assert.Equal(0xFFFE, txId);
        Assert.Equal(2, unitId);
    }
}
