// Advanced configuration and reload tests for full Go parity.
// Covers: CLI override precedence (opts_test.go TestMergeOverrides, TestConfigureOptions),
// configuration defaults (opts_test.go TestDefaultOptions), configuration validation
// (opts_test.go TestMalformedListenAddress, TestMaxClosedClients), NatsOptions model
// defaults, ConfigProcessor parsing, ConfigReloader diff/validate semantics, and
// reload scenarios not covered by ConfigReloadExtendedParityTests.
// Reference: golang/nats-server/server/opts_test.go, 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>
/// Advanced configuration model and hot-reload tests ported from Go's opts_test.go
/// and reload_test.go. Focuses on: NatsOptions defaults, ConfigProcessor parsing,
/// ConfigReloader diff/validate, CLI-override precedence, and reload-time validation
/// paths not exercised by the basic and extended parity suites.
/// </summary>
public class ConfigReloadAdvancedTests
{
    // ─── 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-adv-{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);
    }

    // ─── Tests: NatsOptions Default Values ──────────────────────────────────

    /// <summary>
    /// Go: TestDefaultOptions opts_test.go:52
    /// NatsOptions must be constructed with the correct NATS protocol defaults.
    /// </summary>
    [Fact]
    public void NatsOptions_default_port_is_4222()
    {
        var opts = new NatsOptions();
        opts.Port.ShouldBe(4222);
    }

    /// <summary>
    /// Go: TestDefaultOptions opts_test.go:52
    /// Default host must be the wildcard address to listen on all interfaces.
    /// </summary>
    [Fact]
    public void NatsOptions_default_host_is_wildcard()
    {
        var opts = new NatsOptions();
        opts.Host.ShouldBe("0.0.0.0");
    }

    /// <summary>
    /// Go: TestDefaultOptions opts_test.go:52 (MaxConn = DEFAULT_MAX_CONNECTIONS = 65536)
    /// </summary>
    [Fact]
    public void NatsOptions_default_max_connections_is_65536()
    {
        var opts = new NatsOptions();
        opts.MaxConnections.ShouldBe(65536);
    }

    /// <summary>
    /// Go: TestDefaultOptions opts_test.go:52 (MaxPayload = MAX_PAYLOAD_SIZE = 1MB)
    /// </summary>
    [Fact]
    public void NatsOptions_default_max_payload_is_1_megabyte()
    {
        var opts = new NatsOptions();
        opts.MaxPayload.ShouldBe(1024 * 1024);
    }

    /// <summary>
    /// Go: TestDefaultOptions opts_test.go:52 (MaxControlLine = MAX_CONTROL_LINE_SIZE = 4096)
    /// </summary>
    [Fact]
    public void NatsOptions_default_max_control_line_is_4096()
    {
        var opts = new NatsOptions();
        opts.MaxControlLine.ShouldBe(4096);
    }

    /// <summary>
    /// Go: TestDefaultOptions opts_test.go:52 (PingInterval = DEFAULT_PING_INTERVAL = 2m)
    /// </summary>
    [Fact]
    public void NatsOptions_default_ping_interval_is_two_minutes()
    {
        var opts = new NatsOptions();
        opts.PingInterval.ShouldBe(TimeSpan.FromMinutes(2));
    }

    /// <summary>
    /// Go: TestDefaultOptions opts_test.go:52 (MaxPingsOut = DEFAULT_PING_MAX_OUT = 2)
    /// </summary>
    [Fact]
    public void NatsOptions_default_max_pings_out_is_2()
    {
        var opts = new NatsOptions();
        opts.MaxPingsOut.ShouldBe(2);
    }

    /// <summary>
    /// Go: TestDefaultOptions opts_test.go:52 (AuthTimeout = AUTH_TIMEOUT = 2s)
    /// </summary>
    [Fact]
    public void NatsOptions_default_auth_timeout_is_two_seconds()
    {
        var opts = new NatsOptions();
        opts.AuthTimeout.ShouldBe(TimeSpan.FromSeconds(2));
    }

    /// <summary>
    /// Go: TestDefaultOptions opts_test.go:52 (WriteDeadline = DEFAULT_FLUSH_DEADLINE = 10s)
    /// </summary>
    [Fact]
    public void NatsOptions_default_write_deadline_is_ten_seconds()
    {
        var opts = new NatsOptions();
        opts.WriteDeadline.ShouldBe(TimeSpan.FromSeconds(10));
    }

    /// <summary>
    /// Go: TestDefaultOptions opts_test.go:52 (ConnectErrorReports = 3600)
    /// </summary>
    [Fact]
    public void NatsOptions_default_connect_error_reports()
    {
        var opts = new NatsOptions();
        opts.ConnectErrorReports.ShouldBe(3600);
    }

    // ─── Tests: ConfigProcessor Parsing ────────────────────────────────────

    /// <summary>
    /// Go: TestConfigFile opts_test.go:97 — parsed config overrides default port.
    /// </summary>
    [Fact]
    public void ConfigProcessor_parses_port()
    {
        var opts = ConfigProcessor.ProcessConfig("port: 14222");
        opts.Port.ShouldBe(14222);
    }

    /// <summary>
    /// Go: TestConfigFile opts_test.go:97 — parsed config sets host.
    /// </summary>
    [Fact]
    public void ConfigProcessor_parses_host()
    {
        var opts = ConfigProcessor.ProcessConfig("host: 127.0.0.1");
        opts.Host.ShouldBe("127.0.0.1");
    }

    /// <summary>
    /// Go: TestConfigFile opts_test.go:97 — parsed config sets server_name.
    /// </summary>
    [Fact]
    public void ConfigProcessor_parses_server_name()
    {
        var opts = ConfigProcessor.ProcessConfig("server_name: my-server");
        opts.ServerName.ShouldBe("my-server");
    }

    /// <summary>
    /// Go: TestConfigFile opts_test.go:97 — debug/trace flags parsed from config.
    /// </summary>
    [Fact]
    public void ConfigProcessor_parses_debug_and_trace()
    {
        var opts = ConfigProcessor.ProcessConfig("debug: true\ntrace: true");
        opts.Debug.ShouldBeTrue();
        opts.Trace.ShouldBeTrue();
    }

    /// <summary>
    /// Go: TestConfigFile opts_test.go:97 — max_payload parsed from config.
    /// </summary>
    [Fact]
    public void ConfigProcessor_parses_max_payload()
    {
        var opts = ConfigProcessor.ProcessConfig("max_payload: 65536");
        opts.MaxPayload.ShouldBe(65536);
    }

    /// <summary>
    /// Go: TestPingIntervalNew opts_test.go:1369 — ping_interval parsed as duration string.
    /// </summary>
    [Fact]
    public void ConfigProcessor_parses_ping_interval_duration_string()
    {
        var opts = ConfigProcessor.ProcessConfig("ping_interval: \"60s\"");
        opts.PingInterval.ShouldBe(TimeSpan.FromSeconds(60));
    }

    /// <summary>
    /// Go: TestParseWriteDeadline opts_test.go:1187 — write_deadline as "Xs" duration string.
    /// </summary>
    [Fact]
    public void ConfigProcessor_parses_write_deadline_duration_string()
    {
        var opts = ConfigProcessor.ProcessConfig("write_deadline: \"3s\"");
        opts.WriteDeadline.ShouldBe(TimeSpan.FromSeconds(3));
    }

    /// <summary>
    /// Go: TestMalformedListenAddress opts_test.go:1314
    /// A malformed listen address must produce a parsing exception.
    /// </summary>
    [Fact]
    public void ConfigProcessor_rejects_malformed_listen_address()
    {
        Should.Throw<Exception>(() => ConfigProcessor.ProcessConfig("listen: \":not-a-port\""));
    }

    /// <summary>
    /// Go: TestEmptyConfig opts_test.go:1302
    /// An empty config file must produce options with all default values.
    /// </summary>
    [Fact]
    public void ConfigProcessor_empty_config_produces_defaults()
    {
        var opts = ConfigProcessor.ProcessConfig("");
        opts.Port.ShouldBe(4222);
        opts.Host.ShouldBe("0.0.0.0");
        opts.MaxPayload.ShouldBe(1024 * 1024);
        opts.MaxConnections.ShouldBe(65536);
    }

    // ─── Tests: ConfigReloader Diff / Validate ──────────────────────────────

    /// <summary>
    /// Go: TestConfigReloadUnsupportedHotSwapping reload_test.go:180
    /// ConfigReloader.Diff must detect port change as non-reloadable.
    /// </summary>
    [Fact]
    public void ConfigReloader_diff_detects_port_change_as_non_reloadable()
    {
        var oldOpts = new NatsOptions { Port = 4222 };
        var newOpts = new NatsOptions { Port = 5555 };

        var changes = ConfigReloader.Diff(oldOpts, newOpts);
        var portChange = changes.FirstOrDefault(c => c.Name == "Port");

        portChange.ShouldNotBeNull();
        portChange!.IsNonReloadable.ShouldBeTrue();
    }

    /// <summary>
    /// Go: TestConfigReload reload_test.go:251 — debug flag diff correctly categorised.
    /// ConfigReloader.Diff must categorise debug change as a logging change.
    /// </summary>
    [Fact]
    public void ConfigReloader_diff_categorises_debug_as_logging_change()
    {
        var oldOpts = new NatsOptions { Debug = false };
        var newOpts = new NatsOptions { Debug = true };

        var changes = ConfigReloader.Diff(oldOpts, newOpts);
        var debugChange = changes.FirstOrDefault(c => c.Name == "Debug");

        debugChange.ShouldNotBeNull();
        debugChange!.IsLoggingChange.ShouldBeTrue();
        debugChange.IsNonReloadable.ShouldBeFalse();
    }

    /// <summary>
    /// Go: TestConfigReloadRotateUserAuthentication reload_test.go:658
    /// ConfigReloader.Diff must categorise username/password change as an auth change.
    /// </summary>
    [Fact]
    public void ConfigReloader_diff_categorises_username_as_auth_change()
    {
        var oldOpts = new NatsOptions { Username = "alice" };
        var newOpts = new NatsOptions { Username = "bob" };

        var changes = ConfigReloader.Diff(oldOpts, newOpts);
        var usernameChange = changes.FirstOrDefault(c => c.Name == "Username");

        usernameChange.ShouldNotBeNull();
        usernameChange!.IsAuthChange.ShouldBeTrue();
        usernameChange.IsNonReloadable.ShouldBeFalse();
    }

    /// <summary>
    /// Go: TestConfigReload reload_test.go:251
    /// ConfigReloader.Diff on identical options must return an empty change list.
    /// </summary>
    [Fact]
    public void ConfigReloader_diff_on_identical_options_returns_empty()
    {
        var opts = new NatsOptions { Port = 4222, Debug = false, MaxPayload = 1024 * 1024 };
        var same = new NatsOptions { Port = 4222, Debug = false, MaxPayload = 1024 * 1024 };

        var changes = ConfigReloader.Diff(opts, same);
        changes.ShouldBeEmpty();
    }

    /// <summary>
    /// Go: TestConfigReloadClusterPortUnsupported reload_test.go:1394
    /// ConfigReloader.Diff must detect cluster port change as non-reloadable.
    /// </summary>
    [Fact]
    public void ConfigReloader_diff_detects_cluster_port_change_as_non_reloadable()
    {
        var oldOpts = new NatsOptions { Cluster = new ClusterOptions { Host = "127.0.0.1", Port = 6222 } };
        var newOpts = new NatsOptions { Cluster = new ClusterOptions { Host = "127.0.0.1", Port = 7777 } };

        var changes = ConfigReloader.Diff(oldOpts, newOpts);
        var clusterChange = changes.FirstOrDefault(c => c.Name == "Cluster");

        clusterChange.ShouldNotBeNull();
        clusterChange!.IsNonReloadable.ShouldBeTrue();
    }

    /// <summary>
    /// Go: reload_test.go — JetStream.StoreDir change must be non-reloadable.
    /// </summary>
    [Fact]
    public void ConfigReloader_diff_detects_jetstream_store_dir_change_as_non_reloadable()
    {
        var oldOpts = new NatsOptions { JetStream = new JetStreamOptions { StoreDir = "/tmp/js1" } };
        var newOpts = new NatsOptions { JetStream = new JetStreamOptions { StoreDir = "/tmp/js2" } };

        var changes = ConfigReloader.Diff(oldOpts, newOpts);
        var jsDirChange = changes.FirstOrDefault(c => c.Name == "JetStream.StoreDir");

        jsDirChange.ShouldNotBeNull();
        jsDirChange!.IsNonReloadable.ShouldBeTrue();
    }

    /// <summary>
    /// ConfigReloader.Validate must return errors for all non-reloadable changes.
    /// </summary>
    [Fact]
    public void ConfigReloader_validate_returns_errors_for_non_reloadable_changes()
    {
        var oldOpts = new NatsOptions { Port = 4222 };
        var newOpts = new NatsOptions { Port = 9999 };

        var changes = ConfigReloader.Diff(oldOpts, newOpts);
        var errors = ConfigReloader.Validate(changes);

        errors.ShouldNotBeEmpty();
        errors.ShouldContain(e => e.Contains("Port", StringComparison.OrdinalIgnoreCase));
    }

    // ─── Tests: CLI Override Precedence ────────────────────────────────────

    /// <summary>
    /// Go: TestMergeOverrides opts_test.go:264
    /// ConfigReloader.MergeCliOverrides must restore the CLI port value after a
    /// config reload that tries to set a different port.
    /// </summary>
    [Fact]
    public void ConfigReloader_merge_cli_overrides_restores_port()
    {
        // Simulate: CLI sets port=14222; config file says port=9999.
        var cliValues = new NatsOptions { Port = 14222 };
        var cliFlags = new HashSet<string> { "Port" };
        var fromConfig = new NatsOptions { Port = 9999 };

        ConfigReloader.MergeCliOverrides(fromConfig, cliValues, cliFlags);

        fromConfig.Port.ShouldBe(14222);
    }

    /// <summary>
    /// Go: TestMergeOverrides opts_test.go:264
    /// CLI debug=true must override config debug=false after merge.
    /// </summary>
    [Fact]
    public void ConfigReloader_merge_cli_overrides_restores_debug_flag()
    {
        var cliValues = new NatsOptions { Debug = true };
        var cliFlags = new HashSet<string> { "Debug" };
        var fromConfig = new NatsOptions { Debug = false };

        ConfigReloader.MergeCliOverrides(fromConfig, cliValues, cliFlags);

        fromConfig.Debug.ShouldBeTrue();
    }

    /// <summary>
    /// Go: TestMergeOverrides opts_test.go:264
    /// A flag not present in cliFlags must not override the config value.
    /// </summary>
    [Fact]
    public void ConfigReloader_merge_cli_overrides_ignores_non_cli_fields()
    {
        var cliValues = new NatsOptions { MaxPayload = 512 };
        // MaxPayload is NOT in cliFlags — it came from config, not CLI.
        var cliFlags = new HashSet<string> { "Port" };
        var fromConfig = new NatsOptions { MaxPayload = 1024 * 1024 };

        ConfigReloader.MergeCliOverrides(fromConfig, cliValues, cliFlags);

        // MaxPayload should remain the config-file value, not the CLI stub value.
        fromConfig.MaxPayload.ShouldBe(1024 * 1024);
    }

    // ─── Tests: Config File Parsing Round-Trip ──────────────────────────────

    /// <summary>
    /// Go: TestConfigFile opts_test.go:97 — max_connections parsed and accessible.
    /// </summary>
    [Fact]
    public void ConfigProcessor_parses_max_connections()
    {
        var opts = ConfigProcessor.ProcessConfig("max_connections: 100");
        opts.MaxConnections.ShouldBe(100);
    }

    /// <summary>
    /// Go: TestConfigFile opts_test.go:97 — lame_duck_duration parsed from config.
    /// </summary>
    [Fact]
    public void ConfigProcessor_parses_lame_duck_duration()
    {
        var opts = ConfigProcessor.ProcessConfig("lame_duck_duration: \"4m\"");
        opts.LameDuckDuration.ShouldBe(TimeSpan.FromMinutes(4));
    }

    /// <summary>
    /// Go: TestMaxClosedClients opts_test.go:1340 — max_closed_clients parsed.
    /// </summary>
    [Fact]
    public void ConfigProcessor_parses_max_closed_clients()
    {
        var opts = ConfigProcessor.ProcessConfig("max_closed_clients: 500");
        opts.MaxClosedClients.ShouldBe(500);
    }

    // ─── Tests: Reload Host Change Rejected ────────────────────────────────

    /// <summary>
    /// Go: TestConfigReloadUnsupportedHotSwapping reload_test.go:180
    /// Changing the listen host must be rejected at reload time.
    /// </summary>
    [Fact]
    public async Task Reload_host_change_rejected()
    {
        var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}");
        try
        {
            File.WriteAllText(configPath, $"port: {port}\nhost: 127.0.0.1");
            Should.Throw<InvalidOperationException>(() => server.ReloadConfigOrThrow())
                .Message.ShouldContain("Host");
        }
        finally
        {
            await CleanupAsync(server, cts, configPath);
        }
    }

    // ─── Tests: Reload TLS Settings ────────────────────────────────────────

    /// <summary>
    /// Reloading with allow_non_tls must succeed and not disconnect existing clients.
    /// </summary>
    [Fact]
    public async Task Reload_allow_non_tls_setting()
    {
        var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}");
        try
        {
            WriteConfigAndReload(server, configPath, $"port: {port}\nallow_non_tls: true");

            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: Reload Cluster Name Change ─────────────────────────────────

    /// <summary>
    /// Go: TestConfigReloadClusterName reload_test.go:1893
    /// Adding a cluster block for the first time is a non-reloadable change.
    /// </summary>
    [Fact]
    public async Task Reload_adding_cluster_block_rejected()
    {
        var clusterPort = GetFreePort();
        var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}");
        try
        {
            File.WriteAllText(configPath,
                $"port: {port}\ncluster {{\n  name: new-cluster\n  host: 127.0.0.1\n  port: {clusterPort}\n}}");
            Should.Throw<InvalidOperationException>(() => server.ReloadConfigOrThrow())
                .Message.ShouldContain("Cluster");
        }
        finally
        {
            await CleanupAsync(server, cts, configPath);
        }
    }

    // ─── Tests: JetStream Options Model ────────────────────────────────────

    /// <summary>
    /// JetStreamOptions must have sensible defaults (StoreDir empty, all limits 0).
    /// Go: server/opts.go JetStreamConfig defaults.
    /// </summary>
    [Fact]
    public void JetStreamOptions_defaults_are_empty_and_unlimited()
    {
        var jsOpts = new JetStreamOptions();
        jsOpts.StoreDir.ShouldBe(string.Empty);
        jsOpts.MaxMemoryStore.ShouldBe(0L);
        jsOpts.MaxFileStore.ShouldBe(0L);
        jsOpts.MaxStreams.ShouldBe(0);
        jsOpts.MaxConsumers.ShouldBe(0);
    }

    /// <summary>
    /// ConfigProcessor must correctly parse a jetstream block with store_dir.
    /// Go: server/opts.go parseJetStream.
    /// </summary>
    [Fact]
    public void ConfigProcessor_parses_jetstream_store_dir()
    {
        var storeDir = Path.Combine(Path.GetTempPath(), $"nats-js-parse-{Guid.NewGuid():N}");
        var opts = ConfigProcessor.ProcessConfig(
            $"jetstream {{\n  store_dir: \"{storeDir.Replace("\\", "\\\\")}\"\n}}");

        opts.JetStream.ShouldNotBeNull();
        opts.JetStream!.StoreDir.ShouldBe(storeDir);
    }

    // ─── Tests: Reload max_sub_tokens Validation ────────────────────────────

    /// <summary>
    /// Go: opts_test.go (max_sub_tokens validation) — ConfigProcessor must reject
    /// max_sub_tokens values that exceed 256.
    /// </summary>
    [Fact]
    public void ConfigProcessor_rejects_max_sub_tokens_above_256()
    {
        Should.Throw<ConfigProcessorException>(() =>
            ConfigProcessor.ProcessConfig("max_sub_tokens: 300"));
    }

    /// <summary>
    /// ConfigProcessor must accept max_sub_tokens values of exactly 256.
    /// </summary>
    [Fact]
    public void ConfigProcessor_accepts_max_sub_tokens_at_boundary_256()
    {
        var opts = ConfigProcessor.ProcessConfig("max_sub_tokens: 256");
        opts.MaxSubTokens.ShouldBe(256);
    }

    // ─── Tests: server_name with spaces ────────────────────────────────────

    /// <summary>
    /// Go: opts_test.go server_name validation — server names containing spaces
    /// must be rejected by the config processor.
    /// </summary>
    [Fact]
    public void ConfigProcessor_rejects_server_name_with_spaces()
    {
        Should.Throw<ConfigProcessorException>(() =>
            ConfigProcessor.ProcessConfig("server_name: \"my server\""));
    }
}
