// Copyright 2012-2025 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using System.Text;
using Shouldly;
using ZB.MOM.NatsNet.Server;
using ZB.MOM.NatsNet.Server.Internal;
using ZB.MOM.NatsNet.Server.Protocol;

namespace ZB.MOM.NatsNet.Server.Tests.Protocol;

/// <summary>
/// Tests for the NATS protocol parser.
/// Mirrors Go parser_test.go — 17 test functions.
/// </summary>
public class ProtocolParserTests
{
    // =====================================================================
    // Test helpers — mirrors Go dummyClient/dummyRouteClient
    // =====================================================================

    private static ParseContext DummyClient() => new()
    {
        Kind = ClientKind.Client,
        MaxControlLine = ServerConstants.MaxControlLineSize,
        MaxPayload = -1,
        HasHeaders = false,
    };

    private static ParseContext DummyRouteClient() => new()
    {
        Kind = ClientKind.Router,
        MaxControlLine = ServerConstants.MaxControlLineSize,
        MaxPayload = -1,
    };

    private static TestProtocolHandler DummyHandler() => new();

    // =====================================================================
    // TestParsePing — Go test ID 2598
    // =====================================================================

    [Fact]
    public void ParsePing_ByteByByte()
    {
        var c = DummyClient();
        var h = DummyHandler();
        c.State.ShouldBe(ParserState.OpStart);

        var ping = "PING\r\n"u8.ToArray();

        ProtocolParser.Parse(c, h, ping[..1]).ShouldBeNull();
        c.State.ShouldBe(ParserState.OpP);

        ProtocolParser.Parse(c, h, ping[1..2]).ShouldBeNull();
        c.State.ShouldBe(ParserState.OpPi);

        ProtocolParser.Parse(c, h, ping[2..3]).ShouldBeNull();
        c.State.ShouldBe(ParserState.OpPin);

        ProtocolParser.Parse(c, h, ping[3..4]).ShouldBeNull();
        c.State.ShouldBe(ParserState.OpPing);

        ProtocolParser.Parse(c, h, ping[4..5]).ShouldBeNull();
        c.State.ShouldBe(ParserState.OpPing);

        ProtocolParser.Parse(c, h, ping[5..6]).ShouldBeNull();
        c.State.ShouldBe(ParserState.OpStart);
        h.PingCount.ShouldBe(1);

        // Full message
        ProtocolParser.Parse(c, h, ping).ShouldBeNull();
        c.State.ShouldBe(ParserState.OpStart);
        h.PingCount.ShouldBe(2);

        // Should tolerate spaces
        var pingSpaces = "PING  \r"u8.ToArray();
        ProtocolParser.Parse(c, h, pingSpaces).ShouldBeNull();
        c.State.ShouldBe(ParserState.OpPing);

        c.State = ParserState.OpStart;
        var pingSpaces2 = "PING  \r  \n"u8.ToArray();
        ProtocolParser.Parse(c, h, pingSpaces2).ShouldBeNull();
        c.State.ShouldBe(ParserState.OpStart);
    }

    // =====================================================================
    // TestParsePong — Go test ID 2599
    // =====================================================================

    [Fact]
    public void ParsePong_ByteByByte()
    {
        var c = DummyClient();
        var h = DummyHandler();
        c.State.ShouldBe(ParserState.OpStart);

        var pong = "PONG\r\n"u8.ToArray();

        ProtocolParser.Parse(c, h, pong[..1]).ShouldBeNull();
        c.State.ShouldBe(ParserState.OpP);

        ProtocolParser.Parse(c, h, pong[1..2]).ShouldBeNull();
        c.State.ShouldBe(ParserState.OpPo);

        ProtocolParser.Parse(c, h, pong[2..3]).ShouldBeNull();
        c.State.ShouldBe(ParserState.OpPon);

        ProtocolParser.Parse(c, h, pong[3..4]).ShouldBeNull();
        c.State.ShouldBe(ParserState.OpPong);

        ProtocolParser.Parse(c, h, pong[4..5]).ShouldBeNull();
        c.State.ShouldBe(ParserState.OpPong);

        ProtocolParser.Parse(c, h, pong[5..6]).ShouldBeNull();
        c.State.ShouldBe(ParserState.OpStart);
        h.PongCount.ShouldBe(1);

        // Full message
        ProtocolParser.Parse(c, h, pong).ShouldBeNull();
        c.State.ShouldBe(ParserState.OpStart);
        h.PongCount.ShouldBe(2);

        // Should tolerate spaces
        var pongSpaces = "PONG  \r"u8.ToArray();
        ProtocolParser.Parse(c, h, pongSpaces).ShouldBeNull();
        c.State.ShouldBe(ParserState.OpPong);

        c.State = ParserState.OpStart;
        var pongSpaces2 = "PONG  \r  \n"u8.ToArray();
        ProtocolParser.Parse(c, h, pongSpaces2).ShouldBeNull();
        c.State.ShouldBe(ParserState.OpStart);
    }

    // =====================================================================
    // TestParseConnect — Go test ID 2600
    // =====================================================================

