// Tests for TLS certificate hot reload (E9).
// Verifies that TlsCertificateProvider supports atomic cert swapping
// and that ConfigReloader.ReloadTlsCertificate integrates correctly.
// Reference: golang/nats-server/server/reload_test.go — TestConfigReloadRotateTLS (line 392).

using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using NATS.Server.Configuration;
using NATS.Server.Tls;

namespace NATS.Server.Core.Tests.Configuration;

public class TlsReloadTests
{
    /// <summary>
    /// Generates a self-signed X509Certificate2 for testing.
    /// </summary>
    private static X509Certificate2 GenerateSelfSignedCert(string cn = "test")
    {
        using var rsa = RSA.Create(2048);
        var req = new CertificateRequest($"CN={cn}", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
        var cert = req.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddDays(1));
        // Export and re-import to ensure the cert has the private key bound
        return X509CertificateLoader.LoadPkcs12(cert.Export(X509ContentType.Pkcs12), null);
    }

    [Fact]
    public void CertificateProvider_GetCurrentCertificate_ReturnsInitialCert()
    {
        // Go parity: TestConfigReloadRotateTLS — initial cert is usable
        var cert = GenerateSelfSignedCert("initial");
        using var provider = new TlsCertificateProvider(cert);

        var current = provider.GetCurrentCertificate();

        current.ShouldNotBeNull();
        current.Subject.ShouldContain("initial");
    }

    [Fact]
    public void CertificateProvider_SwapCertificate_ReturnsOldCert()
    {
        // Go parity: TestConfigReloadRotateTLS — cert rotation returns old cert
        var cert1 = GenerateSelfSignedCert("cert1");
        var cert2 = GenerateSelfSignedCert("cert2");
        using var provider = new TlsCertificateProvider(cert1);

        var old = provider.SwapCertificate(cert2);

        old.ShouldNotBeNull();
        old.Subject.ShouldContain("cert1");
        old.Dispose();

        var current = provider.GetCurrentCertificate();
        current.ShouldNotBeNull();
        current.Subject.ShouldContain("cert2");
    }

    [Fact]
    public void CertificateProvider_SwapCertificate_IncrementsVersion()
    {
        // Go parity: TestConfigReloadRotateTLS — version tracking for reload detection
        var cert1 = GenerateSelfSignedCert("v1");
        var cert2 = GenerateSelfSignedCert("v2");
        using var provider = new TlsCertificateProvider(cert1);

        var v0 = provider.Version;
        v0.ShouldBe(0);

        provider.SwapCertificate(cert2)?.Dispose();
        provider.Version.ShouldBe(1);
    }

    [Fact]
    public void CertificateProvider_MultipleSwa_NewConnectionsGetLatest()
    {
        // Go parity: TestConfigReloadRotateTLS — multiple rotations, each new
        // handshake gets the latest certificate
        var cert1 = GenerateSelfSignedCert("round1");
        var cert2 = GenerateSelfSignedCert("round2");
        var cert3 = GenerateSelfSignedCert("round3");
        using var provider = new TlsCertificateProvider(cert1);

        provider.GetCurrentCertificate()!.Subject.ShouldContain("round1");

        provider.SwapCertificate(cert2)?.Dispose();
        provider.GetCurrentCertificate()!.Subject.ShouldContain("round2");

        provider.SwapCertificate(cert3)?.Dispose();
        provider.GetCurrentCertificate()!.Subject.ShouldContain("round3");

        provider.Version.ShouldBe(2);
    }

    [Fact]
    public async Task CertificateProvider_ConcurrentAccess_IsThreadSafe()
    {
        // Go parity: TestConfigReloadRotateTLS — cert swap must be safe under
        // concurrent connection accept
        var cert1 = GenerateSelfSignedCert("concurrent1");
        using var provider = new TlsCertificateProvider(cert1);

        var tasks = new Task[50];
        for (int i = 0; i < tasks.Length; i++)
        {
            var idx = i;
            tasks[i] = Task.Run(() =>
            {
                if (idx % 2 == 0)
                {
                    // Readers — simulate new connections getting current cert
                    var c = provider.GetCurrentCertificate();
                    c.ShouldNotBeNull();
                }
                else
                {
                    // Writers — simulate reload
                    var newCert = GenerateSelfSignedCert($"swap-{idx}");
                    provider.SwapCertificate(newCert)?.Dispose();
                }
            });
        }

        await Task.WhenAll(tasks);

        // After all swaps, the provider should still return a valid cert
        provider.GetCurrentCertificate().ShouldNotBeNull();
    }

