using NATS.Server.Subscriptions;

namespace NATS.Server.Core.Tests;

public class SubListTests
{
    private static Subscription MakeSub(string subject, string? queue = null, string sid = "1")
        => new() { Subject = subject, Queue = queue, Sid = sid };

    [Fact]
    public void Insert_and_match_literal_subject()
    {
        var sl = new SubList();
        var sub = MakeSub("foo.bar");
        sl.Insert(sub);

        var r = sl.Match("foo.bar");
        r.PlainSubs.ShouldHaveSingleItem();
        r.PlainSubs[0].ShouldBeSameAs(sub);
        r.QueueSubs.ShouldBeEmpty();
    }

    [Fact]
    public void Match_returns_empty_for_no_match()
    {
        var sl = new SubList();
        sl.Insert(MakeSub("foo.bar"));

        var r = sl.Match("foo.baz");
        r.PlainSubs.ShouldBeEmpty();
    }

    [Fact]
    public void Match_partial_wildcard()
    {
        var sl = new SubList();
        var sub = MakeSub("foo.*");
        sl.Insert(sub);

        sl.Match("foo.bar").PlainSubs.ShouldHaveSingleItem();
        sl.Match("foo.baz").PlainSubs.ShouldHaveSingleItem();
        sl.Match("foo.bar.baz").PlainSubs.ShouldBeEmpty();
    }

    [Fact]
    public void Match_full_wildcard()
    {
        var sl = new SubList();
        var sub = MakeSub("foo.>");
        sl.Insert(sub);

        sl.Match("foo.bar").PlainSubs.ShouldHaveSingleItem();
        sl.Match("foo.bar.baz").PlainSubs.ShouldHaveSingleItem();
        sl.Match("foo").PlainSubs.ShouldBeEmpty();
    }

    [Fact]
    public void Match_root_full_wildcard()
    {
        var sl = new SubList();
        sl.Insert(MakeSub(">"));

        sl.Match("foo").PlainSubs.ShouldHaveSingleItem();
        sl.Match("foo.bar").PlainSubs.ShouldHaveSingleItem();
        sl.Match("foo.bar.baz").PlainSubs.ShouldHaveSingleItem();
    }

    [Fact]
    public void Match_collects_multiple_subs()
    {
        var sl = new SubList();
        sl.Insert(MakeSub("foo.bar", sid: "1"));
        sl.Insert(MakeSub("foo.*", sid: "2"));
        sl.Insert(MakeSub("foo.>", sid: "3"));
        sl.Insert(MakeSub(">", sid: "4"));

        var r = sl.Match("foo.bar");
        r.PlainSubs.Length.ShouldBe(4);
    }

    [Fact]
    public void Remove_subscription()
    {
        var sl = new SubList();
        var sub = MakeSub("foo.bar");
        sl.Insert(sub);
        sl.Match("foo.bar").PlainSubs.ShouldHaveSingleItem();

        sl.Remove(sub);
        sl.Match("foo.bar").PlainSubs.ShouldBeEmpty();
    }

    [Fact]
    public void Queue_group_subscriptions()
    {
        var sl = new SubList();
        sl.Insert(MakeSub("foo.bar", queue: "workers", sid: "1"));
        sl.Insert(MakeSub("foo.bar", queue: "workers", sid: "2"));
        sl.Insert(MakeSub("foo.bar", queue: "loggers", sid: "3"));

        var r = sl.Match("foo.bar");
        r.PlainSubs.ShouldBeEmpty();
        r.QueueSubs.Length.ShouldBe(2); // 2 queue groups
    }

    [Fact]
    public void Count_tracks_subscriptions()
    {
        var sl = new SubList();
        sl.Count.ShouldBe(0u);

        sl.Insert(MakeSub("foo", sid: "1"));
        sl.Insert(MakeSub("bar", sid: "2"));
        sl.Count.ShouldBe(2u);

        sl.Remove(MakeSub("foo", sid: "1"));
        // Remove by reference won't work — we need the same instance
    }

