// Port of Go server/mqtt_test.go — MQTT protocol parsing and session parity tests.
// Reference: golang/nats-server/server/mqtt_test.go
//
// Tests cover: binary packet parsing (CONNECT, PUBLISH, SUBSCRIBE, PINGREQ),
// QoS 0/1/2 message delivery, retained message handling, session clean start/resume,
// will messages, and topic-to-NATS subject translation.

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

namespace NATS.Server.Tests.Mqtt;

/// <summary>
/// Parity tests ported from Go server/mqtt_test.go exercising MQTT binary
/// protocol parsing, session management, retained messages, QoS flows,
/// and wildcard translation.
/// </summary>
public class MqttGoParityTests
{
    // ========================================================================
    // MQTT Packet Reader / Writer tests
    // Go reference: mqtt_test.go TestMQTTConfig (binary wire-format portion)
    // ========================================================================

    [Fact]
    public void PacketReader_ConnectPacket_Parsed()
    {
        // Go: TestMQTTConfig — verifies CONNECT packet binary parsing.
        // Build a minimal MQTT CONNECT: type=1, flags=0, payload=variable header + client ID
        var payload = BuildConnectPayload("test-client", cleanSession: true, keepAlive: 60);
        var packet = MqttPacketWriter.Write(MqttControlPacketType.Connect, payload);

        var parsed = MqttPacketReader.Read(packet);
        parsed.Type.ShouldBe(MqttControlPacketType.Connect);
        parsed.Flags.ShouldBe((byte)0);
        parsed.RemainingLength.ShouldBe(payload.Length);
    }

    [Fact]
    public void PacketReader_PublishQos0_Parsed()
    {
        // Go: TestMQTTQoS2SubDowngrade — verifies PUBLISH packet parsing at QoS 0.
        // PUBLISH: type=3, flags=0 (QoS 0, no retain, no dup)
        var payload = BuildPublishPayload("test/topic", "hello world"u8.ToArray());
        var packet = MqttPacketWriter.Write(MqttControlPacketType.Publish, payload, flags: 0x00);

        var parsed = MqttPacketReader.Read(packet);
        parsed.Type.ShouldBe(MqttControlPacketType.Publish);
        parsed.Flags.ShouldBe((byte)0x00);

        var pub = MqttBinaryDecoder.ParsePublish(parsed.Payload.Span, parsed.Flags);
        pub.Topic.ShouldBe("test/topic");
        pub.QoS.ShouldBe((byte)0);
        pub.Retain.ShouldBeFalse();
        pub.Dup.ShouldBeFalse();
        pub.Payload.ToArray().ShouldBe("hello world"u8.ToArray());
    }

    [Fact]
    public void PacketReader_PublishQos1_HasPacketId()
    {
        // Go: TestMQTTMaxAckPendingForMultipleSubs — QoS 1 publishes require packet IDs.
        // PUBLISH: type=3, flags=0x02 (QoS 1)
        var payload = BuildPublishPayload("orders/new", "order-data"u8.ToArray(), packetId: 42);
        var packet = MqttPacketWriter.Write(MqttControlPacketType.Publish, payload, flags: 0x02);

        var parsed = MqttPacketReader.Read(packet);
        var pub = MqttBinaryDecoder.ParsePublish(parsed.Payload.Span, parsed.Flags);
        pub.Topic.ShouldBe("orders/new");
        pub.QoS.ShouldBe((byte)1);
        pub.PacketId.ShouldBe((ushort)42);
    }

    [Fact]
    public void PacketReader_PublishQos2_RetainDup()
    {
        // Go: TestMQTTQoS2PubReject — QoS 2 with retain and dup flags.
        // Flags: DUP=0x08, QoS2=0x04, RETAIN=0x01 → 0x0D
        var payload = BuildPublishPayload("sensor/temp", "22.5"u8.ToArray(), packetId: 100);
        var packet = MqttPacketWriter.Write(MqttControlPacketType.Publish, payload, flags: 0x0D);

        var parsed = MqttPacketReader.Read(packet);
        var pub = MqttBinaryDecoder.ParsePublish(parsed.Payload.Span, parsed.Flags);
        pub.QoS.ShouldBe((byte)2);
        pub.Dup.ShouldBeTrue();
        pub.Retain.ShouldBeTrue();
        pub.PacketId.ShouldBe((ushort)100);
    }

