// Go reference: server/client.go routeCache, maxResultCacheSize=8192.

using NATS.Server.Subscriptions;
using Shouldly;

namespace NATS.Server.Core.Tests.Subscriptions;

/// <summary>
/// Unit tests for RouteResultCache — the per-account LRU cache that stores
/// SubListResult objects keyed by subject string to avoid repeated SubList.Match() calls.
/// Go reference: server/client.go routeCache field, maxResultCacheSize=8192.
/// </summary>
public class RouteResultCacheTests
{
    // -------------------------------------------------------------------------
    // Helpers
    // -------------------------------------------------------------------------

    private static SubListResult MakeResult(string subject)
    {
        var sub = new Subscription { Subject = subject, Sid = "1" };
        return new SubListResult([sub], []);
    }

    // =========================================================================
    // Basic lookup behaviour
    // =========================================================================

    /// <summary>
    /// An empty cache must return false for any subject.
    /// </summary>
    [Fact]
    public void TryGet_returns_false_on_empty_cache()
    {
        var cache = new RouteResultCache(capacity: 16);

        var found = cache.TryGet("foo.bar", out var result);

        found.ShouldBeFalse();
        result.ShouldBeNull();
    }

    /// <summary>
    /// A result placed with Set must be retrievable via TryGet.
    /// </summary>
    [Fact]
    public void Set_and_TryGet_round_trips_result()
    {
        var cache = new RouteResultCache(capacity: 16);
        var expected = MakeResult("foo");
        cache.Set("foo", expected);

        var found = cache.TryGet("foo", out var actual);

        found.ShouldBeTrue();
        actual.ShouldBeSameAs(expected);
    }

    // =========================================================================
    // LRU eviction
    // =========================================================================

    /// <summary>
    /// When a capacity+1 entry is added, the oldest (LRU) entry must be evicted.
    /// Go reference: maxResultCacheSize=8192 with LRU eviction.
    /// </summary>
    [Fact]
    public void LRU_eviction_at_capacity()
    {
        const int cap = 4;
        var cache = new RouteResultCache(capacity: cap);

        // Fill to capacity — "first" is the oldest (LRU)
        cache.Set("first", MakeResult("first"));
        cache.Set("second", MakeResult("second"));
        cache.Set("third", MakeResult("third"));
        cache.Set("fourth", MakeResult("fourth"));

        // Adding a 5th entry must evict "first"
        cache.Set("fifth", MakeResult("fifth"));

        cache.Count.ShouldBe(cap);
        cache.TryGet("first", out _).ShouldBeFalse();
        cache.TryGet("fifth", out _).ShouldBeTrue();
    }

    /// <summary>
    /// After accessing A, B, C (capacity=3), accessing A promotes it to MRU.
    /// The next Set evicts B (now the true LRU) rather than A.
    /// </summary>
    [Fact]
    public void LRU_order_updated_on_access()
    {
        var cache = new RouteResultCache(capacity: 3);
        cache.Set("A", MakeResult("A"));
        cache.Set("B", MakeResult("B"));
        cache.Set("C", MakeResult("C"));

        // Access A so it becomes MRU; now LRU order is: B, A, C (front=C, back=B)
        cache.TryGet("A", out _);

        // D evicts the LRU entry — should be B, not A
        cache.Set("D", MakeResult("D"));

        cache.TryGet("A", out _).ShouldBeTrue();
        cache.TryGet("B", out _).ShouldBeFalse();
        cache.TryGet("C", out _).ShouldBeTrue();
        cache.TryGet("D", out _).ShouldBeTrue();
    }

    // =========================================================================
    // Generation / invalidation
    // =========================================================================

    /// <summary>
    /// After Invalidate(), the next TryGet must detect the generation mismatch,
    /// clear all entries, and return false.
    /// </summary>
    [Fact]
    public void Invalidate_clears_on_next_access()
    {
        var cache = new RouteResultCache(capacity: 16);
        cache.Set("a", MakeResult("a"));
        cache.Set("b", MakeResult("b"));

        cache.Invalidate();

        cache.TryGet("a", out var result).ShouldBeFalse();
        result.ShouldBeNull();
        cache.Count.ShouldBe(0);
    }

    /// <summary>
    /// Each call to Invalidate() must increment the generation counter by 1.
    /// </summary>
    [Fact]
    public void Generation_increments_on_invalidate()
    {
        var cache = new RouteResultCache();
        var gen0 = cache.Generation;

        cache.Invalidate();
        cache.Generation.ShouldBe(gen0 + 1);

        cache.Invalidate();
        cache.Generation.ShouldBe(gen0 + 2);
    }

    // =========================================================================
    // Statistics
    // =========================================================================

    /// <summary>
    /// Hits counter increments on each successful TryGet.
    /// </summary>
    [Fact]
    public void Hits_incremented_on_cache_hit()
    {
        var cache = new RouteResultCache(capacity: 16);
        cache.Set("subject", MakeResult("subject"));

        cache.TryGet("subject", out _);
        cache.TryGet("subject", out _);

        cache.Hits.ShouldBe(2);
    }

    /// <summary>
    /// Misses counter increments on each failed TryGet.
    /// </summary>
    [Fact]
    public void Misses_incremented_on_cache_miss()
    {
        var cache = new RouteResultCache(capacity: 16);

        cache.TryGet("nope", out _);
        cache.TryGet("also.nope", out _);

        cache.Misses.ShouldBe(2);
    }

    // =========================================================================
    // Clear
    // =========================================================================

    /// <summary>
    /// Clear() must remove all entries immediately.
    /// </summary>
    [Fact]
    public void Clear_removes_all_entries()
    {
        var cache = new RouteResultCache(capacity: 16);
        cache.Set("x", MakeResult("x"));
        cache.Set("y", MakeResult("y"));

        cache.Clear();

        cache.Count.ShouldBe(0);
        cache.TryGet("x", out _).ShouldBeFalse();
    }

    // =========================================================================
    // Update existing entry
    // =========================================================================

    /// <summary>
    /// Setting the same key twice must replace the value; TryGet returns the latest result.
    /// </summary>
    [Fact]
    public void Set_updates_existing_entry()
    {
        var cache = new RouteResultCache(capacity: 16);
        var first = MakeResult("v1");
        var second = MakeResult("v2");

        cache.Set("key", first);
        cache.Set("key", second);

        cache.Count.ShouldBe(1);
        cache.TryGet("key", out var returned).ShouldBeTrue();
        returned.ShouldBeSameAs(second);
    }
}