    [Fact]
    public void ReloadTlsCertificate_NullProvider_ReturnsFalse()
    {
        // Edge case: server running without TLS
        var opts = new NatsOptions();
        var result = ConfigReloader.ReloadTlsCertificate(opts, null);
        result.ShouldBeFalse();
    }

    [Fact]
    public void ReloadTlsCertificate_NoTlsConfig_ReturnsFalse()
    {
        // Edge case: provider exists but options don't have TLS paths
        var cert = GenerateSelfSignedCert("no-tls");
        using var provider = new TlsCertificateProvider(cert);

        var opts = new NatsOptions(); // HasTls is false (no TlsCert/TlsKey)
        var result = ConfigReloader.ReloadTlsCertificate(opts, provider);
        result.ShouldBeFalse();
    }

    [Fact]
    public void ReloadTlsCertificate_WithCertFiles_SwapsCertAndSslOptions()
    {
        // Go parity: TestConfigReloadRotateTLS — full reload with cert files.
        // Write a self-signed cert to temp files and verify the provider loads it.
        var tempDir = Path.Combine(Path.GetTempPath(), $"nats-tls-test-{Guid.NewGuid():N}");
        Directory.CreateDirectory(tempDir);
        try
        {
            var certPath = Path.Combine(tempDir, "cert.pem");
            var keyPath = Path.Combine(tempDir, "key.pem");
            WriteSelfSignedCertFiles(certPath, keyPath, "reload-test");

            // Create provider with initial cert
            var initialCert = GenerateSelfSignedCert("initial");
            using var provider = new TlsCertificateProvider(initialCert);

            var opts = new NatsOptions { TlsCert = certPath, TlsKey = keyPath };
            var result = ConfigReloader.ReloadTlsCertificate(opts, provider);

            result.ShouldBeTrue();
            provider.Version.ShouldBeGreaterThan(0);
            provider.GetCurrentCertificate().ShouldNotBeNull();
            provider.GetCurrentSslOptions().ShouldNotBeNull();
        }
        finally
        {
            Directory.Delete(tempDir, recursive: true);
        }
    }

    [Fact]
    public void ConfigDiff_DetectsTlsChanges()
    {
        // Go parity: TestConfigReloadEnableTLS, TestConfigReloadDisableTLS
        // Verify that diff detects TLS option changes and flags them
        var oldOpts = new NatsOptions { TlsCert = "/old/cert.pem", TlsKey = "/old/key.pem" };
        var newOpts = new NatsOptions { TlsCert = "/new/cert.pem", TlsKey = "/new/key.pem" };

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

        changes.Count.ShouldBeGreaterThan(0);
        changes.ShouldContain(c => c.IsTlsChange && c.Name == "TlsCert");
        changes.ShouldContain(c => c.IsTlsChange && c.Name == "TlsKey");
    }

    [Fact]
    public void ConfigDiff_TlsVerifyChange_IsTlsChange()
    {
        // Go parity: TestConfigReloadRotateTLS — enabling client verification
        var oldOpts = new NatsOptions { TlsVerify = false };
        var newOpts = new NatsOptions { TlsVerify = true };

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

        changes.ShouldContain(c => c.IsTlsChange && c.Name == "TlsVerify");
    }

    [Fact]
    public void ConfigApplyResult_ReportsTlsChanges()
    {
        // Verify ApplyDiff flags TLS changes correctly
        var changes = new List<IConfigChange>
        {
            new ConfigChange("TlsCert", isTlsChange: true),
            new ConfigChange("TlsKey", isTlsChange: true),
        };
        var oldOpts = new NatsOptions();
        var newOpts = new NatsOptions();

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

        result.HasTlsChanges.ShouldBeTrue();
        result.ChangeCount.ShouldBe(2);
    }


    // ─── ReloadTlsCertificates (TlsReloadResult) tests ─────────────────────────

    [Fact]
    public void No_cert_change_returns_no_change()
    {
        // Go parity: tlsConfigReload — identical cert path means no reload needed
        var oldOpts = new NatsOptions { TlsCert = "/same/cert.pem" };
        var newOpts = new NatsOptions { TlsCert = "/same/cert.pem" };

        var result = ConfigReloader.ReloadTlsCertificates(oldOpts, newOpts);

        result.CertificateChanged.ShouldBeFalse();
        result.CertificateLoaded.ShouldBeFalse();
        result.Error.ShouldBeNull();
    }

    [Fact]
    public void Cert_path_changed_detected()
    {
        // Go parity: tlsConfigReload — different cert path triggers reload
        var oldOpts = new NatsOptions { TlsCert = "/old/cert.pem" };
        var newOpts = new NatsOptions { TlsCert = "/new/cert.pem" };

        var result = ConfigReloader.ReloadTlsCertificates(oldOpts, newOpts);

        result.CertificateChanged.ShouldBeTrue();
    }

