// Binary MQTT packet parser tests.
// Go reference: golang/nats-server/server/mqtt.go
//   CONNECT parsing   — mqttParseConnect               (~line 700)
//   PUBLISH parsing   — mqttParsePublish                (~line 1200)
//   SUBSCRIBE parsing — mqttParseSub                    (~line 1400)
//   Wildcard translation — mqttToNATSSubjectConversion   (~line 2200)

using System.Text;
using NATS.Server.Mqtt;

namespace NATS.Server.Tests.Mqtt;

public class MqttBinaryParserTests
{
    // =========================================================================
    // Helpers — build well-formed CONNECT packet payloads
    // =========================================================================

    /// <summary>
    /// Builds the payload bytes (everything after the fixed header) of an MQTT
    /// 3.1.1 CONNECT packet.
    /// </summary>
    private static byte[] BuildConnectPayload(
        string clientId,
        bool cleanSession = true,
        ushort keepAlive = 60,
        string? username = null,
        string? password = null,
        string? willTopic = null,
        byte[]? willMessage = null,
        byte willQoS = 0,
        bool willRetain = false)
    {
        using var ms = new System.IO.MemoryStream();
        using var w  = new System.IO.BinaryWriter(ms);

        // Protocol name "MQTT"
        WriteString(w, "MQTT");

        // Protocol level 4 (MQTT 3.1.1)
        w.Write((byte)4);

        // Connect flags
        byte flags = 0;
        if (cleanSession) flags |= 0x02;
        if (willTopic != null) flags |= 0x04;
        flags |= (byte)((willQoS & 0x03) << 3);
        if (willRetain) flags |= 0x20;
        if (password != null) flags |= 0x40;
        if (username != null) flags |= 0x80;
        w.Write(flags);

        // Keep-alive (big-endian)
        WriteUInt16BE(w, keepAlive);

        // Payload fields
        WriteString(w, clientId);

        if (willTopic != null)
        {
            WriteString(w, willTopic);
            WriteBinaryField(w, willMessage ?? []);
        }

        if (username != null) WriteString(w, username);
        if (password != null) WriteString(w, password);

        return ms.ToArray();
    }

    private static void WriteString(System.IO.BinaryWriter w, string value)
    {
        var bytes = Encoding.UTF8.GetBytes(value);
        WriteUInt16BE(w, (ushort)bytes.Length);
        w.Write(bytes);
    }

    private static void WriteBinaryField(System.IO.BinaryWriter w, byte[] data)
    {
        WriteUInt16BE(w, (ushort)data.Length);
        w.Write(data);
    }

    private static void WriteUInt16BE(System.IO.BinaryWriter w, ushort value)
    {
        w.Write((byte)(value >> 8));
        w.Write((byte)(value & 0xFF));
    }

    // =========================================================================
    // 1. ParseConnect — valid packet
    // Go reference: server/mqtt.go mqttParseConnect ~line 700
    // =========================================================================

    [Fact]
    public void ParseConnect_ValidPacket_ReturnsConnectInfo()
    {
        // Go: mqttParseConnect — basic CONNECT with protocol name, level, and empty client ID
        var payload = BuildConnectPayload("test-client", cleanSession: true, keepAlive: 30);

        var info = MqttBinaryDecoder.ParseConnect(payload);

        info.ProtocolName.ShouldBe("MQTT");
        info.ProtocolLevel.ShouldBe((byte)4);
        info.CleanSession.ShouldBeTrue();
        info.KeepAlive.ShouldBe((ushort)30);
        info.ClientId.ShouldBe("test-client");
        info.Username.ShouldBeNull();
        info.Password.ShouldBeNull();
        info.WillTopic.ShouldBeNull();
        info.WillMessage.ShouldBeNull();
    }

    // =========================================================================
    // 2. ParseConnect — with credentials
    // Go reference: server/mqtt.go mqttParseConnect ~line 780
    // =========================================================================

    [Fact]
    public void ParseConnect_WithCredentials()
    {
        // Go: mqttParseConnect — username and password flags set in connect flags byte
        var payload = BuildConnectPayload(
            "cred-client",
            cleanSession: true,
            keepAlive: 60,
            username: "alice",
            password: "s3cr3t");

        var info = MqttBinaryDecoder.ParseConnect(payload);

        info.ClientId.ShouldBe("cred-client");
        info.Username.ShouldBe("alice");
        info.Password.ShouldBe("s3cr3t");
    }

    // =========================================================================
    // 3. ParseConnect — with will message
    // Go reference: server/mqtt.go mqttParseConnect ~line 740
    // =========================================================================

