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

public class SubjectTreeTests
{
    // Helper to convert string to byte array (Latin-1).
    private static byte[] B(string s) => System.Text.Encoding.Latin1.GetBytes(s);

    // Helper to count matches.
    private static int MatchCount(SubjectTree<int> st, string filter)
    {
        var count = 0;
        st.Match(B(filter), (_, _) =>
        {
            count++;
            return true;
        });
        return count;
    }

    // -------------------------------------------------------------------------
    // TestSubjectTreeBasics
    // -------------------------------------------------------------------------

    [Fact]
    public void TestSubjectTreeBasics()
    {
        var st = new SubjectTree<int>();
        st.Size().ShouldBe(0);

        // Single leaf insert.
        var (old, updated) = st.Insert(B("foo.bar.baz"), 22);
        old.ShouldBe(default);
        updated.ShouldBeFalse();
        st.Size().ShouldBe(1);

        // Find should not work with a wildcard.
        var (_, found) = st.Find(B("foo.bar.*"));
        found.ShouldBeFalse();

        // Find with literal — single leaf.
        var (val, found2) = st.Find(B("foo.bar.baz"));
        found2.ShouldBeTrue();
        val.ShouldBe(22);

        // Update single leaf.
        var (old2, updated2) = st.Insert(B("foo.bar.baz"), 33);
        old2.ShouldBe(22);
        updated2.ShouldBeTrue();
        st.Size().ShouldBe(1);

        // Split the tree.
        var (old3, updated3) = st.Insert(B("foo.bar"), 22);
        old3.ShouldBe(default);
        updated3.ShouldBeFalse();
        st.Size().ShouldBe(2);

        // Find both entries after split.
        var (v1, f1) = st.Find(B("foo.bar"));
        f1.ShouldBeTrue();
        v1.ShouldBe(22);

        var (v2, f2) = st.Find(B("foo.bar.baz"));
        f2.ShouldBeTrue();
        v2.ShouldBe(33);
    }

    // -------------------------------------------------------------------------
    // TestSubjectTreeConstruction
    // -------------------------------------------------------------------------

    [Fact]
    public void TestSubjectTreeConstruction()
    {
        var st = new SubjectTree<int>();
        st.Insert(B("foo.bar.A"), 1);
        st.Insert(B("foo.bar.B"), 2);
        st.Insert(B("foo.bar.C"), 3);
        st.Insert(B("foo.baz.A"), 11);
        st.Insert(B("foo.baz.B"), 22);
        st.Insert(B("foo.baz.C"), 33);
        st.Insert(B("foo.bar"), 42);

        // Validate structure.
        st._root.ShouldNotBeNull();
        st._root!.Kind.ShouldBe("NODE4");
        st._root.NumChildren.ShouldBe(2);

        // Now delete "foo.bar" and verify structure collapses correctly.
        var (v, found) = st.Delete(B("foo.bar"));
        found.ShouldBeTrue();
        v.ShouldBe(42);

        // The remaining entries should still be findable.
        var (v1, f1) = st.Find(B("foo.bar.A"));
        f1.ShouldBeTrue();
        v1.ShouldBe(1);
        var (v2, f2) = st.Find(B("foo.bar.B"));
        f2.ShouldBeTrue();
        v2.ShouldBe(2);
        var (v3, f3) = st.Find(B("foo.bar.C"));
        f3.ShouldBeTrue();
        v3.ShouldBe(3);
        var (v4, f4) = st.Find(B("foo.baz.A"));
        f4.ShouldBeTrue();
        v4.ShouldBe(11);
    }

    // -------------------------------------------------------------------------
    // TestSubjectTreeNodeGrow
    // -------------------------------------------------------------------------

