// Copyright 2012-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0

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

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

public sealed class OcspResponseCacheTests
{
    [Fact]
    public void LocalDirCache_PutReplaceDelete_AdjustsStats()
    {
        var cache = new LocalDirCache(Path.Combine(Path.GetTempPath(), $"ocsp-{Guid.NewGuid():N}"));
        cache.Start();

        cache.Put("k1", CreateResponse(OcspStatusAssertion.Good, [1, 2, 3]), "subj");
        cache.Put("k1", CreateResponse(OcspStatusAssertion.Revoked, [4, 5, 6]), "subj");
        cache.Put("k2", CreateResponse(OcspStatusAssertion.Unknown, [7, 8]), "subj");

        var statsAfterPut = cache.Stats();
        statsAfterPut.ShouldNotBeNull();
        statsAfterPut.Responses.ShouldBe(2);
        statsAfterPut.Goods.ShouldBe(0);
        statsAfterPut.Revokes.ShouldBe(1);
        statsAfterPut.Unknowns.ShouldBe(1);

        cache.Delete("k1", wasMiss: false);

        var statsAfterDelete = cache.Stats();
        statsAfterDelete.ShouldNotBeNull();
        statsAfterDelete.Responses.ShouldBe(1);
        statsAfterDelete.Revokes.ShouldBe(0);
        statsAfterDelete.Unknowns.ShouldBe(1);
    }

    [Fact]
    public void LocalDirCache_DeletePreserveRevoked_WithMiss_AdjustsHitToMiss()
    {
        var config = OcspHandler.NewOCSPResponseCacheConfig();
        config.PreserveRevoked = true;

        var cache = new LocalDirCache(config);
        cache.Start();
        cache.Put("k1", CreateResponse(OcspStatusAssertion.Revoked, [9, 9, 9]), "subj");
        cache.Get("k1").ShouldNotBeNull();

        cache.Delete("k1", wasMiss: true);

        var stats = cache.Stats();
        stats.ShouldNotBeNull();
        stats.Responses.ShouldBe(1);
        stats.Revokes.ShouldBe(1);
        stats.Hits.ShouldBe(0);
        stats.Misses.ShouldBe(1);
        cache.Get("k1").ShouldNotBeNull();
    }

    [Fact]
    public void LocalDirCache_OnlineTypeConfigStats_FollowLifecycle()
    {
        var config = OcspHandler.NewOCSPResponseCacheConfig();
        var cache = new LocalDirCache(config);

        cache.Online().ShouldBeFalse();
        cache.Type().ShouldBe("local");
        cache.Config().LocalStore.ShouldBe(config.LocalStore);
        cache.Stats().ShouldBeNull();

        cache.Start();
        cache.Online().ShouldBeTrue();
        cache.Stats().ShouldNotBeNull();

        cache.Stop();
        cache.Online().ShouldBeFalse();
    }

    [Fact]
    public void LocalDirCache_CompressDecompress_RoundTripsPayload()
    {
        var cache = new LocalDirCache(Path.Combine(Path.GetTempPath(), $"ocsp-{Guid.NewGuid():N}"));
        var payload = "ocsp-cache-roundtrip-data"u8.ToArray();

        var (compressed, compressError) = cache.Compress(payload);
        compressError.ShouldBeNull();
        compressed.ShouldNotBeNull();

        var (decompressed, decompressError) = cache.Decompress(compressed!);
        decompressError.ShouldBeNull();
        decompressed.ShouldNotBeNull();
        decompressed.ShouldBe(payload);
    }

    [Fact]
    public void LocalDirCache_LoadCache_MissingFile_LeavesCacheEmpty()
    {
        var dir = CreateTempDir();
        try
        {
            var cache = new LocalDirCache(dir);
            var server = NewServer(new ServerOptions { NoSystemAccount = true });

            cache.LoadCache(server);
            cache.Start();

            var stats = cache.Stats();
            stats.ShouldNotBeNull();
            stats.Responses.ShouldBe(0);
        }
        finally
        {
            Directory.Delete(dir, recursive: true);
        }
    }

    [Fact]
    public void LocalDirCache_LoadCache_ValidFile_PopulatesCache()
    {
        var dir = CreateTempDir();
        try
        {
            var cache = new LocalDirCache(dir);
            var cacheFile = Path.Combine(dir, "cache.json");
            var seed = new Dictionary<string, OcspResponseCacheItem>
            {
                ["k1"] = new()
                {
                    Subject = "subj",
                    CachedAt = DateTime.UtcNow,
                    RespStatus = OcspStatusAssertion.Good,
                    RespExpires = DateTime.UtcNow.AddMinutes(5),
                    Resp = cache.Compress([1, 2, 3]).compressed!,
                },
            };

            File.WriteAllBytes(cacheFile, JsonSerializer.SerializeToUtf8Bytes(seed));

            var server = NewServer(new ServerOptions { NoSystemAccount = true });
            cache.LoadCache(server);
            cache.Start();

            cache.Get("k1").ShouldBe([1, 2, 3]);
            cache.Stats()!.Responses.ShouldBe(1);
        }
        finally
        {
            Directory.Delete(dir, recursive: true);
        }
    }

    [Fact]
    public void LocalDirCache_SaveCache_DirtyWritesCacheFile()
    {
        var dir = CreateTempDir();
        try
        {
            var cache = new LocalDirCache(dir);
            var server = NewServer(new ServerOptions { NoSystemAccount = true });
            cache.Start();
            cache.Put("k1", CreateResponse(OcspStatusAssertion.Good, [4, 5, 6]), "subj");

            cache.SaveCache(server);

            File.Exists(Path.Combine(dir, "cache.json")).ShouldBeTrue();
        }
        finally
        {
            Directory.Delete(dir, recursive: true);
        }
    }

    [Fact]
    public void NoOpCache_LifecycleAndStats_ShouldNoOpSafely()
    {
        var noOp = new NoOpCache();
        noOp.Online().ShouldBeFalse();
        noOp.Type().ShouldBe("none");
        noOp.Config().ShouldNotBeNull();
        noOp.Stats().ShouldBeNull();

        noOp.Start();
        noOp.Online().ShouldBeTrue();
        noOp.Stats().ShouldNotBeNull();

        noOp.Put("k", [5]);
        noOp.Get("k").ShouldBeNull();
        noOp.Remove("k");
        noOp.Delete("k");

        noOp.Stop();
        noOp.Online().ShouldBeFalse();
    }

    private static OcspResponse CreateResponse(OcspStatusAssertion status, byte[] raw) =>
        new()
        {
            Status = status,
            ThisUpdate = DateTime.UtcNow.AddMinutes(-1),
            NextUpdate = DateTime.UtcNow.AddMinutes(10),
            Raw = raw,
        };

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

    private static string CreateTempDir()
    {
        var dir = Path.Combine(Path.GetTempPath(), "ocsp-cache-" + Path.GetRandomFileName());
        Directory.CreateDirectory(dir);
        return dir;
    }
}