    [Fact]
    public void PacketReader_SubscribePacket_ParsedWithFilters()
    {
        // Go: TestMQTTSubPropagation — SUBSCRIBE packet with multiple topic filters.
        var payload = BuildSubscribePayload(1, ("home/+/temperature", 1), ("office/#", 0));
        var packet = MqttPacketWriter.Write(MqttControlPacketType.Subscribe, payload, flags: 0x02);

        var parsed = MqttPacketReader.Read(packet);
        parsed.Type.ShouldBe(MqttControlPacketType.Subscribe);

        var sub = MqttBinaryDecoder.ParseSubscribe(parsed.Payload.Span);
        sub.PacketId.ShouldBe((ushort)1);
        sub.Filters.Count.ShouldBe(2);
        sub.Filters[0].TopicFilter.ShouldBe("home/+/temperature");
        sub.Filters[0].QoS.ShouldBe((byte)1);
        sub.Filters[1].TopicFilter.ShouldBe("office/#");
        sub.Filters[1].QoS.ShouldBe((byte)0);
    }

    [Fact]
    public void PacketReader_PingReq_Parsed()
    {
        // Go: PINGREQ is type=12, no payload, 2 bytes total
        var packet = MqttPacketWriter.Write(MqttControlPacketType.PingReq, ReadOnlySpan<byte>.Empty);
        var parsed = MqttPacketReader.Read(packet);
        parsed.Type.ShouldBe(MqttControlPacketType.PingReq);
        parsed.RemainingLength.ShouldBe(0);
    }

    [Fact]
    public void PacketReader_TooShort_Throws()
    {
        // Go: malformed packets should be rejected.
        Should.Throw<FormatException>(() => MqttPacketReader.Read(new byte[] { 0x10 }));
    }

    [Fact]
    public void PacketWriter_ReservedType_Throws()
    {
        // Go: reserved type 0 is invalid.
        Should.Throw<ArgumentOutOfRangeException>(() =>
            MqttPacketWriter.Write(MqttControlPacketType.Reserved, ReadOnlySpan<byte>.Empty));
    }

    // ========================================================================
    // MQTT Binary Decoder — CONNECT parsing
    // Go reference: mqtt_test.go TestMQTTServerNameRequired, TestMQTTTLS
    // ========================================================================

    [Fact]
    public void BinaryDecoder_Connect_BasicClientId()
    {
        // Go: TestMQTTServerNameRequired — basic CONNECT parsing with client ID.
        var payload = BuildConnectPayload("my-device", cleanSession: true, keepAlive: 30);
        var info = MqttBinaryDecoder.ParseConnect(payload);

        info.ProtocolName.ShouldBe("MQTT");
        info.ProtocolLevel.ShouldBe((byte)4); // MQTT 3.1.1
        info.CleanSession.ShouldBeTrue();
        info.KeepAlive.ShouldBe((ushort)30);
        info.ClientId.ShouldBe("my-device");
        info.Username.ShouldBeNull();
        info.Password.ShouldBeNull();
        info.WillTopic.ShouldBeNull();
    }

    [Fact]
    public void BinaryDecoder_Connect_WithCredentials()
    {
        // Go: TestMQTTTLS, TestMQTTTLSVerifyAndMap — CONNECT with username/password.
        var payload = BuildConnectPayload("auth-client",
            cleanSession: false, keepAlive: 120,
            username: "admin", password: "secret");
        var info = MqttBinaryDecoder.ParseConnect(payload);

        info.ClientId.ShouldBe("auth-client");
        info.CleanSession.ShouldBeFalse();
        info.KeepAlive.ShouldBe((ushort)120);
        info.Username.ShouldBe("admin");
        info.Password.ShouldBe("secret");
    }