    [Fact]
    public void ParseConnect_WithWillMessage()
    {
        // Go: mqttParseConnect — WillFlag + WillTopic + WillMessage in payload
        var willBytes = Encoding.UTF8.GetBytes("offline");
        var payload = BuildConnectPayload(
            "will-client",
            willTopic: "status/device",
            willMessage: willBytes,
            willQoS: 1,
            willRetain: true);

        var info = MqttBinaryDecoder.ParseConnect(payload);

        info.ClientId.ShouldBe("will-client");
        info.WillTopic.ShouldBe("status/device");
        info.WillMessage.ShouldNotBeNull();
        info.WillMessage!.ShouldBe(willBytes);
        info.WillQoS.ShouldBe((byte)1);
        info.WillRetain.ShouldBeTrue();
    }

    // =========================================================================
    // 4. ParseConnect — clean session flag
    // Go reference: server/mqtt.go mqttParseConnect ~line 710
    // =========================================================================

    [Fact]
    public void ParseConnect_CleanSessionFlag()
    {
        // Go: mqttParseConnect — clean session bit 1 of connect flags
        var withClean    = BuildConnectPayload("c1", cleanSession: true);
        var withoutClean = BuildConnectPayload("c2", cleanSession: false);

        MqttBinaryDecoder.ParseConnect(withClean).CleanSession.ShouldBeTrue();
        MqttBinaryDecoder.ParseConnect(withoutClean).CleanSession.ShouldBeFalse();
    }

    // =========================================================================
    // 5. ParsePublish — QoS 0 (no packet ID)
    // Go reference: server/mqtt.go mqttParsePublish ~line 1200
    // =========================================================================

    [Fact]
    public void ParsePublish_QoS0()
    {
        // Go: mqttParsePublish — QoS 0: no packet identifier present
        // Build payload: 2-byte length + "sensors/temp" + message bytes
        var topic      = "sensors/temp";
        var topicBytes = Encoding.UTF8.GetBytes(topic);
        var message    = Encoding.UTF8.GetBytes("23.5");

        using var ms = new System.IO.MemoryStream();
        using var w  = new System.IO.BinaryWriter(ms);
        WriteUInt16BE(w, (ushort)topicBytes.Length);
        w.Write(topicBytes);
        w.Write(message);
        var payload = ms.ToArray();

        // flags = 0x00 → QoS 0, no DUP, no RETAIN
        var info = MqttBinaryDecoder.ParsePublish(payload, flags: 0x00);

        info.Topic.ShouldBe("sensors/temp");
        info.QoS.ShouldBe((byte)0);
        info.PacketId.ShouldBe((ushort)0);
        info.Dup.ShouldBeFalse();
        info.Retain.ShouldBeFalse();
        Encoding.UTF8.GetString(info.Payload.Span).ShouldBe("23.5");
    }

    // =========================================================================
    // 6. ParsePublish — QoS 1 (has packet ID)
    // Go reference: server/mqtt.go mqttParsePublish ~line 1230
    // =========================================================================

    [Fact]
    public void ParsePublish_QoS1()
    {
        // Go: mqttParsePublish — QoS 1: 2-byte packet identifier follows topic
        var topic      = "events/click";
        var topicBytes = Encoding.UTF8.GetBytes(topic);
        var message    = Encoding.UTF8.GetBytes("payload-data");

        using var ms = new System.IO.MemoryStream();
        using var w  = new System.IO.BinaryWriter(ms);
        WriteUInt16BE(w, (ushort)topicBytes.Length);
        w.Write(topicBytes);
        WriteUInt16BE(w, 42);          // packet ID = 42
        w.Write(message);
        var payload = ms.ToArray();

        // flags = 0x02 → QoS 1 (bits 2-1 = 01)
        var info = MqttBinaryDecoder.ParsePublish(payload, flags: 0x02);

        info.Topic.ShouldBe("events/click");
        info.QoS.ShouldBe((byte)1);
        info.PacketId.ShouldBe((ushort)42);
        Encoding.UTF8.GetString(info.Payload.Span).ShouldBe("payload-data");
    }

    // =========================================================================
    // 7. ParsePublish — retain flag
    // Go reference: server/mqtt.go mqttParsePublish ~line 1210
    // =========================================================================

