// Go reference: server/gsl/gsl_test.go
// Tests for GenericSubjectList<T> trie-based subject matching.

using NATS.Server.Internal.Gsl;

namespace NATS.Server.Core.Tests.Internal.Gsl;

public class GenericSubjectListTests
{
    /// <summary>
    /// Helper: count matches for a subject.
    /// </summary>
    private static int CountMatches<T>(GenericSubjectList<T> s, string subject) where T : IEquatable<T>
    {
        var count = 0;
        s.Match(subject, _ => count++);
        return count;
    }

    // Go: TestGenericSublistInit server/gsl/gsl_test.go:23
    [Fact]
    public void Init_EmptyList()
    {
        var s = new GenericSubjectList<int>();
        s.Count.ShouldBe(0u);
    }

    // Go: TestGenericSublistInsertCount server/gsl/gsl_test.go:29
    [Fact]
    public void InsertCount_TracksCorrectly()
    {
        var s = new GenericSubjectList<int>();
        s.Insert("foo", 1);
        s.Insert("bar", 2);
        s.Insert("foo.bar", 3);
        s.Count.ShouldBe(3u);
    }

    // Go: TestGenericSublistSimple server/gsl/gsl_test.go:37
    [Fact]
    public void Simple_ExactMatch()
    {
        var s = new GenericSubjectList<int>();
        s.Insert("foo", 1);
        CountMatches(s, "foo").ShouldBe(1);
    }

    // Go: TestGenericSublistSimpleMultiTokens server/gsl/gsl_test.go:43
    [Fact]
    public void SimpleMultiTokens_Match()
    {
        var s = new GenericSubjectList<int>();
        s.Insert("foo.bar.baz", 1);
        CountMatches(s, "foo.bar.baz").ShouldBe(1);
    }

    // Go: TestGenericSublistPartialWildcard server/gsl/gsl_test.go:49
    [Fact]
    public void PartialWildcard_StarMatches()
    {
        var s = new GenericSubjectList<int>();
        s.Insert("a.b.c", 1);
        s.Insert("a.*.c", 2);
        CountMatches(s, "a.b.c").ShouldBe(2);
    }

    // Go: TestGenericSublistPartialWildcardAtEnd server/gsl/gsl_test.go:56
    [Fact]
    public void PartialWildcardAtEnd_StarMatches()
    {
        var s = new GenericSubjectList<int>();
        s.Insert("a.b.c", 1);
        s.Insert("a.b.*", 2);
        CountMatches(s, "a.b.c").ShouldBe(2);
    }

    // Go: TestGenericSublistFullWildcard server/gsl/gsl_test.go:63
    [Fact]
    public void FullWildcard_GreaterThanMatches()
    {
        var s = new GenericSubjectList<int>();
        s.Insert("a.b.c", 1);
        s.Insert("a.>", 2);
        CountMatches(s, "a.b.c").ShouldBe(2);
        CountMatches(s, "a.>").ShouldBe(1);
    }

    // Go: TestGenericSublistRemove server/gsl/gsl_test.go:71
    [Fact]
    public void Remove_DecreasesCount()
    {
        var s = new GenericSubjectList<int>();

        s.Insert("a.b.c.d", 1);
        s.Count.ShouldBe(1u);
        CountMatches(s, "a.b.c.d").ShouldBe(1);

        s.Remove("a.b.c.d", 1);
        s.Count.ShouldBe(0u);
        CountMatches(s, "a.b.c.d").ShouldBe(0);
    }

    // Go: TestGenericSublistRemoveWildcard server/gsl/gsl_test.go:83
    [Fact]
    public void RemoveWildcard_CleansUp()
    {
        var s = new GenericSubjectList<int>();

        s.Insert("a.b.c.d", 11);
        s.Insert("a.b.*.d", 22);
        s.Insert("a.b.>", 33);
        s.Count.ShouldBe(3u);
        CountMatches(s, "a.b.c.d").ShouldBe(3);

        s.Remove("a.b.*.d", 22);
        s.Count.ShouldBe(2u);
        CountMatches(s, "a.b.c.d").ShouldBe(2);

        s.Remove("a.b.>", 33);
        s.Count.ShouldBe(1u);
        CountMatches(s, "a.b.c.d").ShouldBe(1);

        s.Remove("a.b.c.d", 11);
        s.Count.ShouldBe(0u);
        CountMatches(s, "a.b.c.d").ShouldBe(0);
    }