    [Fact]
    public void ParseConnect_ParsesCorrectly()
    {
        var c = DummyClient();
        var h = DummyHandler();

        var connect = Encoding.ASCII.GetBytes(
            "CONNECT {\"verbose\":false,\"pedantic\":true,\"tls_required\":false}\r\n");
        ProtocolParser.Parse(c, h, connect).ShouldBeNull();
        c.State.ShouldBe(ParserState.OpStart);
        h.ConnectArgs.ShouldNotBeNull();

        // Check saved state: arg start should be 8 (after "CONNECT ")
        c.ArgStart.ShouldBe(connect.Length); // After full parse, ArgStart is past the end
    }

    // =====================================================================
    // TestParseSub — Go test ID 2601
    // =====================================================================

    [Fact]
    public void ParseSub_SetsState()
    {
        var c = DummyClient();
        var h = DummyHandler();

        var sub = "SUB foo 1\r"u8.ToArray();
        ProtocolParser.Parse(c, h, sub).ShouldBeNull();
        c.State.ShouldBe(ParserState.SubArg);

        // The arg buffer should have been set up for split buffer
        c.ArgBuf.ShouldNotBeNull();
        Encoding.ASCII.GetString(c.ArgBuf!).ShouldBe("foo 1");
    }

    [Fact]
    public void ClientConnection_InboundDispatchAndPingIntervalHelpers_ShouldBehave()
    {
        var c = new ClientConnection(ClientKind.Client);
        var before = DateTime.UtcNow;
        c.ProcessInboundMsg(Encoding.ASCII.GetBytes("hello"));
        c.LastIn.ShouldBeGreaterThan(before - TimeSpan.FromMilliseconds(1));

        ClientConnection.AdjustPingInterval(ClientKind.Router, TimeSpan.FromHours(1))
            .ShouldBeLessThan(TimeSpan.FromHours(1));
        ClientConnection.AdjustPingInterval(ClientKind.Gateway, TimeSpan.FromHours(1))
            .ShouldBeLessThan(TimeSpan.FromHours(1));
    }

    // =====================================================================
    // TestParsePub — Go test ID 2602
    // =====================================================================

    [Fact]
    public void ParsePub_ParsesSubjectReplySize()
    {
        var c = DummyClient();
        var h = DummyHandler();

        // Simple PUB
        var pub = "PUB foo 5\r\nhello\r"u8.ToArray();
        ProtocolParser.Parse(c, h, pub).ShouldBeNull();
        c.State.ShouldBe(ParserState.MsgEndN);
        Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe("foo");
        c.Pa.Reply.ShouldBeNull();
        c.Pa.Size.ShouldBe(5);

        // Clear snapshots
        c.ArgBuf = null; c.MsgBuf = null; c.State = ParserState.OpStart;

        // PUB with reply
        pub = "PUB foo.bar INBOX.22 11\r\nhello world\r"u8.ToArray();
        ProtocolParser.Parse(c, h, pub).ShouldBeNull();
        c.State.ShouldBe(ParserState.MsgEndN);
        Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe("foo.bar");
        Encoding.ASCII.GetString(c.Pa.Reply!).ShouldBe("INBOX.22");
        c.Pa.Size.ShouldBe(11);

        // Clear snapshots
        c.ArgBuf = null; c.MsgBuf = null; c.State = ParserState.OpStart;

        // Data larger than expected size
        pub = "PUB foo.bar 11\r\nhello world hello world\r"u8.ToArray();
        ProtocolParser.Parse(c, h, pub).ShouldNotBeNull();
        c.MsgBuf.ShouldBeNull();
    }

    // =====================================================================
    // TestParsePubSizeOverflow — Go test ID 2603
    // =====================================================================

    [Fact]
    public void ParsePubSizeOverflow_ReturnsError()
    {
        var c = DummyClient();
        var h = DummyHandler();

        var pub = Encoding.ASCII.GetBytes(
            "PUB foo 3333333333333333333333333333333333333333333333333333333333333333\r\n");
        ProtocolParser.Parse(c, h, pub).ShouldNotBeNull();
    }

    // =====================================================================
    // TestParsePubArg — Go test ID 2604
    // =====================================================================

    [Theory]
    [MemberData(nameof(PubArgTestCases))]
    public void ProcessPub_ParsesArgsCorrectly(string arg, string subject, string reply, int size, string szb)
    {
        var c = DummyClient();
        var err = ProtocolParser.ProcessPub(c, Encoding.ASCII.GetBytes(arg));
        err.ShouldBeNull();
        Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe(subject);
        if (string.IsNullOrEmpty(reply))
            c.Pa.Reply.ShouldBeNull();
        else
            Encoding.ASCII.GetString(c.Pa.Reply!).ShouldBe(reply);
        Encoding.ASCII.GetString(c.Pa.SizeBytes!).ShouldBe(szb);
        c.Pa.Size.ShouldBe(size);
    }

