// Go reference: golang/nats-server/server/sublist_test.go
// Ports Go sublist tests not yet covered by SubListTests.cs or the SubList/ subfolder.

using NATS.Server.Subscriptions;

namespace NATS.Server.Tests;

/// <summary>
/// Go parity tests for SubList ported from sublist_test.go.
/// Covers basic multi-token matching, wildcard removal, cache eviction,
/// subject-validity helpers, queue results, reverse match, HasInterest,
/// NumInterest, and cache hit-rate statistics.
/// </summary>
public class SubListGoParityTests
{
    // -------------------------------------------------------------------------
    // Helpers
    // -------------------------------------------------------------------------
    private static Subscription MakeSub(string subject, string? queue = null, string sid = "1")
        => new() { Subject = subject, Queue = queue, Sid = sid };

    // =========================================================================
    // Basic insert / match
    // =========================================================================

    /// <summary>
    /// Single-token subject round-trips through insert and match.
    /// Ref: TestSublistInit / TestSublistInsertCount (sublist_test.go:117,122)
    /// </summary>
    [Fact]
    public void Init_count_is_zero_and_grows_with_inserts()
    {
        var sl = new SubList();
        sl.Count.ShouldBe(0u);
        sl.Insert(MakeSub("foo", sid: "1"));
        sl.Insert(MakeSub("bar", sid: "2"));
        sl.Insert(MakeSub("foo.bar", sid: "3"));
        sl.Count.ShouldBe(3u);
    }

