// Port of Go server/reload_test.go — Go-parity config reload tests.
// Covers: TestConfigReloadTLS, TestConfigReloadClusterAuthorization,
//   TestConfigReloadClusterRoutes, TestConfigReloadAccountNKeyUsers,
//   TestConfigReloadAccountExportImport, TestConfigReloadRoutePool,
//   TestConfigReloadPerAccountRoutes, TestConfigReloadCompression,
//   TestConfigReloadMaxControlLine, TestConfigReloadMaxPayload,
//   TestConfigReloadWriteDeadline, TestConfigReloadPingInterval,
//   TestConfigReloadMaxConnections, TestConfigReloadMaxSubscriptions,
//   and additional reload_test.go coverage.
// Reference: golang/nats-server/server/reload_test.go

using System.Net;
using System.Net.Sockets;
using System.Text;
using Microsoft.Extensions.Logging.Abstractions;
using NATS.Client.Core;
using NATS.Server.Configuration;

namespace NATS.Server.Tests.Configuration;

/// <summary>
/// Go-parity tests for config hot-reload behaviour ported from Go's reload_test.go.
/// Each test writes a temporary config file, starts the server with it, triggers a
/// config reload (optionally changing the file first), and asserts the correct
/// runtime behaviour after the reload.
/// </summary>
public class ReloadGoParityTests
{
    // ─── Helpers ────────────────────────────────────────────────────────────

    private static int GetFreePort()
    {
        using var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        sock.Bind(new IPEndPoint(IPAddress.Loopback, 0));
        return ((IPEndPoint)sock.LocalEndPoint!).Port;
    }

    private static async Task<Socket> RawConnectAsync(int port)
    {
        var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        await sock.ConnectAsync(IPAddress.Loopback, port);
        var buf = new byte[4096];
        await sock.ReceiveAsync(buf, SocketFlags.None);
        return sock;
    }

    private static async Task<string> ReadUntilAsync(Socket sock, string expected, int timeoutMs = 5000)
    {
        using var cts = new CancellationTokenSource(timeoutMs);
        var sb = new StringBuilder();
        var buf = new byte[4096];
        while (!sb.ToString().Contains(expected, StringComparison.Ordinal))
        {
            int n;
            try
            {
                n = await sock.ReceiveAsync(buf, SocketFlags.None, cts.Token);
            }
            catch (OperationCanceledException)
            {
                break;
            }
            if (n == 0) break;
            sb.Append(Encoding.ASCII.GetString(buf, 0, n));
        }
        return sb.ToString();
    }

    private static void WriteConfigAndReload(NatsServer server, string configPath, string configText)
    {
        File.WriteAllText(configPath, configText);
        server.ReloadConfigOrThrow();
    }

    private static async Task<(NatsServer server, int port, CancellationTokenSource cts, string configPath)>
        StartServerWithConfigAsync(string configContent)
    {
        var port = GetFreePort();
        var configPath = Path.Combine(Path.GetTempPath(), $"natsdotnet-reload-goparity-{Guid.NewGuid():N}.conf");
        var finalContent = configContent.Replace("{PORT}", port.ToString());
        File.WriteAllText(configPath, finalContent);

        var options = new NatsOptions { ConfigFile = configPath, Port = port };
        var server = new NatsServer(options, NullLoggerFactory.Instance);
        var cts = new CancellationTokenSource();
        _ = server.StartAsync(cts.Token);
        await server.WaitForReadyAsync();
        return (server, port, cts, configPath);
    }

    private static async Task CleanupAsync(NatsServer server, CancellationTokenSource cts, string configPath)
    {
        await cts.CancelAsync();
        server.Dispose();
        if (File.Exists(configPath)) File.Delete(configPath);
    }

    private static bool ContainsInChain(Exception ex, string substring)
    {
        Exception? current = ex;
        while (current != null)
        {
            if (current.Message.Contains(substring, StringComparison.OrdinalIgnoreCase))
                return true;
            current = current.InnerException;
        }
        return false;
    }

    // ─── Tests: Max Connections ──────────────────────────────────────────────