    public static TheoryData<string, string, string, int, string> PubArgTestCases => new()
    {
        { "a 2", "a", "", 2, "2" },
        { "a 222", "a", "", 222, "222" },
        { "foo 22", "foo", "", 22, "22" },
        { " foo 22", "foo", "", 22, "22" },
        { "foo 22 ", "foo", "", 22, "22" },
        { "foo   22", "foo", "", 22, "22" },
        { " foo 22 ", "foo", "", 22, "22" },
        { " foo   22 ", "foo", "", 22, "22" },
        { "foo bar 22", "foo", "bar", 22, "22" },
        { " foo bar 22", "foo", "bar", 22, "22" },
        { "foo bar 22 ", "foo", "bar", 22, "22" },
        { "foo  bar  22", "foo", "bar", 22, "22" },
        { " foo bar 22 ", "foo", "bar", 22, "22" },
        { "  foo   bar  22  ", "foo", "bar", 22, "22" },
        { "  foo   bar  2222  ", "foo", "bar", 2222, "2222" },
        { "  foo     2222  ", "foo", "", 2222, "2222" },
        { "a\t2", "a", "", 2, "2" },
        { "a\t222", "a", "", 222, "222" },
        { "foo\t22", "foo", "", 22, "22" },
        { "\tfoo\t22", "foo", "", 22, "22" },
        { "foo\t22\t", "foo", "", 22, "22" },
        { "foo\t\t\t22", "foo", "", 22, "22" },
        { "\tfoo\t22\t", "foo", "", 22, "22" },
        { "\tfoo\t\t\t22\t", "foo", "", 22, "22" },
        { "foo\tbar\t22", "foo", "bar", 22, "22" },
        { "\tfoo\tbar\t22", "foo", "bar", 22, "22" },
        { "foo\tbar\t22\t", "foo", "bar", 22, "22" },
        { "foo\t\tbar\t\t22", "foo", "bar", 22, "22" },
        { "\tfoo\tbar\t22\t", "foo", "bar", 22, "22" },
        { "\t \tfoo\t \t \tbar\t \t22\t \t", "foo", "bar", 22, "22" },
        { "\t\tfoo\t\t\tbar\t\t2222\t\t", "foo", "bar", 2222, "2222" },
        { "\t \tfoo\t \t \t\t\t2222\t \t", "foo", "", 2222, "2222" },
    };

    // =====================================================================
    // TestParsePubBadSize — Go test ID 2605
    // =====================================================================

    [Fact]
    public void ProcessPub_BadSize_ReturnsError()
    {
        var c = DummyClient();
        c.MaxPayload = 32768;
        var err = ProtocolParser.ProcessPub(c, "foo 2222222222222222"u8.ToArray());
        err.ShouldNotBeNull();
    }

    // =====================================================================
    // TestParseHeaderPub — Go test ID 2606
    // =====================================================================

    [Fact]
    public void ParseHeaderPub_ParsesSubjectReplyHdrSize()
    {
        var c = DummyClient();
        c.HasHeaders = true;
        var h = DummyHandler();

        // Simple HPUB
        var hpub = "HPUB foo 12 17\r\nname:derek\r\nHELLO\r"u8.ToArray();
        ProtocolParser.Parse(c, h, hpub).ShouldBeNull();
        c.State.ShouldBe(ParserState.MsgEndN);
        Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe("foo");
        c.Pa.Reply.ShouldBeNull();
        c.Pa.HeaderSize.ShouldBe(12);
        Encoding.ASCII.GetString(c.Pa.HeaderBytes!).ShouldBe("12");
        c.Pa.Size.ShouldBe(17);

        // Clear snapshots
        c.ArgBuf = null; c.MsgBuf = null; c.State = ParserState.OpStart;

        // HPUB with reply
        hpub = "HPUB foo INBOX.22 12 17\r\nname:derek\r\nHELLO\r"u8.ToArray();
        ProtocolParser.Parse(c, h, hpub).ShouldBeNull();
        c.State.ShouldBe(ParserState.MsgEndN);
        Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe("foo");
        Encoding.ASCII.GetString(c.Pa.Reply!).ShouldBe("INBOX.22");
        c.Pa.HeaderSize.ShouldBe(12);
        Encoding.ASCII.GetString(c.Pa.HeaderBytes!).ShouldBe("12");
        c.Pa.Size.ShouldBe(17);

        // Clear snapshots
        c.ArgBuf = null; c.MsgBuf = null; c.State = ParserState.OpStart;

        // HPUB with hdr=0
        hpub = "HPUB foo INBOX.22 0 5\r\nHELLO\r"u8.ToArray();
        ProtocolParser.Parse(c, h, hpub).ShouldBeNull();
        c.State.ShouldBe(ParserState.MsgEndN);
        Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe("foo");
        Encoding.ASCII.GetString(c.Pa.Reply!).ShouldBe("INBOX.22");
        c.Pa.HeaderSize.ShouldBe(0);
        Encoding.ASCII.GetString(c.Pa.HeaderBytes!).ShouldBe("0");
        c.Pa.Size.ShouldBe(5);
    }

    // =====================================================================
    // TestParseHeaderPubArg — Go test ID 2607
    // =====================================================================

    [Theory]
    [MemberData(nameof(HeaderPubArgTestCases))]
    public void ProcessHeaderPub_ParsesArgsCorrectly(
        string arg, string subject, string reply, int hdr, int size, string szb)
    {
        var c = DummyClient();
        c.HasHeaders = true;
        var err = ProtocolParser.ProcessHeaderPub(c, Encoding.ASCII.GetBytes(arg), null);
        err.ShouldBeNull();
        Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe(subject);
        if (string.IsNullOrEmpty(reply))
            c.Pa.Reply.ShouldBeNull();
        else
            Encoding.ASCII.GetString(c.Pa.Reply!).ShouldBe(reply);
        Encoding.ASCII.GetString(c.Pa.SizeBytes!).ShouldBe(szb);
        c.Pa.HeaderSize.ShouldBe(hdr);
        c.Pa.Size.ShouldBe(size);
    }

