// Port of Go server/reload_test.go — TestConfigReloadMaxConnections,
// TestConfigReloadEnableUserAuthentication, TestConfigReloadDisableUserAuthentication,
// and connection-survival during reload.
// Reference: golang/nats-server/server/reload_test.go lines 1978, 720, 781.

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>
/// Parity tests for config hot reload behaviour.
/// Covers the three scenarios from Go's reload_test.go:
///   - MaxConnections reduction takes effect on new connections
///   - Enabling authentication rejects new unauthorised connections
///   - Existing connections survive a benign (logging) config reload
/// </summary>
public class ConfigReloadParityTests
{
    // ─── 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<(NatsServer server, int port, CancellationTokenSource cts)> StartServerAsync(NatsOptions options)
    {
        var port = GetFreePort();
        options.Port = port;
        var server = new NatsServer(options, NullLoggerFactory.Instance);
        var cts = new CancellationTokenSource();
        _ = server.StartAsync(cts.Token);
        await server.WaitForReadyAsync();
        return (server, port, cts);
    }

    /// <summary>
    /// Connects a raw TCP client and reads the initial INFO line.
    /// Returns the connected socket (caller owns disposal).
    /// </summary>
    private static async Task<Socket> RawConnectAsync(int port)
    {
        var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        await sock.ConnectAsync(IPAddress.Loopback, port);

        // Drain the INFO line so subsequent reads start at the NATS protocol layer.
        var buf = new byte[4096];
        await sock.ReceiveAsync(buf, SocketFlags.None);
        return sock;
    }

