// Go reference: golang/nats-server/server/msgtrace_test.go
// Go reference: golang/nats-server/server/closed_conns_test.go
//
// Coverage:
//   Message trace infrastructure — header map generation, connection naming,
//   trace context, header propagation (HPUB/HMSG), server options.
//   Closed connection tracking — ring-buffer accounting, max limit, subs count,
//   auth timeout/violation, max-payload close reason.

using System.Net;
using System.Net.Sockets;
using System.Text;
using Microsoft.Extensions.Logging.Abstractions;
using NATS.Server;
using NATS.Server.Auth;
using NATS.Server.Monitoring;
using NATS.Server.Protocol;
using NATS.Server.TestUtilities;

namespace NATS.Server.Core.Tests;

/// <summary>
/// Go parity tests for message trace header infrastructure and closed-connection
/// tracking.  Full $SYS.TRACE event emission is not yet wired end-to-end; these
/// tests validate the foundational pieces that must be correct first.
/// </summary>
public class MsgTraceGoParityTests : IAsyncLifetime
{
    private NatsServer _server = null!;
    private int _port;
    private CancellationTokenSource _cts = new();

    public async Task InitializeAsync()
    {
        _port = TestPortAllocator.GetFreePort();
        _server = new NatsServer(new NatsOptions { Port = _port }, NullLoggerFactory.Instance);
        _ = _server.StartAsync(_cts.Token);
        await _server.WaitForReadyAsync();
    }

    public async Task DisposeAsync()
    {
        await _cts.CancelAsync();
        _server.Dispose();
    }

    // ─── helpers ────────────────────────────────────────────────────────────



    private async Task<Socket> ConnectClientAsync(bool headers = true)
    {
        var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        await sock.ConnectAsync(IPAddress.Loopback, _port);
        await SocketTestHelper.ReadUntilAsync(sock, "\r\n"); // consume INFO
        var connectJson = headers
            ? "{\"verbose\":false,\"headers\":true}"
            : "{\"verbose\":false}";
        await sock.SendAsync(Encoding.ASCII.GetBytes($"CONNECT {connectJson}\r\n"));
        return sock;
    }

    // ─── message trace: connection naming (msgtrace_test.go:TestMsgTraceConnName) ──

    /// <summary>
    /// MessageTraceContext.Empty has all identity fields null and headers disabled.
    /// Mirrors the Go zero-value trace context.
    /// Go: TestMsgTraceConnName (msgtrace_test.go:40)
    /// </summary>
    [Fact]
    public void MsgTrace_empty_context_has_null_fields()
    {
        // Go: TestMsgTraceConnName — zero-value context
        var ctx = MessageTraceContext.Empty;

        ctx.ClientName.ShouldBeNull();
        ctx.ClientLang.ShouldBeNull();
        ctx.ClientVersion.ShouldBeNull();
        ctx.HeadersEnabled.ShouldBeFalse();
    }

    /// <summary>
    /// CreateFromConnect with null produces Empty.
    /// Go: TestMsgTraceConnName (msgtrace_test.go:40)
    /// </summary>
    [Fact]
    public void MsgTrace_create_from_null_opts_returns_empty()
    {
        // Go: TestMsgTraceConnName — null opts fallback
        var ctx = MessageTraceContext.CreateFromConnect(null);
        ctx.ShouldBe(MessageTraceContext.Empty);
    }

    /// <summary>
    /// CreateFromConnect captures name / lang / version / headers from ClientOptions.
    /// Go: TestMsgTraceConnName (msgtrace_test.go:40) — client identity on trace event
    /// </summary>
    [Fact]
    public void MsgTrace_create_from_connect_captures_identity()
    {
        // Go: TestMsgTraceConnName (msgtrace_test.go:40)
        var opts = new ClientOptions
        {
            Name = "my-tracer",
            Lang = "nats.go",
            Version = "1.30.0",
            Headers = true,
        };

        var ctx = MessageTraceContext.CreateFromConnect(opts);

        ctx.ClientName.ShouldBe("my-tracer");
        ctx.ClientLang.ShouldBe("nats.go");
        ctx.ClientVersion.ShouldBe("1.30.0");
        ctx.HeadersEnabled.ShouldBeTrue();
    }