    [Fact]
    public void TestSubjectTreeNodeGrow()
    {
        var st = new SubjectTree<int>();

        // Fill a node4 (4 children).
        for (var i = 0; i < 4; i++)
        {
            var subj = B($"foo.bar.{(char)('A' + i)}");
            var (old, upd) = st.Insert(subj, 22);
            old.ShouldBe(default);
            upd.ShouldBeFalse();
        }
        st._root.ShouldBeOfType<SubjectTreeNode4<int>>();

        // 5th child causes grow to node10.
        st.Insert(B("foo.bar.E"), 22);
        st._root.ShouldBeOfType<SubjectTreeNode10<int>>();

        // Fill to 10.
        for (var i = 5; i < 10; i++)
        {
            st.Insert(B($"foo.bar.{(char)('A' + i)}"), 22);
        }
        // 11th child causes grow to node16.
        st.Insert(B("foo.bar.K"), 22);
        st._root.ShouldBeOfType<SubjectTreeNode16<int>>();

        // Fill to 16.
        for (var i = 11; i < 16; i++)
        {
            st.Insert(B($"foo.bar.{(char)('A' + i)}"), 22);
        }
        // 17th child causes grow to node48.
        st.Insert(B("foo.bar.Q"), 22);
        st._root.ShouldBeOfType<SubjectTreeNode48<int>>();

        // Fill the node48.
        for (var i = 17; i < 48; i++)
        {
            st.Insert(B($"foo.bar.{(char)('A' + i)}"), 22);
        }
        // 49th child causes grow to node256.
        var subjLast = B($"foo.bar.{(char)('A' + 49)}");
        st.Insert(subjLast, 22);
        st._root.ShouldBeOfType<SubjectTreeNode256<int>>();
    }

    // -------------------------------------------------------------------------
    // TestSubjectTreeInsertSamePivot (same pivot bug)
    // -------------------------------------------------------------------------

    [Fact]
    public void TestSubjectTreeInsertSamePivot()
    {
        var testSubjects = new[]
        {
            B("0d00.2abbb82c1d.6e16.fa7f85470e.3e46"),
            B("534b12.3486c17249.4dde0666"),
            B("6f26aabd.920ee3.d4d3.5ffc69f6"),
            B("8850.ade3b74c31.aa533f77.9f59.a4bd8415.b3ed7b4111"),
            B("5a75047dcb.5548e845b6.76024a34.14d5b3.80c426.51db871c3a"),
            B("825fa8acfc.5331.00caf8bbbd.107c4b.c291.126d1d010e"),
        };

        var st = new SubjectTree<int>();
        foreach (var subj in testSubjects)
        {
            var (old, upd) = st.Insert(subj, 22);
            old.ShouldBe(default);
            upd.ShouldBeFalse();

            var (_, found) = st.Find(subj);
            found.ShouldBeTrue($"Could not find subject '{System.Text.Encoding.Latin1.GetString(subj)}' after insert");
        }
    }

    // -------------------------------------------------------------------------
    // TestSubjectTreeInsertLonger
    // -------------------------------------------------------------------------

    [Fact]
    public void TestSubjectTreeInsertLonger()
    {
        var st = new SubjectTree<int>();
        st.Insert(B("a1.aaaaaaaaaaaaaaaaaaaaaa0"), 1);
        st.Insert(B("a2.0"), 2);
        st.Insert(B("a1.aaaaaaaaaaaaaaaaaaaaaa1"), 3);
        st.Insert(B("a2.1"), 4);

        // Simulate purge of a2.>
        st.Delete(B("a2.0"));
        st.Delete(B("a2.1"));

        st.Size().ShouldBe(2);
        var (v1, f1) = st.Find(B("a1.aaaaaaaaaaaaaaaaaaaaaa0"));
        f1.ShouldBeTrue();
        v1.ShouldBe(1);
        var (v2, f2) = st.Find(B("a1.aaaaaaaaaaaaaaaaaaaaaa1"));
        f2.ShouldBeTrue();
        v2.ShouldBe(3);
    }

    // -------------------------------------------------------------------------
    // TestInsertEdgeCases
    // -------------------------------------------------------------------------

    [Fact]
    public void TestInsertEdgeCases()
    {
        var st = new SubjectTree<int>();

        // Reject subject with noPivot byte (127).
        var (old, upd) = st.Insert(new byte[] { (byte)'f', (byte)'o', (byte)'o', 127 }, 1);
        old.ShouldBe(default);
        upd.ShouldBeFalse();
        st.Size().ShouldBe(0);

        // Empty-ish subjects.
        st.Insert(B("a"), 1);
        st.Insert(B("b"), 2);
        st.Size().ShouldBe(2);
    }