    [Fact]
    public void Count_tracks_with_same_instance()
    {
        var sl = new SubList();
        var sub = MakeSub("foo");
        sl.Insert(sub);
        sl.Count.ShouldBe(1u);
        sl.Remove(sub);
        sl.Count.ShouldBe(0u);
    }

    [Fact]
    public void Cache_invalidation_on_insert()
    {
        var sl = new SubList();
        sl.Insert(MakeSub("foo.bar", sid: "1"));

        // Prime the cache
        var r1 = sl.Match("foo.bar");
        r1.PlainSubs.ShouldHaveSingleItem();

        // Insert a wildcard that matches — cache should be invalidated
        sl.Insert(MakeSub("foo.*", sid: "2"));

        var r2 = sl.Match("foo.bar");
        r2.PlainSubs.Length.ShouldBe(2);
    }

    [Fact]
    public void Match_partial_wildcard_at_different_levels()
    {
        var sl = new SubList();
        sl.Insert(MakeSub("*.bar.baz", sid: "1"));
        sl.Insert(MakeSub("foo.*.baz", sid: "2"));
        sl.Insert(MakeSub("foo.bar.*", sid: "3"));

        var r = sl.Match("foo.bar.baz");
        r.PlainSubs.Length.ShouldBe(3);
    }

    [Fact]
    public void Stats_returns_correct_values()
    {
        var sl = new SubList();
        sl.Insert(MakeSub("foo.bar", sid: "1"));
        sl.Insert(MakeSub("foo.baz", sid: "2"));
        sl.Match("foo.bar");
        sl.Match("foo.bar"); // cache hit

        var stats = sl.Stats();
        stats.NumSubs.ShouldBe(2u);
        stats.NumInserts.ShouldBe(2ul);
        stats.NumMatches.ShouldBe(2ul);
        stats.CacheHitRate.ShouldBeGreaterThan(0.0);
    }

    [Fact]
    public void HasInterest_returns_true_when_subscribers_exist()
    {
        var sl = new SubList();
        sl.Insert(MakeSub("foo.bar"));
        sl.HasInterest("foo.bar").ShouldBeTrue();
        sl.HasInterest("foo.baz").ShouldBeFalse();
    }

    [Fact]
    public void HasInterest_with_wildcards()
    {
        var sl = new SubList();
        sl.Insert(MakeSub("foo.*"));
        sl.HasInterest("foo.bar").ShouldBeTrue();
        sl.HasInterest("bar.baz").ShouldBeFalse();
    }

    [Fact]
    public void NumInterest_counts_subscribers()
    {
        var sl = new SubList();
        sl.Insert(MakeSub("foo.bar", sid: "1"));
        sl.Insert(MakeSub("foo.*", sid: "2"));
        sl.Insert(MakeSub("foo.bar", queue: "q1", sid: "3"));

        var (np, nq) = sl.NumInterest("foo.bar");
        np.ShouldBe(2); // foo.bar + foo.*
        nq.ShouldBe(1); // queue sub
    }

    [Fact]
    public void RemoveBatch_removes_all()
    {
        var sl = new SubList();
        var sub1 = MakeSub("foo.bar", sid: "1");
        var sub2 = MakeSub("foo.baz", sid: "2");
        var sub3 = MakeSub("bar.qux", sid: "3");
        sl.Insert(sub1);
        sl.Insert(sub2);
        sl.Insert(sub3);
        sl.Count.ShouldBe(3u);

        sl.RemoveBatch([sub1, sub2]);
        sl.Count.ShouldBe(1u);
        sl.Match("foo.bar").PlainSubs.ShouldBeEmpty();
        sl.Match("bar.qux").PlainSubs.ShouldHaveSingleItem();
    }

    [Fact]
    public void All_returns_every_subscription()
    {
        var sl = new SubList();
        var sub1 = MakeSub("foo.bar", sid: "1");
        var sub2 = MakeSub("foo.*", sid: "2");
        var sub3 = MakeSub("bar.>", queue: "q", sid: "3");
        sl.Insert(sub1);
        sl.Insert(sub2);
        sl.Insert(sub3);

        var all = sl.All();
        all.Count.ShouldBe(3);
        all.ShouldContain(sub1);
        all.ShouldContain(sub2);
        all.ShouldContain(sub3);
    }