    /// <summary>
    /// A multi-token literal subject matches itself exactly.
    /// Ref: TestSublistSimpleMultiTokens (sublist_test.go:154)
    /// </summary>
    [Fact]
    public void Simple_multi_token_match()
    {
        var sl = new SubList();
        var sub = MakeSub("foo.bar.baz");
        sl.Insert(sub);

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

    /// <summary>
    /// A partial wildcard at the end of a pattern matches the final literal token.
    /// Ref: TestSublistPartialWildcardAtEnd (sublist_test.go:190)
    /// </summary>
    [Fact]
    public void Partial_wildcard_at_end_matches_final_token()
    {
        var sl = new SubList();
        var lsub = MakeSub("a.b.c", sid: "1");
        var psub = MakeSub("a.b.*", sid: "2");
        sl.Insert(lsub);
        sl.Insert(psub);

        var r = sl.Match("a.b.c");
        r.PlainSubs.Length.ShouldBe(2);
        r.PlainSubs.ShouldContain(lsub);
        r.PlainSubs.ShouldContain(psub);
    }

    /// <summary>
    /// Subjects with two tokens do not match a single-token subscription.
    /// Ref: TestSublistTwoTokenPubMatchSingleTokenSub (sublist_test.go:749)
    /// </summary>
    [Fact]
    public void Two_token_pub_does_not_match_single_token_sub()
    {
        var sl = new SubList();
        var sub = MakeSub("foo");
        sl.Insert(sub);

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

    // =========================================================================
    // Removal with wildcards
    // =========================================================================

    /// <summary>
    /// Removing wildcard subscriptions decrements the count and clears match results.
    /// Ref: TestSublistRemoveWildcard (sublist_test.go:255)
    /// </summary>
    [Fact]
    public void Remove_wildcard_subscriptions()
    {
        var sl = new SubList();
        var sub  = MakeSub("a.b.c.d",  sid: "1");
        var psub = MakeSub("a.b.*.d",  sid: "2");
        var fsub = MakeSub("a.b.>",    sid: "3");
        sl.Insert(sub);
        sl.Insert(psub);
        sl.Insert(fsub);
        sl.Count.ShouldBe(3u);

        sl.Match("a.b.c.d").PlainSubs.Length.ShouldBe(3);

        sl.Remove(sub);
        sl.Count.ShouldBe(2u);
        sl.Remove(fsub);
        sl.Count.ShouldBe(1u);
        sl.Remove(psub);
        sl.Count.ShouldBe(0u);
        sl.Match("a.b.c.d").PlainSubs.ShouldBeEmpty();
    }

    /// <summary>
    /// Inserting a subscription with a wildcard literal token (e.g. "foo.*-") and
    /// then removing it leaves the list empty and no spurious match on "foo.bar".
    /// Ref: TestSublistRemoveWithWildcardsAsLiterals (sublist_test.go:789)
    /// </summary>
    [Theory]
    [InlineData("foo.*-")]
    [InlineData("foo.>-")]
    public void Remove_with_wildcard_as_literal(string subject)
    {
        var sl  = new SubList();
        var sub = MakeSub(subject);
        sl.Insert(sub);

        // Removing a non-existent subscription does nothing
        sl.Remove(MakeSub("foo.bar"));
        sl.Count.ShouldBe(1u);

        sl.Remove(sub);
        sl.Count.ShouldBe(0u);
    }

    // =========================================================================
    // Cache behaviour
    // =========================================================================

    /// <summary>
    /// After inserting three subscriptions, adding a new wildcard subscription
    /// invalidates the cached result and subsequent matches include the new sub.
    /// Ref: TestSublistCache (sublist_test.go:423)
    /// </summary>
    [Fact]
    public void Cache_invalidated_by_subsequent_inserts()
    {
        var sl   = new SubList();
        var sub  = MakeSub("a.b.c.d", sid: "1");
        var psub = MakeSub("a.b.*.d", sid: "2");
        var fsub = MakeSub("a.b.>",   sid: "3");

        sl.Insert(sub);
        sl.Match("a.b.c.d").PlainSubs.ShouldHaveSingleItem();

        sl.Insert(psub);
        sl.Insert(fsub);
        sl.Count.ShouldBe(3u);

        var r = sl.Match("a.b.c.d");
        r.PlainSubs.Length.ShouldBe(3);
        r.PlainSubs.ShouldContain(sub);
        r.PlainSubs.ShouldContain(psub);
        r.PlainSubs.ShouldContain(fsub);

        sl.Remove(sub);
        sl.Remove(fsub);
        sl.Remove(psub);
        sl.Count.ShouldBe(0u);
        // Cache is cleared by each removal (generation bump), but a subsequent Match
        // may re-populate it with an empty result — verify no matching subs are found.
        sl.Match("a.b.c.d").PlainSubs.ShouldBeEmpty();
    }

    /// <summary>
    /// Inserting a fwc sub after cache has been primed causes the next match to
    /// return all three matching subs.
    /// Ref: TestSublistCache (wildcard part) (sublist_test.go:465)
    /// </summary>
    [Fact]
    public void Cache_updated_when_new_wildcard_inserted()
    {
        var sl = new SubList();
        sl.Insert(MakeSub("foo.*",  sid: "1"));
        sl.Insert(MakeSub("foo.bar", sid: "2"));

        sl.Match("foo.baz").PlainSubs.ShouldHaveSingleItem();
        sl.Match("foo.bar").PlainSubs.Length.ShouldBe(2);

        sl.Insert(MakeSub("foo.>", sid: "3"));
        sl.Match("foo.bar").PlainSubs.Length.ShouldBe(3);
    }

    /// <summary>
    /// Empty result is a shared singleton — two calls that yield no matches return
    /// the same object reference.
    /// Ref: TestSublistSharedEmptyResult (sublist_test.go:1049)
    /// </summary>
    [Fact]
    public void Empty_result_is_shared_singleton()
    {
        var sl = new SubList();
        var r1 = sl.Match("foo");
        var r2 = sl.Match("bar");
        r1.PlainSubs.ShouldBeEmpty();
        r2.PlainSubs.ShouldBeEmpty();
        ReferenceEquals(r1, r2).ShouldBeTrue();
    }

    // =========================================================================
    // Queue subscriptions
    // =========================================================================

    /// <summary>
    /// After inserting two queue groups, adding a plain sub makes it visible
    /// in PlainSubs; adding more members to each group expands QueueSubs.
    /// Removing members correctly shrinks group counts.
    /// Ref: TestSublistBasicQueueResults (sublist_test.go:486)
    /// </summary>
    [Fact]
    public void Basic_queue_results_lifecycle()
    {
        var sl   = new SubList();
        const string subject = "foo";
        var sub  = MakeSub(subject, sid: "plain");
        var sub1 = MakeSub(subject, queue: "bar",  sid: "q1");
        var sub2 = MakeSub(subject, queue: "baz",  sid: "q2");
        var sub3 = MakeSub(subject, queue: "bar",  sid: "q3");
        var sub4 = MakeSub(subject, queue: "baz",  sid: "q4");

        sl.Insert(sub1);
        var r = sl.Match(subject);
        r.PlainSubs.ShouldBeEmpty();
        r.QueueSubs.Length.ShouldBe(1);

        sl.Insert(sub2);
        r = sl.Match(subject);
        r.QueueSubs.Length.ShouldBe(2);

        sl.Insert(sub);
        r = sl.Match(subject);
        r.PlainSubs.ShouldHaveSingleItem();
        r.QueueSubs.Length.ShouldBe(2);

        sl.Insert(sub3);
        sl.Insert(sub4);
        r = sl.Match(subject);
        r.PlainSubs.ShouldHaveSingleItem();
        r.QueueSubs.Length.ShouldBe(2);
        // Each group should have 2 members
        r.QueueSubs.ShouldAllBe(g => g.Length == 2);

        // Remove the plain sub
        sl.Remove(sub);
        r = sl.Match(subject);
        r.PlainSubs.ShouldBeEmpty();
        r.QueueSubs.Length.ShouldBe(2);

        // Remove one member from "bar" group
        sl.Remove(sub1);
        r = sl.Match(subject);
        r.QueueSubs.Length.ShouldBe(2); // both groups still present

        // Remove remaining "bar" member
        sl.Remove(sub3);
        r = sl.Match(subject);
        r.QueueSubs.Length.ShouldBe(1); // only "baz" group remains

        // Remove both "baz" members
        sl.Remove(sub2);
        sl.Remove(sub4);
        r = sl.Match(subject);
        r.PlainSubs.ShouldBeEmpty();
        r.QueueSubs.ShouldBeEmpty();
    }

    // =========================================================================
    // Subject validity helpers
    // =========================================================================

    /// <summary>
    /// IsValidPublishSubject (IsLiteral) rejects wildcard tokens and partial-wildcard
    /// embedded in longer tokens is treated as a literal.
    /// Ref: TestSublistValidLiteralSubjects (sublist_test.go:585)
    /// </summary>
    [Theory]
    [InlineData("foo",        true)]
    [InlineData(".foo",       false)]
    [InlineData("foo.",       false)]
    [InlineData("foo..bar",   false)]
    [InlineData("foo.bar.*",  false)]
    [InlineData("foo.bar.>",  false)]
    [InlineData("*",          false)]
    [InlineData(">",          false)]
    [InlineData("foo*",       true)]   // embedded * not a wildcard
    [InlineData("foo**",      true)]
    [InlineData("foo.**",     true)]
    [InlineData("foo*bar",    true)]
    [InlineData("foo.*bar",   true)]
    [InlineData("foo*.bar",   true)]
    [InlineData("*bar",       true)]
    [InlineData("foo>",       true)]
    [InlineData("foo>>",      true)]
    [InlineData("foo.>>",     true)]
    [InlineData("foo>bar",    true)]
    [InlineData("foo.>bar",   true)]
    [InlineData("foo>.bar",   true)]
    [InlineData(">bar",       true)]
    public void IsValidPublishSubject_cases(string subject, bool expected)
    {
        // Ref: TestSublistValidLiteralSubjects (sublist_test.go:585)
        SubjectMatch.IsValidPublishSubject(subject).ShouldBe(expected);
    }

    /// <summary>
    /// IsValidSubject accepts subjects with embedded wildcard characters
    /// that are not standalone tokens, and rejects subjects with empty tokens.
    /// Ref: TestSublistValidSubjects (sublist_test.go:612)
    /// </summary>
    [Theory]
    [InlineData(".",          false)]
    [InlineData(".foo",       false)]
    [InlineData("foo.",       false)]
    [InlineData("foo..bar",   false)]
    [InlineData(">.bar",      false)]
    [InlineData("foo.>.bar",  false)]
    [InlineData("foo",        true)]
    [InlineData("foo.bar.*",  true)]
    [InlineData("foo.bar.>",  true)]
    [InlineData("*",          true)]
    [InlineData(">",          true)]
    [InlineData("foo*",       true)]
    [InlineData("foo**",      true)]
    [InlineData("foo.**",     true)]
    [InlineData("foo*bar",    true)]
    [InlineData("foo.*bar",   true)]
    [InlineData("foo*.bar",   true)]
    [InlineData("*bar",       true)]
    [InlineData("foo>",       true)]
    [InlineData("foo>>",      true)]
    [InlineData("foo.>>",     true)]
    [InlineData("foo>bar",    true)]
    [InlineData("foo.>bar",   true)]
    [InlineData("foo>.bar",   true)]
    [InlineData(">bar",       true)]
    public void IsValidSubject_cases(string subject, bool expected)
    {
        // Ref: TestSublistValidSubjects (sublist_test.go:612)
        SubjectMatch.IsValidSubject(subject).ShouldBe(expected);
    }

    /// <summary>
    /// IsLiteral correctly identifies subjects with embedded wildcard characters
    /// (but not standalone wildcard tokens) as literal.
    /// Ref: TestSubjectIsLiteral (sublist_test.go:673)
    /// </summary>
    [Theory]
    [InlineData("foo",           true)]
    [InlineData("foo.bar",       true)]
    [InlineData("foo*.bar",      true)]
    [InlineData("*",             false)]
    [InlineData(">",             false)]
    [InlineData("foo.*",         false)]
    [InlineData("foo.>",         false)]
    [InlineData("foo.*.>",       false)]
    [InlineData("foo.*.bar",     false)]
    [InlineData("foo.bar.>",     false)]
    public void IsLiteral_cases(string subject, bool expected)
    {
        // Ref: TestSubjectIsLiteral (sublist_test.go:673)
        SubjectMatch.IsLiteral(subject).ShouldBe(expected);
    }

    /// <summary>
    /// MatchLiteral handles embedded wildcard-chars-as-literals correctly.
    /// Ref: TestSublistMatchLiterals (sublist_test.go:644)
    /// </summary>
    [Theory]
    [InlineData("foo",            "foo",            true)]
    [InlineData("foo",            "bar",            false)]
    [InlineData("foo",            "*",              true)]
    [InlineData("foo",            ">",              true)]
    [InlineData("foo.bar",        ">",              true)]
    [InlineData("foo.bar",        "foo.>",          true)]
    [InlineData("foo.bar",        "bar.>",          false)]
    [InlineData("stats.test.22",  "stats.>",        true)]
    [InlineData("stats.test.22",  "stats.*.*",      true)]
    [InlineData("foo.bar",        "foo",            false)]
    [InlineData("stats.test.foos","stats.test.foos",true)]
    [InlineData("stats.test.foos","stats.test.foo", false)]
    [InlineData("stats.test",     "stats.test.*",   false)]
    [InlineData("stats.test.foos","stats.*",        false)]
    [InlineData("stats.test.foos","stats.*.*.foos", false)]
    // Embedded wildcard chars treated as literals
    [InlineData("*bar",            "*bar",           true)]
    [InlineData("foo*",            "foo*",           true)]
    [InlineData("foo*bar",         "foo*bar",        true)]
    [InlineData("foo.***.bar",     "foo.***.bar",    true)]
    [InlineData(">bar",            ">bar",           true)]
    [InlineData("foo>",            "foo>",           true)]
    [InlineData("foo>bar",         "foo>bar",        true)]
    [InlineData("foo.>>>.bar",     "foo.>>>.bar",    true)]
    public void MatchLiteral_extended_cases(string literal, string pattern, bool expected)
    {
        // Ref: TestSublistMatchLiterals (sublist_test.go:644)
        SubjectMatch.MatchLiteral(literal, pattern).ShouldBe(expected);
    }

    // =========================================================================
    // Subject collide / subset
    // =========================================================================

    /// <summary>
    /// SubjectsCollide correctly identifies whether two subject patterns can
    /// match the same literal subject.
    /// Ref: TestSublistSubjectCollide (sublist_test.go:1548)
    /// </summary>
    [Theory]
    [InlineData("foo.*",        "foo.*.bar.>",  false)]
    [InlineData("foo.*.bar.>",  "foo.*",        false)]
    [InlineData("foo.*",        "foo.foo",      true)]
    [InlineData("foo.*",        "*.foo",        true)]
    [InlineData("foo.bar.>",    "*.bar.foo",    true)]
    public void SubjectsCollide_cases(string s1, string s2, bool expected)
    {
        // Ref: TestSublistSubjectCollide (sublist_test.go:1548)
        SubjectMatch.SubjectsCollide(s1, s2).ShouldBe(expected);
    }

    // =========================================================================
    // tokenAt (0-based in .NET vs 1-based in Go)
    // =========================================================================

    /// <summary>
    /// TokenAt returns the nth dot-separated token (0-based in .NET).
    /// The Go tokenAt helper uses 1-based indexing with "" for index 0; the .NET
    /// port uses 0-based indexing throughout.
    /// Ref: TestSubjectToken (sublist_test.go:707)
    /// </summary>
    [Theory]
    [InlineData("foo.bar.baz.*", 0, "foo")]
    [InlineData("foo.bar.baz.*", 1, "bar")]
    [InlineData("foo.bar.baz.*", 2, "baz")]
    [InlineData("foo.bar.baz.*", 3, "*")]
    [InlineData("foo.bar.baz.*", 4, "")]   // out of range
    public void TokenAt_zero_based(string subject, int index, string expected)
    {
        // Ref: TestSubjectToken (sublist_test.go:707)
        SubjectMatch.TokenAt(subject, index).ToString().ShouldBe(expected);
    }

    // =========================================================================
    // Stats / cache hit rate
    // =========================================================================

    /// <summary>
    /// Cache hit rate is computed correctly after 4 Match calls on the same subject
    /// (first call misses, subsequent three hit the cache).
    /// Ref: TestSublistAddCacheHitRate (sublist_test.go:1556)
    /// </summary>
    [Fact]
    public void Cache_hit_rate_is_computed_correctly()
    {
        var sl = new SubList();
        sl.Insert(MakeSub("foo"));
        for (var i = 0; i < 4; i++)
            sl.Match("foo");

        // 4 calls total, first is a cache miss, next 3 hit → 3/4 = 0.75
        var stats = sl.Stats();
        stats.CacheHitRate.ShouldBe(0.75, 1e-9);
    }

    /// <summary>
    /// Stats.NumCache is 0 when cache is empty (no matches have been performed yet).
    /// Ref: TestSublistNoCacheStats (sublist_test.go:1064)
    /// </summary>
    [Fact]
    public void Stats_NumCache_reflects_cache_population()
    {
        var sl = new SubList();
        sl.Insert(MakeSub("foo",     sid: "1"));
        sl.Insert(MakeSub("bar",     sid: "2"));
        sl.Insert(MakeSub("baz",     sid: "3"));
        sl.Insert(MakeSub("foo.bar.baz", sid: "4"));

        // No matches performed yet — cache should be empty
        sl.Stats().NumCache.ShouldBe(0u);

        sl.Match("a.b.c");
        sl.Match("bar");

        // Two distinct subjects have been matched, so cache should have 2 entries
        sl.Stats().NumCache.ShouldBe(2u);
    }

    // =========================================================================
    // HasInterest
    // =========================================================================

    /// <summary>
    /// HasInterest returns true for subjects with matching subscriptions and false
    /// otherwise, including after removal. Wildcard subscriptions match correctly.
    /// Ref: TestSublistHasInterest (sublist_test.go:1609)
    /// </summary>
    [Fact]
    public void HasInterest_with_plain_and_wildcard_subs()
    {
        var sl      = new SubList();
        var fooSub  = MakeSub("foo", sid: "1");
        sl.Insert(fooSub);

        sl.HasInterest("foo").ShouldBeTrue();
        sl.HasInterest("bar").ShouldBeFalse();

        sl.Remove(fooSub);
        sl.HasInterest("foo").ShouldBeFalse();

        // Partial wildcard
        var pwcSub = MakeSub("foo.*", sid: "2");
        sl.Insert(pwcSub);
        sl.HasInterest("foo").ShouldBeFalse();
        sl.HasInterest("foo.bar").ShouldBeTrue();
        sl.HasInterest("foo.bar.baz").ShouldBeFalse();

        sl.Remove(pwcSub);
        sl.HasInterest("foo.bar").ShouldBeFalse();

        // Full wildcard
        var fwcSub = MakeSub("foo.>", sid: "3");
        sl.Insert(fwcSub);
        sl.HasInterest("foo").ShouldBeFalse();
        sl.HasInterest("foo.bar").ShouldBeTrue();
        sl.HasInterest("foo.bar.baz").ShouldBeTrue();

        sl.Remove(fwcSub);
        sl.HasInterest("foo.bar").ShouldBeFalse();
        sl.HasInterest("foo.bar.baz").ShouldBeFalse();
    }

    /// <summary>
    /// HasInterest handles queue subscriptions: a queue sub creates interest
    /// even though PlainSubs is empty.
    /// Ref: TestSublistHasInterest (queue part) (sublist_test.go:1682)
    /// </summary>
    [Fact]
    public void HasInterest_with_queue_subscriptions()
    {
        var sl   = new SubList();
        var qsub = MakeSub("foo", queue: "bar",  sid: "1");
        var qsub2 = MakeSub("foo", queue: "baz", sid: "2");
        sl.Insert(qsub);
        sl.HasInterest("foo").ShouldBeTrue();
        sl.HasInterest("foo.bar").ShouldBeFalse();

        sl.Insert(qsub2);
        sl.HasInterest("foo").ShouldBeTrue();

        sl.Remove(qsub);
        sl.HasInterest("foo").ShouldBeTrue(); // qsub2 still present

        sl.Remove(qsub2);
        sl.HasInterest("foo").ShouldBeFalse();
    }

    /// <summary>
    /// HasInterest correctly handles overlapping subscriptions where a literal
    /// subject coexists with a wildcard at the same level.
    /// Ref: TestSublistHasInterestOverlapping (sublist_test.go:1775)
    /// </summary>
    [Fact]
    public void HasInterest_overlapping_subscriptions()
    {
        var sl = new SubList();
        sl.Insert(MakeSub("stream.A.child", sid: "1"));
        sl.Insert(MakeSub("stream.*",       sid: "2"));

        sl.HasInterest("stream.A.child").ShouldBeTrue();
        sl.HasInterest("stream.A").ShouldBeTrue();
    }

    // =========================================================================
    // NumInterest
    // =========================================================================

    /// <summary>
    /// NumInterest returns counts of plain and queue subscribers separately for
    /// literal subjects, wildcards, and queue-group subjects.
    /// Ref: TestSublistNumInterest (sublist_test.go:1783)
    /// </summary>
    [Fact]
    public void NumInterest_with_plain_subs()
    {
        var sl     = new SubList();
        var fooSub = MakeSub("foo", sid: "1");
        sl.Insert(fooSub);

        var (np, nq) = sl.NumInterest("foo");
        np.ShouldBe(1);
        nq.ShouldBe(0);

        sl.NumInterest("bar").ShouldBe((0, 0));

        sl.Remove(fooSub);
        sl.NumInterest("foo").ShouldBe((0, 0));
    }

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

        sl.NumInterest("foo").ShouldBe((0, 0));
        sl.NumInterest("foo.bar").ShouldBe((1, 0));
        sl.NumInterest("foo.bar.baz").ShouldBe((0, 0));

        sl.Remove(sub);
        sl.NumInterest("foo.bar").ShouldBe((0, 0));
    }

    [Fact]
    public void NumInterest_with_queue_subs()
    {
        var sl    = new SubList();
        var qsub  = MakeSub("foo", queue: "bar", sid: "1");
        var qsub2 = MakeSub("foo", queue: "baz", sid: "2");
        var qsub3 = MakeSub("foo", queue: "baz", sid: "3");
        sl.Insert(qsub);
        sl.NumInterest("foo").ShouldBe((0, 1));

        sl.Insert(qsub2);
        sl.NumInterest("foo").ShouldBe((0, 2));

        sl.Insert(qsub3);
        sl.NumInterest("foo").ShouldBe((0, 3));

        sl.Remove(qsub);
        sl.NumInterest("foo").ShouldBe((0, 2));

        sl.Remove(qsub2);
        sl.NumInterest("foo").ShouldBe((0, 1));

        sl.Remove(qsub3);
        sl.NumInterest("foo").ShouldBe((0, 0));
    }

    // =========================================================================
    // Reverse match
    // =========================================================================

    /// <summary>
    /// ReverseMatch finds registered patterns that would match a given literal or
    /// wildcard subject, covering all combinations of *, >, and literals.
    /// Ref: TestSublistReverseMatch (sublist_test.go:1440)
    /// </summary>
    [Fact]
    public void ReverseMatch_comprehensive()
    {
        var sl           = new SubList();
        var fooSub       = MakeSub("foo",         sid: "1");
        var barSub       = MakeSub("bar",         sid: "2");
        var fooBarSub    = MakeSub("foo.bar",      sid: "3");
        var fooBazSub    = MakeSub("foo.baz",      sid: "4");
        var fooBarBazSub = MakeSub("foo.bar.baz",  sid: "5");
        sl.Insert(fooSub);
        sl.Insert(barSub);
        sl.Insert(fooBarSub);
        sl.Insert(fooBazSub);
        sl.Insert(fooBarBazSub);

        // ReverseMatch("foo") — only fooSub
        var r = sl.ReverseMatch("foo");
        r.PlainSubs.Length.ShouldBe(1);
        r.PlainSubs.ShouldContain(fooSub);

        // ReverseMatch("bar") — only barSub
        r = sl.ReverseMatch("bar");
        r.PlainSubs.ShouldHaveSingleItem();
        r.PlainSubs.ShouldContain(barSub);

        // ReverseMatch("*") — single-token subs: foo and bar
        r = sl.ReverseMatch("*");
        r.PlainSubs.Length.ShouldBe(2);
        r.PlainSubs.ShouldContain(fooSub);
        r.PlainSubs.ShouldContain(barSub);

        // ReverseMatch("baz") — no match
        sl.ReverseMatch("baz").PlainSubs.ShouldBeEmpty();

        // ReverseMatch("foo.*") — foo.bar and foo.baz
        r = sl.ReverseMatch("foo.*");
        r.PlainSubs.Length.ShouldBe(2);
        r.PlainSubs.ShouldContain(fooBarSub);
        r.PlainSubs.ShouldContain(fooBazSub);

        // ReverseMatch("*.*") — same two
        r = sl.ReverseMatch("*.*");
        r.PlainSubs.Length.ShouldBe(2);
        r.PlainSubs.ShouldContain(fooBarSub);
        r.PlainSubs.ShouldContain(fooBazSub);

        // ReverseMatch("*.bar") — only fooBarSub
        r = sl.ReverseMatch("*.bar");
        r.PlainSubs.ShouldHaveSingleItem();
        r.PlainSubs.ShouldContain(fooBarSub);

        // ReverseMatch("bar.*") — no match
        sl.ReverseMatch("bar.*").PlainSubs.ShouldBeEmpty();

        // ReverseMatch("foo.>") — 3 subs under foo
        r = sl.ReverseMatch("foo.>");
        r.PlainSubs.Length.ShouldBe(3);
        r.PlainSubs.ShouldContain(fooBarSub);
        r.PlainSubs.ShouldContain(fooBazSub);
        r.PlainSubs.ShouldContain(fooBarBazSub);

        // ReverseMatch(">") — all 5 subs
        r = sl.ReverseMatch(">");
        r.PlainSubs.Length.ShouldBe(5);
    }

    /// <summary>
    /// ReverseMatch finds a subscription even when the query has extra wildcard
    /// tokens beyond what the stored pattern has.
    /// Ref: TestSublistReverseMatchWider (sublist_test.go:1508)
    /// </summary>
    [Fact]
    public void ReverseMatch_wider_query()
    {
        var sl  = new SubList();
        var sub = MakeSub("uplink.*.*.>");
        sl.Insert(sub);

        sl.ReverseMatch("uplink.1.*.*.>").PlainSubs.ShouldHaveSingleItem();
        sl.ReverseMatch("uplink.1.2.3.>").PlainSubs.ShouldHaveSingleItem();
    }

    // =========================================================================
    // Match with empty tokens (should yield no results)
    // =========================================================================

    /// <summary>
    /// Subjects with empty tokens (leading/trailing/double dots) never match any
    /// subscription, even when a catch-all '>' subscription is present.
    /// Ref: TestSublistMatchWithEmptyTokens (sublist_test.go:1522)
    /// </summary>
    [Theory]
    [InlineData(".foo")]
    [InlineData("..foo")]
    [InlineData("foo..")]
    [InlineData("foo.")]
    [InlineData("foo..bar")]
    [InlineData("foo...bar")]
    public void Match_with_empty_tokens_returns_empty(string badSubject)
    {
        // Ref: TestSublistMatchWithEmptyTokens (sublist_test.go:1522)
        var sl = new SubList();
        sl.Insert(MakeSub(">",        sid: "1"));
        sl.Insert(MakeSub(">", queue: "queue", sid: "2"));

        var r = sl.Match(badSubject);
        r.PlainSubs.ShouldBeEmpty();
        r.QueueSubs.ShouldBeEmpty();
    }

    // =========================================================================
    // Interest notification (adapted from Go's channel-based API to .NET events)
    // =========================================================================

    /// <summary>
    /// The InterestChanged event fires when subscriptions are inserted or removed.
    /// Inserting the first subscriber fires LocalAdded; removing the last fires
    /// LocalRemoved. Adding a second subscriber does not fire a redundant event.
    /// Ref: TestSublistRegisterInterestNotification (sublist_test.go:1126) —
    /// the Go API uses RegisterNotification with a channel; the .NET port exposes
    /// an <see cref="InterestChange"/> event instead.
    /// </summary>
    [Fact]
    public void InterestChanged_fires_on_first_insert_and_last_remove()
    {
        using var sl = new SubList();
        var events = new List<InterestChange>();
        sl.InterestChanged += e => events.Add(e);

        var sub1 = MakeSub("foo", sid: "1");
        var sub2 = MakeSub("foo", sid: "2");

        sl.Insert(sub1);
        events.Count.ShouldBe(1);
        events[0].Kind.ShouldBe(InterestChangeKind.LocalAdded);
        events[0].Subject.ShouldBe("foo");

        sl.Insert(sub2);
        events.Count.ShouldBe(2); // second insert still fires (event per operation)

        sl.Remove(sub1);
        events.Count.ShouldBe(3);
        events[2].Kind.ShouldBe(InterestChangeKind.LocalRemoved);

        sl.Remove(sub2);
        events.Count.ShouldBe(4);
        events[3].Kind.ShouldBe(InterestChangeKind.LocalRemoved);
    }

    /// <summary>
    /// InterestChanged events are raised for queue subscriptions with the correct
    /// Queue field populated.
    /// Ref: TestSublistRegisterInterestNotification (queue sub section) (sublist_test.go:1321)
    /// </summary>
    [Fact]
    public void InterestChanged_carries_queue_name_for_queue_subs()
    {
        using var sl = new SubList();
        var events = new List<InterestChange>();
        sl.InterestChanged += e => events.Add(e);

        var qsub = MakeSub("foo.bar.baz", queue: "q1", sid: "1");
        sl.Insert(qsub);
        events[0].Queue.ShouldBe("q1");
        events[0].Subject.ShouldBe("foo.bar.baz");
        events[0].Kind.ShouldBe(InterestChangeKind.LocalAdded);

        sl.Remove(qsub);
        events[1].Kind.ShouldBe(InterestChangeKind.LocalRemoved);
        events[1].Queue.ShouldBe("q1");
    }

    /// <summary>
    /// RemoveBatch removes all specified subscriptions in a single operation.
    /// Unlike individual Remove calls, RemoveBatch performs the removal atomically
    /// under a single write lock and does not fire InterestChanged per element —
    /// it is optimised for bulk teardown (e.g. client disconnect).
    /// After the batch, Match confirms that all removed subjects are gone.
    /// Ref: TestSublistRegisterInterestNotification (batch insert/remove) (sublist_test.go:1311)
    /// </summary>
    [Fact]
    public void RemoveBatch_removes_all_and_subscription_count_drops_to_zero()
    {
        using var sl = new SubList();
        var inserts = new List<InterestChange>();
        sl.InterestChanged += e =>
        {
            if (e.Kind == InterestChangeKind.LocalAdded) inserts.Add(e);
        };

        var subs = Enumerable.Range(1, 4)
                             .Select(i => MakeSub("foo", sid: i.ToString()))
                             .ToArray();
        foreach (var s in subs) sl.Insert(s);

        inserts.Count.ShouldBe(4);
        sl.Count.ShouldBe(4u);

        // RemoveBatch atomically removes all — count goes to zero
        sl.RemoveBatch(subs);
        sl.Count.ShouldBe(0u);
        sl.Match("foo").PlainSubs.ShouldBeEmpty();
    }
}