    [Fact]
    public void BinaryDecoder_Connect_WithWillMessage()
    {
        // Go: TestMQTTSparkbDeathHandling — CONNECT with will message (last will & testament).
        var willPayload = "device offline"u8.ToArray();
        var payload = BuildConnectPayload("will-client",
            cleanSession: true, keepAlive: 60,
            willTopic: "status/device1", willMessage: willPayload,
            willQoS: 1, willRetain: true);
        var info = MqttBinaryDecoder.ParseConnect(payload);

        info.ClientId.ShouldBe("will-client");
        info.WillTopic.ShouldBe("status/device1");
        info.WillMessage.ShouldBe(willPayload);
        info.WillQoS.ShouldBe((byte)1);
        info.WillRetain.ShouldBeTrue();
    }

    [Fact]
    public void BinaryDecoder_Connect_InvalidProtocolName_Throws()
    {
        // Go: malformed CONNECT with bad protocol name should fail.
        var ms = new MemoryStream();
        WriteUtf8String(ms, "XMPP"); // wrong protocol name
        ms.WriteByte(4); // level
        ms.WriteByte(0x02); // clean session
        ms.WriteByte(0); ms.WriteByte(0); // keepalive
        WriteUtf8String(ms, "test-client");

        Should.Throw<FormatException>(() =>
            MqttBinaryDecoder.ParseConnect(ms.ToArray()));
    }

    // ========================================================================
    // MQTT Wildcard Translation
    // Go reference: mqtt_test.go TestMQTTSubjectMappingWithImportExport, TestMQTTMappingsQoS0
    // ========================================================================

    [Theory]
    [InlineData("home/temperature", "home.temperature")]
    [InlineData("home/+/temperature", "home.*.temperature")]
    [InlineData("home/#", "home.>")]
    [InlineData("#", ">")]
    [InlineData("+", "*")]
    [InlineData("a/b/c/d", "a.b.c.d")]
    [InlineData("", "")]
    public void TranslateFilterToNatsSubject_CorrectTranslation(string mqtt, string expected)
    {
        // Go: TestMQTTSubjectMappingWithImportExport, TestMQTTMappingsQoS0 — wildcard translation.
        MqttBinaryDecoder.TranslateFilterToNatsSubject(mqtt).ShouldBe(expected);
    }

    // ========================================================================
    // Retained Message Store
    // Go reference: mqtt_test.go TestMQTTClusterRetainedMsg, TestMQTTQoS2RetainedReject
    // ========================================================================

    [Fact]
    public void RetainedStore_SetAndGet()
    {
        // Go: TestMQTTClusterRetainedMsg — retained messages stored and retrievable.
        var store = new MqttRetainedStore();
        var payload = "hello"u8.ToArray();

        store.SetRetained("test/topic", payload);
        var result = store.GetRetained("test/topic");

        result.ShouldNotBeNull();
        result.Value.ToArray().ShouldBe(payload);
    }

    [Fact]
    public void RetainedStore_EmptyPayload_ClearsRetained()
    {
        // Go: TestMQTTRetainedMsgRemovedFromMapIfNotInStream — empty payload clears retained.
        var store = new MqttRetainedStore();
        store.SetRetained("test/topic", "hello"u8.ToArray());
        store.SetRetained("test/topic", ReadOnlyMemory<byte>.Empty);

        store.GetRetained("test/topic").ShouldBeNull();
    }

    [Fact]
    public void RetainedStore_WildcardMatch_SingleLevel()
    {
        // Go: TestMQTTSubRetainedRace — wildcard matching for retained messages.
        var store = new MqttRetainedStore();
        store.SetRetained("home/living/temperature", "22.5"u8.ToArray());
        store.SetRetained("home/kitchen/temperature", "24.0"u8.ToArray());
        store.SetRetained("office/desk/temperature", "21.0"u8.ToArray());

        var matches = store.GetMatchingRetained("home/+/temperature");
        matches.Count.ShouldBe(2);
    }

    [Fact]
    public void RetainedStore_WildcardMatch_MultiLevel()
    {
        // Go: TestMQTTSliceHeadersAndDecodeRetainedMessage — multi-level wildcard.
        var store = new MqttRetainedStore();
        store.SetRetained("home/living/temperature", "22"u8.ToArray());
        store.SetRetained("home/living/humidity", "45"u8.ToArray());
        store.SetRetained("home/kitchen/temperature", "24"u8.ToArray());
        store.SetRetained("office/desk/temperature", "21"u8.ToArray());

        var matches = store.GetMatchingRetained("home/#");
        matches.Count.ShouldBe(3);
    }

