using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text.Json;
using Shouldly;
using ZB.MOM.NatsNet.Server;
using ZB.MOM.NatsNet.Server.Auth.CertificateIdentityProvider;
using ZB.MOM.NatsNet.Server.Auth.Ocsp;

namespace ZB.MOM.NatsNet.Server.Tests.Auth;

public sealed class OcspFoundationTests : IDisposable
{
    private readonly List<string> _tempDirs = [];
    private readonly List<X509Certificate2> _certs = [];

    [Fact]
    public void GetNextRun_NoCachedResponse_ReturnsDefaultInterval()
    {
        var monitor = new OcspMonitor();

        monitor.GetNextRun().ShouldBe(TimeSpan.FromHours(24));
    }

    [Fact]
    public void GetLocalStatus_StoreDirMissing_ReturnsError()
    {
        var monitor = new OcspMonitor
        {
            Server = NewServer(new ServerOptions()),
            Leaf = CreateSelfSignedCertificate("CN=leaf"),
            Issuer = CreateSelfSignedCertificate("CN=issuer"),
        };

        var (_, _, err) = monitor.GetLocalStatus();

        err.ShouldNotBeNull();
        err!.Message.ShouldContain("store_dir");
    }

    [Fact]
    public void GetStatus_UsesLocalStatus_WhenCacheIsEmpty()
    {
        var dir = MakeTempDir();
        var monitor = NewMonitorWithStore(dir);
        var key = GetLeafKey(monitor.Leaf!);
        var responseBytes = SerializeResponse(
            status: (int)OcspStatusAssertion.Good,
            thisUpdate: DateTime.UtcNow.AddMinutes(-1),
            nextUpdate: DateTime.UtcNow.AddHours(1));

        Directory.CreateDirectory(Path.Combine(dir, "ocsp"));
        File.WriteAllBytes(Path.Combine(dir, "ocsp", key), responseBytes);

        var (raw, response, err) = monitor.GetStatus();

        err.ShouldBeNull();
        raw.ShouldNotBeNull();
        response.ShouldNotBeNull();
        response.Status.ShouldBe(OcspStatusAssertion.Good);
    }

    [Fact]
    public void GetRemoteStatus_NoResponders_ReturnsError()
    {
        var monitor = NewMonitorWithStore(MakeTempDir());

        var (_, _, err) = monitor.GetRemoteStatus();

        err.ShouldNotBeNull();
        err!.Message.ShouldContain("no available ocsp servers");
    }

    [Fact]
    public void WriteOcspStatus_ValidPath_WritesFile()
    {
        var dir = MakeTempDir();
        Directory.CreateDirectory(Path.Combine(dir, "ocsp"));
        var monitor = NewMonitorWithStore(dir);

        var err = monitor.WriteOCSPStatus(dir, "status.bin", [1, 2, 3, 4]);

        err.ShouldBeNull();
        var path = Path.Combine(dir, "ocsp", "status.bin");
        File.Exists(path).ShouldBeTrue();
        File.ReadAllBytes(path).ShouldBe([1, 2, 3, 4]);
    }

    [Fact]
    public async Task Run_CancelledToken_Completes()
    {
        var monitor = new OcspMonitor();
        using var cts = new CancellationTokenSource();
        cts.Cancel();

        await monitor.Run(cts.Token);
    }

    [Fact]
    public void ParseCertPem_CertificateFile_ReturnsCertificate()
    {
        var cert = CreateSelfSignedCertificate("CN=parsecert");
        var path = Path.Combine(MakeTempDir(), "ca.pem");
        File.WriteAllText(path, cert.ExportCertificatePem());

        var (certs, err) = OcspHandler.ParseCertPEM(path);

        err.ShouldBeNull();
        certs.ShouldNotBeNull();
        certs.Count.ShouldBe(1);
    }

    [Fact]
    public void GetOcspIssuerLocally_LeafAndIssuerBundle_ReturnsIssuer()
    {
        var (leaf, issuer) = CreateLeafAndIssuer();

        var (resolvedIssuer, err) = OcspHandler.GetOCSPIssuerLocally([], [leaf, issuer]);

        err.ShouldBeNull();
        resolvedIssuer.ShouldNotBeNull();
        resolvedIssuer!.Subject.ShouldBe(issuer.Subject);
    }

    [Fact]
    public void GetOcspIssuer_WithChain_ReturnsIssuer()
    {
        var (leaf, issuer) = CreateLeafAndIssuer();
        var chain = new[] { leaf.RawData, issuer.RawData };

        var (resolvedIssuer, err) = OcspHandler.GetOCSPIssuer(string.Empty, chain);

        err.ShouldBeNull();
        resolvedIssuer.ShouldNotBeNull();
        resolvedIssuer!.Subject.ShouldBe(issuer.Subject);
    }

