using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Authorization;

namespace ZB.MOM.WW.OtOpcUa.Core.Tests.Authorization;

[Trait("Category", "Unit")]
public sealed class PermissionTrieCacheTests
{
    private static PermissionTrie Trie(string cluster, long generation) => new()
    {
        ClusterId = cluster,
        GenerationId = generation,
    };

    /// <summary>Verifies that GetTrie returns null when the cache is empty.</summary>
    [Fact]
    public void GetTrie_Empty_ReturnsNull()
    {
        new PermissionTrieCache().GetTrie("c1").ShouldBeNull();
    }

    /// <summary>Verifies that a trie installed can be retrieved with matching generation id.</summary>
    [Fact]
    public void Install_ThenGet_RoundTrips()
    {
        var cache = new PermissionTrieCache();
        cache.Install(Trie("c1", 5));

        cache.GetTrie("c1")!.GenerationId.ShouldBe(5);
        cache.CurrentGenerationId("c1").ShouldBe(5);
    }

    /// <summary>Verifies that installing a new generation makes it the current generation.</summary>
    [Fact]
    public void NewGeneration_BecomesCurrent()
    {
        var cache = new PermissionTrieCache();
        cache.Install(Trie("c1", 1));
        cache.Install(Trie("c1", 2));

        cache.CurrentGenerationId("c1").ShouldBe(2);
        cache.GetTrie("c1", 1).ShouldNotBeNull("prior generation retained for in-flight requests");
        cache.GetTrie("c1", 2).ShouldNotBeNull();
    }

    /// <summary>Verifies that out-of-order installs do not downgrade the current generation.</summary>
    [Fact]
    public void OutOfOrder_Install_DoesNotDowngrade_Current()
    {
        var cache = new PermissionTrieCache();
        cache.Install(Trie("c1", 3));
        cache.Install(Trie("c1", 1)); // late-arriving older generation

        cache.CurrentGenerationId("c1").ShouldBe(3, "older generation must not become current");
        cache.GetTrie("c1", 1).ShouldNotBeNull("but older is still retrievable by explicit lookup");
    }

    /// <summary>Verifies that Invalidate drops only the specified cluster.</summary>
    [Fact]
    public void Invalidate_DropsCluster()
    {
        var cache = new PermissionTrieCache();
        cache.Install(Trie("c1", 1));
        cache.Install(Trie("c2", 1));

        cache.Invalidate("c1");

        cache.GetTrie("c1").ShouldBeNull();
        cache.GetTrie("c2").ShouldNotBeNull("sibling cluster unaffected");
    }

    /// <summary>Verifies that Prune retains the most recent generations.</summary>
    [Fact]
    public void Prune_RetainsMostRecent()
    {
        var cache = new PermissionTrieCache();
        for (var g = 1L; g <= 5; g++) cache.Install(Trie("c1", g));

        cache.Prune("c1", keepLatest: 2);

        cache.GetTrie("c1", 5).ShouldNotBeNull();
        cache.GetTrie("c1", 4).ShouldNotBeNull();
        cache.GetTrie("c1", 3).ShouldBeNull();
        cache.GetTrie("c1", 1).ShouldBeNull();
    }

    /// <summary>Verifies that Prune with keepLatest greater than the cache size is a no-op.</summary>
    [Fact]
    public void Prune_LessThanKeep_IsNoOp()
    {
        var cache = new PermissionTrieCache();
        cache.Install(Trie("c1", 1));
        cache.Install(Trie("c1", 2));

        cache.Prune("c1", keepLatest: 10);

        cache.CachedTrieCount.ShouldBe(2);
    }

    /// <summary>Verifies that different clusters maintain independent generation tracking.</summary>
    [Fact]
    public void ClusterIsolation()
    {
        var cache = new PermissionTrieCache();
        cache.Install(Trie("c1", 1));
        cache.Install(Trie("c2", 9));

        cache.CurrentGenerationId("c1").ShouldBe(1);
        cache.CurrentGenerationId("c2").ShouldBe(9);
    }

    /// <summary>
    ///     Core-005 regression: a concurrent Install that adds a new generation between a Prune
    ///     read and write must not be silently overwritten. We can't deterministically reproduce
    ///     a true data race in a unit test, but we can verify that Prune performed after Install
    ///     honours the newly-installed generation rather than clobbering it.
    /// </summary>
    [Fact]
    public void Prune_AfterConcurrentInstall_PreservesLatestGeneration()
    {
        var cache = new PermissionTrieCache();
        for (var g = 1L; g <= 5; g++) cache.Install(Trie("c1", g));

        // Simulate: Install of generation 6 races with a pending Prune(keepLatest:2).
        // After Prune runs, generation 6 (the "just installed" one) must still be current.
        cache.Install(Trie("c1", 6));
        cache.Prune("c1", keepLatest: 2);

        cache.CurrentGenerationId("c1").ShouldBe(6, "current must remain the newest installed generation");
        cache.GetTrie("c1", 6).ShouldNotBeNull("generation 6 must be retained as current");
        cache.GetTrie("c1", 5).ShouldNotBeNull("generation 5 retained (keepLatest=2)");
        cache.GetTrie("c1", 4).ShouldBeNull("generation 4 was pruned");
    }

    /// <summary>Verifies that the current generation pointer is preserved after pruning.</summary>
    [Fact]
    public void Prune_Current_Pointer_Survives_Pruning()
    {
        // Current is always the highest generation; verify it is not lost after prune.
        var cache = new PermissionTrieCache();
        for (var g = 1L; g <= 6; g++) cache.Install(Trie("c1", g));

        cache.Prune("c1", keepLatest: 3);

        cache.GetTrie("c1")!.GenerationId.ShouldBe(6, "current must still be the highest generation after pruning");
    }
}