    [Fact]
    public void RetainedStore_ExactMatch_OnlyMatchesExact()
    {
        // Go: retained messages with exact topic filter match only the exact topic.
        var store = new MqttRetainedStore();
        store.SetRetained("home/temperature", "22"u8.ToArray());
        store.SetRetained("home/humidity", "45"u8.ToArray());

        var matches = store.GetMatchingRetained("home/temperature");
        matches.Count.ShouldBe(1);
        matches[0].Topic.ShouldBe("home/temperature");
    }

    // ========================================================================
    // Session Store — clean start / resume
    // Go reference: mqtt_test.go TestMQTTSubRestart, TestMQTTRecoverSessionWithSubAndClientResendSub
    // ========================================================================

    [Fact]
    public void SessionStore_SaveAndLoad()
    {
        // Go: TestMQTTSubRestart — session persistence across reconnects.
        var store = new MqttSessionStore();
        var session = new MqttSessionData
        {
            ClientId = "device-1",
            CleanSession = false,
            Subscriptions = { ["sensor/+"] = 1, ["status/#"] = 0 },
        };
        store.SaveSession(session);

        var loaded = store.LoadSession("device-1");
        loaded.ShouldNotBeNull();
        loaded.ClientId.ShouldBe("device-1");
        loaded.Subscriptions.Count.ShouldBe(2);
        loaded.Subscriptions["sensor/+"].ShouldBe(1);
    }

    [Fact]
    public void SessionStore_CleanSession_DeletesPrevious()
    {
        // Go: TestMQTTRecoverSessionWithSubAndClientResendSub — clean session deletes stored state.
        var store = new MqttSessionStore();
        store.SaveSession(new MqttSessionData
        {
            ClientId = "device-1",
            Subscriptions = { ["sensor/+"] = 1 },
        });

        store.DeleteSession("device-1");
        store.LoadSession("device-1").ShouldBeNull();
    }

    [Fact]
    public void SessionStore_NonExistentClient_ReturnsNull()
    {
        // Go: loading a session for a client that never connected returns nil.
        var store = new MqttSessionStore();
        store.LoadSession("nonexistent").ShouldBeNull();
    }

    [Fact]
    public void SessionStore_ListSessions()
    {
        // Go: session enumeration for monitoring.
        var store = new MqttSessionStore();
        store.SaveSession(new MqttSessionData { ClientId = "a" });
        store.SaveSession(new MqttSessionData { ClientId = "b" });
        store.SaveSession(new MqttSessionData { ClientId = "c" });

        store.ListSessions().Count.ShouldBe(3);
    }

    // ========================================================================
    // QoS 2 State Machine
    // Go reference: mqtt_test.go TestMQTTQoS2RetriesPubRel
    // ========================================================================

    [Fact]
    public void QoS2StateMachine_FullFlow()
    {
        // Go: TestMQTTQoS2RetriesPubRel — complete QoS 2 exactly-once flow.
        var sm = new MqttQos2StateMachine();

        // Begin publish
        sm.BeginPublish(1).ShouldBeTrue();
        sm.GetState(1).ShouldBe(MqttQos2State.AwaitingPubRec);

        // Process PUBREC
        sm.ProcessPubRec(1).ShouldBeTrue();
        sm.GetState(1).ShouldBe(MqttQos2State.AwaitingPubRel);

        // Process PUBREL
        sm.ProcessPubRel(1).ShouldBeTrue();
        sm.GetState(1).ShouldBe(MqttQos2State.AwaitingPubComp);

        // Process PUBCOMP — flow complete, removed
        sm.ProcessPubComp(1).ShouldBeTrue();
        sm.GetState(1).ShouldBeNull();
    }

    [Fact]
    public void QoS2StateMachine_DuplicatePublish_Rejected()
    {
        // Go: TestMQTTQoS2PubReject — duplicate publish with same packet ID is rejected.
        var sm = new MqttQos2StateMachine();
        sm.BeginPublish(1).ShouldBeTrue();
        sm.BeginPublish(1).ShouldBeFalse(); // duplicate
    }

