// Ported from golang/nats-server/server/mqtt_test.go — TestMQTTReader, TestMQTTWriter, and
// packet-level scenarios exercised inline throughout the Go test suite.
// Go reference: server/mqtt.go constants mqttPacketConnect=0x10, mqttPacketPub=0x30,
// mqttPacketSub=0x80, mqttPacketUnsub=0xa0, mqttPacketPing=0xc0, mqttPacketDisconnect=0xe0.

using NATS.Server.Mqtt;

namespace NATS.Server.Tests.Mqtt;

public class MqttPacketParsingParityTests
{
    // -------------------------------------------------------------------------
    // 1. CONNECT packet parsing
    // -------------------------------------------------------------------------

    [Fact]
    public void Connect_packet_type_is_parsed_from_first_nibble()
    {
        // Fixed header 0x10 = type 1 (Connect), flags 0.
        // Variable header: protocol name "MQTT" (4 bytes + 2-byte length prefix),
        // protocol level 0x04, connect flags 0x02 (clean session), keepalive 0x00 0x3C (60s).
        // Payload: 2-byte length-prefixed empty client-id.
        ReadOnlySpan<byte> bytes =
        [
            0x10, 0x0C,                                   // CONNECT, remaining length 12
            0x00, 0x04, (byte)'M', (byte)'Q', (byte)'T', (byte)'T',
            0x04, 0x02, 0x00, 0x3C,                       // protocol level 4, clean-session flag, keepalive 60
            0x00, 0x00,                                   // empty client-id
        ];

        var packet = MqttPacketReader.Read(bytes);

        packet.Type.ShouldBe(MqttControlPacketType.Connect);
        packet.Flags.ShouldBe((byte)0x00);
        packet.RemainingLength.ShouldBe(12);
        packet.Payload.Length.ShouldBe(12);
    }

    [Fact]
    public void Connect_packet_payload_contains_protocol_name_and_flags()
    {
        // The variable-header for a CONNECT begins with a 2-byte-length-prefixed protocol
        // name ("MQTT"), then protocol level (4), then connect-flags byte.
        ReadOnlySpan<byte> bytes =
        [
            0x10, 0x0C,
            0x00, 0x04, (byte)'M', (byte)'Q', (byte)'T', (byte)'T',
            0x04, 0x02, 0x00, 0x3C,
            0x00, 0x00,
        ];

        var packet = MqttPacketReader.Read(bytes);
        var payload = packet.Payload.Span;

        // Bytes 0-5: 0x00 0x04 'M' 'Q' 'T' 'T'
        payload[0].ShouldBe((byte)0x00);
        payload[1].ShouldBe((byte)0x04);
        payload[2].ShouldBe((byte)'M');
        payload[3].ShouldBe((byte)'Q');
        payload[4].ShouldBe((byte)'T');
        payload[5].ShouldBe((byte)'T');
        // Byte 6: protocol level 4
        payload[6].ShouldBe((byte)0x04);
        // Byte 7: connect flags — 0x02 = clean-session
        payload[7].ShouldBe((byte)0x02);
    }

    [Fact]
    public void Connect_keepalive_bytes_are_present_in_payload()
    {
        // Keepalive is a big-endian uint16 at bytes 8-9 of the variable header.
        // Here 0x00 0x3C = 60 seconds.
        ReadOnlySpan<byte> bytes =
        [
            0x10, 0x0C,
            0x00, 0x04, (byte)'M', (byte)'Q', (byte)'T', (byte)'T',
            0x04, 0x02, 0x00, 0x3C,
            0x00, 0x00,
        ];

        var packet = MqttPacketReader.Read(bytes);
        var payload = packet.Payload.Span;

        var keepalive = (payload[8] << 8) | payload[9];
        keepalive.ShouldBe(60);
    }

    // -------------------------------------------------------------------------
    // 2. PUBLISH packet parsing — QoS 0 and QoS 1
    // -------------------------------------------------------------------------