    /// <summary>
    /// Client without headers support produces HeadersEnabled = false.
    /// Go: TestMsgTraceBasic (msgtrace_test.go:172)
    /// </summary>
    [Fact]
    public void MsgTrace_headers_disabled_when_connect_opts_headers_false()
    {
        // Go: TestMsgTraceBasic (msgtrace_test.go:172)
        var opts = new ClientOptions { Name = "legacy", Headers = false };
        var ctx = MessageTraceContext.CreateFromConnect(opts);

        ctx.HeadersEnabled.ShouldBeFalse();
        ctx.ClientName.ShouldBe("legacy");
    }

    /// <summary>
    /// MessageTraceContext is a record — value equality by fields.
    /// Go: TestMsgTraceConnName (msgtrace_test.go:40)
    /// </summary>
    [Fact]
    public void MsgTrace_context_record_equality()
    {
        // Go: TestMsgTraceConnName (msgtrace_test.go:40) — deterministic identity
        var a = new MessageTraceContext("app", "nats.go", "1.0", true);
        var b = new MessageTraceContext("app", "nats.go", "1.0", true);

        a.ShouldBe(b);
        a.GetHashCode().ShouldBe(b.GetHashCode());
    }

    // ─── GenHeaderMap — trace header parsing (msgtrace_test.go:TestMsgTraceGenHeaderMap) ──

    /// <summary>
    /// NatsHeaderParser correctly parses Nats-Trace-Dest from an HPUB block.
    /// Go: TestMsgTraceGenHeaderMap (msgtrace_test.go:80)
    /// </summary>
    [Fact]
    public void MsgTrace_header_parser_parses_trace_dest_header()
    {
        // Go: TestMsgTraceGenHeaderMap (msgtrace_test.go:80) — "trace header first"
        const string raw = "NATS/1.0\r\nNats-Trace-Dest: trace.inbox\r\n\r\n";
        var headers = NatsHeaderParser.Parse(Encoding.ASCII.GetBytes(raw));

        headers.ShouldNotBe(NatsHeaders.Invalid);
        headers.Headers.ContainsKey("Nats-Trace-Dest").ShouldBeTrue();
        headers.Headers["Nats-Trace-Dest"].ShouldContain("trace.inbox");
    }

    /// <summary>
    /// NatsHeaderParser returns Invalid when prefix is wrong.
    /// Go: TestMsgTraceGenHeaderMap (msgtrace_test.go:80) — "missing header line"
    /// </summary>
    [Fact]
    public void MsgTrace_header_parser_returns_invalid_for_bad_prefix()
    {
        // Go: TestMsgTraceGenHeaderMap (msgtrace_test.go:80) — "missing header line"
        var headers = NatsHeaderParser.Parse("Nats-Trace-Dest: val\r\n"u8.ToArray());
        headers.ShouldBe(NatsHeaders.Invalid);
    }

    /// <summary>
    /// No trace headers present → parser returns Invalid / empty map.
    /// Go: TestMsgTraceGenHeaderMap (msgtrace_test.go:80) — "no trace header present"
    /// </summary>
    [Fact]
    public void MsgTrace_header_parser_parses_empty_nats_header_block()
    {
        // Go: TestMsgTraceGenHeaderMap (msgtrace_test.go:80) — empty block
        var headers = NatsHeaderParser.Parse("NATS/1.0\r\n\r\n"u8.ToArray());
        headers.ShouldNotBe(NatsHeaders.Invalid);
        headers.Headers.Count.ShouldBe(0);
    }

    /// <summary>
    /// Multiple headers including Nats-Trace-Dest are all parsed.
    /// Go: TestMsgTraceGenHeaderMap (msgtrace_test.go:80) — "trace header first"
    /// </summary>
    [Fact]
    public void MsgTrace_header_parser_parses_multiple_headers_with_trace_dest()
    {
        // Go: TestMsgTraceGenHeaderMap (msgtrace_test.go:80) — "trace header first"
        const string raw =
            "NATS/1.0\r\n" +
            "X-App-Id: 42\r\n" +
            "Nats-Trace-Dest: my.trace.inbox\r\n" +
            "X-Correlation: abc\r\n" +
            "\r\n";

        var headers = NatsHeaderParser.Parse(Encoding.ASCII.GetBytes(raw));

        headers.Headers.Count.ShouldBe(3);
        headers.Headers["Nats-Trace-Dest"].ShouldContain("my.trace.inbox");
        headers.Headers["X-App-Id"].ShouldContain("42");
        headers.Headers["X-Correlation"].ShouldContain("abc");
    }