    public static TheoryData<string, string, string, int, int, string> HeaderPubArgTestCases => new()
    {
        { "a 2 4", "a", "", 2, 4, "4" },
        { "a 22 222", "a", "", 22, 222, "222" },
        { "foo 3 22", "foo", "", 3, 22, "22" },
        { " foo   1 22", "foo", "", 1, 22, "22" },
        { "foo 0   22 ", "foo", "", 0, 22, "22" },
        { "foo  0     22", "foo", "", 0, 22, "22" },
        { " foo 1 22 ", "foo", "", 1, 22, "22" },
        { " foo   3 22 ", "foo", "", 3, 22, "22" },
        { "foo bar 1 22", "foo", "bar", 1, 22, "22" },
        { " foo bar 11 22", "foo", "bar", 11, 22, "22" },
        { "foo bar 11 22 ", "foo", "bar", 11, 22, "22" },
        { "foo  bar  11  22", "foo", "bar", 11, 22, "22" },
        { " foo bar  11 22 ", "foo", "bar", 11, 22, "22" },
        { "  foo   bar  11   22  ", "foo", "bar", 11, 22, "22" },
        { "  foo   bar  22   2222  ", "foo", "bar", 22, 2222, "2222" },
        { "  foo 1    2222  ", "foo", "", 1, 2222, "2222" },
        { "a\t2\t22", "a", "", 2, 22, "22" },
        { "a\t2\t\t222", "a", "", 2, 222, "222" },
        { "foo\t2 22", "foo", "", 2, 22, "22" },
        { "\tfoo\t11\t  22", "foo", "", 11, 22, "22" },
        { "foo\t11\t22\t", "foo", "", 11, 22, "22" },
        { "foo\t\t\t11 22", "foo", "", 11, 22, "22" },
        { "\tfoo\t11\t \t 22\t", "foo", "", 11, 22, "22" },
        { "\tfoo\t\t\t11 22\t", "foo", "", 11, 22, "22" },
        { "foo\tbar\t2 22", "foo", "bar", 2, 22, "22" },
        { "\tfoo\tbar\t11\t22", "foo", "bar", 11, 22, "22" },
        { "foo\tbar\t11\t\t22\t ", "foo", "bar", 11, 22, "22" },
        { "foo\t\tbar\t\t11\t\t\t22", "foo", "bar", 11, 22, "22" },
        { "\tfoo\tbar\t11\t22\t", "foo", "bar", 11, 22, "22" },
        { "\t \tfoo\t \t \tbar\t \t11\t 22\t \t", "foo", "bar", 11, 22, "22" },
        { "\t\tfoo\t\t\tbar\t\t22\t\t\t2222\t\t", "foo", "bar", 22, 2222, "2222" },
        { "\t \tfoo\t \t \t\t\t11\t\t 2222\t \t", "foo", "", 11, 2222, "2222" },
    };

    // =====================================================================
    // TestParseRoutedHeaderMsg — Go test ID 2608
    // =====================================================================

    [Fact]
    public void ParseRoutedHeaderMsg_ParsesCorrectly()
    {
        var c = DummyRouteClient();
        var h = DummyHandler();

        // hdr > size should error
        var pub = "HMSG $foo foo 10 8\r\nXXXhello\r"u8.ToArray();
        ProtocolParser.Parse(c, h, pub).ShouldNotBeNull();

        // Clear snapshots
        c.ArgBuf = null; c.MsgBuf = null; c.State = ParserState.OpStart;

        // Simple HMSG
        pub = "HMSG $foo foo 3 8\r\nXXXhello\r"u8.ToArray();
        ProtocolParser.Parse(c, h, pub).ShouldBeNull();
        c.State.ShouldBe(ParserState.MsgEndN);
        Encoding.ASCII.GetString(c.Pa.Account!).ShouldBe("$foo");
        Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe("foo");
        c.Pa.Reply.ShouldBeNull();
        c.Pa.HeaderSize.ShouldBe(3);
        c.Pa.Size.ShouldBe(8);

        // Clear snapshots
        c.ArgBuf = null; c.MsgBuf = null; c.State = ParserState.OpStart;

        // HMSG with reply
        pub = "HMSG $G foo.bar INBOX.22 3 14\r\nOK:hello world\r"u8.ToArray();
        ProtocolParser.Parse(c, h, pub).ShouldBeNull();
        c.State.ShouldBe(ParserState.MsgEndN);
        Encoding.ASCII.GetString(c.Pa.Account!).ShouldBe("$G");
        Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe("foo.bar");
        Encoding.ASCII.GetString(c.Pa.Reply!).ShouldBe("INBOX.22");
        c.Pa.HeaderSize.ShouldBe(3);
        c.Pa.Size.ShouldBe(14);

        // Clear snapshots
        c.ArgBuf = null; c.MsgBuf = null; c.State = ParserState.OpStart;

        // HMSG with + reply and queue
        pub = "HMSG $G foo.bar + reply baz 3 14\r\nOK:hello world\r"u8.ToArray();
        ProtocolParser.Parse(c, h, pub).ShouldBeNull();
        c.State.ShouldBe(ParserState.MsgEndN);
        Encoding.ASCII.GetString(c.Pa.Account!).ShouldBe("$G");
        Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe("foo.bar");
        Encoding.ASCII.GetString(c.Pa.Reply!).ShouldBe("reply");
        c.Pa.Queues.ShouldNotBeNull();
        c.Pa.Queues!.Count.ShouldBe(1);
        Encoding.ASCII.GetString(c.Pa.Queues[0]).ShouldBe("baz");
        c.Pa.HeaderSize.ShouldBe(3);
        c.Pa.Size.ShouldBe(14);

        // Clear snapshots
        c.ArgBuf = null; c.MsgBuf = null; c.State = ParserState.OpStart;

        // HMSG with | queue (no reply)
        pub = "HMSG $G foo.bar | baz 3 14\r\nOK:hello world\r"u8.ToArray();
        ProtocolParser.Parse(c, h, pub).ShouldBeNull();
        c.State.ShouldBe(ParserState.MsgEndN);
        Encoding.ASCII.GetString(c.Pa.Account!).ShouldBe("$G");
        Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe("foo.bar");
        Encoding.ASCII.GetString(c.Pa.Reply!).ShouldBe("");
        c.Pa.Queues.ShouldNotBeNull();
        c.Pa.Queues!.Count.ShouldBe(1);
        Encoding.ASCII.GetString(c.Pa.Queues[0]).ShouldBe("baz");
        c.Pa.HeaderSize.ShouldBe(3);
        c.Pa.Size.ShouldBe(14);
    }