    [Fact]
    public void ReverseMatch_finds_patterns_matching_literal()
    {
        var sl = new SubList();
        var sub1 = MakeSub("foo.bar", sid: "1");
        var sub2 = MakeSub("foo.*", sid: "2");
        var sub3 = MakeSub("foo.>", sid: "3");
        var sub4 = MakeSub("bar.baz", sid: "4");
        sl.Insert(sub1);
        sl.Insert(sub2);
        sl.Insert(sub3);
        sl.Insert(sub4);

        var result = sl.ReverseMatch("foo.bar");
        result.PlainSubs.Length.ShouldBe(3); // foo.bar, foo.*, foo.>
        result.PlainSubs.ShouldContain(sub1);
        result.PlainSubs.ShouldContain(sub2);
        result.PlainSubs.ShouldContain(sub3);
    }

    [Fact]
    public void Generation_ID_invalidates_cache()
    {
        var sl = new SubList();
        sl.Insert(MakeSub("foo.bar", sid: "1"));

        // Prime cache
        var r1 = sl.Match("foo.bar");
        r1.PlainSubs.Length.ShouldBe(1);

        // Insert another sub (bumps generation)
        sl.Insert(MakeSub("foo.bar", sid: "2"));

        // Cache should be invalidated by generation mismatch
        var r2 = sl.Match("foo.bar");
        r2.PlainSubs.Length.ShouldBe(2);
    }

    // -----------------------------------------------------------------------
    // Concurrency and edge case tests
    // Ported from: golang/nats-server/server/sublist_test.go
    // TestSublistRaceOnRemove, TestSublistRaceOnInsert, TestSublistRaceOnMatch,
    // TestSublistRemoveWithLargeSubs, TestSublistInvalidSubjectsInsert,
    // TestSublistInsertWithWildcardsAsLiterals
    // -----------------------------------------------------------------------

    /// <summary>
    /// Verifies that removing subscriptions concurrently while reading cached
    /// match results does not corrupt the subscription data. Reads the cached
    /// result before removals begin and iterates queue entries while removals
    /// run in parallel.
    /// Ref: testSublistRaceOnRemove (sublist_test.go:823)
    /// </summary>
    [Fact]
    public async Task Race_on_remove_does_not_corrupt_cache()
    {
        var sl = new SubList();
        const int total = 100;
        var subs = new Subscription[total];

        for (int i = 0; i < total; i++)
        {
            subs[i] = new Subscription { Subject = "foo", Queue = "bar", Sid = i.ToString() };
            sl.Insert(subs[i]);
        }

        // Prime cache with one warm-up call then capture result
        sl.Match("foo");
        var cached = sl.Match("foo");

        // Start removing all subs concurrently while we inspect the cached result
        var removeTask = Task.Run(() =>
        {
            foreach (var sub in subs)
                sl.Remove(sub);
        });

        // Iterate all queue groups in the cached snapshot — must not throw
        foreach (var qgroup in cached.QueueSubs)
        {
            foreach (var sub in qgroup)
            {
                sub.Queue.ShouldBe("bar");
            }
        }

        await removeTask;

        // After all removals, no interest should remain
        var afterRemoval = sl.Match("foo");
        afterRemoval.PlainSubs.ShouldBeEmpty();
        afterRemoval.QueueSubs.ShouldBeEmpty();
    }