    /// <summary>
    /// Header lookup is case-insensitive.
    /// Go: TestMsgTraceGenHeaderMap (msgtrace_test.go:80) — case handling
    /// </summary>
    [Fact]
    public void MsgTrace_header_lookup_is_case_insensitive()
    {
        // Go: TestMsgTraceGenHeaderMap (msgtrace_test.go:80)
        const string raw = "NATS/1.0\r\nNats-Trace-Dest: inbox.trace\r\n\r\n";
        var headers = NatsHeaderParser.Parse(Encoding.ASCII.GetBytes(raw));

        headers.Headers.ContainsKey("nats-trace-dest").ShouldBeTrue();
        headers.Headers.ContainsKey("NATS-TRACE-DEST").ShouldBeTrue();
        headers.Headers["nats-trace-dest"][0].ShouldBe("inbox.trace");
    }

    // ─── wire-level Nats-Trace-Dest header propagation (msgtrace_test.go:TestMsgTraceBasic) ──

    /// <summary>
    /// Nats-Trace-Dest in an HPUB is delivered verbatim in the HMSG.
    /// Go: TestMsgTraceBasic (msgtrace_test.go:172)
    /// </summary>
    [Fact]
    public async Task MsgTrace_hpub_trace_dest_header_delivered_verbatim()
    {
        // Go: TestMsgTraceBasic (msgtrace_test.go:172) — header pass-through
        using var sub = await ConnectClientAsync();
        using var pub = await ConnectClientAsync();

        await sub.SendAsync("SUB trace.test 1\r\nPING\r\n"u8.ToArray());
        await SocketTestHelper.ReadUntilAsync(sub, "PONG");

        const string hdrBlock = "NATS/1.0\r\nNats-Trace-Dest: trace.inbox\r\n\r\n";
        const string payload = "hello";
        int hdrLen = Encoding.ASCII.GetByteCount(hdrBlock);
        int totalLen = hdrLen + Encoding.ASCII.GetByteCount(payload);

        await pub.SendAsync(Encoding.ASCII.GetBytes(
            $"HPUB trace.test {hdrLen} {totalLen}\r\n{hdrBlock}{payload}\r\n"));

        var received = await SocketTestHelper.ReadUntilAsync(sub, "Nats-Trace-Dest");

        received.ShouldContain("HMSG trace.test");
        received.ShouldContain("Nats-Trace-Dest: trace.inbox");
        received.ShouldContain("hello");
    }

    /// <summary>
    /// Nats-Trace-Dest header is preserved through a wildcard subscription match.
    /// Go: TestMsgTraceBasic (msgtrace_test.go:172) — wildcard delivery
    /// </summary>
    [Fact]
    public async Task MsgTrace_hpub_trace_dest_preserved_through_wildcard()
    {
        // Go: TestMsgTraceBasic (msgtrace_test.go:172) — wildcard subscriber
        using var sub = await ConnectClientAsync();
        using var pub = await ConnectClientAsync();

        await sub.SendAsync("SUB trace.* 1\r\nPING\r\n"u8.ToArray());
        await SocketTestHelper.ReadUntilAsync(sub, "PONG");

        const string hdrBlock = "NATS/1.0\r\nNats-Trace-Dest: t.inbox.1\r\n\r\n";
        const string payload = "wildcard-msg";
        int hdrLen = Encoding.ASCII.GetByteCount(hdrBlock);
        int totalLen = hdrLen + Encoding.ASCII.GetByteCount(payload);

        await pub.SendAsync(Encoding.ASCII.GetBytes(
            $"HPUB trace.subject {hdrLen} {totalLen}\r\n{hdrBlock}{payload}\r\n"));

        var received = await SocketTestHelper.ReadUntilAsync(sub, "Nats-Trace-Dest");

        received.ShouldContain("HMSG trace.subject");
        received.ShouldContain("Nats-Trace-Dest: t.inbox.1");
        received.ShouldContain("wildcard-msg");
    }