    // -------------------------------------------------------------------------
    // TestFindEdgeCases
    // -------------------------------------------------------------------------

    [Fact]
    public void TestFindEdgeCases()
    {
        var st = new SubjectTree<int>();

        var (_, found) = st.Find(B("anything"));
        found.ShouldBeFalse();

        st.Insert(B("foo"), 42);
        var (v, f) = st.Find(B("foo"));
        f.ShouldBeTrue();
        v.ShouldBe(42);

        var (_, f2) = st.Find(B("fo"));
        f2.ShouldBeFalse();

        var (_, f3) = st.Find(B("foobar"));
        f3.ShouldBeFalse();
    }

    // -------------------------------------------------------------------------
    // TestSubjectTreeNodeDelete
    // -------------------------------------------------------------------------

    [Fact]
    public void TestSubjectTreeNodeDelete()
    {
        var st = new SubjectTree<int>();
        st.Insert(B("foo.bar.A"), 22);

        var (v, found) = st.Delete(B("foo.bar.A"));
        found.ShouldBeTrue();
        v.ShouldBe(22);
        st._root.ShouldBeNull();

        // Delete non-existent.
        var (v2, found2) = st.Delete(B("foo.bar.A"));
        found2.ShouldBeFalse();
        v2.ShouldBe(default);

        // Fill to node4 then shrink back through deletes.
        st.Insert(B("foo.bar.A"), 11);
        st.Insert(B("foo.bar.B"), 22);
        st.Insert(B("foo.bar.C"), 33);

        var (vC, fC) = st.Delete(B("foo.bar.C"));
        fC.ShouldBeTrue();
        vC.ShouldBe(33);

        var (vB, fB) = st.Delete(B("foo.bar.B"));
        fB.ShouldBeTrue();
        vB.ShouldBe(22);

        // Should have shrunk to a leaf.
        st._root.ShouldNotBeNull();
        st._root!.IsLeaf.ShouldBeTrue();

        var (vA, fA) = st.Delete(B("foo.bar.A"));
        fA.ShouldBeTrue();
        vA.ShouldBe(11);
        st._root.ShouldBeNull();

        // Pop up to node10 and shrink back.
        for (var i = 0; i < 5; i++)
            st.Insert(B($"foo.bar.{(char)('A' + i)}"), 22);

        st._root.ShouldBeOfType<SubjectTreeNode10<int>>();

        var (vDel, fDel) = st.Delete(B("foo.bar.A"));
        fDel.ShouldBeTrue();
        vDel.ShouldBe(22);
        st._root.ShouldBeOfType<SubjectTreeNode4<int>>();

        // Pop up to node16 and shrink back.
        for (var i = 0; i < 11; i++)
            st.Insert(B($"foo.bar.{(char)('A' + i)}"), 22);

        st._root.ShouldBeOfType<SubjectTreeNode16<int>>();
        var (vDel2, fDel2) = st.Delete(B("foo.bar.A"));
        fDel2.ShouldBeTrue();
        vDel2.ShouldBe(22);
        st._root.ShouldBeOfType<SubjectTreeNode10<int>>();

        // Pop up to node48 and shrink back.
        st = new SubjectTree<int>();
        for (var i = 0; i < 17; i++)
            st.Insert(B($"foo.bar.{(char)('A' + i)}"), 22);

        st._root.ShouldBeOfType<SubjectTreeNode48<int>>();
        var (vDel3, fDel3) = st.Delete(B("foo.bar.A"));
        fDel3.ShouldBeTrue();
        vDel3.ShouldBe(22);
        st._root.ShouldBeOfType<SubjectTreeNode16<int>>();

        // Pop up to node256 and shrink back.
        st = new SubjectTree<int>();
        for (var i = 0; i < 49; i++)
            st.Insert(B($"foo.bar.{(char)('A' + i)}"), 22);

        st._root.ShouldBeOfType<SubjectTreeNode256<int>>();
        var (vDel4, fDel4) = st.Delete(B("foo.bar.A"));
        fDel4.ShouldBeTrue();
        vDel4.ShouldBe(22);
        st._root.ShouldBeOfType<SubjectTreeNode48<int>>();
    }