    // Go: TestGenericSublistRemoveCleanup server/gsl/gsl_test.go:105
    [Fact]
    public void RemoveCleanup_PrunesEmptyNodes()
    {
        var s = new GenericSubjectList<int>();
        s.NumLevels().ShouldBe(0);
        s.Insert("a.b.c.d.e.f", 1);
        s.NumLevels().ShouldBe(6);
        s.Remove("a.b.c.d.e.f", 1);
        s.NumLevels().ShouldBe(0);
    }

    // Go: TestGenericSublistRemoveCleanupWildcards server/gsl/gsl_test.go:114
    [Fact]
    public void RemoveCleanupWildcards_PrunesEmptyNodes()
    {
        var s = new GenericSubjectList<int>();
        s.NumLevels().ShouldBe(0);
        s.Insert("a.b.*.d.e.>", 1);
        s.NumLevels().ShouldBe(6);
        s.Remove("a.b.*.d.e.>", 1);
        s.NumLevels().ShouldBe(0);
    }

    // Go: TestGenericSublistInvalidSubjectsInsert server/gsl/gsl_test.go:123
    [Fact]
    public void InvalidSubjectsInsert_RejectsInvalid()
    {
        var s = new GenericSubjectList<int>();

        // Empty tokens and FWC not terminal
        Should.Throw<InvalidOperationException>(() => s.Insert(".foo", 1));
        Should.Throw<InvalidOperationException>(() => s.Insert("foo.", 1));
        Should.Throw<InvalidOperationException>(() => s.Insert("foo..bar", 1));
        Should.Throw<InvalidOperationException>(() => s.Insert("foo.bar..baz", 1));
        Should.Throw<InvalidOperationException>(() => s.Insert("foo.>.baz", 1));
    }

    // Go: TestGenericSublistBadSubjectOnRemove server/gsl/gsl_test.go:134
    [Fact]
    public void BadSubjectOnRemove_RejectsInvalid()
    {
        var s = new GenericSubjectList<int>();
        Should.Throw<InvalidOperationException>(() => s.Insert("a.b..d", 1));
        Should.Throw<InvalidOperationException>(() => s.Remove("a.b..d", 1));
        Should.Throw<InvalidOperationException>(() => s.Remove("a.>.b", 1));
    }

    // Go: TestGenericSublistTwoTokenPubMatchSingleTokenSub server/gsl/gsl_test.go:141
    [Fact]
    public void TwoTokenPub_DoesNotMatchSingleTokenSub()
    {
        var s = new GenericSubjectList<int>();
        s.Insert("foo", 1);
        CountMatches(s, "foo").ShouldBe(1);
        CountMatches(s, "foo.bar").ShouldBe(0);
    }

    // Go: TestGenericSublistInsertWithWildcardsAsLiterals server/gsl/gsl_test.go:148
    [Fact]
    public void InsertWithWildcardsAsLiterals_TreatsAsLiteral()
    {
        var s = new GenericSubjectList<int>();
        var subjects = new[] { "foo.*-", "foo.>-" };
        for (var i = 0; i < subjects.Length; i++)
        {
            s.Insert(subjects[i], i);
            CountMatches(s, "foo.bar").ShouldBe(0);
            CountMatches(s, subjects[i]).ShouldBe(1);
        }
    }