    /// <summary>
    /// Nats-Trace-Dest preserved through queue group delivery.
    /// Go: TestMsgTraceBasic (msgtrace_test.go:172) — queue group subscriber
    /// </summary>
    [Fact]
    public async Task MsgTrace_hpub_trace_dest_preserved_through_queue_group()
    {
        // Go: TestMsgTraceBasic (msgtrace_test.go:172) — queue-group delivery
        using var qsub = await ConnectClientAsync();
        using var pub = await ConnectClientAsync();

        // Subscribe via a queue group
        await qsub.SendAsync("SUB trace.q workers 1\r\nPING\r\n"u8.ToArray());
        await SocketTestHelper.ReadUntilAsync(qsub, "PONG");

        const string hdrBlock = "NATS/1.0\r\nNats-Trace-Dest: qg.trace\r\n\r\n";
        const string payload = "queued";
        int hdrLen = Encoding.ASCII.GetByteCount(hdrBlock);
        int totalLen = hdrLen + Encoding.ASCII.GetByteCount(payload);

        // Publish from a separate connection
        await pub.SendAsync(Encoding.ASCII.GetBytes(
            $"HPUB trace.q {hdrLen} {totalLen}\r\n{hdrBlock}{payload}\r\n"));

        var received = await SocketTestHelper.ReadUntilAsync(qsub, "Nats-Trace-Dest", 3000);

        received.ShouldContain("Nats-Trace-Dest: qg.trace");
        received.ShouldContain("queued");
    }

    /// <summary>
    /// Multiple custom headers alongside Nats-Trace-Dest all arrive intact.
    /// Go: TestMsgTraceBasic (msgtrace_test.go:172) — full header block preserved
    /// </summary>
    [Fact]
    public async Task MsgTrace_hpub_multiple_headers_with_trace_dest_all_delivered_intact()
    {
        // Go: TestMsgTraceBasic (msgtrace_test.go:172) — multi-header block
        using var sub = await ConnectClientAsync();
        using var pub = await ConnectClientAsync();

        await sub.SendAsync("SUB multi.hdr 1\r\nPING\r\n"u8.ToArray());
        await SocketTestHelper.ReadUntilAsync(sub, "PONG");

        const string hdrBlock =
            "NATS/1.0\r\n" +
            "X-Request-Id: req-99\r\n" +
            "Nats-Trace-Dest: t.multi\r\n" +
            "X-Priority: high\r\n" +
            "\r\n";
        const string payload = "multi-hdr-payload";
        int hdrLen = Encoding.ASCII.GetByteCount(hdrBlock);
        int totalLen = hdrLen + Encoding.ASCII.GetByteCount(payload);

        await pub.SendAsync(Encoding.ASCII.GetBytes(
            $"HPUB multi.hdr {hdrLen} {totalLen}\r\n{hdrBlock}{payload}\r\n"));

        var received = await SocketTestHelper.ReadUntilAsync(sub, "X-Priority");

        received.ShouldContain("X-Request-Id: req-99");
        received.ShouldContain("Nats-Trace-Dest: t.multi");
        received.ShouldContain("X-Priority: high");
        received.ShouldContain("multi-hdr-payload");
    }

    // ─── server trace options (msgtrace_test.go/opts.go) ─────────────────────

    /// <summary>
    /// NatsOptions.Trace is false by default.
    /// Go: opts.go — trace=false by default
    /// </summary>
    [Fact]
    public void MsgTrace_server_trace_is_false_by_default()
    {
        // Go: opts.go default
        new NatsOptions().Trace.ShouldBeFalse();
    }

    /// <summary>
    /// NatsOptions.TraceVerbose is false by default.
    /// Go: opts.go — trace_verbose=false by default
    /// </summary>
    [Fact]
    public void MsgTrace_server_trace_verbose_is_false_by_default()
    {
        // Go: opts.go default
        new NatsOptions().TraceVerbose.ShouldBeFalse();
    }

    /// <summary>
    /// NatsOptions.MaxTracedMsgLen is 0 by default (unlimited).
    /// Go: opts.go — max_traced_msg_len default=0
    /// </summary>
    [Fact]
    public void MsgTrace_max_traced_msg_len_is_zero_by_default()
    {
        // Go: opts.go default
        new NatsOptions().MaxTracedMsgLen.ShouldBe(0);
    }

    /// <summary>
    /// Server with Trace=true starts normally and accepts connections.
    /// Go: TestMsgTraceBasic (msgtrace_test.go:172) — server setup
    /// </summary>
    [Fact]
    public async Task MsgTrace_server_with_trace_enabled_starts_and_accepts_connections()
    {
        // Go: TestMsgTraceBasic (msgtrace_test.go:172)
        var port = TestPortAllocator.GetFreePort();
        using var cts = new CancellationTokenSource();
        using var server = new NatsServer(
            new NatsOptions { Port = port, Trace = true }, NullLoggerFactory.Instance);
        _ = server.StartAsync(cts.Token);
        await server.WaitForReadyAsync();

        using var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        await sock.ConnectAsync(IPAddress.Loopback, port);
        var info = await SocketTestHelper.ReadUntilAsync(sock, "\r\n");
        info.ShouldStartWith("INFO ");

        await cts.CancelAsync();
    }