    // -------------------------------------------------------------------------
    // TestDeleteEdgeCases
    // -------------------------------------------------------------------------

    [Fact]
    public void TestDeleteEdgeCases()
    {
        var st = new SubjectTree<int>();

        // Delete from empty tree.
        var (v, f) = st.Delete(B("foo"));
        f.ShouldBeFalse();
        v.ShouldBe(default);

        // Insert and delete the only item.
        st.Insert(B("foo"), 1);
        var (v2, f2) = st.Delete(B("foo"));
        f2.ShouldBeTrue();
        v2.ShouldBe(1);
        st.Size().ShouldBe(0);
        st._root.ShouldBeNull();

        // Delete a non-existent item in a non-empty tree.
        st.Insert(B("bar"), 2);
        var (v3, f3) = st.Delete(B("baz"));
        f3.ShouldBeFalse();
        v3.ShouldBe(default);
        st.Size().ShouldBe(1);
    }

    // -------------------------------------------------------------------------
    // TestSubjectTreeMatchLeafOnly
    // -------------------------------------------------------------------------

    [Fact]
    public void TestSubjectTreeMatchLeafOnly()
    {
        var st = new SubjectTree<int>();
        st.Insert(B("foo.bar.baz.A"), 1);

        // All positions of pwc.
        MatchCount(st, "foo.bar.*.A").ShouldBe(1);
        MatchCount(st, "foo.*.baz.A").ShouldBe(1);
        MatchCount(st, "foo.*.*.A").ShouldBe(1);
        MatchCount(st, "foo.*.*.*").ShouldBe(1);
        MatchCount(st, "*.*.*.*").ShouldBe(1);

        // fwc tests.
        MatchCount(st, ">").ShouldBe(1);
        MatchCount(st, "foo.>").ShouldBe(1);
        MatchCount(st, "foo.*.>").ShouldBe(1);
        MatchCount(st, "foo.bar.>").ShouldBe(1);
        MatchCount(st, "foo.bar.*.>").ShouldBe(1);

        // Partial match should not trigger.
        MatchCount(st, "foo.bar.baz").ShouldBe(0);
    }

    // -------------------------------------------------------------------------
    // TestSubjectTreeMatchNodes
    // -------------------------------------------------------------------------

    [Fact]
    public void TestSubjectTreeMatchNodes()
    {
        var st = new SubjectTree<int>();
        st.Insert(B("foo.bar.A"), 1);
        st.Insert(B("foo.bar.B"), 2);
        st.Insert(B("foo.bar.C"), 3);
        st.Insert(B("foo.baz.A"), 11);
        st.Insert(B("foo.baz.B"), 22);
        st.Insert(B("foo.baz.C"), 33);

        // Literals.
        MatchCount(st, "foo.bar.A").ShouldBe(1);
        MatchCount(st, "foo.baz.A").ShouldBe(1);
        MatchCount(st, "foo.bar").ShouldBe(0);

        // Internal pwc.
        MatchCount(st, "foo.*.A").ShouldBe(2);

        // Terminal pwc.
        MatchCount(st, "foo.bar.*").ShouldBe(3);
        MatchCount(st, "foo.baz.*").ShouldBe(3);

        // fwc.
        MatchCount(st, ">").ShouldBe(6);
        MatchCount(st, "foo.>").ShouldBe(6);
        MatchCount(st, "foo.bar.>").ShouldBe(3);
        MatchCount(st, "foo.baz.>").ShouldBe(3);

        // No false positives on prefix.
        MatchCount(st, "foo.ba").ShouldBe(0);

        // Add "foo.bar" and re-test.
        st.Insert(B("foo.bar"), 42);
        MatchCount(st, "foo.bar.A").ShouldBe(1);
        MatchCount(st, "foo.bar").ShouldBe(1);
        MatchCount(st, "foo.*.A").ShouldBe(2);
        MatchCount(st, "foo.bar.*").ShouldBe(3);
        MatchCount(st, ">").ShouldBe(7);
        MatchCount(st, "foo.>").ShouldBe(7);
        MatchCount(st, "foo.bar.>").ShouldBe(3);
        MatchCount(st, "foo.baz.>").ShouldBe(3);
    }