    [Fact]
    public void Publish_qos0_packet_fixed_header_byte_is_0x30()
    {
        // PUBLISH with QoS=0, DUP=0, RETAIN=0 → fixed header high nibble 0x3, flags nibble 0x0.
        // Topic "a/b" (length 3, encoded as 0x00 0x03 'a' '/' 'b') + payload "hello".
        ReadOnlySpan<byte> bytes =
        [
            0x30, 0x0A,                          // PUBLISH QoS 0, remaining length 10
            0x00, 0x03, (byte)'a', (byte)'/', (byte)'b',  // topic "a/b"
            (byte)'h', (byte)'e', (byte)'l', (byte)'l', (byte)'o',  // payload "hello"
        ];

        var packet = MqttPacketReader.Read(bytes);

        packet.Type.ShouldBe(MqttControlPacketType.Publish);
        packet.Flags.ShouldBe((byte)0x00);
        packet.RemainingLength.ShouldBe(10);
    }

    [Fact]
    public void Publish_qos1_flags_nibble_is_0x02()
    {
        // PUBLISH with QoS=1 → flags nibble 0x2.  Packet identifier (2 bytes) follows topic.
        // Topic "t" (0x00 0x01 't') + packet-id 0x00 0x01 + payload "data".
        ReadOnlySpan<byte> bytes =
        [
            0x32, 0x09,                              // PUBLISH QoS 1 (flags=0x02), remaining length 9
            0x00, 0x01, (byte)'t',                   // topic "t"
            0x00, 0x01,                              // packet identifier 1
            (byte)'d', (byte)'a', (byte)'t', (byte)'a',  // payload "data"
        ];

        var packet = MqttPacketReader.Read(bytes);

        packet.Type.ShouldBe(MqttControlPacketType.Publish);
        // QoS 1 is encoded in bits 2-1 of the flags nibble: 0x02
        packet.Flags.ShouldBe((byte)0x02);
        packet.RemainingLength.ShouldBe(9);
    }

    [Fact]
    public void Publish_payload_starts_after_topic_length_prefix()
    {
        // Topic "ab" length-prefix 0x00 0x02, payload bytes follow remaining-length boundary.
        ReadOnlySpan<byte> bytes =
        [
            0x30, 0x07,
            0x00, 0x02, (byte)'a', (byte)'b',
            (byte)'x', (byte)'y', (byte)'z',
        ];

        var packet = MqttPacketReader.Read(bytes);
        var payload = packet.Payload.Span;

        // payload[0..1] = topic length, [2..3] = "ab", [4..6] = "xyz"
        payload.Length.ShouldBe(7);
        payload[4].ShouldBe((byte)'x');
        payload[5].ShouldBe((byte)'y');
        payload[6].ShouldBe((byte)'z');
    }

    // -------------------------------------------------------------------------
    // 3. SUBSCRIBE packet parsing
    // -------------------------------------------------------------------------

    [Fact]
    public void Subscribe_packet_type_is_parsed_correctly()
    {
        // SUBSCRIBE fixed header = 0x82 (type 0x80 | flags 0x02 — required by MQTT spec).
        // Variable header: packet-id 0x00 0x01.
        // Payload: topic filter "test/#" with QoS 0.
        ReadOnlySpan<byte> bytes =
        [
            0x82, 0x0B,                                               // SUBSCRIBE, remaining length 11
            0x00, 0x01,                                               // packet identifier 1
            0x00, 0x06,                                               // topic filter length 6
            (byte)'t', (byte)'e', (byte)'s', (byte)'t', (byte)'/', (byte)'#',
            0x00,                                                     // requested QoS 0
        ];

        var packet = MqttPacketReader.Read(bytes);

        packet.Type.ShouldBe(MqttControlPacketType.Subscribe);
        packet.Flags.ShouldBe((byte)0x02);
        packet.RemainingLength.ShouldBe(11);
    }