    // =====================================================================
    // TestParseRouteMsg — Go test ID 2609
    // =====================================================================

    [Fact]
    public void ParseRouteMsg_ParsesCorrectly()
    {
        var c = DummyRouteClient();
        var h = DummyHandler();

        // MSG from route should error (must use RMSG)
        var pub = "MSG $foo foo 5\r\nhello\r"u8.ToArray();
        ProtocolParser.Parse(c, h, pub).ShouldNotBeNull();

        // Reset
        c.ArgBuf = null; c.MsgBuf = null; c.State = ParserState.OpStart;

        // RMSG simple
        pub = "RMSG $foo foo 5\r\nhello\r"u8.ToArray();
        ProtocolParser.Parse(c, h, pub).ShouldBeNull();
        c.State.ShouldBe(ParserState.MsgEndN);
        Encoding.ASCII.GetString(c.Pa.Account!).ShouldBe("$foo");
        Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe("foo");
        c.Pa.Reply.ShouldBeNull();
        c.Pa.Size.ShouldBe(5);

        // Clear
        c.ArgBuf = null; c.MsgBuf = null; c.State = ParserState.OpStart;

        // RMSG with reply
        pub = "RMSG $G foo.bar INBOX.22 11\r\nhello world\r"u8.ToArray();
        ProtocolParser.Parse(c, h, pub).ShouldBeNull();
        c.State.ShouldBe(ParserState.MsgEndN);
        Encoding.ASCII.GetString(c.Pa.Account!).ShouldBe("$G");
        Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe("foo.bar");
        Encoding.ASCII.GetString(c.Pa.Reply!).ShouldBe("INBOX.22");
        c.Pa.Size.ShouldBe(11);

        // Clear
        c.ArgBuf = null; c.MsgBuf = null; c.State = ParserState.OpStart;

        // RMSG with + reply and queue
        pub = "RMSG $G foo.bar + reply baz 11\r\nhello world\r"u8.ToArray();
        ProtocolParser.Parse(c, h, pub).ShouldBeNull();
        c.State.ShouldBe(ParserState.MsgEndN);
        Encoding.ASCII.GetString(c.Pa.Account!).ShouldBe("$G");
        Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe("foo.bar");
        Encoding.ASCII.GetString(c.Pa.Reply!).ShouldBe("reply");
        c.Pa.Queues.ShouldNotBeNull();
        c.Pa.Queues!.Count.ShouldBe(1);
        Encoding.ASCII.GetString(c.Pa.Queues[0]).ShouldBe("baz");

        // Clear
        c.ArgBuf = null; c.MsgBuf = null; c.State = ParserState.OpStart;

        // RMSG with | queue (no reply)
        pub = "RMSG $G foo.bar | baz 11\r\nhello world\r"u8.ToArray();
        ProtocolParser.Parse(c, h, pub).ShouldBeNull();
        c.State.ShouldBe(ParserState.MsgEndN);
        Encoding.ASCII.GetString(c.Pa.Account!).ShouldBe("$G");
        Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe("foo.bar");
        Encoding.ASCII.GetString(c.Pa.Reply!).ShouldBe("");
        c.Pa.Queues.ShouldNotBeNull();
        c.Pa.Queues!.Count.ShouldBe(1);
        Encoding.ASCII.GetString(c.Pa.Queues[0]).ShouldBe("baz");
    }

    // =====================================================================
    // TestParseMsgSpace — Go test ID 2610
    // =====================================================================

    [Fact]
    public void ParseMsgSpace_ErrorsCorrectly()
    {
        // MSG <SPC> from route should error
        var c = DummyRouteClient();
        var h = DummyHandler();
        ProtocolParser.Parse(c, h, "MSG \r\n"u8.ToArray()).ShouldNotBeNull();

        // M from client should error
        c = DummyClient();
        ProtocolParser.Parse(c, h, "M"u8.ToArray()).ShouldNotBeNull();
    }