    /// <summary>
    /// Verifies that inserting subscriptions from one task while another task
    /// is continuously calling Match does not cause crashes or produce invalid
    /// results (wrong queue names, corrupted subjects).
    /// Ref: testSublistRaceOnInsert (sublist_test.go:904)
    /// </summary>
    [Fact]
    public async Task Race_on_insert_does_not_corrupt_cache()
    {
        var sl = new SubList();
        const int total = 100;
        var qsubs = new Subscription[total];
        for (int i = 0; i < total; i++)
            qsubs[i] = new Subscription { Subject = "foo", Queue = "bar", Sid = i.ToString() };

        // Insert queue subs from background task while matching concurrently
        var insertTask = Task.Run(() =>
        {
            foreach (var sub in qsubs)
                sl.Insert(sub);
        });

        for (int i = 0; i < 1000; i++)
        {
            var r = sl.Match("foo");
            foreach (var qgroup in r.QueueSubs)
            {
                foreach (var sub in qgroup)
                    sub.Queue.ShouldBe("bar");
            }
        }

        await insertTask;

        // Now repeat for plain subs
        var sl2 = new SubList();
        var psubs = new Subscription[total];
        for (int i = 0; i < total; i++)
            psubs[i] = new Subscription { Subject = "foo", Sid = i.ToString() };

        var insertTask2 = Task.Run(() =>
        {
            foreach (var sub in psubs)
                sl2.Insert(sub);
        });

        for (int i = 0; i < 1000; i++)
        {
            var r = sl2.Match("foo");
            foreach (var sub in r.PlainSubs)
                sub.Subject.ShouldBe("foo");
        }

        await insertTask2;
    }

    /// <summary>
    /// Verifies that multiple concurrent goroutines matching the same subject
    /// simultaneously never observe corrupted subscription data (wrong subjects
    /// or queue names).
    /// Ref: TestSublistRaceOnMatch (sublist_test.go:956)
    /// </summary>
    [Fact]
    public async Task Race_on_match_during_concurrent_mutations()
    {
        var sl = new SubList();
        sl.Insert(new Subscription { Subject = "foo.*", Queue = "workers", Sid = "1" });
        sl.Insert(new Subscription { Subject = "foo.bar", Queue = "workers", Sid = "2" });
        sl.Insert(new Subscription { Subject = "foo.*", Sid = "3" });
        sl.Insert(new Subscription { Subject = "foo.bar", Sid = "4" });

        var errors = new System.Collections.Concurrent.ConcurrentBag<string>();

        async Task MatchRepeatedly()
        {
            for (int i = 0; i < 10; i++)
            {
                var r = sl.Match("foo.bar");
                foreach (var sub in r.PlainSubs)
                {
                    if (!sub.Subject.StartsWith("foo.", StringComparison.Ordinal))
                        errors.Add($"Wrong subject: {sub.Subject}");
                }
                foreach (var qgroup in r.QueueSubs)
                {
                    foreach (var sub in qgroup)
                    {
                        if (sub.Queue != "workers")
                            errors.Add($"Wrong queue name: {sub.Queue}");
                    }
                }
                await Task.Yield();
            }
        }

        await Task.WhenAll(MatchRepeatedly(), MatchRepeatedly());

        errors.ShouldBeEmpty();
    }

    /// <summary>
    /// Verifies that removing individual subscriptions from a list that has
    /// crossed the high-fanout threshold (plistMin=256) produces the correct
    /// remaining count. Mirrors the Go plistMin*2 scenario.
    /// Ref: testSublistRemoveWithLargeSubs (sublist_test.go:330)
    /// </summary>
    [Fact]
    public void Remove_from_large_subscription_list()
    {
        // plistMin in Go is 256; the .NET port uses 256 as PackedListEnabled threshold.
        // We use 200 to keep the test fast while still exercising the large-list path.
        const int subCount = 200;
        var sl = new SubList();
        var inserted = new Subscription[subCount];

        for (int i = 0; i < subCount; i++)
        {
            inserted[i] = new Subscription { Subject = "foo", Sid = i.ToString() };
            sl.Insert(inserted[i]);
        }

        var r = sl.Match("foo");
        r.PlainSubs.Length.ShouldBe(subCount);

        // Remove one from the middle, one from the start, one from the end
        sl.Remove(inserted[subCount / 2]);
        sl.Remove(inserted[0]);
        sl.Remove(inserted[subCount - 1]);

        var r2 = sl.Match("foo");
        r2.PlainSubs.Length.ShouldBe(subCount - 3);
    }

