// Copyright 2025 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using Shouldly;
using ZB.MOM.NatsNet.Server.Internal.DataStructures;

namespace ZB.MOM.NatsNet.Server.Tests.Internal.DataStructures;

/// <summary>
/// Ports all 21 tests from Go's gsl/gsl_test.go.
/// </summary>
public sealed class GenericSublistTests
{
    // -------------------------------------------------------------------------
    // Helpers (mirror Go's require_* functions)
    // -------------------------------------------------------------------------

    /// <summary>
    /// Counts how many values the sublist matches for <paramref name="subject"/>
    /// and asserts that count equals <paramref name="expected"/>.
    /// Mirrors Go's <c>require_Matches</c>.
    /// </summary>
    private static void RequireMatches<T>(GenericSublist<T> s, string subject, int expected)
        where T : notnull
    {
        var matches = 0;
        s.Match(subject, _ => matches++);
        matches.ShouldBe(expected);
    }

    // -------------------------------------------------------------------------
    // TestGenericSublistInit
    // -------------------------------------------------------------------------

    [Fact]
    public void TestGenericSublistInit()
    {
        var s = GenericSublist<EmptyStruct>.NewSublist();
        s.Count.ShouldBe(0u);
    }

    // -------------------------------------------------------------------------
    // TestGenericSublistInsertCount
    // -------------------------------------------------------------------------

    [Fact]
    public void TestGenericSublistInsertCount()
    {
        var s = GenericSublist<EmptyStruct>.NewSublist();
        s.Insert("foo", EmptyStruct.Value);
        s.Insert("bar", EmptyStruct.Value);
        s.Insert("foo.bar", EmptyStruct.Value);
        s.Count.ShouldBe(3u);
    }

    // -------------------------------------------------------------------------
    // TestGenericSublistSimple
    // -------------------------------------------------------------------------

    [Fact]
    public void TestGenericSublistSimple()
    {
        var s = GenericSublist<EmptyStruct>.NewSublist();
        s.Insert("foo", EmptyStruct.Value);
        RequireMatches(s, "foo", 1);
    }

    // -------------------------------------------------------------------------
    // TestGenericSublistSimpleMultiTokens
    // -------------------------------------------------------------------------

    [Fact]
    public void TestGenericSublistSimpleMultiTokens()
    {
        var s = GenericSublist<EmptyStruct>.NewSublist();
        s.Insert("foo.bar.baz", EmptyStruct.Value);
        RequireMatches(s, "foo.bar.baz", 1);
    }

    // -------------------------------------------------------------------------
    // TestGenericSublistPartialWildcard
    // -------------------------------------------------------------------------

    [Fact]
    public void TestGenericSublistPartialWildcard()
    {
        var s = GenericSublist<EmptyStruct>.NewSublist();
        s.Insert("a.b.c", EmptyStruct.Value);
        s.Insert("a.*.c", EmptyStruct.Value);
        RequireMatches(s, "a.b.c", 2);
    }

    // -------------------------------------------------------------------------
    // TestGenericSublistPartialWildcardAtEnd
    // -------------------------------------------------------------------------

    [Fact]
    public void TestGenericSublistPartialWildcardAtEnd()
    {
        var s = GenericSublist<EmptyStruct>.NewSublist();
        s.Insert("a.b.c", EmptyStruct.Value);
        s.Insert("a.b.*", EmptyStruct.Value);
        RequireMatches(s, "a.b.c", 2);
    }

    // -------------------------------------------------------------------------
    // TestGenericSublistFullWildcard
    // -------------------------------------------------------------------------

    [Fact]
    public void TestGenericSublistFullWildcard()
    {
        var s = GenericSublist<EmptyStruct>.NewSublist();
        s.Insert("a.b.c", EmptyStruct.Value);
        s.Insert("a.>", EmptyStruct.Value);
        RequireMatches(s, "a.b.c", 2);
        RequireMatches(s, "a.>", 1);
    }

    // -------------------------------------------------------------------------
    // TestGenericSublistRemove
    // -------------------------------------------------------------------------