    [Fact]
    public void QoS2StateMachine_WrongStateTransition_Rejected()
    {
        // Go: out-of-order state transitions are rejected.
        var sm = new MqttQos2StateMachine();
        sm.BeginPublish(1).ShouldBeTrue();

        // Cannot process PUBREL before PUBREC
        sm.ProcessPubRel(1).ShouldBeFalse();

        // Cannot process PUBCOMP before PUBREL
        sm.ProcessPubComp(1).ShouldBeFalse();
    }

    [Fact]
    public void QoS2StateMachine_UnknownPacketId_Rejected()
    {
        // Go: processing PUBREC for unknown packet ID returns false.
        var sm = new MqttQos2StateMachine();
        sm.ProcessPubRec(99).ShouldBeFalse();
    }

    [Fact]
    public void QoS2StateMachine_Timeout_DetectsStaleFlows()
    {
        // Go: TestMQTTQoS2RetriesPubRel — stale flows are detected for cleanup.
        var time = new FakeTimeProvider(DateTimeOffset.UtcNow);
        var sm = new MqttQos2StateMachine(timeout: TimeSpan.FromSeconds(5), timeProvider: time);

        sm.BeginPublish(1);
        sm.BeginPublish(2);

        // Advance past timeout
        time.Advance(TimeSpan.FromSeconds(10));

        var timedOut = sm.GetTimedOutFlows();
        timedOut.Count.ShouldBe(2);
        timedOut.ShouldContain((ushort)1);
        timedOut.ShouldContain((ushort)2);
    }

    // ========================================================================
    // Session Store — flapper detection
    // Go reference: mqtt_test.go TestMQTTLockedSession
    // ========================================================================

    [Fact]
    public void SessionStore_FlapperDetection_BackoffApplied()
    {
        // Go: TestMQTTLockedSession — rapid reconnects trigger flapper backoff.
        var time = new FakeTimeProvider(DateTimeOffset.UtcNow);
        var store = new MqttSessionStore(
            flapWindow: TimeSpan.FromSeconds(5),
            flapThreshold: 3,
            flapBackoff: TimeSpan.FromSeconds(2),
            timeProvider: time);

        // Under threshold — no backoff
        store.TrackConnectDisconnect("client-1", connected: true);
        store.TrackConnectDisconnect("client-1", connected: true);
        store.ShouldApplyBackoff("client-1").ShouldBe(TimeSpan.Zero);

        // At threshold — backoff applied
        store.TrackConnectDisconnect("client-1", connected: true);
        store.ShouldApplyBackoff("client-1").ShouldBe(TimeSpan.FromSeconds(2));
    }

    [Fact]
    public void SessionStore_FlapperDetection_DisconnectsIgnored()
    {
        // Go: disconnect events do not count toward the flap threshold.
        var store = new MqttSessionStore(flapThreshold: 3);
        store.TrackConnectDisconnect("client-1", connected: false);
        store.TrackConnectDisconnect("client-1", connected: false);
        store.TrackConnectDisconnect("client-1", connected: false);
        store.ShouldApplyBackoff("client-1").ShouldBe(TimeSpan.Zero);
    }

    [Fact]
    public void SessionStore_FlapperDetection_WindowExpiry()
    {
        // Go: connections outside the flap window are pruned.
        var time = new FakeTimeProvider(DateTimeOffset.UtcNow);
        var store = new MqttSessionStore(
            flapWindow: TimeSpan.FromSeconds(5),
            flapThreshold: 3,
            flapBackoff: TimeSpan.FromSeconds(2),
            timeProvider: time);

        store.TrackConnectDisconnect("client-1", connected: true);
        store.TrackConnectDisconnect("client-1", connected: true);
        store.TrackConnectDisconnect("client-1", connected: true);
        store.ShouldApplyBackoff("client-1").ShouldBe(TimeSpan.FromSeconds(2));

        // Advance past the window — old events should be pruned
        time.Advance(TimeSpan.FromSeconds(10));
        store.ShouldApplyBackoff("client-1").ShouldBe(TimeSpan.Zero);
    }