    // -------------------------------------------------------------------------
    // TestSubjectTreePartialTermination (partial termination)
    // -------------------------------------------------------------------------

    [Fact]
    public void TestSubjectTreePartialTermination()
    {
        var st = new SubjectTree<int>();
        st.Insert(B("STATE.GLOBAL.CELL1.7PDSGAALXNN000010.PROPERTY-A"), 5);
        st.Insert(B("STATE.GLOBAL.CELL1.7PDSGAALXNN000010.PROPERTY-B"), 1);
        st.Insert(B("STATE.GLOBAL.CELL1.7PDSGAALXNN000010.PROPERTY-C"), 2);
        MatchCount(st, "STATE.GLOBAL.CELL1.7PDSGAALXNN000010.*").ShouldBe(3);
    }

    // -------------------------------------------------------------------------
    // TestSubjectTreeMatchMultiple
    // -------------------------------------------------------------------------

    [Fact]
    public void TestSubjectTreeMatchMultiple()
    {
        var st = new SubjectTree<int>();
        st.Insert(B("A.B.C.D.0.G.H.I.0"), 22);
        st.Insert(B("A.B.C.D.1.G.H.I.0"), 22);
        MatchCount(st, "A.B.*.D.1.*.*.I.0").ShouldBe(1);
    }

    // -------------------------------------------------------------------------
    // TestSubjectTreeMatchSubject (verify correct subject bytes in callback)
    // -------------------------------------------------------------------------

    [Fact]
    public void TestSubjectTreeMatchSubject()
    {
        var st = new SubjectTree<int>();
        st.Insert(B("foo.bar.A"), 1);
        st.Insert(B("foo.bar.B"), 2);
        st.Insert(B("foo.bar.C"), 3);
        st.Insert(B("foo.baz.A"), 11);
        st.Insert(B("foo.baz.B"), 22);
        st.Insert(B("foo.baz.C"), 33);
        st.Insert(B("foo.bar"), 42);

        var checkValMap = new Dictionary<string, int>
        {
            ["foo.bar.A"] = 1,
            ["foo.bar.B"] = 2,
            ["foo.bar.C"] = 3,
            ["foo.baz.A"] = 11,
            ["foo.baz.B"] = 22,
            ["foo.baz.C"] = 33,
            ["foo.bar"] = 42,
        };

        st.Match(B(">"), (subject, val) =>
        {
            var subjectStr = System.Text.Encoding.Latin1.GetString(subject);
            checkValMap.ShouldContainKey(subjectStr);
            val.ShouldBe(checkValMap[subjectStr]);
            return true;
        });
    }

    // -------------------------------------------------------------------------
    // TestMatchEdgeCases
    // -------------------------------------------------------------------------

    [Fact]
    public void TestMatchEdgeCases()
    {
        var st = new SubjectTree<int>();
        st.Insert(B("foo.123"), 22);
        st.Insert(B("one.two.three.four.five"), 22);

        // Basic fwc.
        MatchCount(st, ">").ShouldBe(2);

        // No matches.
        MatchCount(st, "invalid.>").ShouldBe(0);

        // fwc after content is not terminal — should not match.
        MatchCount(st, "foo.>.bar").ShouldBe(0);
    }

    // -------------------------------------------------------------------------
    // TestSubjectTreeIterOrdered
    // -------------------------------------------------------------------------

