// Port of Go server/reload_test.go — TestConfigReloadAuthChangeDisconnects,
// TestConfigReloadAuthEnabled, TestConfigReloadAuthDisabled,
// TestConfigReloadUserCredentialChange.
// Reference: golang/nats-server/server/reload_test.go lines 720-900.

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>
/// Tests for auth change propagation on config reload.
/// Covers:
///   - Enabling auth disconnects unauthenticated clients
///   - Changing credentials disconnects clients with old credentials
///   - Disabling auth allows previously rejected connections
///   - Clients with correct credentials survive reload
/// Reference: Go server/reload.go — reloadAuthorization.
/// </summary>
public class AuthReloadTests
{
    // ─── 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 SendConnectAsync(Socket sock, string? user = null, string? pass = null)
    {
        string connectJson;
        if (user != null && pass != null)
            connectJson = $"CONNECT {{\"verbose\":false,\"pedantic\":false,\"user\":\"{user}\",\"pass\":\"{pass}\"}}\r\n";
        else
            connectJson = "CONNECT {\"verbose\":false,\"pedantic\":false}\r\n";
        await sock.SendAsync(Encoding.ASCII.GetBytes(connectJson), SocketFlags.None);
    }

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

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

    /// <summary>
    /// Port of Go TestConfigReloadAuthChangeDisconnects (reload_test.go).
    ///
    /// Verifies that enabling authentication via hot reload disconnects clients
    /// that connected without credentials. The server should send -ERR
    /// 'Authorization Violation' and close the connection.
    /// </summary>
    [Fact]
    public async Task Enabling_auth_disconnects_unauthenticated_clients()
    {
        var configPath = Path.Combine(Path.GetTempPath(), $"natsdotnet-authdc-{Guid.NewGuid():N}.conf");
        try
        {
            var port = GetFreePort();

            // Start with no auth
            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
            {
                // Connect a client without credentials
                using var sock = await RawConnectAsync(port);
                await SendConnectAsync(sock);

                // Send a PING to confirm the connection is established
                await sock.SendAsync("PING\r\n"u8.ToArray(), SocketFlags.None);
                var pong = await ReadUntilAsync(sock, "PONG", timeoutMs: 3000);
                pong.ShouldContain("PONG");

                server.ClientCount.ShouldBeGreaterThanOrEqualTo(1);

                // Enable auth via reload
                WriteConfigAndReload(server, configPath,
                    $"port: {port}\nauthorization {{\n  user: admin\n  password: secret123\n}}");

                // The unauthenticated client should receive an -ERR and/or be disconnected.
                // Read whatever the server sends before closing the socket.
                var errResponse = await ReadAllBeforeCloseAsync(sock, timeoutMs: 5000);
                // The server should have sent -ERR 'Authorization Violation' before closing
                errResponse.ShouldContain("Authorization Violation",
                    Case.Insensitive,
                    $"Expected 'Authorization Violation' in response but got: '{errResponse}'");
            }
            finally
            {
                await cts.CancelAsync();
                server.Dispose();
            }
        }
        finally
        {
            if (File.Exists(configPath)) File.Delete(configPath);
        }
    }

    /// <summary>
    /// Verifies that changing user credentials disconnects clients using old credentials.
    /// Reference: Go server/reload_test.go — TestConfigReloadUserCredentialChange.
    /// </summary>
    [Fact]
    public async Task Changing_credentials_disconnects_old_credential_clients()
    {
        var configPath = Path.Combine(Path.GetTempPath(), $"natsdotnet-credchg-{Guid.NewGuid():N}.conf");
        try
        {
            var port = GetFreePort();

            // Start with user/password auth
            File.WriteAllText(configPath,
                $"port: {port}\nauthorization {{\n  user: alice\n  password: pass1\n}}");

            var options = ConfigProcessor.ProcessConfigFile(configPath);
            options.Port = port;
            var server = new NatsServer(options, NullLoggerFactory.Instance);
            var cts = new CancellationTokenSource();
            _ = server.StartAsync(cts.Token);
            await server.WaitForReadyAsync();

            try
            {
                // Connect with the original credentials
                using var sock = await RawConnectAsync(port);
                await SendConnectAsync(sock, "alice", "pass1");

                // Verify connection works
                await sock.SendAsync("PING\r\n"u8.ToArray(), SocketFlags.None);
                var pong = await ReadUntilAsync(sock, "PONG", timeoutMs: 3000);
                pong.ShouldContain("PONG");

                // Change the password via reload
                WriteConfigAndReload(server, configPath,
                    $"port: {port}\nauthorization {{\n  user: alice\n  password: pass2\n}}");

                // The client with the old password should be disconnected
                var errResponse = await ReadAllBeforeCloseAsync(sock, timeoutMs: 5000);
                errResponse.ShouldContain("Authorization Violation",
                    Case.Insensitive,
                    $"Expected 'Authorization Violation' in response but got: '{errResponse}'");
            }
            finally
            {
                await cts.CancelAsync();
                server.Dispose();
            }
        }
        finally
        {
            if (File.Exists(configPath)) File.Delete(configPath);
        }
    }