    [Fact]
    public void Cert_path_set_returns_path()
    {
        // Verify CertificatePath reflects the new cert path when changed
        var oldOpts = new NatsOptions { TlsCert = "/old/cert.pem" };
        var newOpts = new NatsOptions { TlsCert = "/new/cert.pem" };

        var result = ConfigReloader.ReloadTlsCertificates(oldOpts, newOpts);

        result.CertificatePath.ShouldBe("/new/cert.pem");
    }

    [Fact]
    public void Missing_cert_file_returns_error()
    {
        // Go parity: tlsConfigReload — non-existent cert path returns an error
        var oldOpts = new NatsOptions { TlsCert = "/old/cert.pem" };
        var newOpts = new NatsOptions { TlsCert = "/nonexistent/path/cert.pem" };

        var result = ConfigReloader.ReloadTlsCertificates(oldOpts, newOpts);

        result.Error.ShouldNotBeNullOrWhiteSpace();
        result.Error!.ShouldContain("/nonexistent/path/cert.pem");
        result.CertificateLoaded.ShouldBeFalse();
    }

    [Fact]
    public void Null_to_null_no_change()
    {
        // Both TlsCert null — no TLS configured on either side, no change
        var oldOpts = new NatsOptions { TlsCert = null };
        var newOpts = new NatsOptions { TlsCert = null };

        var result = ConfigReloader.ReloadTlsCertificates(oldOpts, newOpts);

        result.CertificateChanged.ShouldBeFalse();
    }

    [Fact]
    public void Null_to_value_detected()
    {
        // Go parity: TestConfigReloadEnableTLS — enabling TLS is detected as a change
        var oldOpts = new NatsOptions { TlsCert = null };
        var newOpts = new NatsOptions { TlsCert = "/new/cert.pem" };

        var result = ConfigReloader.ReloadTlsCertificates(oldOpts, newOpts);

        result.CertificateChanged.ShouldBeTrue();
    }

    [Fact]
    public void Value_to_null_detected()
    {
        // Go parity: TestConfigReloadDisableTLS — disabling TLS is a change, loads successfully
        var oldOpts = new NatsOptions { TlsCert = "/old/cert.pem" };
        var newOpts = new NatsOptions { TlsCert = null };

        var result = ConfigReloader.ReloadTlsCertificates(oldOpts, newOpts);

        result.CertificateChanged.ShouldBeTrue();
        result.CertificateLoaded.ShouldBeTrue();
        result.Error.ShouldBeNull();
    }

    [Fact]
    public void Valid_cert_path_loaded_true()
    {
        // Go parity: tlsConfigReload — file exists, so CertificateLoaded is true
        var tempFile = Path.GetTempFileName();
        try
        {
            var oldOpts = new NatsOptions { TlsCert = "/old/cert.pem" };
            var newOpts = new NatsOptions { TlsCert = tempFile };

            var result = ConfigReloader.ReloadTlsCertificates(oldOpts, newOpts);

            result.CertificateChanged.ShouldBeTrue();
            result.CertificateLoaded.ShouldBeTrue();
            result.Error.ShouldBeNull();
        }
        finally
        {
            File.Delete(tempFile);
        }
    }

    [Fact]
    public void Error_null_on_success()
    {
        // Successful reload (file exists) must have Error as null
        var tempFile = Path.GetTempFileName();
        try
        {
            var oldOpts = new NatsOptions { TlsCert = "/old/cert.pem" };
            var newOpts = new NatsOptions { TlsCert = tempFile };

            var result = ConfigReloader.ReloadTlsCertificates(oldOpts, newOpts);

            result.Error.ShouldBeNull();
        }
        finally
        {
            File.Delete(tempFile);
        }
    }

    [Fact]
    public void Same_empty_strings_no_change()
    {
        // Both TlsCert are empty string — treat as equal, no change
        var oldOpts = new NatsOptions { TlsCert = string.Empty };
        var newOpts = new NatsOptions { TlsCert = string.Empty };

        var result = ConfigReloader.ReloadTlsCertificates(oldOpts, newOpts);

        result.CertificateChanged.ShouldBeFalse();
    }

    /// <summary>
    /// Helper to write a self-signed certificate to PEM files.
    /// </summary>
    private static void WriteSelfSignedCertFiles(string certPath, string keyPath, string cn)
    {
        using var rsa = RSA.Create(2048);
        var req = new CertificateRequest($"CN={cn}", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
        var cert = req.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddDays(1));

        File.WriteAllText(certPath, cert.ExportCertificatePem());
        File.WriteAllText(keyPath, rsa.ExportRSAPrivateKeyPem());
    }
}