    [Fact]
    public void TestSubjectTreeIterOrdered()
    {
        var st = new SubjectTree<int>();
        st.Insert(B("foo.bar.A"), 1);
        st.Insert(B("foo.bar.B"), 2);
        st.Insert(B("foo.bar.C"), 3);
        st.Insert(B("foo.baz.A"), 11);
        st.Insert(B("foo.baz.B"), 22);
        st.Insert(B("foo.baz.C"), 33);
        st.Insert(B("foo.bar"), 42);

        var checkValMap = new Dictionary<string, int>
        {
            ["foo.bar"] = 42,
            ["foo.bar.A"] = 1,
            ["foo.bar.B"] = 2,
            ["foo.bar.C"] = 3,
            ["foo.baz.A"] = 11,
            ["foo.baz.B"] = 22,
            ["foo.baz.C"] = 33,
        };
        var checkOrder = new[]
        {
            "foo.bar",
            "foo.bar.A",
            "foo.bar.B",
            "foo.bar.C",
            "foo.baz.A",
            "foo.baz.B",
            "foo.baz.C",
        };

        var received = new List<string>();
        st.IterOrdered((subject, val) =>
        {
            var subjectStr = System.Text.Encoding.Latin1.GetString(subject);
            received.Add(subjectStr);
            val.ShouldBe(checkValMap[subjectStr]);
            return true;
        });

        received.Count.ShouldBe(checkOrder.Length);
        for (var i = 0; i < checkOrder.Length; i++)
            received[i].ShouldBe(checkOrder[i]);

        // Make sure we can terminate early.
        var count = 0;
        st.IterOrdered((_, _) =>
        {
            count++;
            return count != 4;
        });
        count.ShouldBe(4);
    }

    // -------------------------------------------------------------------------
    // TestSubjectTreeIterFast
    // -------------------------------------------------------------------------

    [Fact]
    public void TestSubjectTreeIterFast()
    {
        var st = new SubjectTree<int>();
        st.Insert(B("foo.bar.A"), 1);
        st.Insert(B("foo.bar.B"), 2);
        st.Insert(B("foo.bar.C"), 3);
        st.Insert(B("foo.baz.A"), 11);
        st.Insert(B("foo.baz.B"), 22);
        st.Insert(B("foo.baz.C"), 33);
        st.Insert(B("foo.bar"), 42);

        var checkValMap = new Dictionary<string, int>
        {
            ["foo.bar.A"] = 1,
            ["foo.bar.B"] = 2,
            ["foo.bar.C"] = 3,
            ["foo.baz.A"] = 11,
            ["foo.baz.B"] = 22,
            ["foo.baz.C"] = 33,
            ["foo.bar"] = 42,
        };

        var received = 0;
        st.IterFast((subject, val) =>
        {
            received++;
            var subjectStr = System.Text.Encoding.Latin1.GetString(subject);
            checkValMap.ShouldContainKey(subjectStr);
            val.ShouldBe(checkValMap[subjectStr]);
            return true;
        });
        received.ShouldBe(checkValMap.Count);

        // Early termination.
        received = 0;
        st.IterFast((_, _) =>
        {
            received++;
            return received != 4;
        });
        received.ShouldBe(4);
    }

    // -------------------------------------------------------------------------
    // TestSubjectTreeEmpty
    // -------------------------------------------------------------------------

    [Fact]
    public void TestSubjectTreeEmpty()
    {
        var st = new SubjectTree<int>();
        st.Empty().ShouldBeTrue();
        st.Insert(B("foo"), 1);
        st.Empty().ShouldBeFalse();
        st.Delete(B("foo"));
        st.Empty().ShouldBeTrue();
    }

    // -------------------------------------------------------------------------
    // TestSizeOnEmptyTree
    // -------------------------------------------------------------------------

    [Fact]
    public void TestSizeOnEmptyTree()
    {
        var st = new SubjectTree<int>();
        st.Size().ShouldBe(0);
    }

    // -------------------------------------------------------------------------
    // TestSubjectTreeNilNoPanic (nil/null safety)
    // -------------------------------------------------------------------------