    [Theory]
    [InlineData(0, "good")]
    [InlineData(1, "revoked")]
    [InlineData(99, "unknown")]
    public void OcspStatusString_AnyStatus_ReturnsExpectedString(int status, string expected)
    {
        OcspHandler.OcspStatusString(status).ShouldBe(expected);
    }

    [Fact]
    public void ValidOcspResponse_ExpiredNextUpdate_ReturnsError()
    {
        var response = new OcspResponse
        {
            Status = OcspStatusAssertion.Good,
            ThisUpdate = DateTime.UtcNow.AddMinutes(-2),
            NextUpdate = DateTime.UtcNow.AddMinutes(-1),
        };

        var err = OcspHandler.ValidOCSPResponse(response);

        err.ShouldNotBeNull();
    }

    [Fact]
    public void ValidOcspResponse_CurrentResponse_ReturnsNull()
    {
        var response = new OcspResponse
        {
            Status = OcspStatusAssertion.Good,
            ThisUpdate = DateTime.UtcNow.AddMinutes(-1),
            NextUpdate = DateTime.UtcNow.AddMinutes(10),
        };

        var err = OcspHandler.ValidOCSPResponse(response);

        err.ShouldBeNull();
    }

    public void Dispose()
    {
        foreach (var cert in _certs)
        {
            cert.Dispose();
        }

        foreach (var dir in _tempDirs)
        {
            try { Directory.Delete(dir, recursive: true); } catch { }
        }
    }

    private NatsServer NewServer(ServerOptions options)
    {
        var (server, err) = NatsServer.NewServer(options);
        err.ShouldBeNull();
        return server!;
    }

    private OcspMonitor NewMonitorWithStore(string storeDir)
    {
        var opts = new ServerOptions
        {
            StoreDir = storeDir,
            OcspConfig = new OcspConfig(),
        };

        return new OcspMonitor
        {
            Server = NewServer(opts),
            Leaf = CreateSelfSignedCertificate("CN=leaf"),
            Issuer = CreateSelfSignedCertificate("CN=issuer"),
        };
    }

    private string MakeTempDir()
    {
        var path = Path.Combine(Path.GetTempPath(), "ocsp-foundation-" + Path.GetRandomFileName());
        Directory.CreateDirectory(path);
        _tempDirs.Add(path);
        return path;
    }

    private X509Certificate2 CreateSelfSignedCertificate(string subjectName)
    {
        var request = new CertificateRequest(
            subjectName,
            RSA.Create(2048),
            HashAlgorithmName.SHA256,
            RSASignaturePadding.Pkcs1);

        var cert = request.CreateSelfSigned(
            DateTimeOffset.UtcNow.AddDays(-1),
            DateTimeOffset.UtcNow.AddDays(30));

        _certs.Add(cert);
        return cert;
    }

    private (X509Certificate2 leaf, X509Certificate2 issuer) CreateLeafAndIssuer()
    {
        var issuerRequest = new CertificateRequest(
            "CN=issuer",
            RSA.Create(2048),
            HashAlgorithmName.SHA256,
            RSASignaturePadding.Pkcs1);
        issuerRequest.CertificateExtensions.Add(new X509BasicConstraintsExtension(true, false, 0, true));
        issuerRequest.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(issuerRequest.PublicKey, false));

        var issuer = issuerRequest.CreateSelfSigned(
            DateTimeOffset.UtcNow.AddDays(-2),
            DateTimeOffset.UtcNow.AddDays(365));

        var leafRequest = new CertificateRequest(
            "CN=leaf",
            RSA.Create(2048),
            HashAlgorithmName.SHA256,
            RSASignaturePadding.Pkcs1);
        leafRequest.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, true));
        leafRequest.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(leafRequest.PublicKey, false));

        var serial = new byte[16];
        RandomNumberGenerator.Fill(serial);
        var leaf = leafRequest.Create(
            issuer,
            DateTimeOffset.UtcNow.AddDays(-1),
            DateTimeOffset.UtcNow.AddDays(90),
            serial);

        _certs.Add(issuer);
        _certs.Add(leaf);
        return (leaf, issuer);
    }

    private static byte[] SerializeResponse(int status, DateTime thisUpdate, DateTime nextUpdate)
    {
        var payload = new
        {
            Status = status,
            ThisUpdate = thisUpdate,
            NextUpdate = nextUpdate,
        };
        return JsonSerializer.SerializeToUtf8Bytes(payload);
    }

    private static string GetLeafKey(X509Certificate2 cert)
    {
        var hash = SHA256.HashData(cert.RawData);
        return Convert.ToHexString(hash).ToLowerInvariant();
    }
}