    /// <summary>
    /// Reads from <paramref name="sock"/> until the accumulated response contains
    /// <paramref name="expected"/> or the timeout elapses.
    /// </summary>
    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();
    }

    /// <summary>
    /// Writes a config file, then calls <see cref="NatsServer.ReloadConfigOrThrow"/>.
    /// Mirrors the pattern from JetStreamClusterReloadTests.
    /// </summary>
    private static void WriteConfigAndReload(NatsServer server, string configPath, string configText)
    {
        File.WriteAllText(configPath, configText);
        server.ReloadConfigOrThrow();
    }

    // ─── Tests ──────────────────────────────────────────────────────────────

    /// <summary>
    /// Port of Go TestConfigReloadMaxConnections (reload_test.go:1978).
    ///
    /// Verifies that reducing MaxConnections via hot reload causes the server to
    /// reject new connections that would exceed the new limit. The .NET server
    /// enforces the limit at accept-time, so existing connections are preserved
    /// while future ones beyond the cap receive a -ERR response.
    ///
    /// Go reference: max_connections.conf sets max_connections: 1 and the Go
    /// server then closes one existing client; the .NET implementation rejects
    /// new connections instead of kicking established ones.
    /// </summary>
    [Fact]
    public async Task Reload_max_connections_takes_effect()
    {
        var configPath = Path.Combine(Path.GetTempPath(), $"natsdotnet-maxconn-{Guid.NewGuid():N}.conf");
        try
        {
            // Allocate a port first so we can embed it in the config file.
            // The server will bind to this port; the config file must match
            // to avoid a non-reloadable Port-change error on reload.
            var port = GetFreePort();

            // Start with no connection limit.
            File.WriteAllText(configPath, $"port: {port}\nmax_connections: 65536");

            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();

            try
            {
                // Establish two raw connections before limiting.
                using var c1 = await RawConnectAsync(port);
                using var c2 = await RawConnectAsync(port);

                server.ClientCount.ShouldBe(2);

                // Reload with MaxConnections = 2 (equal to current count).
                // New connections beyond this cap must be rejected.
                WriteConfigAndReload(server, configPath, $"port: {port}\nmax_connections: 2");

                // Verify the limit is now in effect: a third connection should be
                // rejected with -ERR 'maximum connections exceeded'.
                using var c3 = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                await c3.ConnectAsync(IPAddress.Loopback, port);

                // The server sends INFO then immediately -ERR and closes the socket.
                var response = await ReadUntilAsync(c3, "-ERR", timeoutMs: 5000);
                response.ShouldContain("maximum connections exceeded");
            }
            finally
            {
                await cts.CancelAsync();
                server.Dispose();
            }
        }
        finally
        {
            if (File.Exists(configPath)) File.Delete(configPath);
        }
    }

    /// <summary>
    /// Port of Go TestConfigReloadEnableUserAuthentication (reload_test.go:720).
    ///
    /// Verifies that enabling username/password authentication via hot reload
    /// causes new unauthenticated connections to be rejected with an
    /// "Authorization Violation" error, while connections using the new
    /// credentials succeed.
    /// </summary>
    [Fact]
    public async Task Reload_auth_changes_take_effect()
    {
        var configPath = Path.Combine(Path.GetTempPath(), $"natsdotnet-auth-{Guid.NewGuid():N}.conf");
        try
        {
            // Allocate a port and embed it in every config write to prevent a
            // non-reloadable Port-change error when the config file is updated.
            var port = GetFreePort();

            // Start with no authentication required.
            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();

            try
            {
                // Confirm a connection works with no credentials.
                await using var preReloadClient = new NatsConnection(new NatsOpts
                {
                    Url = $"nats://127.0.0.1:{port}",
                });
                await preReloadClient.ConnectAsync();
                await preReloadClient.PingAsync();

                // Reload with user/password authentication enabled.
                WriteConfigAndReload(server, configPath,
                    $"port: {port}\nauthorization {{\n  user: tyler\n  password: T0pS3cr3t\n}}");

                // New connections without credentials must be rejected.
                await using var noAuthClient = new NatsConnection(new NatsOpts
                {
                    Url = $"nats://127.0.0.1:{port}",
                    MaxReconnectRetry = 0,
                });

                var ex = await Should.ThrowAsync<NatsException>(async () =>
                {
                    await noAuthClient.ConnectAsync();
                    await noAuthClient.PingAsync();
                });

                ContainsInChain(ex, "Authorization Violation").ShouldBeTrue(
                    $"Expected 'Authorization Violation' in exception chain, but got: {ex}");

                // New connections with the correct credentials must succeed.
                await using var authClient = new NatsConnection(new NatsOpts
                {
                    Url = $"nats://tyler:T0pS3cr3t@127.0.0.1:{port}",
                });
                await authClient.ConnectAsync();
                await authClient.PingAsync();
            }
            finally
            {
                await cts.CancelAsync();
                server.Dispose();
            }
        }
        finally
        {
            if (File.Exists(configPath)) File.Delete(configPath);
        }
    }

    /// <summary>
    /// Port of Go TestConfigReloadDisableUserAuthentication (reload_test.go:781).
    ///
    /// Verifies that disabling authentication via hot reload allows new
    /// connections without credentials to succeed. Also verifies that
    /// connections established before the reload survive the reload cycle
    /// (the server must not close healthy clients on a logging-only reload).
    /// </summary>
    [Fact]
    public async Task Reload_preserves_existing_connections()
    {
        var configPath = Path.Combine(Path.GetTempPath(), $"natsdotnet-preserve-{Guid.NewGuid():N}.conf");
        try
        {
            // Allocate a port and embed it in every config write to prevent a
            // non-reloadable Port-change error when the config file is updated.
            var port = GetFreePort();

            // Start with debug disabled.
            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();

            try
            {
                // Establish a connection before the reload.
                await using var client = new NatsConnection(new NatsOpts
                {
                    Url = $"nats://127.0.0.1:{port}",
                });
                await client.ConnectAsync();
                await client.PingAsync();

                // The connection should be alive before reload.
                client.ConnectionState.ShouldBe(NatsConnectionState.Open);

                // Reload with a logging-only change (debug flag); this must not
                // disconnect existing clients.
                WriteConfigAndReload(server, configPath, $"port: {port}\ndebug: true");

                // Give the server a moment to apply changes.
                await Task.Delay(100);

                // The pre-reload connection should still be alive.
                client.ConnectionState.ShouldBe(NatsConnectionState.Open,
                    "Existing connection should survive a logging-only config reload");

                // Verify the connection is still functional.
                await client.PingAsync();
            }
            finally
            {
                await cts.CancelAsync();
                server.Dispose();
            }
        }
        finally
        {
            if (File.Exists(configPath)) File.Delete(configPath);
        }
    }

    // ─── Private helpers ────────────────────────────────────────────────────

    /// <summary>
    /// Checks whether any exception in the chain contains the given substring,
    /// matching the pattern used in AuthIntegrationTests.
    /// </summary>
    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;
    }
}