    [Fact]
    public void Subscribe_payload_contains_packet_id_and_topic_filter()
    {
        ReadOnlySpan<byte> bytes =
        [
            0x82, 0x0B,
            0x00, 0x01,
            0x00, 0x06,
            (byte)'t', (byte)'e', (byte)'s', (byte)'t', (byte)'/', (byte)'#',
            0x00,
        ];

        var packet = MqttPacketReader.Read(bytes);
        var payload = packet.Payload.Span;

        // Packet identifier at bytes 0-1
        var packetId = (payload[0] << 8) | payload[1];
        packetId.ShouldBe(1);

        // Topic filter length at bytes 2-3
        var filterLen = (payload[2] << 8) | payload[3];
        filterLen.ShouldBe(6);

        // Topic filter characters
        payload[4].ShouldBe((byte)'t');
        payload[9].ShouldBe((byte)'#');

        // QoS byte at the end
        payload[10].ShouldBe((byte)0x00);
    }

    // -------------------------------------------------------------------------
    // 4. UNSUBSCRIBE and DISCONNECT parsing
    // -------------------------------------------------------------------------

    [Fact]
    public void Unsubscribe_packet_type_is_parsed_correctly()
    {
        // UNSUBSCRIBE fixed header = 0xA2 (type 0xA0 | flags 0x02).
        // Variable header: packet-id 0x00 0x02.
        // Payload: topic filter "sensors/+" (length 9).
        ReadOnlySpan<byte> bytes =
        [
            0xA2, 0x0D,
            0x00, 0x02,
            0x00, 0x09,
            (byte)'s', (byte)'e', (byte)'n', (byte)'s', (byte)'o', (byte)'r', (byte)'s', (byte)'/', (byte)'+',
        ];

        var packet = MqttPacketReader.Read(bytes);

        // 0xA0 >> 4 = 10, which is not in the MqttControlPacketType enum — the reader
        // returns whatever type byte is encoded; cast to byte for verification.
        ((byte)packet.Type).ShouldBe((byte)10);
        packet.Flags.ShouldBe((byte)0x02);
        packet.RemainingLength.ShouldBe(13);
    }

    [Fact]
    public void Disconnect_packet_is_two_bytes_with_zero_remaining_length()
    {
        // DISCONNECT fixed header = 0xE0, remaining length = 0x00.
        // Total wire size: exactly 2 bytes (Go: mqttPacketDisconnect = 0xe0).
        ReadOnlySpan<byte> bytes = [0xE0, 0x00];

        var packet = MqttPacketReader.Read(bytes);

        ((byte)packet.Type).ShouldBe((byte)14); // MqttControlPacketType.Disconnect = 14
        packet.Type.ShouldBe(MqttControlPacketType.Disconnect);
        packet.Flags.ShouldBe((byte)0x00);
        packet.RemainingLength.ShouldBe(0);
        packet.Payload.Length.ShouldBe(0);
    }

    [Fact]
    public void Pingreq_packet_is_two_bytes_with_zero_remaining_length()
    {
        // PINGREQ fixed header = 0xC0, remaining length = 0x00.
        // Go: mqttPacketPing = 0xc0.
        ReadOnlySpan<byte> bytes = [0xC0, 0x00];

        var packet = MqttPacketReader.Read(bytes);

        packet.Type.ShouldBe(MqttControlPacketType.PingReq);
        packet.Flags.ShouldBe((byte)0x00);
        packet.RemainingLength.ShouldBe(0);
        packet.Payload.Length.ShouldBe(0);
    }

    [Fact]
    public void Pingresp_packet_is_two_bytes_with_zero_remaining_length()
    {
        // PINGRESP fixed header = 0xD0, remaining length = 0x00.
        // Go: mqttPacketPingResp = 0xd0.
        ReadOnlySpan<byte> bytes = [0xD0, 0x00];

        var packet = MqttPacketReader.Read(bytes);

        packet.Type.ShouldBe(MqttControlPacketType.PingResp);
        packet.RemainingLength.ShouldBe(0);
    }

    // -------------------------------------------------------------------------
    // 5. Remaining length encoding edge cases (Go TestMQTTWriter VarInt table)
    // -------------------------------------------------------------------------
    // Go test: ints = {0,1,127,128,16383,16384,2097151,2097152,268435455}
    //          lens = {1,1,1,  2,  2,    3,    3,      4,      4}