    [Fact]
    public void TestSubjectTreeNullNoPanic()
    {
        var st = new SubjectTree<int>();

        // Operations on empty tree should not throw.
        st.Size().ShouldBe(0);
        st.Empty().ShouldBeTrue();

        var (_, f1) = st.Find(B("foo"));
        f1.ShouldBeFalse();

        var (_, f2) = st.Delete(B("foo"));
        f2.ShouldBeFalse();

        // Match on empty tree.
        var count = 0;
        st.Match(B(">"), (_, _) => { count++; return true; });
        count.ShouldBe(0);

        // MatchUntil on empty tree.
        var completed = st.MatchUntil(B(">"), (_, _) => { count++; return true; });
        completed.ShouldBeTrue();

        // Iter on empty tree.
        st.IterOrdered((_, _) => { count++; return true; });
        st.IterFast((_, _) => { count++; return true; });
        count.ShouldBe(0);
    }

    // -------------------------------------------------------------------------
    // TestSubjectTreeMatchUntil
    // -------------------------------------------------------------------------

    [Fact]
    public void TestSubjectTreeMatchUntil()
    {
        var st = new SubjectTree<int>();
        st.Insert(B("foo.bar.A"), 1);
        st.Insert(B("foo.bar.B"), 2);
        st.Insert(B("foo.bar.C"), 3);
        st.Insert(B("foo.baz.A"), 11);
        st.Insert(B("foo.baz.B"), 22);
        st.Insert(B("foo.baz.C"), 33);
        st.Insert(B("foo.bar"), 42);

        // Early stop terminates traversal.
        var n = 0;
        var completed = st.MatchUntil(B("foo.>"), (_, _) =>
        {
            n++;
            return n < 3;
        });
        n.ShouldBe(3);
        completed.ShouldBeFalse();

        // Match that completes normally.
        n = 0;
        completed = st.MatchUntil(B("foo.bar"), (_, _) =>
        {
            n++;
            return true;
        });
        n.ShouldBe(1);
        completed.ShouldBeTrue();

        // Stop after 4 (more than available in "foo.baz.*").
        n = 0;
        completed = st.MatchUntil(B("foo.baz.*"), (_, _) =>
        {
            n++;
            return n < 4;
        });
        n.ShouldBe(3);
        completed.ShouldBeTrue();
    }

    // -------------------------------------------------------------------------
    // TestSubjectTreeGSLIntersect (basic lazy intersect equivalent)
    // -------------------------------------------------------------------------

    [Fact]
    public void TestSubjectTreeLazyIntersect()
    {
        // Build two trees and verify that inserting matching keys from both yields correct count.
        var tl = new SubjectTree<int>();
        var tr = new SubjectTree<int>();

        tl.Insert(B("foo.bar"), 1);
        tl.Insert(B("foo.baz"), 2);
        tl.Insert(B("other"), 3);

        tr.Insert(B("foo.bar"), 10);
        tr.Insert(B("foo.baz"), 20);

        // Manually intersect: iterate smaller tree, find in larger.
        var matches = new List<(string key, int vl, int vr)>();
        tl.IterFast((key, vl) =>
        {
            var (vr, found) = tr.Find(key);
            if (found)
                matches.Add((System.Text.Encoding.Latin1.GetString(key), vl, vr));
            return true;
        });

        matches.Count.ShouldBe(2);
    }

    // -------------------------------------------------------------------------
    // TestSubjectTreePrefixMismatch
    // -------------------------------------------------------------------------

    [Fact]
    public void TestSubjectTreePrefixMismatch()
    {
        var st = new SubjectTree<int>();
        st.Insert(B("foo.bar.A"), 11);
        st.Insert(B("foo.bar.B"), 22);
        st.Insert(B("foo.bar.C"), 33);

        // This will force a split.
        st.Insert(B("foo.foo.A"), 44);

        var (v1, f1) = st.Find(B("foo.bar.A"));
        f1.ShouldBeTrue();
        v1.ShouldBe(11);
        var (v2, f2) = st.Find(B("foo.bar.B"));
        f2.ShouldBeTrue();
        v2.ShouldBe(22);
        var (v3, f3) = st.Find(B("foo.bar.C"));
        f3.ShouldBeTrue();
        v3.ShouldBe(33);
        var (v4, f4) = st.Find(B("foo.foo.A"));
        f4.ShouldBeTrue();
        v4.ShouldBe(44);
    }