    [Fact]
    public void ParsePublish_RetainFlag()
    {
        // Go: mqttParsePublish — RETAIN flag is bit 0 of the fixed-header flags nibble
        var topicBytes = Encoding.UTF8.GetBytes("home/light");

        using var ms = new System.IO.MemoryStream();
        using var w  = new System.IO.BinaryWriter(ms);
        WriteUInt16BE(w, (ushort)topicBytes.Length);
        w.Write(topicBytes);
        w.Write(Encoding.UTF8.GetBytes("on"));
        var payload = ms.ToArray();

        // flags = 0x01 → RETAIN set, QoS 0
        var info = MqttBinaryDecoder.ParsePublish(payload, flags: 0x01);

        info.Topic.ShouldBe("home/light");
        info.Retain.ShouldBeTrue();
        info.QoS.ShouldBe((byte)0);
    }

    // =========================================================================
    // 8. ParseSubscribe — single topic
    // Go reference: server/mqtt.go mqttParseSub ~line 1400
    // =========================================================================

    [Fact]
    public void ParseSubscribe_SingleTopic()
    {
        // Go: mqttParseSub — SUBSCRIBE with a single topic filter entry
        // Payload: 2-byte packet-id + (2-byte len + topic + 1-byte QoS) per entry
        using var ms = new System.IO.MemoryStream();
        using var w  = new System.IO.BinaryWriter(ms);

        WriteUInt16BE(w, 7);                     // packet ID = 7
        WriteString(w, "sport/tennis/#");        // topic filter
        w.Write((byte)0);                        // QoS 0

        var payload = ms.ToArray();
        var info = MqttBinaryDecoder.ParseSubscribe(payload);

        info.PacketId.ShouldBe((ushort)7);
        info.Filters.Count.ShouldBe(1);
        info.Filters[0].TopicFilter.ShouldBe("sport/tennis/#");
        info.Filters[0].QoS.ShouldBe((byte)0);
    }

    // =========================================================================
    // 9. ParseSubscribe — multiple topics with different QoS
    // Go reference: server/mqtt.go mqttParseSub ~line 1420
    // =========================================================================

    [Fact]
    public void ParseSubscribe_MultipleTopics()
    {
        // Go: mqttParseSub — multiple topic filter entries in one SUBSCRIBE
        using var ms = new System.IO.MemoryStream();
        using var w  = new System.IO.BinaryWriter(ms);

        WriteUInt16BE(w, 99);                    // packet ID = 99
        WriteString(w, "sensors/+");             // filter 1
        w.Write((byte)0);                        // QoS 0
        WriteString(w, "events/#");              // filter 2
        w.Write((byte)1);                        // QoS 1
        WriteString(w, "alerts/critical");       // filter 3
        w.Write((byte)2);                        // QoS 2

        var payload = ms.ToArray();
        var info = MqttBinaryDecoder.ParseSubscribe(payload);

        info.PacketId.ShouldBe((ushort)99);
        info.Filters.Count.ShouldBe(3);

        info.Filters[0].TopicFilter.ShouldBe("sensors/+");
        info.Filters[0].QoS.ShouldBe((byte)0);

        info.Filters[1].TopicFilter.ShouldBe("events/#");
        info.Filters[1].QoS.ShouldBe((byte)1);

        info.Filters[2].TopicFilter.ShouldBe("alerts/critical");
        info.Filters[2].QoS.ShouldBe((byte)2);
    }

    // =========================================================================
    // 10. TranslateWildcard — '+' → '*'
    // Go reference: server/mqtt.go mqttToNATSSubjectConversion ~line 2200
    // =========================================================================

    [Fact]
    public void TranslateWildcard_Plus()
    {
        // Go: mqttToNATSSubjectConversion — '+' maps to '*' (single-level)
        var result = MqttBinaryDecoder.TranslateFilterToNatsSubject("+");
        result.ShouldBe("*");
    }

    // =========================================================================
    // 11. TranslateWildcard — '#' → '>'
    // Go reference: server/mqtt.go mqttToNATSSubjectConversion ~line 2210
    // =========================================================================

    [Fact]
    public void TranslateWildcard_Hash()
    {
        // Go: mqttToNATSSubjectConversion — '#' maps to '>' (multi-level)
        var result = MqttBinaryDecoder.TranslateFilterToNatsSubject("#");
        result.ShouldBe(">");
    }

    // =========================================================================
    // 12. TranslateWildcard — '/' → '.'
    // Go reference: server/mqtt.go mqttToNATSSubjectConversion ~line 2220
    // =========================================================================

    [Fact]
    public void TranslateWildcard_Slash()
    {
        // Go: mqttToNATSSubjectConversion — '/' separator maps to '.'
        var result = MqttBinaryDecoder.TranslateFilterToNatsSubject("a/b/c");
        result.ShouldBe("a.b.c");
    }

    // =========================================================================
    // 13. TranslateWildcard — complex combined translation
    // Go reference: server/mqtt.go mqttToNATSSubjectConversion ~line 2200
    // =========================================================================