    [Theory]
    [InlineData(0, 1, new byte[] { 0x00 })]
    [InlineData(1, 1, new byte[] { 0x01 })]
    [InlineData(127, 1, new byte[] { 0x7F })]
    [InlineData(128, 2, new byte[] { 0x80, 0x01 })]
    [InlineData(16383, 2, new byte[] { 0xFF, 0x7F })]
    [InlineData(16384, 3, new byte[] { 0x80, 0x80, 0x01 })]
    [InlineData(2097151, 3, new byte[] { 0xFF, 0xFF, 0x7F })]
    [InlineData(2097152, 4, new byte[] { 0x80, 0x80, 0x80, 0x01 })]
    [InlineData(268435455, 4, new byte[] { 0xFF, 0xFF, 0xFF, 0x7F })]
    public void Remaining_length_encodes_to_correct_byte_count_and_bytes(
        int value, int expectedByteCount, byte[] expectedBytes)
    {
        var encoded = MqttPacketWriter.EncodeRemainingLength(value);

        encoded.Length.ShouldBe(expectedByteCount);
        encoded.ShouldBe(expectedBytes);
    }

    [Theory]
    [InlineData(new byte[] { 0x00 }, 0)]
    [InlineData(new byte[] { 0x01 }, 1)]
    [InlineData(new byte[] { 0x7F }, 127)]
    [InlineData(new byte[] { 0x80, 0x01 }, 128)]
    [InlineData(new byte[] { 0xFF, 0x7F }, 16383)]
    [InlineData(new byte[] { 0x80, 0x80, 0x01 }, 16384)]
    [InlineData(new byte[] { 0xFF, 0xFF, 0x7F }, 2097151)]
    [InlineData(new byte[] { 0x80, 0x80, 0x80, 0x01 }, 2097152)]
    [InlineData(new byte[] { 0xFF, 0xFF, 0xFF, 0x7F }, 268435455)]
    public void Remaining_length_decodes_from_correct_byte_sequences(byte[] encoded, int expectedValue)
    {
        var decoded = MqttPacketReader.DecodeRemainingLength(encoded, out var consumed);

        decoded.ShouldBe(expectedValue);
        consumed.ShouldBe(encoded.Length);
    }

    [Fact]
    public void Remaining_length_two_byte_encoding_round_trips_through_reader()
    {
        // Go TestMQTTReader: r.reset([]byte{0x82, 0xff, 0x3}); expects l == 0xff82
        // 0x82 0xFF 0x03 → value = (0x02) + (0x7F * 128) + (0x03 * 16384)
        //                        = 2 + 16256 + 49152 = 65410 = 0xFF82
        ReadOnlySpan<byte> encoded = [0x82, 0xFF, 0x03];

        var value = MqttPacketReader.DecodeRemainingLength(encoded, out var consumed);

        value.ShouldBe(0xFF82);
        consumed.ShouldBe(3);
    }

    [Fact]
    public void Writer_round_trips_remaining_length_through_reader_for_all_boundary_values()
    {
        // Mirrors the Go TestMQTTWriter loop: encode then decode each boundary value.
        int[] values = [0, 1, 127, 128, 16383, 16384, 2097151, 2097152, 268435455];

        foreach (var v in values)
        {
            var encoded = MqttPacketWriter.EncodeRemainingLength(v);
            var decoded = MqttPacketReader.DecodeRemainingLength(encoded, out _);
            decoded.ShouldBe(v, $"Round-trip failed for value {v}");
        }
    }

    // -------------------------------------------------------------------------
    // 6. Invalid packet handling
    // -------------------------------------------------------------------------

    [Fact]
    public void Read_throws_on_buffer_shorter_than_two_bytes()
    {
        // Any MQTT packet must have at least 2 bytes (fixed header + remaining length byte).
        // Use byte[] so the array can be captured inside the Should.Throw lambda.
        byte[] tooShort = [0x10];

        var ex = Should.Throw<FormatException>(() => MqttPacketReader.Read(tooShort));
        ex.Message.ShouldContain("shorter than fixed header");
    }

    [Fact]
    public void Read_throws_on_empty_buffer()
    {
        byte[] empty = [];

        Should.Throw<FormatException>(() => MqttPacketReader.Read(empty));
    }