    [Fact]
    public void TestGenericSublistRemove()
    {
        var s = GenericSublist<EmptyStruct>.NewSublist();

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

        s.Remove("a.b.c.d", EmptyStruct.Value);
        s.Count.ShouldBe(0u);
        RequireMatches(s, "a.b.c.d", 0);
    }

    // -------------------------------------------------------------------------
    // TestGenericSublistRemoveWildcard
    // -------------------------------------------------------------------------

    [Fact]
    public void TestGenericSublistRemoveWildcard()
    {
        var s = GenericSublist<int>.NewSublist();

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

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

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

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

    // -------------------------------------------------------------------------
    // TestGenericSublistRemoveCleanup
    // -------------------------------------------------------------------------

    [Fact]
    public void TestGenericSublistRemoveCleanup()
    {
        var s = GenericSublist<EmptyStruct>.NewSublist();
        s.NumLevels().ShouldBe(0);
        s.Insert("a.b.c.d.e.f", EmptyStruct.Value);
        s.NumLevels().ShouldBe(6);
        s.Remove("a.b.c.d.e.f", EmptyStruct.Value);
        s.NumLevels().ShouldBe(0);
    }

    // -------------------------------------------------------------------------
    // TestGenericSublistRemoveCleanupWildcards
    // -------------------------------------------------------------------------

    [Fact]
    public void TestGenericSublistRemoveCleanupWildcards()
    {
        var s = GenericSublist<EmptyStruct>.NewSublist();
        s.NumLevels().ShouldBe(0);
        s.Insert("a.b.*.d.e.>", EmptyStruct.Value);
        s.NumLevels().ShouldBe(6);
        s.Remove("a.b.*.d.e.>", EmptyStruct.Value);
        s.NumLevels().ShouldBe(0);
    }

    // -------------------------------------------------------------------------
    // TestGenericSublistInvalidSubjectsInsert
    // -------------------------------------------------------------------------

    [Fact]
    public void TestGenericSublistInvalidSubjectsInsert()
    {
        var s = GenericSublist<EmptyStruct>.NewSublist();

        // Insert, or subscriptions, can have wildcards, but not empty tokens,
        // and can not have a FWC that is not the terminal token.
        Should.Throw<ArgumentException>(() => s.Insert(".foo", EmptyStruct.Value));
        Should.Throw<ArgumentException>(() => s.Insert("foo.", EmptyStruct.Value));
        Should.Throw<ArgumentException>(() => s.Insert("foo..bar", EmptyStruct.Value));
        Should.Throw<ArgumentException>(() => s.Insert("foo.bar..baz", EmptyStruct.Value));
        Should.Throw<ArgumentException>(() => s.Insert("foo.>.baz", EmptyStruct.Value));
    }

    // -------------------------------------------------------------------------
    // TestGenericSublistBadSubjectOnRemove
    // -------------------------------------------------------------------------

    [Fact]
    public void TestGenericSublistBadSubjectOnRemove()
    {
        var s = GenericSublist<EmptyStruct>.NewSublist();
        Should.Throw<ArgumentException>(() => s.Insert("a.b..d", EmptyStruct.Value));
        Should.Throw<ArgumentException>(() => s.Remove("a.b..d", EmptyStruct.Value));
        Should.Throw<ArgumentException>(() => s.Remove("a.>.b", EmptyStruct.Value));
    }

    // -------------------------------------------------------------------------
    // TestGenericSublistTwoTokenPubMatchSingleTokenSub
    // -------------------------------------------------------------------------

    [Fact]
    public void TestGenericSublistTwoTokenPubMatchSingleTokenSub()
    {
        var s = GenericSublist<EmptyStruct>.NewSublist();
        s.Insert("foo", EmptyStruct.Value);
        RequireMatches(s, "foo", 1);
        RequireMatches(s, "foo.bar", 0);
    }

    // -------------------------------------------------------------------------
    // TestGenericSublistInsertWithWildcardsAsLiterals
    // -------------------------------------------------------------------------

    [Fact]
    public void TestGenericSublistInsertWithWildcardsAsLiterals()
    {
        var s = GenericSublist<int>.NewSublist();
        var subjects = new[] { "foo.*-", "foo.>-" };
        for (var i = 0; i < subjects.Length; i++)
        {
            var subject = subjects[i];
            s.Insert(subject, i);
            RequireMatches(s, "foo.bar", 0);
            RequireMatches(s, subject, 1);
        }
    }

    // -------------------------------------------------------------------------
    // TestGenericSublistRemoveWithWildcardsAsLiterals
    // -------------------------------------------------------------------------

    [Fact]
    public void TestGenericSublistRemoveWithWildcardsAsLiterals()
    {
        var s = GenericSublist<int>.NewSublist();
        var subjects = new[] { "foo.*-", "foo.>-" };
        for (var i = 0; i < subjects.Length; i++)
        {
            var subject = subjects[i];
            s.Insert(subject, i);
            RequireMatches(s, "foo.bar", 0);
            RequireMatches(s, subject, 1);
            Should.Throw<KeyNotFoundException>(() => s.Remove("foo.bar", i));
            s.Count.ShouldBe(1u);
            s.Remove(subject, i);
            s.Count.ShouldBe(0u);
        }
    }

    // -------------------------------------------------------------------------
    // TestGenericSublistMatchWithEmptyTokens
    // -------------------------------------------------------------------------

    [Fact]
    public void TestGenericSublistMatchWithEmptyTokens()
    {
        var s = GenericSublist<EmptyStruct>.NewSublist();
        s.Insert(">", EmptyStruct.Value);

        var subjects = new[]
        {
            ".foo", "..foo", "foo..", "foo.", "foo..bar", "foo...bar"
        };

        foreach (var subject in subjects)
        {
            RequireMatches(s, subject, 0);
        }
    }

    // -------------------------------------------------------------------------
    // TestGenericSublistHasInterest
    // -------------------------------------------------------------------------

    [Fact]
    public void TestGenericSublistHasInterest()
    {
        var s = GenericSublist<int>.NewSublist();
        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.
        RequireMatches(s, "bar", 0);
        s.HasInterest("bar").ShouldBeFalse();

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

        // Try with some wildcards.
        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();

        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();

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

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

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

    // -------------------------------------------------------------------------
    // TestGenericSublistHasInterestOverlapping
    // -------------------------------------------------------------------------

    [Fact]
    public void TestGenericSublistHasInterestOverlapping()
    {
        var s = GenericSublist<int>.NewSublist();
        s.Insert("stream.A.child", 11);
        s.Insert("stream.*", 11);
        s.HasInterest("stream.A.child").ShouldBeTrue();
        s.HasInterest("stream.A").ShouldBeTrue();
    }

    // -------------------------------------------------------------------------
    // TestGenericSublistHasInterestStartingInRace
    // Tests that HasInterestStartingIn is safe to call concurrently with
    // modifications to the sublist. Mirrors Go's goroutine test using Tasks.
    // -------------------------------------------------------------------------

    [Fact]
    public async Task TestGenericSublistHasInterestStartingInRace()
    {
        var s = GenericSublist<int>.NewSublist();

        // 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;

        // Task 1: repeatedly call HasInterestStartingIn.
        var task1 = 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.
        var task2 = Task.Run(() =>
        {
            for (var i = 0; i < iterations; i++)
            {
                var val = 1000 + i;
                var dynSubject = "test.subject." + (char)('a' + i % 26);
                s.Insert(dynSubject, val);
                s.Insert("foo.*.test", val);
                // Remove may fail if not found (concurrent), so swallow KeyNotFoundException.
                try { s.Remove(dynSubject, val); } catch (KeyNotFoundException) { }
                try { s.Remove("foo.*.test", val); } catch (KeyNotFoundException) { }
            }
        });

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

        await Task.WhenAll(task1, task2, task3);
    }

    // -------------------------------------------------------------------------
    // TestGenericSublistNumInterest
    // -------------------------------------------------------------------------

    [Fact]
    public void TestGenericSublistNumInterest()
    {
        var s = GenericSublist<int>.NewSublist();
        s.Insert("foo", 11);

        void RequireNumInterest(string subj, int expected)
        {
            RequireMatches(s, subj, 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 some wildcards.
        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);

        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);

        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);
    }
}