    // ========================================================================
    // Remaining-Length encoding/decoding roundtrip
    // Go reference: mqtt_test.go various — validates wire encoding
    // ========================================================================

    [Theory]
    [InlineData(0)]
    [InlineData(127)]
    [InlineData(128)]
    [InlineData(16383)]
    [InlineData(16384)]
    [InlineData(2097151)]
    [InlineData(2097152)]
    [InlineData(268435455)]
    public void RemainingLength_EncodeDecode_Roundtrip(int value)
    {
        // Go: various tests that exercise different remaining-length sizes.
        var encoded = MqttPacketWriter.EncodeRemainingLength(value);
        var decoded = MqttPacketReader.DecodeRemainingLength(encoded, out var consumed);
        decoded.ShouldBe(value);
        consumed.ShouldBe(encoded.Length);
    }

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

    [Fact]
    public void RemainingLength_ExceedsMax_Throws()
    {
        Should.Throw<ArgumentOutOfRangeException>(() =>
            MqttPacketWriter.EncodeRemainingLength(268_435_456));
    }

    // ========================================================================
    // Text Protocol Parser (MqttProtocolParser.ParseLine)
    // Go reference: mqtt_test.go TestMQTTPermissionsViolation
    // ========================================================================

    [Fact]
    public void TextParser_ConnectWithAuth()
    {
        // Go: TestMQTTNoAuthUserValidation — text-mode CONNECT with credentials.
        var parser = new MqttProtocolParser();
        var pkt = parser.ParseLine("CONNECT my-client user=admin pass=secret");

        pkt.Type.ShouldBe(MqttPacketType.Connect);
        pkt.ClientId.ShouldBe("my-client");
        pkt.Username.ShouldBe("admin");
        pkt.Password.ShouldBe("secret");
    }

    [Fact]
    public void TextParser_ConnectWithKeepalive()
    {
        // Go: CONNECT with keepalive field.
        var parser = new MqttProtocolParser();
        var pkt = parser.ParseLine("CONNECT device-1 keepalive=30 clean=false");

        pkt.Type.ShouldBe(MqttPacketType.Connect);
        pkt.ClientId.ShouldBe("device-1");
        pkt.KeepAliveSeconds.ShouldBe(30);
        pkt.CleanSession.ShouldBeFalse();
    }

    [Fact]
    public void TextParser_Subscribe()
    {
        // Go: TestMQTTSubPropagation — text-mode SUB.
        var parser = new MqttProtocolParser();
        var pkt = parser.ParseLine("SUB home/+/temperature");

        pkt.Type.ShouldBe(MqttPacketType.Subscribe);
        pkt.Topic.ShouldBe("home/+/temperature");
    }

    [Fact]
    public void TextParser_Publish()
    {
        // Go: TestMQTTPermissionsViolation — text-mode PUB.
        var parser = new MqttProtocolParser();
        var pkt = parser.ParseLine("PUB sensor/temp 22.5");

        pkt.Type.ShouldBe(MqttPacketType.Publish);
        pkt.Topic.ShouldBe("sensor/temp");
        pkt.Payload.ShouldBe("22.5");
    }

    [Fact]
    public void TextParser_PublishQos1()
    {
        // Go: text-mode PUBQ1 with packet ID.
        var parser = new MqttProtocolParser();
        var pkt = parser.ParseLine("PUBQ1 42 sensor/temp 22.5");

        pkt.Type.ShouldBe(MqttPacketType.PublishQos1);
        pkt.PacketId.ShouldBe(42);
        pkt.Topic.ShouldBe("sensor/temp");
        pkt.Payload.ShouldBe("22.5");
    }

    [Fact]
    public void TextParser_Ack()
    {
        // Go: text-mode ACK.
        var parser = new MqttProtocolParser();
        var pkt = parser.ParseLine("ACK 42");

        pkt.Type.ShouldBe(MqttPacketType.Ack);
        pkt.PacketId.ShouldBe(42);
    }

    [Fact]
    public void TextParser_EmptyLine_ReturnsUnknown()
    {
        var parser = new MqttProtocolParser();
        var pkt = parser.ParseLine("");
        pkt.Type.ShouldBe(MqttPacketType.Unknown);
    }