    // =====================================================================
    // TestShouldFail — Go test ID 2611
    // =====================================================================

    [Theory]
    [MemberData(nameof(ShouldFailClientProtos))]
    public void ShouldFail_ClientProtos(string proto)
    {
        var c = DummyClient();
        var h = DummyHandler();
        ProtocolParser.Parse(c, h, Encoding.ASCII.GetBytes(proto)).ShouldNotBeNull();
    }

    public static TheoryData<string> ShouldFailClientProtos => new()
    {
        "xxx",
        "Px", "PIx", "PINx", " PING",
        "POx", "PONx",
        "+x", "+Ox",
        "-x", "-Ex", "-ERx", "-ERRx",
        "Cx", "COx", "CONx", "CONNx", "CONNEx", "CONNECx", "CONNECT \r\n",
        "PUx", "PUB foo\r\n", "PUB  \r\n", "PUB foo bar       \r\n",
        "PUB foo 2\r\nok \r\n", "PUB foo 2\r\nok\r \n",
        "Sx", "SUx", "SUB\r\n", "SUB  \r\n", "SUB foo\r\n",
        "SUB foo bar baz 22\r\n",
        "Ux", "UNx", "UNSx", "UNSUx", "UNSUBx", "UNSUBUNSUB 1\r\n", "UNSUB_2\r\n",
        "UNSUB_UNSUB_UNSUB 2\r\n", "UNSUB_\t2\r\n", "UNSUB\r\n", "UNSUB \r\n",
        "UNSUB          \t       \r\n",
        "Ix", "INx", "INFx", "INFO  \r\n",
    };

    [Theory]
    [MemberData(nameof(ShouldFailRouterProtos))]
    public void ShouldFail_RouterProtos(string proto)
    {
        var c = DummyClient();
        c.Kind = ClientKind.Router;
        var h = DummyHandler();
        ProtocolParser.Parse(c, h, Encoding.ASCII.GetBytes(proto)).ShouldNotBeNull();
    }

    public static TheoryData<string> ShouldFailRouterProtos => new()
    {
        "Mx", "MSx", "MSGx", "MSG  \r\n",
    };

    // =====================================================================
    // TestProtoSnippet — Go test ID 2612
    // =====================================================================

    [Fact]
    public void ProtoSnippet_MatchesGoOutput()
    {
        var sample = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"u8.ToArray();

        var tests = new (int Start, string Expected)[]
        {
            (0, "\"abcdefghijklmnopqrstuvwxyzABCDEF\""),
            (1, "\"bcdefghijklmnopqrstuvwxyzABCDEFG\""),
            (2, "\"cdefghijklmnopqrstuvwxyzABCDEFGH\""),
            (3, "\"defghijklmnopqrstuvwxyzABCDEFGHI\""),
            (4, "\"efghijklmnopqrstuvwxyzABCDEFGHIJ\""),
            (5, "\"fghijklmnopqrstuvwxyzABCDEFGHIJK\""),
            (6, "\"ghijklmnopqrstuvwxyzABCDEFGHIJKL\""),
            (7, "\"hijklmnopqrstuvwxyzABCDEFGHIJKLM\""),
            (8, "\"ijklmnopqrstuvwxyzABCDEFGHIJKLMN\""),
            (9, "\"jklmnopqrstuvwxyzABCDEFGHIJKLMNO\""),
            (10, "\"klmnopqrstuvwxyzABCDEFGHIJKLMNOP\""),
            (11, "\"lmnopqrstuvwxyzABCDEFGHIJKLMNOPQ\""),
            (12, "\"mnopqrstuvwxyzABCDEFGHIJKLMNOPQR\""),
            (13, "\"nopqrstuvwxyzABCDEFGHIJKLMNOPQRS\""),
            (14, "\"opqrstuvwxyzABCDEFGHIJKLMNOPQRST\""),
            (15, "\"pqrstuvwxyzABCDEFGHIJKLMNOPQRSTU\""),
            (16, "\"qrstuvwxyzABCDEFGHIJKLMNOPQRSTUV\""),
            (17, "\"rstuvwxyzABCDEFGHIJKLMNOPQRSTUVW\""),
            (18, "\"stuvwxyzABCDEFGHIJKLMNOPQRSTUVWX\""),
            (19, "\"tuvwxyzABCDEFGHIJKLMNOPQRSTUVWXY\""),
            (20, "\"uvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\""),
            (21, "\"vwxyzABCDEFGHIJKLMNOPQRSTUVWXY\""),
            (22, "\"wxyzABCDEFGHIJKLMNOPQRSTUVWXY\""),
            (23, "\"xyzABCDEFGHIJKLMNOPQRSTUVWXY\""),
            (24, "\"yzABCDEFGHIJKLMNOPQRSTUVWXY\""),
            (25, "\"zABCDEFGHIJKLMNOPQRSTUVWXY\""),
            (26, "\"ABCDEFGHIJKLMNOPQRSTUVWXY\""),
            (27, "\"BCDEFGHIJKLMNOPQRSTUVWXY\""),
            (28, "\"CDEFGHIJKLMNOPQRSTUVWXY\""),
            (29, "\"DEFGHIJKLMNOPQRSTUVWXY\""),
            (30, "\"EFGHIJKLMNOPQRSTUVWXY\""),
            (31, "\"FGHIJKLMNOPQRSTUVWXY\""),
            (32, "\"GHIJKLMNOPQRSTUVWXY\""),
            (33, "\"HIJKLMNOPQRSTUVWXY\""),
            (34, "\"IJKLMNOPQRSTUVWXY\""),
            (35, "\"JKLMNOPQRSTUVWXY\""),
            (36, "\"KLMNOPQRSTUVWXY\""),
            (37, "\"LMNOPQRSTUVWXY\""),
            (38, "\"MNOPQRSTUVWXY\""),
            (39, "\"NOPQRSTUVWXY\""),
            (40, "\"OPQRSTUVWXY\""),
            (41, "\"PQRSTUVWXY\""),
            (42, "\"QRSTUVWXY\""),
            (43, "\"RSTUVWXY\""),
            (44, "\"STUVWXY\""),
            (45, "\"TUVWXY\""),
            (46, "\"UVWXY\""),
            (47, "\"VWXY\""),
            (48, "\"WXY\""),
            (49, "\"XY\""),
            (50, "\"Y\""),
            (51, "\"\""),
            (52, "\"\""),
            (53, "\"\""),
            (54, "\"\""),
        };

        foreach (var (start, expected) in tests)
        {
            var got = ProtocolParser.ProtoSnippet(start, ServerConstants.ProtoSnippetSize, sample);
            got.ShouldBe(expected, $"start={start}");
        }
    }

