// Port of Go server/reload_test.go — TestConfigReloadSIGHUP, TestReloadAsync,
// TestApplyDiff, TestReloadConfigOrThrow.
// Reference: golang/nats-server/server/reload_test.go, reload.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>
/// Tests for SIGHUP-triggered config reload and the ConfigReloader async API.
/// Covers:
///   - PosixSignalRegistration for SIGHUP wired to ReloadConfig
///   - ConfigReloader.ReloadAsync parses, diffs, and validates
///   - ConfigReloader.ApplyDiff returns correct category flags
///   - End-to-end reload via config file rewrite and ReloadConfigOrThrow
/// Reference: Go server/reload.go — Reload, applyOptions.
/// </summary>
public class SignalReloadTests
{
    // ─── 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();
    }

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

    /// <summary>
    /// Verifies that HandleSignals registers a SIGHUP handler that calls ReloadConfig.
    /// We cannot actually send SIGHUP in a test, but we verify the handler is registered
    /// by confirming ReloadConfig works when called directly, and that the server survives
    /// signal registration without error.
    /// Reference: Go server/signals_unix.go — handleSignals.
    /// </summary>
    [Fact]
    public async Task HandleSignals_registers_sighup_handler()
    {
        var configPath = Path.Combine(Path.GetTempPath(), $"natsdotnet-sighup-{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
            {
                // Register signal handlers — should not throw
                server.HandleSignals();

                // Verify the reload mechanism works by calling it directly
                // (simulating what SIGHUP would trigger)
                File.WriteAllText(configPath, $"port: {port}\ndebug: true");
                server.ReloadConfig();

                // The server should still be operational
                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();
            }
        }
        finally
        {
            if (File.Exists(configPath)) File.Delete(configPath);
        }
    }

    /// <summary>
    /// Verifies that ConfigReloader.ReloadAsync correctly detects an unchanged config file.
    /// </summary>
    [Fact]
    public async Task ReloadAsync_detects_unchanged_config()
    {
        var configPath = Path.Combine(Path.GetTempPath(), $"natsdotnet-noop-{Guid.NewGuid():N}.conf");
        try
        {
            File.WriteAllText(configPath, "port: 4222\ndebug: false");

            var currentOpts = new NatsOptions { ConfigFile = configPath, Port = 4222 };

            // Compute initial digest
            var (_, initialDigest) = NatsConfParser.ParseFileWithDigest(configPath);

            var result = await ConfigReloader.ReloadAsync(
                configPath, currentOpts, initialDigest, null, [], CancellationToken.None);

            result.Unchanged.ShouldBeTrue();
        }
        finally
        {
            if (File.Exists(configPath)) File.Delete(configPath);
        }
    }

    /// <summary>
    /// Verifies that ConfigReloader.ReloadAsync correctly detects config changes.
    /// </summary>
    [Fact]
    public async Task ReloadAsync_detects_changes()
    {
        var configPath = Path.Combine(Path.GetTempPath(), $"natsdotnet-change-{Guid.NewGuid():N}.conf");
        try
        {
            File.WriteAllText(configPath, "port: 4222\ndebug: false");

            var currentOpts = new NatsOptions { ConfigFile = configPath, Port = 4222, Debug = false };

            // Compute initial digest
            var (_, initialDigest) = NatsConfParser.ParseFileWithDigest(configPath);

            // Change the config file
            File.WriteAllText(configPath, "port: 4222\ndebug: true");

            var result = await ConfigReloader.ReloadAsync(
                configPath, currentOpts, initialDigest, null, [], CancellationToken.None);

            result.Unchanged.ShouldBeFalse();
            result.NewOptions.ShouldNotBeNull();
            result.NewOptions!.Debug.ShouldBeTrue();
            result.Changes.ShouldNotBeNull();
            result.Changes!.Count.ShouldBeGreaterThan(0);
            result.HasErrors.ShouldBeFalse();
        }
        finally
        {
            if (File.Exists(configPath)) File.Delete(configPath);
        }
    }

    /// <summary>
    /// Verifies that ConfigReloader.ReloadAsync reports errors for non-reloadable changes.
    /// </summary>
    [Fact]
    public async Task ReloadAsync_reports_non_reloadable_errors()
    {
        var configPath = Path.Combine(Path.GetTempPath(), $"natsdotnet-nonreload-{Guid.NewGuid():N}.conf");
        try
        {
            File.WriteAllText(configPath, "port: 4222\nserver_name: original");

            var currentOpts = new NatsOptions
            {
                ConfigFile = configPath,
                Port = 4222,
                ServerName = "original",
            };

            var (_, initialDigest) = NatsConfParser.ParseFileWithDigest(configPath);

            // Change a non-reloadable option
            File.WriteAllText(configPath, "port: 4222\nserver_name: changed");

            var result = await ConfigReloader.ReloadAsync(
                configPath, currentOpts, initialDigest, null, [], CancellationToken.None);

            result.Unchanged.ShouldBeFalse();
            result.HasErrors.ShouldBeTrue();
            result.Errors!.ShouldContain(e => e.Contains("ServerName"));
        }
        finally
        {
            if (File.Exists(configPath)) File.Delete(configPath);
        }
    }

    /// <summary>
    /// Verifies that ConfigReloader.ApplyDiff returns correct category flags.
    /// </summary>
    [Fact]
    public void ApplyDiff_returns_correct_category_flags()
    {
        var oldOpts = new NatsOptions { Debug = false, Username = "old" };
        var newOpts = new NatsOptions { Debug = true, Username = "new" };

        var changes = ConfigReloader.Diff(oldOpts, newOpts);
        var result = ConfigReloader.ApplyDiff(changes, oldOpts, newOpts);

        result.HasLoggingChanges.ShouldBeTrue();
        result.HasAuthChanges.ShouldBeTrue();
        result.ChangeCount.ShouldBeGreaterThan(0);
    }

    /// <summary>
    /// Verifies that ApplyDiff detects TLS changes.
    /// </summary>
    [Fact]
    public void ApplyDiff_detects_tls_changes()
    {
        var oldOpts = new NatsOptions { TlsCert = null };
        var newOpts = new NatsOptions { TlsCert = "/path/to/cert.pem" };

        var changes = ConfigReloader.Diff(oldOpts, newOpts);
        var result = ConfigReloader.ApplyDiff(changes, oldOpts, newOpts);

        result.HasTlsChanges.ShouldBeTrue();
    }

    /// <summary>
    /// Verifies that ReloadAsync preserves CLI overrides during reload.
    /// </summary>
    [Fact]
    public async Task ReloadAsync_preserves_cli_overrides()
    {
        var configPath = Path.Combine(Path.GetTempPath(), $"natsdotnet-cli-{Guid.NewGuid():N}.conf");
        try
        {
            File.WriteAllText(configPath, "port: 4222\ndebug: false");

            var currentOpts = new NatsOptions { ConfigFile = configPath, Port = 4222, Debug = true };
            var cliSnapshot = new NatsOptions { Debug = true };
            var cliFlags = new HashSet<string> { "Debug" };

            var (_, initialDigest) = NatsConfParser.ParseFileWithDigest(configPath);

            // Change config — debug goes to true in file, but CLI override also says true
            File.WriteAllText(configPath, "port: 4222\ndebug: true");

            var result = await ConfigReloader.ReloadAsync(
                configPath, currentOpts, initialDigest, cliSnapshot, cliFlags, CancellationToken.None);

            // Config changed, so it should not be "unchanged"
            result.Unchanged.ShouldBeFalse();
            result.NewOptions.ShouldNotBeNull();
            result.NewOptions!.Debug.ShouldBeTrue("CLI override should preserve debug=true");
        }
        finally
        {
            if (File.Exists(configPath)) File.Delete(configPath);
        }
    }

    /// <summary>
    /// Verifies end-to-end: rewrite config file and call ReloadConfigOrThrow
    /// to apply max_connections changes, then verify new connections are rejected.
    /// Reference: Go server/reload_test.go — TestConfigReloadMaxConnections.
    /// </summary>
    [Fact]
    public async Task Reload_via_config_file_rewrite_applies_changes()
    {
        var configPath = Path.Combine(Path.GetTempPath(), $"natsdotnet-e2e-{Guid.NewGuid():N}.conf");
        try
        {
            var port = GetFreePort();
            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 one connection
                using var c1 = await RawConnectAsync(port);
                server.ClientCount.ShouldBe(1);

                // Reduce max_connections to 1 via reload
                WriteConfigAndReload(server, configPath, $"port: {port}\nmax_connections: 1");

                // New connection should be rejected
                using var c2 = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                await c2.ConnectAsync(IPAddress.Loopback, port);
                var response = await ReadUntilAsync(c2, "-ERR", timeoutMs: 5000);
                response.ShouldContain("maximum connections exceeded");
            }
            finally
            {
                await cts.CancelAsync();
                server.Dispose();
            }
        }
        finally
        {
            if (File.Exists(configPath)) File.Delete(configPath);
        }
    }

    /// <summary>
    /// Verifies that ReloadConfigOrThrow throws for non-reloadable changes.
    /// </summary>
    [Fact]
    public async Task ReloadConfigOrThrow_throws_on_non_reloadable_change()
    {
        var configPath = Path.Combine(Path.GetTempPath(), $"natsdotnet-throw-{Guid.NewGuid():N}.conf");
        try
        {
            var port = GetFreePort();
            File.WriteAllText(configPath, $"port: {port}\nserver_name: original");

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

            try
            {
                // Try to change a non-reloadable option
                File.WriteAllText(configPath, $"port: {port}\nserver_name: changed");

                Should.Throw<InvalidOperationException>(() => server.ReloadConfigOrThrow())
                    .Message.ShouldContain("ServerName");
            }
            finally
            {
                await cts.CancelAsync();
                server.Dispose();
            }
        }
        finally
        {
            if (File.Exists(configPath)) File.Delete(configPath);
        }
    }

    /// <summary>
    /// Verifies that ReloadConfig does not throw when no config file is specified
    /// (it logs a warning and returns).
    /// </summary>
    [Fact]
    public void ReloadConfig_no_config_file_does_not_throw()
    {
        var options = new NatsOptions { Port = 0 };
        using var server = new NatsServer(options, NullLoggerFactory.Instance);

        // Should not throw; just logs a warning
        Should.NotThrow(() => server.ReloadConfig());
    }

    /// <summary>
    /// Verifies that ReloadConfigOrThrow throws when no config file is specified.
    /// </summary>
    [Fact]
    public void ReloadConfigOrThrow_throws_when_no_config_file()
    {
        var options = new NatsOptions { Port = 0 };
        using var server = new NatsServer(options, NullLoggerFactory.Instance);

        Should.Throw<InvalidOperationException>(() => server.ReloadConfigOrThrow())
            .Message.ShouldContain("No config file");
    }
}