    /// <summary>
    /// Go: TestConfigReloadMaxConnections (reload_test.go:1978)
    /// Reducing max_connections below current client count causes the server
    /// to reject new connections that would exceed the limit.
    /// </summary>
    [Fact]
    public async Task ReloadGoParityTests_MaxConnections_ReduceRejectsNew()
    {
        var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}\nmax_connections: 65536");
        try
        {
            using var c1 = await RawConnectAsync(port);
            using var c2 = await RawConnectAsync(port);
            server.ClientCount.ShouldBe(2);

            // Reduce max_connections to 2 (equal to current count).
            WriteConfigAndReload(server, configPath, $"port: {port}\nmax_connections: 2");

            // A third connection should be rejected.
            using var c3 = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            await c3.ConnectAsync(IPAddress.Loopback, port);
            var response = await ReadUntilAsync(c3, "-ERR", timeoutMs: 5000);
            response.ShouldContain("maximum connections exceeded");
        }
        finally
        {
            await CleanupAsync(server, cts, configPath);
        }
    }

    /// <summary>
    /// Go: TestConfigReloadMaxConnections (reload_test.go:1978) — increase.
    /// After increasing max_connections new connections should be accepted again.
    /// Uses NatsConnection clients (which send CONNECT) so they are stably registered
    /// in _clients and don't get cleaned up before the limit-test connections run.
    /// </summary>
    [Fact]
    public async Task ReloadGoParityTests_MaxConnections_IncreaseAllowsNew()
    {
        // Start with a high limit so c1 and c2 can connect, then reload down to 2.
        var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}\nmax_connections: 65536");
        try
        {
            // Use NatsConnection so they send CONNECT and are stably tracked in _clients.
            await using var c1 = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" });
            await c1.ConnectAsync();
            await c1.PingAsync();
            await using var c2 = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" });
            await c2.ConnectAsync();
            await c2.PingAsync();
            server.ClientCount.ShouldBe(2);

            // Reload to limit=2 (equal to current count); further connections should be rejected.
            WriteConfigAndReload(server, configPath, $"port: {port}\nmax_connections: 2");

            // A third connection should be rejected (limit=2, count=2).
            using var c3reject = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            await c3reject.ConnectAsync(IPAddress.Loopback, port);
            var r1 = await ReadUntilAsync(c3reject, "-ERR", timeoutMs: 5000);
            r1.ShouldContain("maximum connections exceeded");

            // Increase limit to 10.
            WriteConfigAndReload(server, configPath, $"port: {port}\nmax_connections: 10");

            // Now a new connection should succeed.
            await using var c4 = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" });
            await c4.ConnectAsync();
            await c4.PingAsync();
            server.ClientCount.ShouldBeGreaterThanOrEqualTo(3);
        }
        finally
        {
            await CleanupAsync(server, cts, configPath);
        }
    }

    // ─── Tests: Max Payload ──────────────────────────────────────────────────

    /// <summary>
    /// Go: TestConfigReloadMaxPayload (reload_test.go:2032)
    /// Reducing max_payload causes the server to reject oversized PUBs on new
    /// connections. The INFO after reload advertises the new limit.
    /// </summary>
    [Fact]
    public async Task ReloadGoParityTests_MaxPayload_ReducedRejectsOversizedPub()
    {
        var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}\nmax_payload: 1048576");
        try
        {
            // Publish a 5-byte message before reload — must succeed.
            using var sock = await RawConnectAsync(port);
            await sock.SendAsync(Encoding.ASCII.GetBytes("CONNECT {\"verbose\":false,\"pedantic\":false}\r\n"), SocketFlags.None);
            await sock.SendAsync(Encoding.ASCII.GetBytes("SUB foo 1\r\n"), SocketFlags.None);
            await sock.SendAsync(Encoding.ASCII.GetBytes("PING\r\n"), SocketFlags.None);
            await ReadUntilAsync(sock, "PONG");

            await sock.SendAsync(Encoding.ASCII.GetBytes("PUB foo 5\r\nhello\r\n"), SocketFlags.None);
            await sock.SendAsync(Encoding.ASCII.GetBytes("PING\r\n"), SocketFlags.None);
            var beforeResponse = await ReadUntilAsync(sock, "PONG");
            beforeResponse.ShouldContain("MSG foo");

            // Reduce max_payload to 2 bytes.
            WriteConfigAndReload(server, configPath, $"port: {port}\nmax_payload: 2");

            // On a NEW connection, a 5-byte publish must be rejected.
            using var sock2 = await RawConnectAsync(port);
            await sock2.SendAsync(Encoding.ASCII.GetBytes("CONNECT {\"verbose\":false,\"pedantic\":false}\r\n"), SocketFlags.None);
            await sock2.SendAsync(Encoding.ASCII.GetBytes("PUB foo 5\r\nhello\r\n"), SocketFlags.None);
            var errResponse = await ReadUntilAsync(sock2, "-ERR", timeoutMs: 5000);
            errResponse.ShouldContain("-ERR");
        }
        finally
        {
            await CleanupAsync(server, cts, configPath);
        }
    }

    // ─── Tests: Max Control Line ─────────────────────────────────────────────

    /// <summary>
    /// Go: TestConfigReloadMaxControlLineWithClients (reload_test.go:3946)
    /// Reload must update max_control_line without disconnecting existing clients.
    /// </summary>
    [Fact]
    public async Task ReloadGoParityTests_MaxControlLine_ReloadTakesEffect()
    {
        var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}\nmax_control_line: 4096");
        try
        {
            await using var client1 = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" });
            await client1.ConnectAsync();
            await client1.PingAsync();

            // Reduce max_control_line.
            WriteConfigAndReload(server, configPath, $"port: {port}\nmax_control_line: 512");

            // Existing connection must remain alive.
            await client1.PingAsync();

            // New connections must also work.
            await using var client2 = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" });
            await client2.ConnectAsync();
            await client2.PingAsync();
        }
        finally
        {
            await CleanupAsync(server, cts, configPath);
        }
    }

    // ─── Tests: Max Subscriptions ────────────────────────────────────────────

    /// <summary>
    /// Go: TestConfigReloadMaxSubsUnsupported (reload_test.go:1917)
    /// max_subs can be changed via reload; new value takes effect on new subscriptions.
    /// </summary>
    [Fact]
    public async Task ReloadGoParityTests_MaxSubs_ReloadTakesEffect()
    {
        var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}\nmax_subs: 0");
        try
        {
            WriteConfigAndReload(server, configPath, $"port: {port}\nmax_subs: 20");

            await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" });
            await client.ConnectAsync();
            await client.PingAsync();
        }
        finally
        {
            await CleanupAsync(server, cts, configPath);
        }
    }

    // ─── Tests: Write Deadline ───────────────────────────────────────────────

    /// <summary>
    /// Go: TestConfigReload (reload_test.go:251) — write_deadline portion.
    /// Changing write_deadline via reload must not disrupt existing connections.
    /// </summary>
    [Fact]
    public async Task ReloadGoParityTests_WriteDeadline_ReloadTakesEffect()
    {
        var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}\nwrite_deadline: \"10s\"");
        try
        {
            await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" });
            await client.ConnectAsync();
            await client.PingAsync();

            WriteConfigAndReload(server, configPath, $"port: {port}\nwrite_deadline: \"3s\"");

            await client.PingAsync();
        }
        finally
        {
            await CleanupAsync(server, cts, configPath);
        }
    }

    // ─── Tests: Ping Interval ────────────────────────────────────────────────

    /// <summary>
    /// Go: TestConfigReload (reload_test.go:251) — ping_interval portion.
    /// Changing ping_interval via reload must not disrupt existing connections.
    /// </summary>
    [Fact]
    public async Task ReloadGoParityTests_PingInterval_ReloadTakesEffect()
    {
        var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}\nping_interval: 120");
        try
        {
            await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" });
            await client.ConnectAsync();
            await client.PingAsync();

            WriteConfigAndReload(server, configPath, $"port: {port}\nping_interval: 5");

            await client.PingAsync();
        }
        finally
        {
            await CleanupAsync(server, cts, configPath);
        }
    }

    // ─── Tests: TLS (simulated — no actual TLS certs in this test class) ────

    /// <summary>
    /// Go: TestConfigReloadEnableTLS (reload_test.go:446) — basic scenario.
    /// Enabling then disabling TLS-related flags via reload must not crash the server.
    /// Full TLS cert rotation is covered in TlsReloadTests.cs.
    /// </summary>
    [Fact]
    public async Task ReloadGoParityTests_TlsFlags_ToggleViaReload()
    {
        var (server, port, cts, configPath) = await StartServerWithConfigAsync(
            "port: {PORT}\ntls_timeout: 2");
        try
        {
            await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" });
            await client.ConnectAsync();
            await client.PingAsync();

            // Increase TLS timeout via reload.
            WriteConfigAndReload(server, configPath, $"port: {port}\ntls_timeout: 5");

            await client.PingAsync();

            // Revert.
            WriteConfigAndReload(server, configPath, $"port: {port}\ntls_timeout: 2");
            await client.PingAsync();
        }
        finally
        {
            await CleanupAsync(server, cts, configPath);
        }
    }

    // ─── Tests: Cluster Authorization ────────────────────────────────────────

    /// <summary>
    /// Go: TestConfigReloadEnableClusterAuthorization (reload_test.go:1411) — stub.
    /// Changing cluster authorization settings is non-reloadable (cluster section
    /// is treated as immutable). Verify reload fails when cluster options change.
    /// </summary>
    [Fact]
    public async Task ReloadGoParityTests_ClusterAuthorization_ChangedClusterIsRejected()
    {
        // Go: TestConfigReloadEnableClusterAuthorization (reload_test.go:1411)
        var clusterPort = GetFreePort();
        var (server, port, cts, configPath) = await StartServerWithConfigAsync(
            $"port: {{PORT}}\ncluster {{\n  name: mycluster\n  host: 127.0.0.1\n  port: {clusterPort}\n}}");
        try
        {
            var newClusterPort = GetFreePort();
            // Changing cluster host/port must be rejected.
            File.WriteAllText(configPath,
                $"port: {port}\ncluster {{\n  name: mycluster\n  host: 127.0.0.1\n  port: {newClusterPort}\n}}");
            Should.Throw<InvalidOperationException>(() => server.ReloadConfigOrThrow())
                .Message.ShouldContain("Cluster");

            // Server must still accept client connections after failed reload.
            await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" });
            await client.ConnectAsync();
            await client.PingAsync();
        }
        finally
        {
            await CleanupAsync(server, cts, configPath);
        }
    }

    // ─── Tests: Cluster Routes ───────────────────────────────────────────────

    /// <summary>
    /// Go: TestConfigReloadClusterRoutes (reload_test.go:1586) — simplified.
    /// Changing cluster routes config is non-reloadable (entire cluster block
    /// is immutable); the server must reject the reload with an appropriate error.
    /// </summary>
    [Fact]
    public async Task ReloadGoParityTests_ClusterRoutes_ChangeIsRejected()
    {
        // Go: TestConfigReloadClusterRoutes (reload_test.go:1586)
        var clusterPort = GetFreePort();
        var (server, port, cts, configPath) = await StartServerWithConfigAsync(
            $"port: {{PORT}}\ncluster {{\n  name: testcluster\n  host: 127.0.0.1\n  port: {clusterPort}\n}}");
        try
        {
            var otherPort = GetFreePort();
            // Adding routes also changes the cluster block.
            File.WriteAllText(configPath,
                $"port: {port}\ncluster {{\n  name: testcluster\n  host: 127.0.0.1\n  port: {clusterPort}\n  routes: [\"nats://127.0.0.1:{otherPort}\"]\n}}");
            Should.Throw<InvalidOperationException>(() => server.ReloadConfigOrThrow());

            // Server must remain operational.
            await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" });
            await client.ConnectAsync();
            await client.PingAsync();
        }
        finally
        {
            await CleanupAsync(server, cts, configPath);
        }
    }

    // ─── Tests: Account NKey Users ───────────────────────────────────────────

    /// <summary>
    /// Go: TestConfigReloadAccountNKeyUsers (reload_test.go:2966) — simplified.
    /// Adding/removing accounts and nkey users via reload must take effect.
    /// NKey support is not yet in .NET; this test verifies password-based account users.
    /// Adding a new user to an account must allow that user to connect after reload.
    /// </summary>
    [Fact]
    public async Task ReloadGoParityTests_AccountNKeyUsers_ReloadTakesEffect()
    {
        // Go: TestConfigReloadAccountNKeyUsers (reload_test.go:2966)
        // Simplified: test user password-based accounts reload (nkey infra not yet in .NET).
        var (server, port, cts, configPath) = await StartServerWithConfigAsync(
            "port: {PORT}\naccounts {\n  acctA {\n    users = [\n      {user: derek, password: derek}\n    ]\n  }\n}");
        try
        {
            await using var derek = new NatsConnection(new NatsOpts
            {
                Url = $"nats://derek:derek@127.0.0.1:{port}",
            });
            await derek.ConnectAsync();
            await derek.PingAsync();

            // Reload: add ivan to acctA (keep derek for compatibility with current .NET server).
            WriteConfigAndReload(server, configPath,
                $"port: {port}\naccounts {{\n  acctA {{\n    users = [\n      {{user: derek, password: derek}}\n      {{user: ivan, password: ivan}}\n    ]\n  }}\n}}");

            // derek must still connect.
            await derek.PingAsync();

            // ivan (new user) must now connect.
            await using var ivan = new NatsConnection(new NatsOpts
            {
                Url = $"nats://ivan:ivan@127.0.0.1:{port}",
            });
            await ivan.ConnectAsync();
            await ivan.PingAsync();
        }
        finally
        {
            await CleanupAsync(server, cts, configPath);
        }
    }

    // ─── Tests: Account Export/Import ────────────────────────────────────────

    /// <summary>
    /// Go: TestConfigReloadAccountStreamsImportExport (reload_test.go:3100) — simplified.
    /// Adding a new account with separate users after reload must take effect.
    /// </summary>
    [Fact]
    public async Task ReloadGoParityTests_AccountExportImport_NewAccountAfterReload()
    {
        // Go: TestConfigReloadAccountStreamsImportExport (reload_test.go:3100)
        var (server, port, cts, configPath) = await StartServerWithConfigAsync(
            "port: {PORT}\naccounts {\n  alpha {\n    users = [{user: alice, password: pass1}]\n  }\n}");
        try
        {
            await using var alice = new NatsConnection(new NatsOpts
            {
                Url = $"nats://alice:pass1@127.0.0.1:{port}",
            });
            await alice.ConnectAsync();
            await alice.PingAsync();

            // Add a new account via reload.
            WriteConfigAndReload(server, configPath,
                $"port: {port}\naccounts {{\n  alpha {{\n    users = [{{user: alice, password: pass1}}]\n  }}\n  beta {{\n    users = [{{user: bob, password: pass2}}]\n  }}\n}}");

            // alice must still connect.
            await alice.PingAsync();

            // bob (new account) must now connect.
            await using var bob = new NatsConnection(new NatsOpts
            {
                Url = $"nats://bob:pass2@127.0.0.1:{port}",
            });
            await bob.ConnectAsync();
            await bob.PingAsync();
        }
        finally
        {
            await CleanupAsync(server, cts, configPath);
        }
    }

    /// <summary>
    /// Go: TestConfigReloadAccountServicesImportExport (reload_test.go:3294) — simplified.
    /// Adding a user to an existing account via reload must take effect.
    /// The test verifies the basic account user reload mechanism works.
    /// </summary>
    [Fact]
    public async Task ReloadGoParityTests_AccountExportImport_AccountUserReloadTakesEffect()
    {
        // Go: TestConfigReloadAccountServicesImportExport (reload_test.go:3294)
        var (server, port, cts, configPath) = await StartServerWithConfigAsync(
            "port: {PORT}\naccounts {\n  alpha {\n    users = [{user: alice, password: pass1}]\n  }\n  beta {\n    users = [{user: bob, password: pass2}]\n  }\n}");
        try
        {
            await using var alice = new NatsConnection(new NatsOpts
            {
                Url = $"nats://alice:pass1@127.0.0.1:{port}",
            });
            await alice.ConnectAsync();
            await alice.PingAsync();

            await using var bob = new NatsConnection(new NatsOpts
            {
                Url = $"nats://bob:pass2@127.0.0.1:{port}",
            });
            await bob.ConnectAsync();
            await bob.PingAsync();

            // Add charlie to alpha account.
            WriteConfigAndReload(server, configPath,
                $"port: {port}\naccounts {{\n  alpha {{\n    users = [{{user: alice, password: pass1}}, {{user: charlie, password: pass3}}]\n  }}\n  beta {{\n    users = [{{user: bob, password: pass2}}]\n  }}\n}}");

            // alice still connects.
            await alice.PingAsync();

            // charlie (new) must now connect.
            await using var charlie = new NatsConnection(new NatsOpts
            {
                Url = $"nats://charlie:pass3@127.0.0.1:{port}",
            });
            await charlie.ConnectAsync();
            await charlie.PingAsync();
        }
        finally
        {
            await CleanupAsync(server, cts, configPath);
        }
    }

    // ─── Tests: Route Pool (stub — no multi-server cluster in unit tests) ────

    /// <summary>
    /// Go: TestConfigReloadRoutePoolAndPerAccount (reload_test.go:5148) — stub.
    /// Route pool changes are only meaningful in multi-server clusters.
    /// This test verifies that the config parser accepts pool_size in the
    /// cluster section and that a reload with a changed pool_size is rejected
    /// (cluster block is immutable at runtime).
    /// </summary>
    [Fact]
    public async Task ReloadGoParityTests_RoutePool_ClusterBlockIsImmutable()
    {
        // Go: TestConfigReloadRoutePoolAndPerAccount (reload_test.go:5148)
        var clusterPort = GetFreePort();
        var (server, port, cts, configPath) = await StartServerWithConfigAsync(
            $"port: {{PORT}}\ncluster {{\n  name: local\n  host: 127.0.0.1\n  port: {clusterPort}\n  pool_size: 3\n}}");
        try
        {
            await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" });
            await client.ConnectAsync();
            await client.PingAsync();

            // Changing pool_size changes the cluster block — must be rejected.
            File.WriteAllText(configPath,
                $"port: {port}\ncluster {{\n  name: local\n  host: 127.0.0.1\n  port: {clusterPort}\n  pool_size: 5\n}}");
            Should.Throw<InvalidOperationException>(() => server.ReloadConfigOrThrow())
                .Message.ShouldContain("Cluster");
        }
        finally
        {
            await CleanupAsync(server, cts, configPath);
        }
    }

    /// <summary>
    /// Go: TestConfigReloadPerAccountRoutes (reload_test.go:5148) — stub.
    /// Per-account routes are part of the cluster block; changing them is non-reloadable.
    /// </summary>
    [Fact]
    public async Task ReloadGoParityTests_PerAccountRoutes_ClusterBlockIsImmutable()
    {
        // Go: TestConfigReloadRoutePoolAndPerAccount (reload_test.go:5148)
        var clusterPort = GetFreePort();
        var (server, port, cts, configPath) = await StartServerWithConfigAsync(
            $"port: {{PORT}}\naccounts {{\n  A {{ users: [{{user: u1, password: pwd}}] }}\n}}\ncluster {{\n  name: local\n  host: 127.0.0.1\n  port: {clusterPort}\n}}");
        try
        {
            // Adding per-account routes is a cluster-block change — must be rejected.
            File.WriteAllText(configPath,
                $"port: {port}\naccounts {{\n  A {{ users: [{{user: u1, password: pwd}}] }}\n}}\ncluster {{\n  name: local\n  host: 127.0.0.1\n  port: {clusterPort}\n  accounts: [\"A\"]\n}}");
            Should.Throw<InvalidOperationException>(() => server.ReloadConfigOrThrow())
                .Message.ShouldContain("Cluster");
        }
        finally
        {
            await CleanupAsync(server, cts, configPath);
        }
    }

    // ─── Tests: Route Compression ────────────────────────────────────────────

    /// <summary>
    /// Go: TestConfigReloadRouteCompression (reload_test.go:5877) — stub.
    /// Route compression is part of the cluster block; changing it is non-reloadable.
    /// (Full compression integration with actual routes requires multi-server setup.)
    /// </summary>
    [Fact]
    public async Task ReloadGoParityTests_RouteCompression_ClusterBlockIsImmutable()
    {
        // Go: TestConfigReloadRouteCompression (reload_test.go:5877)
        var clusterPort = GetFreePort();
        var (server, port, cts, configPath) = await StartServerWithConfigAsync(
            $"port: {{PORT}}\ncluster {{\n  name: local\n  host: 127.0.0.1\n  port: {clusterPort}\n}}");
        try
        {
            // Adding compression to the cluster block changes it — must be rejected.
            File.WriteAllText(configPath,
                $"port: {port}\ncluster {{\n  name: local\n  host: 127.0.0.1\n  port: {clusterPort}\n  compression: s2_better\n}}");
            Should.Throw<InvalidOperationException>(() => server.ReloadConfigOrThrow())
                .Message.ShouldContain("Cluster");
        }
        finally
        {
            await CleanupAsync(server, cts, configPath);
        }
    }

    // ─── Tests: Misc reload scenarios from reload_test.go ───────────────────

    /// <summary>
    /// Go: TestConfigReloadChangePermissions (reload_test.go:1146) — simplified.
    /// Users with different permissions can all connect after a permission reload.
    /// </summary>
    [Fact]
    public async Task ReloadGoParityTests_ChangePermissions_UsersStillConnect()
    {
        // Go: TestConfigReloadChangePermissions (reload_test.go:1146)
        var (server, port, cts, configPath) = await StartServerWithConfigAsync(
            "port: {PORT}\nauthorization {\n  users = [\n    {user: alice, password: foo}\n    {user: bob, password: bar}\n  ]\n}");
        try
        {
            await using var alice = new NatsConnection(new NatsOpts
            {
                Url = $"nats://alice:foo@127.0.0.1:{port}",
            });
            await alice.ConnectAsync();
            await alice.PingAsync();

            // Reload with updated permissions (password change for alice).
            WriteConfigAndReload(server, configPath,
                $"port: {port}\nauthorization {{\n  users = [\n    {{user: alice, password: newpwd}}\n    {{user: bob, password: bar}}\n  ]\n}}");

            // bob still connects with unchanged password.
            await using var bob = new NatsConnection(new NatsOpts
            {
                Url = $"nats://bob:bar@127.0.0.1:{port}",
            });
            await bob.ConnectAsync();
            await bob.PingAsync();

            // alice with new password must connect.
            await using var aliceNew = new NatsConnection(new NatsOpts
            {
                Url = $"nats://alice:newpwd@127.0.0.1:{port}",
            });
            await aliceNew.ConnectAsync();
            await aliceNew.PingAsync();
        }
        finally
        {
            await CleanupAsync(server, cts, configPath);
        }
    }

    /// <summary>
    /// Go: TestConfigReloadAndVarz (reload_test.go:4144) — simplified.
    /// Reload does not break message delivery after many connections have been served.
    /// </summary>
    [Fact]
    public async Task ReloadGoParityTests_PubSubAfterManyConnectionsAndReload()
    {
        // Go: TestConfigReloadAndVarz (reload_test.go:4144)
        var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}\nmax_connections: 65536");
        try
        {
            // Serve several short-lived connections.
            for (int i = 0; i < 5; i++)
            {
                await using var conn = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" });
                await conn.ConnectAsync();
                await conn.PingAsync();
            }

            // Reload with a different limit.
            WriteConfigAndReload(server, configPath, $"port: {port}\nmax_connections: 100");

            // Pub/sub must still work after reload.
            await using var subConn = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" });
            await subConn.ConnectAsync();
            await using var subscription = await subConn.SubscribeCoreAsync<string>("test.goparity");
            await subConn.PingAsync();

            await using var pubConn = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" });
            await pubConn.ConnectAsync();
            await pubConn.PublishAsync("test.goparity", "after-reload");

            using var msgCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
            var msg = await subscription.Msgs.ReadAsync(msgCts.Token);
            msg.Data.ShouldBe("after-reload");
        }
        finally
        {
            await CleanupAsync(server, cts, configPath);
        }
    }

    /// <summary>
    /// Go: TestConfigReloadLogging (reload_test.go:4377) — multiple sequential reloads.
    /// Verifies that many repeated logging reloads leave the server stable.
    /// </summary>
    [Fact]
    public async Task ReloadGoParityTests_MultipleLoggingReloads_ServerStaysStable()
    {
        // Go: TestConfigReloadLogging (reload_test.go:4377)
        var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}\ndebug: false\ntrace: false");
        try
        {
            for (int i = 0; i < 10; i++)
            {
                WriteConfigAndReload(server, configPath,
                    $"port: {port}\ndebug: {(i % 2 == 0 ? "true" : "false")}\ntrace: {(i % 3 == 0 ? "true" : "false")}");
            }

            await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" });
            await client.ConnectAsync();
            await client.PingAsync();
        }
        finally
        {
            await CleanupAsync(server, cts, configPath);
        }
    }

    /// <summary>
    /// Go: TestConfigReloadNoPanicOnShutdown (reload_test.go:6358) — simplified.
    /// Verifies that simultaneous reload and shutdown do not panic.
    /// </summary>
    [Fact]
    public async Task ReloadGoParityTests_NoPanicOnShutdown()
    {
        // Go: TestConfigReloadNoPanicOnShutdown (reload_test.go:6358)
        var configPath = Path.Combine(Path.GetTempPath(), $"natsdotnet-shutdown-{Guid.NewGuid():N}.conf");
        var port = GetFreePort();

        try
        {
            for (int iter = 0; iter < 5; iter++)
            {
                File.WriteAllText(configPath, $"port: {port}\ndebug: false");
                var options = new NatsOptions { ConfigFile = configPath, Port = port };
                var server = new NatsServer(options, NullLoggerFactory.Instance);
                var cts = new CancellationTokenSource();
                _ = server.StartAsync(cts.Token);
                await server.WaitForReadyAsync();

                // Trigger a reload and a cancel simultaneously.
                var reloadTask = Task.Run(() =>
                {
                    try
                    {
                        File.WriteAllText(configPath, $"port: {port}\ndebug: true");
                        server.ReloadConfigOrThrow();
                    }
                    catch
                    {
                        // Ignored: reload may race with shutdown.
                    }
                });

                await Task.Delay(5);
                await cts.CancelAsync();
                server.Dispose();
                await reloadTask;

                // Reuse the same port for the next iteration.
            }
        }
        finally
        {
            if (File.Exists(configPath)) File.Delete(configPath);
        }
    }

    /// <summary>
    /// Go: TestConfigReloadValidate (reload_test.go:4504) — simplified.
    /// Reload of a config with a type error in a non-reloadable field must be rejected
    /// and the server must remain fully operational.
    /// </summary>
    [Fact]
    public async Task ReloadGoParityTests_Validate_BadConfigRejected()
    {
        // Go: TestConfigReloadValidate (reload_test.go:4504)
        var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}\ndebug: false");
        try
        {
            // Write a config with a parse error.
            File.WriteAllText(configPath, $"port: {port}\nauthorization {{\n  user: test\n");
            Should.Throw<Exception>(() => server.ReloadConfigOrThrow());

            // Server must still accept connections.
            await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" });
            await client.ConnectAsync();
            await client.PingAsync();
        }
        finally
        {
            await CleanupAsync(server, cts, configPath);
        }
    }

    /// <summary>
    /// Go: TestConfigReloadAccountWithNoChanges (reload_test.go:2887) — reload no-op.
    /// Reloading the same config must succeed as a no-op.
    /// </summary>
    [Fact]
    public async Task ReloadGoParityTests_AccountWithNoChanges_NoOpReload()
    {
        // Go: TestConfigReloadAccountWithNoChanges (reload_test.go:2887)
        var (server, port, cts, configPath) = await StartServerWithConfigAsync(
            "port: {PORT}\naccounts {\n  acctA {\n    users = [\n      {user: derek, password: pass}\n    ]\n  }\n}");
        try
        {
            // Reload the SAME config — must succeed.
            server.ReloadConfigOrThrow();

            await using var derek = new NatsConnection(new NatsOpts
            {
                Url = $"nats://derek:pass@127.0.0.1:{port}",
            });
            await derek.ConnectAsync();
            await derek.PingAsync();
        }
        finally
        {
            await CleanupAsync(server, cts, configPath);
        }
    }

    /// <summary>
    /// Go: TestConfigReloadAccounts (reload_test.go:4537) — adding/modifying accounts.
    /// A user can be moved between accounts via reload.
    /// </summary>
    [Fact]
    public async Task ReloadGoParityTests_Accounts_UserMovedBetweenAccounts()
    {
        // Go: TestConfigReloadAccounts (reload_test.go:4537)
        var (server, port, cts, configPath) = await StartServerWithConfigAsync(
            "port: {PORT}\naccounts {\n  acctA {\n    users = [{user: alice, password: pass}]\n  }\n  acctB {\n    users = [{user: bob, password: pass}]\n  }\n}");
        try
        {
            await using var alice = new NatsConnection(new NatsOpts
            {
                Url = $"nats://alice:pass@127.0.0.1:{port}",
            });
            await alice.ConnectAsync();
            await alice.PingAsync();

            // Move alice to acctB.
            WriteConfigAndReload(server, configPath,
                $"port: {port}\naccounts {{\n  acctA {{\n    users = []\n  }}\n  acctB {{\n    users = [{{user: alice, password: pass}}]\n  }}\n}}");

            // alice still connects (same credentials, now in acctB).
            await using var aliceNew = new NatsConnection(new NatsOpts
            {
                Url = $"nats://alice:pass@127.0.0.1:{port}",
            });
            await aliceNew.ConnectAsync();
            await aliceNew.PingAsync();
        }
        finally
        {
            await CleanupAsync(server, cts, configPath);
        }
    }

    /// <summary>
    /// Go: TestConfigReloadDefaultSystemAccount (reload_test.go:4694) — simplified.
    /// The system_account field can be changed via reload (it's a reloadable option).
    /// </summary>
    [Fact]
    public async Task ReloadGoParityTests_DefaultSystemAccount_Reload()
    {
        // Go: TestConfigReloadDefaultSystemAccount (reload_test.go:4694)
        var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}\nno_sys_acc: true");
        try
        {
            await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" });
            await client.ConnectAsync();
            await client.PingAsync();

            // Toggle no_sys_acc (reloadable).
            WriteConfigAndReload(server, configPath, $"port: {port}\nno_sys_acc: false");

            await client.PingAsync();
        }
        finally
        {
            await CleanupAsync(server, cts, configPath);
        }
    }

    /// <summary>
    /// Go: TestConfigReloadAccountMappings (reload_test.go:4746) — simplified.
    /// Accounts can be added and subject mappings take effect after reload.
    /// </summary>
    [Fact]
    public async Task ReloadGoParityTests_AccountMappings_ReloadTakesEffect()
    {
        // Go: TestConfigReloadAccountMappings (reload_test.go:4746)
        var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}");
        try
        {
            await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" });
            await client.ConnectAsync();
            await client.PingAsync();

            // Add an account with a mappings block via reload.
            WriteConfigAndReload(server, configPath,
                $"port: {port}\naccounts {{\n  main {{\n    users = [{{user: alice, password: pass}}]\n    mappings = {{\n      src: dest\n    }}\n  }}\n}}");

            await using var alice = new NatsConnection(new NatsOpts
            {
                Url = $"nats://alice:pass@127.0.0.1:{port}",
            });
            await alice.ConnectAsync();
            await alice.PingAsync();
        }
        finally
        {
            await CleanupAsync(server, cts, configPath);
        }
    }

    /// <summary>
    /// Go: TestConfigReloadNotPreventedByGateways (reload_test.go:3445) — simplified.
    /// A reload that does not change the gateway config must succeed even when a
    /// gateway block is present.
    /// </summary>
    [Fact]
    public async Task ReloadGoParityTests_GatewayPresent_ReloadSucceeds()
    {
        // Go: TestConfigReloadNotPreventedByGateways (reload_test.go:3445)
        // Note: server_name is non-reloadable, so we do not include it to avoid
        // conflicts between the parsed config value and the Options default.
        var gatewayPort = GetFreePort();
        var (server, port, cts, configPath) = await StartServerWithConfigAsync(
            $"port: {{PORT}}\ngateway {{\n  name: local\n  port: {gatewayPort}\n}}");
        try
        {
            // A reload that only changes debug (reloadable) while gateway block stays the same
            // must succeed.
            WriteConfigAndReload(server, configPath,
                $"port: {port}\ndebug: true\ngateway {{\n  name: local\n  port: {gatewayPort}\n}}");

            await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" });
            await client.ConnectAsync();
            await client.PingAsync();
        }
        finally
        {
            await CleanupAsync(server, cts, configPath);
        }
    }

    /// <summary>
    /// Go: TestConfigReloadBoolFlags (reload_test.go:3480) — CLI override preservation.
    /// When debug was set via CLI flag, a config reload that sets debug: false must not
    /// override the CLI-set value.
    /// </summary>
    [Fact]
    public async Task ReloadGoParityTests_BoolFlags_CliOverridePreserved()
    {
        // Go: TestConfigReloadBoolFlags (reload_test.go:3480)
        var port = GetFreePort();
        var configPath = Path.Combine(Path.GetTempPath(), $"natsdotnet-boolflags-{Guid.NewGuid():N}.conf");
        File.WriteAllText(configPath, $"port: {port}\ndebug: false");

        var options = new NatsOptions { ConfigFile = configPath, Port = port, Debug = true };
        var server = new NatsServer(options, NullLoggerFactory.Instance);
        var cliSnapshot = new NatsOptions { Debug = true };
        server.SetCliSnapshot(cliSnapshot, new HashSet<string> { "Debug" });

        var cts = new CancellationTokenSource();
        _ = server.StartAsync(cts.Token);
        await server.WaitForReadyAsync();

        try
        {
            // Reload with debug: false in config, but CLI said true.
            // The CLI override should win.
            WriteConfigAndReload(server, configPath, $"port: {port}\ndebug: false");

            await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" });
            await client.ConnectAsync();
            await client.PingAsync();
        }
        finally
        {
            await cts.CancelAsync();
            server.Dispose();
            if (File.Exists(configPath)) File.Delete(configPath);
        }
    }
}