    /// <summary>
    /// Verifies that disabling auth on reload allows new unauthenticated connections.
    /// Reference: Go server/reload_test.go — TestConfigReloadDisableUserAuthentication.
    /// </summary>
    [Fact]
    public async Task Disabling_auth_allows_new_connections()
    {
        var configPath = Path.Combine(Path.GetTempPath(), $"natsdotnet-authoff-{Guid.NewGuid():N}.conf");
        try
        {
            var port = GetFreePort();

            // Start with auth enabled
            File.WriteAllText(configPath,
                $"port: {port}\nauthorization {{\n  user: bob\n  password: secret\n}}");

            var options = ConfigProcessor.ProcessConfigFile(configPath);
            options.Port = port;
            var server = new NatsServer(options, NullLoggerFactory.Instance);
            var cts = new CancellationTokenSource();
            _ = server.StartAsync(cts.Token);
            await server.WaitForReadyAsync();

            try
            {
                // Verify unauthenticated connections are 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();

                // Disable auth via reload
                WriteConfigAndReload(server, configPath, $"port: {port}\ndebug: false");

                // New connections without credentials should now succeed
                await using var newClient = new NatsConnection(new NatsOpts
                {
                    Url = $"nats://127.0.0.1:{port}",
                });
                await newClient.ConnectAsync();
                await newClient.PingAsync();
            }
            finally
            {
                await cts.CancelAsync();
                server.Dispose();
            }
        }
        finally
        {
            if (File.Exists(configPath)) File.Delete(configPath);
        }
    }

    /// <summary>
    /// Verifies that clients with the new correct credentials survive an auth reload.
    /// This connects a new client after the reload with the new credentials and
    /// verifies it works.
    /// Reference: Go server/reload_test.go — TestConfigReloadEnableUserAuthentication.
    /// </summary>
    [Fact]
    public async Task New_clients_with_correct_credentials_work_after_auth_reload()
    {
        var configPath = Path.Combine(Path.GetTempPath(), $"natsdotnet-newauth-{Guid.NewGuid():N}.conf");
        try
        {
            var port = GetFreePort();

            // Start with no auth
            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
            {
                // Enable auth via reload
                WriteConfigAndReload(server, configPath,
                    $"port: {port}\nauthorization {{\n  user: carol\n  password: newpass\n}}");

                // New connection with correct credentials should succeed
                await using var authClient = new NatsConnection(new NatsOpts
                {
                    Url = $"nats://carol:newpass@127.0.0.1:{port}",
                });
                await authClient.ConnectAsync();
                await authClient.PingAsync();

                // New connection without credentials should 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();
            }
            finally
            {
                await cts.CancelAsync();
                server.Dispose();
            }
        }
        finally
        {
            if (File.Exists(configPath)) File.Delete(configPath);
        }
    }

    /// <summary>
    /// Verifies that PropagateAuthChanges is a no-op when auth is disabled.
    /// </summary>
    [Fact]
    public async Task PropagateAuthChanges_noop_when_auth_disabled()
    {
        var configPath = Path.Combine(Path.GetTempPath(), $"natsdotnet-noauth-{Guid.NewGuid():N}.conf");
        try
        {
            var port = GetFreePort();
            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
            {
                // Connect a client
                using var sock = await RawConnectAsync(port);
                await SendConnectAsync(sock);
                await sock.SendAsync("PING\r\n"u8.ToArray(), SocketFlags.None);
                var pong = await ReadUntilAsync(sock, "PONG", timeoutMs: 3000);
                pong.ShouldContain("PONG");

                var countBefore = server.ClientCount;

                // Reload with a logging change only (no auth change)
                WriteConfigAndReload(server, configPath, $"port: {port}\ndebug: true");

                // Wait a moment for any async operations
                await Task.Delay(200);

                // Client count should remain the same (no disconnections)
                server.ClientCount.ShouldBe(countBefore);

                // Client should still be responsive
                await sock.SendAsync("PING\r\n"u8.ToArray(), SocketFlags.None);
                var pong2 = await ReadUntilAsync(sock, "PONG", timeoutMs: 3000);
                pong2.ShouldContain("PONG");
            }
            finally
            {
                await cts.CancelAsync();
                server.Dispose();
            }
        }
        finally
        {
            if (File.Exists(configPath)) File.Delete(configPath);
        }
    }

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

    /// <summary>
    /// Reads all data from the socket until the connection is closed or timeout elapses.
    /// This is more robust than ReadUntilAsync for cases where the server sends an error
    /// and immediately closes the connection — we want to capture everything sent.
    /// </summary>
    private static async Task<string> ReadAllBeforeCloseAsync(Socket sock, int timeoutMs = 5000)
    {
        using var cts = new CancellationTokenSource(timeoutMs);
        var sb = new StringBuilder();
        var buf = new byte[4096];
        while (true)
        {
            int n;
            try
            {
                n = await sock.ReceiveAsync(buf, SocketFlags.None, cts.Token);
            }
            catch (OperationCanceledException) { break; }
            catch (SocketException) { break; }
            if (n == 0) break; // Connection closed
            sb.Append(Encoding.ASCII.GetString(buf, 0, n));
        }
        return sb.ToString();
    }

    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;
    }
}