    // Go: TestGenericSublistRemoveWithWildcardsAsLiterals server/gsl/gsl_test.go:157
    [Fact]
    public void RemoveWithWildcardsAsLiterals_RemovesCorrectly()
    {
        var s = new GenericSubjectList<int>();
        var subjects = new[] { "foo.*-", "foo.>-" };
        for (var i = 0; i < subjects.Length; i++)
        {
            s.Insert(subjects[i], i);
            CountMatches(s, "foo.bar").ShouldBe(0);
            CountMatches(s, subjects[i]).ShouldBe(1);
            Should.Throw<KeyNotFoundException>(() => s.Remove("foo.bar", i));
            s.Count.ShouldBe(1u);
            s.Remove(subjects[i], i);
            s.Count.ShouldBe(0u);
        }
    }

    // Go: TestGenericSublistMatchWithEmptyTokens server/gsl/gsl_test.go:170
    [Theory]
    [InlineData(".foo")]
    [InlineData("..foo")]
    [InlineData("foo..")]
    [InlineData("foo.")]
    [InlineData("foo..bar")]
    [InlineData("foo...bar")]
    public void MatchWithEmptyTokens_HandlesEdgeCase(string subject)
    {
        var s = new GenericSubjectList<int>();
        s.Insert(">", 1);
        CountMatches(s, subject).ShouldBe(0);
    }

    // Go: TestGenericSublistHasInterest server/gsl/gsl_test.go:180
    [Fact]
    public void HasInterest_ReturnsTrueForMatchingSubjects()
    {
        var s = new GenericSubjectList<int>();
        s.Insert("foo", 11);

        // Expect to find that "foo" matches but "bar" doesn't.
        s.HasInterest("foo").ShouldBeTrue();
        s.HasInterest("bar").ShouldBeFalse();

        // Call Match on a subject we know there is no match.
        CountMatches(s, "bar").ShouldBe(0);
        s.HasInterest("bar").ShouldBeFalse();

        // Remove fooSub and check interest again
        s.Remove("foo", 11);
        s.HasInterest("foo").ShouldBeFalse();

        // Try with partial wildcard *
        s.Insert("foo.*", 22);
        s.HasInterest("foo").ShouldBeFalse();
        s.HasInterest("foo.bar").ShouldBeTrue();
        s.HasInterest("foo.bar.baz").ShouldBeFalse();

        // Remove sub, there should be no interest
        s.Remove("foo.*", 22);
        s.HasInterest("foo").ShouldBeFalse();
        s.HasInterest("foo.bar").ShouldBeFalse();
        s.HasInterest("foo.bar.baz").ShouldBeFalse();

        // Try with full wildcard >
        s.Insert("foo.>", 33);
        s.HasInterest("foo").ShouldBeFalse();
        s.HasInterest("foo.bar").ShouldBeTrue();
        s.HasInterest("foo.bar.baz").ShouldBeTrue();

        s.Remove("foo.>", 33);
        s.HasInterest("foo").ShouldBeFalse();
        s.HasInterest("foo.bar").ShouldBeFalse();
        s.HasInterest("foo.bar.baz").ShouldBeFalse();

        // Try with *.>
        s.Insert("*.>", 44);
        s.HasInterest("foo").ShouldBeFalse();
        s.HasInterest("foo.bar").ShouldBeTrue();
        s.HasInterest("foo.baz").ShouldBeTrue();
        s.Remove("*.>", 44);

        // Try with *.bar
        s.Insert("*.bar", 55);
        s.HasInterest("foo").ShouldBeFalse();
        s.HasInterest("foo.bar").ShouldBeTrue();
        s.HasInterest("foo.baz").ShouldBeFalse();
        s.Remove("*.bar", 55);

        // Try with *
        s.Insert("*", 66);
        s.HasInterest("foo").ShouldBeTrue();
        s.HasInterest("foo.bar").ShouldBeFalse();
        s.Remove("*", 66);
    }

    // Go: TestGenericSublistHasInterestOverlapping server/gsl/gsl_test.go:237
    [Fact]
    public void HasInterestOverlapping_HandlesOverlap()
    {
        var s = new GenericSubjectList<int>();
        s.Insert("stream.A.child", 11);
        s.Insert("stream.*", 11);
        s.HasInterest("stream.A.child").ShouldBeTrue();
        s.HasInterest("stream.A").ShouldBeTrue();
    }