    // -------------------------------------------------------------------------
    // TestSubjectTreeNodesAndPaths
    // -------------------------------------------------------------------------

    [Fact]
    public void TestSubjectTreeNodesAndPaths()
    {
        var st = new SubjectTree<int>();

        void Check(string subj)
        {
            var (val, found) = st.Find(B(subj));
            found.ShouldBeTrue();
            val.ShouldBe(22);
        }

        st.Insert(B("foo.bar.A"), 22);
        st.Insert(B("foo.bar.B"), 22);
        st.Insert(B("foo.bar.C"), 22);
        st.Insert(B("foo.bar"), 22);

        Check("foo.bar.A");
        Check("foo.bar.B");
        Check("foo.bar.C");
        Check("foo.bar");

        // Deletion that involves shrinking / prefix adjustment.
        st.Delete(B("foo.bar"));
        Check("foo.bar.A");
        Check("foo.bar.B");
        Check("foo.bar.C");
    }

    // -------------------------------------------------------------------------
    // TestSubjectTreeRandomTrack (basic random insert/find)
    // -------------------------------------------------------------------------

    [Fact]
    public void TestSubjectTreeRandomTrack()
    {
        var st = new SubjectTree<int>();
        var tracked = new Dictionary<string, bool>();
        var rng = new Random(42);

        for (var i = 0; i < 200; i++)
        {
            var tokens = rng.Next(1, 5);
            var parts = new List<string>();
            for (var t = 0; t < tokens; t++)
            {
                var len = rng.Next(2, 7);
                var chars = new char[len];
                for (var c = 0; c < len; c++)
                    chars[c] = (char)('a' + rng.Next(26));
                parts.Add(new string(chars));
            }
            var subj = string.Join(".", parts);
            if (tracked.ContainsKey(subj)) continue;
            tracked[subj] = true;
            st.Insert(B(subj), 1);
        }

        foreach (var subj in tracked.Keys)
        {
            var (_, found) = st.Find(B(subj));
            found.ShouldBeTrue($"Subject '{subj}' not found after insert");
        }

        st.Size().ShouldBe(tracked.Count);
    }

    // -------------------------------------------------------------------------
    // TestSubjectTreeNode48 (detailed node48 operations)
    // -------------------------------------------------------------------------

    [Fact]
    public void TestSubjectTreeNode48Operations()
    {
        var st = new SubjectTree<int>();

        // Insert 26 single-char subjects (no prefix — goes directly to node48).
        for (var i = 0; i < 26; i++)
            st.Insert(new[] { (byte)('A' + i) }, 22);

        st._root.ShouldBeOfType<SubjectTreeNode48<int>>();
        st._root!.NumChildren.ShouldBe(26);

        st.Delete(new[] { (byte)'B' });
        st._root.NumChildren.ShouldBe(25);

        st.Delete(new[] { (byte)'Z' });
        st._root.NumChildren.ShouldBe(24);

        // Remaining subjects should still be findable.
        for (var i = 0; i < 26; i++)
        {
            var ch = (byte)('A' + i);
            if (ch == (byte)'B' || ch == (byte)'Z') continue;
            var (_, found) = st.Find(new[] { ch });
            found.ShouldBeTrue();
        }
    }

    // -------------------------------------------------------------------------
    // TestSubjectTreeMatchTsepSecondThenPartial (bug regression)
    // -------------------------------------------------------------------------

    [Fact]
    public void TestSubjectTreeMatchTsepSecondThenPartial()
    {
        var st = new SubjectTree<int>();
        st.Insert(B("foo.xxxxx.foo1234.zz"), 22);
        st.Insert(B("foo.yyy.foo123.zz"), 22);
        st.Insert(B("foo.yyybar789.zz"), 22);
        st.Insert(B("foo.yyy.foo12345.zz"), 22);
        st.Insert(B("foo.yyy.foo12345.yy"), 22);
        st.Insert(B("foo.yyy.foo123456789.zz"), 22);

        MatchCount(st, "foo.*.foo123456789.*").ShouldBe(1);
        MatchCount(st, "foo.*.*.zzz.foo.>").ShouldBe(0);
    }
}