    // =====================================================================
    // TestParseOK — Go test ID 2613 (mapped from Go TestParseOK)
    // =====================================================================

    [Fact]
    public void ParseOK_ByteByByte()
    {
        var c = DummyClient();
        var h = DummyHandler();
        c.State.ShouldBe(ParserState.OpStart);

        var ok = "+OK\r\n"u8.ToArray();

        ProtocolParser.Parse(c, h, ok[..1]).ShouldBeNull();
        c.State.ShouldBe(ParserState.OpPlus);

        ProtocolParser.Parse(c, h, ok[1..2]).ShouldBeNull();
        c.State.ShouldBe(ParserState.OpPlusO);

        ProtocolParser.Parse(c, h, ok[2..3]).ShouldBeNull();
        c.State.ShouldBe(ParserState.OpPlusOk);

        ProtocolParser.Parse(c, h, ok[3..4]).ShouldBeNull();
        c.State.ShouldBe(ParserState.OpPlusOk);

        ProtocolParser.Parse(c, h, ok[4..5]).ShouldBeNull();
        c.State.ShouldBe(ParserState.OpStart);
    }

    // =====================================================================
    // TestMaxControlLine — Go test ID 2614
    // =====================================================================

    [Theory]
    [InlineData(ClientKind.Client, true)]
    [InlineData(ClientKind.Leaf, false)]
    [InlineData(ClientKind.Router, false)]
    [InlineData(ClientKind.Gateway, false)]
    public void MaxControlLine_EnforcedForClientOnly(ClientKind kind, bool shouldFail)
    {
        var pub = "PUB foo.bar.baz 2\r\nok\r\n"u8.ToArray();

        var c = new ParseContext
        {
            Kind = kind,
            MaxControlLine = 8, // Very small limit
            MaxPayload = -1,
        };
        var h = DummyHandler();

        // For non-client kinds, we need to set up the OP appropriately
        // Routes use RMSG not PUB, but PUB is fine for testing mcl enforcement
        // since the state machine handles it the same way.

        var err = ProtocolParser.Parse(c, h, pub);
        if (shouldFail)
        {
            err.ShouldNotBeNull();
            ErrorContextHelper.ErrorIs(err, ServerErrors.ErrMaxControlLine).ShouldBeTrue();
        }
        else
        {
            // Non-client kinds don't enforce max control line
            err.ShouldBeNull();
        }
    }

    [Fact]
    public void ParsePub_SplitPayload_ClonesPubArgAndCompletesMessage()
    {
        var c = DummyClient();
        var h = DummyHandler();

        var first = "PUB foo 5\r\nhe"u8.ToArray();
        ProtocolParser.Parse(c, h, first).ShouldBeNull();
        c.State.ShouldBe(ParserState.MsgPayload);
        c.ArgBuf.ShouldNotBeNull();
        Encoding.ASCII.GetString(c.ArgBuf!).ShouldBe("foo 5");
        c.MsgBuf.ShouldNotBeNull();
        Encoding.ASCII.GetString(c.MsgBuf!).ShouldBe("he");
        h.InboundMessages.Count.ShouldBe(0);

        var second = "llo\r\n"u8.ToArray();
        ProtocolParser.Parse(c, h, second).ShouldBeNull();
        c.State.ShouldBe(ParserState.OpStart);
        h.InboundMessages.Count.ShouldBe(1);
        Encoding.ASCII.GetString(h.InboundMessages[0]).ShouldBe("hello\r\n");
    }

    [Fact]
    public void ClonePubArg_ClientBranch_ReprocessesPubArgs()
    {
        var c = DummyClient();
        var h = DummyHandler();
        c.Pa.Arg = "foo 5"u8.ToArray();
        c.Pa.HeaderSize = -1;

        var err = ProtocolParser.ClonePubArg(c, h, lmsg: false);

        err.ShouldBeNull();
        c.ArgBuf.ShouldNotBeNull();
        Encoding.ASCII.GetString(c.ArgBuf!).ShouldBe("foo 5");
        Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe("foo");
        c.Pa.Size.ShouldBe(5);
    }