    // Go: TestGenericSublistHasInterestStartingInRace server/gsl/gsl_test.go:247
    [Fact]
    public async Task HasInterestStartingIn_ThreadSafe()
    {
        var s = new GenericSubjectList<int>();

        // Pre-populate with some patterns
        for (var i = 0; i < 10; i++)
        {
            s.Insert("foo.bar.baz", i);
            s.Insert("foo.*.baz", i + 10);
            s.Insert("foo.>", i + 20);
        }

        const int iterations = 1000;
        var tasks = new List<Task>();

        // Task 1: repeatedly call HasInterestStartingIn
        tasks.Add(Task.Run(() =>
        {
            for (var i = 0; i < iterations; i++)
            {
                s.HasInterestStartingIn("foo");
                s.HasInterestStartingIn("foo.bar");
                s.HasInterestStartingIn("foo.bar.baz");
                s.HasInterestStartingIn("other.subject");
            }
        }));

        // Task 2: repeatedly modify the sublist
        tasks.Add(Task.Run(() =>
        {
            for (var i = 0; i < iterations; i++)
            {
                var val = 1000 + i;
                var ch = (char)('a' + (i % 26));
                s.Insert($"test.subject.{ch}", val);
                s.Insert("foo.*.test", val);
                s.Remove($"test.subject.{ch}", val);
                s.Remove("foo.*.test", val);
            }
        }));

        // Task 3: also call HasInterest (which does lock)
        tasks.Add(Task.Run(() =>
        {
            for (var i = 0; i < iterations; i++)
            {
                s.HasInterest("foo.bar.baz");
                s.HasInterest("foo.something.baz");
            }
        }));

        // Wait for all tasks - should not throw (no deadlocks or data races)
        await Task.WhenAll(tasks);
    }

    // Go: TestGenericSublistNumInterest server/gsl/gsl_test.go:298
    [Fact]
    public void NumInterest_CountsMatchingSubscriptions()
    {
        var s = new GenericSubjectList<int>();
        s.Insert("foo", 11);

        // Helper to check both Match count and NumInterest agree
        void RequireNumInterest(string subj, int expected)
        {
            CountMatches(s, subj).ShouldBe(expected);
            s.NumInterest(subj).ShouldBe(expected);
        }

        // Expect to find that "foo" matches but "bar" doesn't.
        RequireNumInterest("foo", 1);
        RequireNumInterest("bar", 0);

        // Remove fooSub and check interest again
        s.Remove("foo", 11);
        RequireNumInterest("foo", 0);

        // Try with partial wildcard *
        s.Insert("foo.*", 22);
        RequireNumInterest("foo", 0);
        RequireNumInterest("foo.bar", 1);
        RequireNumInterest("foo.bar.baz", 0);

        // Remove sub, there should be no interest
        s.Remove("foo.*", 22);
        RequireNumInterest("foo", 0);
        RequireNumInterest("foo.bar", 0);
        RequireNumInterest("foo.bar.baz", 0);

        // Full wildcard >
        s.Insert("foo.>", 33);
        RequireNumInterest("foo", 0);
        RequireNumInterest("foo.bar", 1);
        RequireNumInterest("foo.bar.baz", 1);

        s.Remove("foo.>", 33);
        RequireNumInterest("foo", 0);
        RequireNumInterest("foo.bar", 0);
        RequireNumInterest("foo.bar.baz", 0);

        // *.>
        s.Insert("*.>", 44);
        RequireNumInterest("foo", 0);
        RequireNumInterest("foo.bar", 1);
        RequireNumInterest("foo.bar.baz", 1);
        s.Remove("*.>", 44);

        // *.bar
        s.Insert("*.bar", 55);
        RequireNumInterest("foo", 0);
        RequireNumInterest("foo.bar", 1);
        RequireNumInterest("foo.bar.baz", 0);
        s.Remove("*.bar", 55);

        // *
        s.Insert("*", 66);
        RequireNumInterest("foo", 1);
        RequireNumInterest("foo.bar", 0);
        s.Remove("*", 66);
    }
}