    [Fact]
    public void TranslateWildcard_Complex()
    {
        // Go: mqttToNATSSubjectConversion — combines '/', '+', '#'
        // sport/+/score/#  →  sport.*.score.>
        var result = MqttBinaryDecoder.TranslateFilterToNatsSubject("sport/+/score/#");
        result.ShouldBe("sport.*.score.>");
    }

    // =========================================================================
    // 14. DecodeRemainingLength — multi-byte values (VarInt edge cases)
    // Go reference: server/mqtt.go TestMQTTReader / TestMQTTWriter
    // =========================================================================

    [Theory]
    [InlineData(new byte[] { 0x00 }, 0, 1)]
    [InlineData(new byte[] { 0x01 }, 1, 1)]
    [InlineData(new byte[] { 0x7F }, 127, 1)]
    [InlineData(new byte[] { 0x80, 0x01 }, 128, 2)]
    [InlineData(new byte[] { 0xFF, 0x7F }, 16383, 2)]
    [InlineData(new byte[] { 0x80, 0x80, 0x01 }, 16384, 3)]
    [InlineData(new byte[] { 0xFF, 0xFF, 0x7F }, 2097151, 3)]
    [InlineData(new byte[] { 0x80, 0x80, 0x80, 0x01 }, 2097152, 4)]
    [InlineData(new byte[] { 0xFF, 0xFF, 0xFF, 0x7F }, 268435455, 4)]
    public void DecodeRemainingLength_MultiByteValues(byte[] encoded, int expectedValue, int expectedConsumed)
    {
        // Go TestMQTTReader: verifies variable-length integer decoding at all boundary values
        var value = MqttPacketReader.DecodeRemainingLength(encoded, out var consumed);

        value.ShouldBe(expectedValue);
        consumed.ShouldBe(expectedConsumed);
    }

    // =========================================================================
    // Additional edge-case tests
    // =========================================================================

    [Fact]
    public void ParsePublish_DupFlag_IsSet()
    {
        // DUP flag is bit 3 of the fixed-header flags nibble (0x08).
        // When QoS > 0, a 2-byte packet identifier must follow the topic.
        var topicBytes = Encoding.UTF8.GetBytes("dup/topic");

        using var ms = new System.IO.MemoryStream();
        using var w  = new System.IO.BinaryWriter(ms);
        WriteUInt16BE(w, (ushort)topicBytes.Length);
        w.Write(topicBytes);
        WriteUInt16BE(w, 5);    // packet ID = 5 (required for QoS 1)
        var payload = ms.ToArray();

        // flags = 0x0A → DUP (bit 3) + QoS 1 (bits 2-1)
        var info = MqttBinaryDecoder.ParsePublish(payload, flags: 0x0A);

        info.Dup.ShouldBeTrue();
        info.QoS.ShouldBe((byte)1);
        info.PacketId.ShouldBe((ushort)5);
    }

    [Fact]
    public void ParseConnect_EmptyClientId_IsAllowed()
    {
        // MQTT 3.1.1 §3.1.3.1 allows empty client IDs with CleanSession=true
        var payload = BuildConnectPayload("", cleanSession: true);

        var info = MqttBinaryDecoder.ParseConnect(payload);

        info.ClientId.ShouldBe(string.Empty);
        info.CleanSession.ShouldBeTrue();
    }

    [Fact]
    public void TranslateWildcard_EmptyString_ReturnsEmpty()
    {
        var result = MqttBinaryDecoder.TranslateFilterToNatsSubject(string.Empty);
        result.ShouldBe(string.Empty);
    }

    [Fact]
    public void TranslateWildcard_PlainTopic_NoChange()
    {
        // A topic with no wildcards or slashes should pass through unchanged
        var result = MqttBinaryDecoder.TranslateFilterToNatsSubject("plainword");
        result.ShouldBe("plainword");
    }

    [Fact]
    public void ParsePublish_EmptyPayload_IsAllowed()
    {
        // A PUBLISH with no application payload is valid (e.g. retain-delete)
        var topicBytes = Encoding.UTF8.GetBytes("empty/payload");

        using var ms = new System.IO.MemoryStream();
        using var w  = new System.IO.BinaryWriter(ms);
        WriteUInt16BE(w, (ushort)topicBytes.Length);
        w.Write(topicBytes);
        var payload = ms.ToArray();

        var info = MqttBinaryDecoder.ParsePublish(payload, flags: 0x00);

        info.Topic.ShouldBe("empty/payload");
        info.Payload.Length.ShouldBe(0);
    }
}