    // ─── ClientFlags.TraceMode ────────────────────────────────────────────────

    /// <summary>
    /// ClientFlagHolder.TraceMode is not set by default.
    /// Go: client.go — trace flag starts unset
    /// </summary>
    [Fact]
    public void MsgTrace_client_flag_trace_mode_unset_by_default()
    {
        // Go: client.go — clientFlag trace bit
        var holder = new ClientFlagHolder();
        holder.HasFlag(ClientFlags.TraceMode).ShouldBeFalse();
    }

    /// <summary>
    /// SetFlag/ClearFlag round-trips TraceMode correctly.
    /// Go: client.go setTraceMode
    /// </summary>
    [Fact]
    public void MsgTrace_client_flag_trace_mode_set_and_clear()
    {
        // Go: client.go setTraceMode
        var holder = new ClientFlagHolder();

        holder.SetFlag(ClientFlags.TraceMode);
        holder.HasFlag(ClientFlags.TraceMode).ShouldBeTrue();

        holder.ClearFlag(ClientFlags.TraceMode);
        holder.HasFlag(ClientFlags.TraceMode).ShouldBeFalse();
    }

    /// <summary>
    /// TraceMode is independent of other flags.
    /// Go: client.go — per-bit flag isolation
    /// </summary>
    [Fact]
    public void MsgTrace_client_flag_trace_mode_does_not_affect_other_flags()
    {
        // Go: client.go — per-bit flag isolation
        var holder = new ClientFlagHolder();
        holder.SetFlag(ClientFlags.ConnectReceived);
        holder.SetFlag(ClientFlags.FirstPongSent);

        holder.SetFlag(ClientFlags.TraceMode);
        holder.ClearFlag(ClientFlags.TraceMode);

        holder.HasFlag(ClientFlags.ConnectReceived).ShouldBeTrue();
        holder.HasFlag(ClientFlags.FirstPongSent).ShouldBeTrue();
        holder.HasFlag(ClientFlags.TraceMode).ShouldBeFalse();
    }

    // ─── closed connection tracking (closed_conns_test.go) ───────────────────

    /// <summary>
    /// Server tracks a closed connection in the closed-clients ring buffer.
    /// Go: TestClosedConnsAccounting (closed_conns_test.go:46)
    /// </summary>
    [Fact]
    public async Task ClosedConns_accounting_tracks_one_closed_client()
    {
        // Go: TestClosedConnsAccounting (closed_conns_test.go:46)
        using var sock = await ConnectClientAsync();

        // Do a full handshake so the client is accepted
        await sock.SendAsync("PING\r\n"u8.ToArray());
        await SocketTestHelper.ReadUntilAsync(sock, "PONG");

        // Close the connection
        sock.Close();

        // Wait for the server to register the close
        var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(5);
        while (DateTime.UtcNow < deadline)
        {
            if (_server.GetClosedClients().Any())
                break;
            await Task.Delay(10);
        }

        _server.GetClosedClients().ShouldNotBeEmpty();
    }

    /// <summary>
    /// Closed-clients ring buffer is capped at MaxClosedClients.
    /// Go: TestClosedConnsAccounting (closed_conns_test.go:46)
    /// </summary>
    [Fact]
    public async Task ClosedConns_ring_buffer_bounded_by_max_closed_clients()
    {
        // Go: TestClosedConnsAccounting (closed_conns_test.go:46)
        // Build a server with a tiny ring buffer
        var port = TestPortAllocator.GetFreePort();
        using var cts = new CancellationTokenSource();
        using var server = new NatsServer(
            new NatsOptions { Port = port, MaxClosedClients = 5 },
            NullLoggerFactory.Instance);
        _ = server.StartAsync(cts.Token);
        await server.WaitForReadyAsync();

        // Open and close 10 connections
        for (int i = 0; i < 10; i++)
        {
            using var s = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            await s.ConnectAsync(IPAddress.Loopback, port);
            await SocketTestHelper.ReadUntilAsync(s, "\r\n"); // INFO
            await s.SendAsync(Encoding.ASCII.GetBytes("CONNECT {\"verbose\":false}\r\nPING\r\n"));
            await SocketTestHelper.ReadUntilAsync(s, "PONG");
            s.Close();
            // brief pause to let server process
            await Task.Delay(5);
        }

        // Allow processing
        await Task.Delay(200);

        var closed = server.GetClosedClients().ToList();
        closed.Count.ShouldBeLessThanOrEqualTo(5);

        await cts.CancelAsync();
    }