    [Fact]
    public void Read_throws_when_remaining_length_exceeds_buffer()
    {
        // Fixed header says remaining length = 10, but only 2 extra bytes are provided.
        byte[] truncated = [0x30, 0x0A, 0x00, 0x02];

        Should.Throw<FormatException>(() => MqttPacketReader.Read(truncated));
    }

    [Fact]
    public void Read_throws_on_malformed_five_byte_varint_remaining_length()
    {
        // Go TestMQTTReader: r.reset([]byte{0xff, 0xff, 0xff, 0xff, 0xff}); expects "malformed" error.
        // Five continuation bytes with no terminator — the MQTT spec caps remaining-length at 4 bytes.
        // We embed this after a valid type byte to exercise the length-decode path.
        byte[] malformed = [0x30, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF];

        Should.Throw<FormatException>(() => MqttPacketReader.Read(malformed));
    }

    [Fact]
    public void Remaining_length_encoder_throws_on_negative_value()
    {
        Should.Throw<ArgumentOutOfRangeException>(
            () => MqttPacketWriter.EncodeRemainingLength(-1));
    }

    [Fact]
    public void Remaining_length_encoder_throws_on_value_exceeding_maximum()
    {
        // Maximum MQTT remaining length is 268435455 (0x0FFFFFFF).
        Should.Throw<ArgumentOutOfRangeException>(
            () => MqttPacketWriter.EncodeRemainingLength(268_435_456));
    }

    // -------------------------------------------------------------------------
    // 7. Round-trip: writer → reader
    // -------------------------------------------------------------------------

    [Fact]
    public void Puback_packet_round_trips_through_writer_and_reader()
    {
        // PUBACK carries a 2-byte packet identifier in its payload (remaining length = 2).
        ReadOnlySpan<byte> piPayload = [0x00, 0x07]; // packet-id = 7

        var encoded = MqttPacketWriter.Write(MqttControlPacketType.PubAck, piPayload);
        var decoded = MqttPacketReader.Read(encoded);

        decoded.Type.ShouldBe(MqttControlPacketType.PubAck);
        decoded.RemainingLength.ShouldBe(2);
        decoded.Payload.Span[0].ShouldBe((byte)0x00);
        decoded.Payload.Span[1].ShouldBe((byte)0x07);
    }

    [Fact]
    public void Subscribe_packet_round_trips_with_flags_preserved()
    {
        // SUBSCRIBE requires flags = 0x02 per the MQTT 3.1.1 spec.
        ReadOnlySpan<byte> subPayload =
        [
            0x00, 0x05,                             // packet-id 5
            0x00, 0x03, (byte)'a', (byte)'/', (byte)'b',  // topic "a/b"
            0x01,                                   // QoS 1
        ];

        var encoded = MqttPacketWriter.Write(MqttControlPacketType.Subscribe, subPayload, flags: 0x02);
        var decoded = MqttPacketReader.Read(encoded);

        decoded.Type.ShouldBe(MqttControlPacketType.Subscribe);
        decoded.Flags.ShouldBe((byte)0x02);
        decoded.RemainingLength.ShouldBe(subPayload.Length);
    }

    [Fact]
    public void Large_publish_payload_remaining_length_encodes_to_two_bytes()
    {
        // A 130-byte payload requires a 2-byte remaining-length encoding
        // (128 = 0x80 0x01; anything ≥ 128 crosses the 1-byte boundary).
        var payload = new byte[130];
        payload.AsSpan().Fill(0xAB);

        var encoded = MqttPacketWriter.Write(MqttControlPacketType.Publish, payload);

        // Byte 0: fixed header 0x30 (PUBLISH, QoS 0)
        encoded[0].ShouldBe((byte)0x30);
        // Bytes 1-2: remaining length 130 encoded as 0x82 0x01
        encoded[1].ShouldBe((byte)0x82);
        encoded[2].ShouldBe((byte)0x01);

        var decoded = MqttPacketReader.Read(encoded);
        decoded.RemainingLength.ShouldBe(130);
        decoded.Payload.Length.ShouldBe(130);
    }
}