    [Fact]
    public void ClonePubArg_RouterLmsgBranch_ReprocessesRoutedOriginArgs()
    {
        var c = DummyRouteClient();
        var h = DummyHandler();
        c.Pa.Arg = "$G foo 5"u8.ToArray();
        c.Pa.HeaderSize = -1;

        var err = ProtocolParser.ClonePubArg(c, h, lmsg: true);

        err.ShouldBeNull();
        c.ArgBuf.ShouldNotBeNull();
        Encoding.ASCII.GetString(c.ArgBuf!).ShouldBe("$G foo 5");
        Encoding.ASCII.GetString(c.Pa.Account!).ShouldBe("$G");
        Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe("foo");
        c.Pa.Size.ShouldBe(5);
    }

    [Fact]
    public void ClonePubArg_LeafHeaderBranch_ReprocessesLeafHeaderArgs()
    {
        var c = new ParseContext { Kind = ClientKind.Leaf, MaxPayload = -1 };
        var h = DummyHandler();
        c.Pa.Arg = "$G foo 2 5"u8.ToArray();
        c.Pa.HeaderSize = 2;

        var err = ProtocolParser.ClonePubArg(c, h, lmsg: false);

        err.ShouldBeNull();
        c.ArgBuf.ShouldNotBeNull();
        Encoding.ASCII.GetString(c.ArgBuf!).ShouldBe("$G foo 2 5");
        Encoding.ASCII.GetString(c.Pa.Account!).ShouldBe("$G");
        Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe("foo");
        c.Pa.HeaderSize.ShouldBe(2);
        c.Pa.Size.ShouldBe(5);
    }

    [Fact]
    public void ClientConnection_ExposesParserCompatibilityMethods()
    {
        var flags = System.Reflection.BindingFlags.Instance
                    | System.Reflection.BindingFlags.NonPublic
                    | System.Reflection.BindingFlags.Public;
        var methods = typeof(ClientConnection).GetMethods(flags);

        methods.Any(method =>
            method.Name == "Parse" &&
            method.GetParameters().Length == 2 &&
            method.GetParameters()[0].ParameterType == typeof(byte[]) &&
            method.GetParameters()[1].ParameterType == typeof(IProtocolHandler)).ShouldBeTrue();

        methods.Any(method =>
            method.Name == "OverMaxControlLineLimit" &&
            method.GetParameters().Length == 3 &&
            method.GetParameters()[0].ParameterType == typeof(byte[]) &&
            method.GetParameters()[1].ParameterType == typeof(int) &&
            method.GetParameters()[2].ParameterType == typeof(IProtocolHandler)).ShouldBeTrue();

        methods.Any(method =>
            method.Name == "ClonePubArg" &&
            method.GetParameters().Length == 2 &&
            method.GetParameters()[0].ParameterType == typeof(IProtocolHandler) &&
            method.GetParameters()[1].ParameterType == typeof(bool)).ShouldBeTrue();
    }

    // =====================================================================
    // TestProtocolHandler — stub handler for tests
    // =====================================================================

    private sealed class TestProtocolHandler : IProtocolHandler
    {
        public bool IsMqtt => false;
        public bool Trace => false;
        public bool HasMappings => false;
        public bool IsAwaitingAuth => false;
        public bool TryRegisterNoAuthUser() => true; // Allow all
        public bool IsGatewayInboundNotConnected => false;

        public int PingCount { get; private set; }
        public int PongCount { get; private set; }
        public byte[]? ConnectArgs { get; private set; }
        public List<byte[]> InboundMessages { get; } = [];

        public Exception? ProcessConnect(byte[] arg) { ConnectArgs = arg; return null; }
        public Exception? ProcessInfo(byte[] arg) => null;
        public void ProcessPing() => PingCount++;
        public void ProcessPong() => PongCount++;
        public void ProcessErr(string arg) { }
        public Exception? ProcessClientSub(byte[] arg) => null;
        public Exception? ProcessClientUnsub(byte[] arg) => null;
        public Exception? ProcessRemoteSub(byte[] arg, bool isLeaf) => null;
        public Exception? ProcessRemoteUnsub(byte[] arg, bool isLeafUnsub) => null;
        public Exception? ProcessGatewayRSub(byte[] arg) => null;
        public Exception? ProcessGatewayRUnsub(byte[] arg) => null;
        public Exception? ProcessLeafSub(byte[] arg) => null;
        public Exception? ProcessLeafUnsub(byte[] arg) => null;
        public Exception? ProcessAccountSub(byte[] arg) => null;
        public void ProcessAccountUnsub(byte[] arg) { }
        public void ProcessInboundMsg(byte[] msg) => InboundMessages.Add(msg);
        public bool SelectMappedSubject() => false;
        public void TraceInOp(string name, byte[]? arg) { }
        public void TraceMsg(byte[] msg) { }
        public void SendErr(string msg) { }
        public void AuthViolation() { }
        public void CloseConnection(int reason) { }
        public string KindString() => "CLIENT";
    }
}