    /// <summary>
    /// ClosedClient record exposes the Cid and Reason fields populated on close.
    /// Go: TestClosedConnsAccounting (closed_conns_test.go:46)
    /// </summary>
    [Fact]
    public void ClosedConns_record_has_cid_and_reason_fields()
    {
        // Go: TestClosedConnsAccounting (closed_conns_test.go:46) — ClosedClient fields
        var cc = new ClosedClient
        {
            Cid = 42,
            Reason = "Client Closed",
        };

        cc.Cid.ShouldBe(42UL);
        cc.Reason.ShouldBe("Client Closed");
    }

    /// <summary>
    /// MaxClosedClients defaults to 10_000 in NatsOptions.
    /// Go: server.go — MaxClosedClients default
    /// </summary>
    [Fact]
    public void ClosedConns_max_closed_clients_default_is_10000()
    {
        // Go: server.go default MaxClosedClients = 10000
        new NatsOptions().MaxClosedClients.ShouldBe(10_000);
    }

    /// <summary>
    /// Connection closed due to MaxPayload exceeded is tracked with correct reason.
    /// Go: TestClosedMaxPayload (closed_conns_test.go:219)
    /// </summary>
    [Fact]
    public async Task ClosedConns_max_payload_close_reason_tracked()
    {
        // Go: TestClosedMaxPayload (closed_conns_test.go:219)
        var port = TestPortAllocator.GetFreePort();
        using var cts = new CancellationTokenSource();
        using var server = new NatsServer(
            new NatsOptions { Port = port, MaxPayload = 100 },
            NullLoggerFactory.Instance);
        _ = server.StartAsync(cts.Token);
        await server.WaitForReadyAsync();

        var conn = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        await conn.ConnectAsync(IPAddress.Loopback, port);
        await SocketTestHelper.ReadUntilAsync(conn, "\r\n"); // INFO

        // Establish connection first
        await conn.SendAsync("CONNECT {\"verbose\":false}\r\nPING\r\n"u8.ToArray());
        await SocketTestHelper.ReadUntilAsync(conn, "PONG");

        // Send a PUB with payload > MaxPayload (200 bytes > 100 byte limit)
        // Must include the full payload so the parser yields the command to NatsClient
        var bigPayload = new byte[200];
        var pubLine = $"PUB foo.bar {bigPayload.Length}\r\n";
        var fullMsg = Encoding.ASCII.GetBytes(pubLine).Concat(bigPayload).Concat("\r\n"u8.ToArray()).ToArray();
        await conn.SendAsync(fullMsg);

        // Wait for server to close and record it
        var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(5);
        while (DateTime.UtcNow < deadline)
        {
            if (server.GetClosedClients().Any())
                break;
            await Task.Delay(10);
        }
        conn.Dispose();

        var conns = server.GetClosedClients().ToList();
        conns.Count.ShouldBeGreaterThan(0);
        // The reason should indicate max-payload exceeded
        conns.Any(c => c.Reason.Contains("Maximum Payload", StringComparison.OrdinalIgnoreCase)
                    || c.Reason.Contains("Payload", StringComparison.OrdinalIgnoreCase))
             .ShouldBeTrue();

        await cts.CancelAsync();
    }