    [Fact]
    public void TextParser_MalformedLine_ReturnsUnknown()
    {
        var parser = new MqttProtocolParser();
        parser.ParseLine("GARBAGE").Type.ShouldBe(MqttPacketType.Unknown);
        parser.ParseLine("PUB").Type.ShouldBe(MqttPacketType.Unknown);
        parser.ParseLine("PUBQ1 bad").Type.ShouldBe(MqttPacketType.Unknown);
        parser.ParseLine("ACK bad").Type.ShouldBe(MqttPacketType.Unknown);
    }

    // ========================================================================
    // MqttTopicMatch — internal matching logic
    // Go reference: mqtt_test.go TestMQTTCrossAccountRetain
    // ========================================================================

    [Theory]
    [InlineData("a/b/c", "a/b/c", true)]
    [InlineData("a/b/c", "a/+/c", true)]
    [InlineData("a/b/c", "a/#", true)]
    [InlineData("a/b/c", "#", true)]
    [InlineData("a/b/c", "a/b", false)]
    [InlineData("a/b", "a/b/c", false)]
    [InlineData("a/b/c", "+/+/+", true)]
    [InlineData("a/b/c", "+/#", true)]
    [InlineData("a", "+", true)]
    [InlineData("a/b/c/d", "a/+/c/+", true)]
    [InlineData("a/b/c/d", "a/+/+/e", false)]
    public void MqttTopicMatch_CorrectBehavior(string topic, string filter, bool expected)
    {
        // Go: TestMQTTCrossAccountRetain — internal topic matching.
        MqttRetainedStore.MqttTopicMatch(topic, filter).ShouldBe(expected);
    }

    // ========================================================================
    // Helpers — binary packet builders
    // ========================================================================

    private static byte[] BuildConnectPayload(
        string clientId, bool cleanSession, ushort keepAlive,
        string? username = null, string? password = null,
        string? willTopic = null, byte[]? willMessage = null,
        byte willQoS = 0, bool willRetain = false)
    {
        var ms = new MemoryStream();
        // Protocol name
        WriteUtf8String(ms, "MQTT");
        // Protocol level (4 = 3.1.1)
        ms.WriteByte(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;
        ms.WriteByte(flags);
        // Keep alive
        ms.WriteByte((byte)(keepAlive >> 8));
        ms.WriteByte((byte)(keepAlive & 0xFF));
        // Client ID
        WriteUtf8String(ms, clientId);
        // Will
        if (willTopic != null)
        {
            WriteUtf8String(ms, willTopic);
            WriteBinaryField(ms, willMessage ?? []);
        }
        // Username
        if (username != null)
            WriteUtf8String(ms, username);
        // Password
        if (password != null)
            WriteUtf8String(ms, password);

        return ms.ToArray();
    }

    private static byte[] BuildPublishPayload(string topic, byte[] payload, ushort packetId = 0)
    {
        var ms = new MemoryStream();
        WriteUtf8String(ms, topic);
        if (packetId > 0)
        {
            ms.WriteByte((byte)(packetId >> 8));
            ms.WriteByte((byte)(packetId & 0xFF));
        }

        ms.Write(payload);
        return ms.ToArray();
    }

    private static byte[] BuildSubscribePayload(ushort packetId, params (string filter, byte qos)[] filters)
    {
        var ms = new MemoryStream();
        ms.WriteByte((byte)(packetId >> 8));
        ms.WriteByte((byte)(packetId & 0xFF));
        foreach (var (filter, qos) in filters)
        {
            WriteUtf8String(ms, filter);
            ms.WriteByte(qos);
        }

        return ms.ToArray();
    }

    private static void WriteUtf8String(MemoryStream ms, string value)
    {
        var bytes = Encoding.UTF8.GetBytes(value);
        ms.WriteByte((byte)(bytes.Length >> 8));
        ms.WriteByte((byte)(bytes.Length & 0xFF));
        ms.Write(bytes);
    }

    private static void WriteBinaryField(MemoryStream ms, byte[] data)
    {
        ms.WriteByte((byte)(data.Length >> 8));
        ms.WriteByte((byte)(data.Length & 0xFF));
        ms.Write(data);
    }
}