    /// <summary>
    /// Verifies that attempting to insert subscriptions with invalid subjects
    /// (empty leading or middle tokens, or a full-wildcard that is not the
    /// terminal token) causes an ArgumentException to be thrown.
    /// Note: a trailing dot ("foo.") is not rejected by the current .NET
    /// TokenEnumerator because the empty token after the trailing separator is
    /// never yielded — the Go implementation's Insert validates this via a
    /// separate length check that the .NET port has not yet added.
    /// Ref: testSublistInvalidSubjectsInsert (sublist_test.go:396)
    /// </summary>
    [Theory]
    [InlineData(".foo")]         // leading empty token — first token is ""
    [InlineData("foo..bar")]     // empty middle token
    [InlineData("foo.bar..baz")] // empty middle token variant
    [InlineData("foo.>.bar")]    // full-wildcard not terminal
    public void Insert_invalid_subject_is_rejected(string subject)
    {
        var sl = new SubList();
        var sub = new Subscription { Subject = subject, Sid = "1" };
        Should.Throw<ArgumentException>(() => sl.Insert(sub));
    }

    /// <summary>
    /// Verifies that subjects whose tokens contain wildcard characters as part
    /// of a longer token (e.g. "foo.*-", "foo.>-") are treated as literals and
    /// do not match via wildcard semantics. The exact subject string matches
    /// itself, but a plain "foo.bar" does not match.
    /// Ref: testSublistInsertWithWildcardsAsLiterals (sublist_test.go:775)
    /// </summary>
    [Theory]
    [InlineData("foo.*-")]   // token contains * but is not the single-char wildcard
    [InlineData("foo.>-")]   // token contains > but is not the single-char wildcard
    public void Wildcards_as_literals_not_matched_as_wildcards(string subject)
    {
        var sl = new SubList();
        var sub = new Subscription { Subject = subject, Sid = "1" };
        sl.Insert(sub);

        // A subject that would match if * / > were real wildcards must NOT match
        sl.Match("foo.bar").PlainSubs.ShouldBeEmpty();

        // The literal subject itself must match exactly
        sl.Match(subject).PlainSubs.ShouldHaveSingleItem();
    }

    /// <summary>
    /// Verifies edge-case handling for subjects with empty tokens at different
    /// positions. Empty string, leading dot, and consecutive dots produce no
    /// match results (the Tokenize helper returns null for invalid subjects).
    /// Insert with leading or middle empty tokens throws ArgumentException.
    /// Note: "foo." (trailing dot) is not rejected by Insert because the
    /// TokenEnumerator stops before yielding the trailing empty token — it is
    /// a known behavioural gap vs. Go that does not affect correctness of the
    /// trie but is documented here for future parity work.
    /// </summary>
    [Fact]
    public void Empty_subject_tokens_handled()
    {
        var sl = new SubList();

        // Insert a valid sub so the list is not empty
        sl.Insert(MakeSub("foo.bar", sid: "valid"));

        // Matching against subjects with empty tokens returns no results
        // (the Match tokenizer returns null / empty for invalid subjects)
        sl.Match("").PlainSubs.ShouldBeEmpty();
        sl.Match("foo..bar").PlainSubs.ShouldBeEmpty();
        sl.Match(".foo").PlainSubs.ShouldBeEmpty();
        sl.Match("foo.").PlainSubs.ShouldBeEmpty();

        // Inserting a subject with a leading empty token throws
        Should.Throw<ArgumentException>(() => sl.Insert(new Subscription { Subject = ".foo", Sid = "x" }));
        // Inserting a subject with a middle empty token throws
        Should.Throw<ArgumentException>(() => sl.Insert(new Subscription { Subject = "foo..bar", Sid = "x" }));

        // The original valid sub remains unaffected — failed inserts must not corrupt state
        sl.Count.ShouldBe(1u);
        sl.Match("foo.bar").PlainSubs.ShouldHaveSingleItem();
    }
}