    /// <summary>
    /// Auth timeout connection is tracked with reason containing "Authentication Timeout".
    /// Go: TestClosedAuthorizationTimeout (closed_conns_test.go:143)
    /// </summary>
    [Fact]
    public async Task ClosedConns_auth_timeout_close_reason_tracked()
    {
        // Go: TestClosedAuthorizationTimeout (closed_conns_test.go:143)
        var port = TestPortAllocator.GetFreePort();
        using var cts = new CancellationTokenSource();
        using var server = new NatsServer(
            new NatsOptions
            {
                Port = port,
                Authorization = "required_token",
                AuthTimeout = TimeSpan.FromMilliseconds(200),
            },
            NullLoggerFactory.Instance);
        _ = server.StartAsync(cts.Token);
        await server.WaitForReadyAsync();

        // Just connect without sending CONNECT — auth timeout fires
        var conn = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        await conn.ConnectAsync(IPAddress.Loopback, port);
        await SocketTestHelper.ReadUntilAsync(conn, "\r\n"); // INFO

        // Don't send CONNECT — wait for auth timeout
        var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(5);
        while (DateTime.UtcNow < deadline)
        {
            if (server.GetClosedClients().Any())
                break;
            await Task.Delay(10);
        }
        conn.Dispose();

        var conns = server.GetClosedClients().ToList();
        conns.Count.ShouldBeGreaterThan(0);
        conns.Any(c => c.Reason.Contains("Authentication Timeout", StringComparison.OrdinalIgnoreCase))
             .ShouldBeTrue();

        await cts.CancelAsync();
    }

    /// <summary>
    /// Auth violation connection (wrong token) is tracked with reason containing "Authorization".
    /// Go: TestClosedAuthorizationViolation (closed_conns_test.go:164)
    /// </summary>
    [Fact]
    public async Task ClosedConns_auth_violation_close_reason_tracked()
    {
        // Go: TestClosedAuthorizationViolation (closed_conns_test.go:164)
        var port = TestPortAllocator.GetFreePort();
        using var cts = new CancellationTokenSource();
        using var server = new NatsServer(
            new NatsOptions { Port = port, Authorization = "correct_token" },
            NullLoggerFactory.Instance);
        _ = server.StartAsync(cts.Token);
        await server.WaitForReadyAsync();

        // Connect with wrong token
        var conn = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        await conn.ConnectAsync(IPAddress.Loopback, port);
        await SocketTestHelper.ReadUntilAsync(conn, "\r\n"); // INFO

        await conn.SendAsync(
            "CONNECT {\"verbose\":false,\"auth_token\":\"wrong_token\"}\r\nPING\r\n"u8.ToArray());

        // Wait for close and error response
        await SocketTestHelper.ReadUntilAsync(conn, "-ERR", 2000);
        conn.Dispose();

        var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(5);
        while (DateTime.UtcNow < deadline)
        {
            if (server.GetClosedClients().Any())
                break;
            await Task.Delay(10);
        }

        var conns = server.GetClosedClients().ToList();
        conns.Count.ShouldBeGreaterThan(0);
        conns.Any(c => c.Reason.Contains("Authorization", StringComparison.OrdinalIgnoreCase)
                    || c.Reason.Contains("Authentication", StringComparison.OrdinalIgnoreCase))
             .ShouldBeTrue();

        await cts.CancelAsync();
    }

    /// <summary>
    /// Username/password authorization violations are tracked in closed connections.
    /// Go: TestClosedUPAuthorizationViolation (closed_conns_test.go:187)
    /// </summary>
    [Fact]
    public async Task ClosedConns_up_auth_violation_close_reason_tracked()
    {
        // Go: TestClosedUPAuthorizationViolation (closed_conns_test.go:187)
        var port = TestPortAllocator.GetFreePort();
        using var cts = new CancellationTokenSource();
        using var server = new NatsServer(
            new NatsOptions
            {
                Port = port,
                Users =
                [
                    new User { Username = "my_user", Password = "my_secret" },
                ],
            },
            NullLoggerFactory.Instance);
        _ = server.StartAsync(cts.Token);
        await server.WaitForReadyAsync();

        // No credentials
        using (var conn1 = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
        {
            await conn1.ConnectAsync(IPAddress.Loopback, port);
            await SocketTestHelper.ReadUntilAsync(conn1, "\r\n"); // INFO
            await conn1.SendAsync("CONNECT {\"verbose\":false}\r\nPING\r\n"u8.ToArray());
            await SocketTestHelper.ReadUntilAsync(conn1, "-ERR", 2000);
        }

        // Wrong password
        using (var conn2 = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
        {
            await conn2.ConnectAsync(IPAddress.Loopback, port);
            await SocketTestHelper.ReadUntilAsync(conn2, "\r\n"); // INFO
            await conn2.SendAsync(
                "CONNECT {\"verbose\":false,\"user\":\"my_user\",\"pass\":\"wrong_pass\"}\r\nPING\r\n"u8.ToArray());
            await SocketTestHelper.ReadUntilAsync(conn2, "-ERR", 2000);
        }

        var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(5);
        while (DateTime.UtcNow < deadline)
        {
            if (server.GetClosedClients().Count >= 2)
                break;
            await Task.Delay(10);
        }

        var conns = server.GetClosedClients().ToList();
        conns.Count.ShouldBeGreaterThanOrEqualTo(2);
        conns.Take(2).All(c => c.Reason.Contains("Authorization Violation", StringComparison.OrdinalIgnoreCase))
             .ShouldBeTrue();

        await cts.CancelAsync();
    }

    /// <summary>
    /// TLS handshake failures are tracked in closed connections with the TLS reason.
    /// Go: TestClosedTLSHandshake (closed_conns_test.go:247)
    /// </summary>
    [Fact]
    public async Task ClosedConns_tls_handshake_close_reason_tracked()
    {
        // Go: TestClosedTLSHandshake (closed_conns_test.go:247)
        var (certPath, keyPath) = TestCertHelper.GenerateTestCertFiles();
        try
        {
            var port = TestPortAllocator.GetFreePort();
            using var cts = new CancellationTokenSource();
            using var server = new NatsServer(
                new NatsOptions
                {
                    Port = port,
                    TlsCert = certPath,
                    TlsKey = keyPath,
                    TlsVerify = true,
                    AllowNonTls = false,
                },
                NullLoggerFactory.Instance);
            _ = server.StartAsync(cts.Token);
            await server.WaitForReadyAsync();

            // Plain TCP client against TLS-required port should fail handshake.
            using (var conn = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
            {
                await conn.ConnectAsync(IPAddress.Loopback, port);
                await SocketTestHelper.ReadUntilAsync(conn, "\r\n"); // INFO
                await conn.SendAsync("CONNECT {\"verbose\":false}\r\nPING\r\n"u8.ToArray());
                _ = await SocketTestHelper.ReadUntilAsync(conn, "-ERR", 1000);
            }

            var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(5);
            while (DateTime.UtcNow < deadline)
            {
                if (server.GetClosedClients().Any())
                    break;
                await Task.Delay(10);
            }

            var conns = server.GetClosedClients().ToList();
            conns.Count.ShouldBeGreaterThan(0);
            conns.Any(c => c.Reason.Contains("TLS Handshake Error", StringComparison.OrdinalIgnoreCase))
                 .ShouldBeTrue();

            await cts.CancelAsync();
        }
        finally
        {
            File.Delete(certPath);
            File.Delete(keyPath);
        }
    }

    // ─── ClosedState enum (closed_conns_test.go — checkReason) ───────────────

    /// <summary>
    /// ClosedState enum contains at least the core close reasons checked by Go tests.
    /// Go: closed_conns_test.go:136 — checkReason helper
    /// </summary>
    [Fact]
    public void ClosedState_contains_expected_values()
    {
        // Go: closed_conns_test.go:136 checkReason — AuthenticationTimeout, AuthenticationViolation,
        // MaxPayloadExceeded, TLSHandshakeError
        var values = Enum.GetValues<ClosedState>();
        values.ShouldContain(ClosedState.AuthenticationTimeout);
        values.ShouldContain(ClosedState.AuthenticationViolation);
        values.ShouldContain(ClosedState.MaxPayloadExceeded);
        values.ShouldContain(ClosedState.TLSHandshakeError);
        values.ShouldContain(ClosedState.ClientClosed);
    }

    /// <summary>
    /// ClientClosedReason.ToReasonString returns expected human-readable strings.
    /// Go: closed_conns_test.go:136 — checkReason, conns[0].Reason
    /// </summary>
    [Theory]
    [InlineData(ClientClosedReason.ClientClosed, "Client Closed")]
    [InlineData(ClientClosedReason.AuthenticationTimeout, "Authentication Timeout")]
    [InlineData(ClientClosedReason.MaxPayloadExceeded, "Maximum Payload Exceeded")]
    [InlineData(ClientClosedReason.StaleConnection, "Stale Connection")]
    [InlineData(ClientClosedReason.ServerShutdown, "Server Shutdown")]
    public void ClosedState_reason_string_contains_human_readable_text(
        ClientClosedReason reason, string expectedSubstring)
    {
        // Go: closed_conns_test.go:136 — checkReason
        reason.ToReasonString().ShouldContain(expectedSubstring);
    }
}
