// Go reference: server/stree/stree_test.go
using System.Security.Cryptography;
using System.Text;
using NATS.Server.Internal.SubjectTree;

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

/// <summary>
/// Tests for the Adaptive Radix Tree (ART) based SubjectTree.
/// Ported from Go: server/stree/stree_test.go (59 tests)
/// </summary>
public class SubjectTreeTests
{
    private static byte[] B(string s) => Encoding.UTF8.GetBytes(s);

    private static void MatchCount(SubjectTree<int> st, string filter, int expected)
    {
        var matches = new List<int>();
        st.Match(B(filter), (_, v) => matches.Add(v));
        matches.Count.ShouldBe(expected, $"filter={filter}");
    }

    private static (int Count, bool Completed) MatchUntilCount<T>(SubjectTree<T> st, string filter, int stopAfter)
    {
        int n = 0;
        var completed = st.MatchUntil(B(filter), (_, _) =>
        {
            n++;
            return n < stopAfter;
        });
        return (n, completed);
    }

    #region Basic CRUD

    // Go: TestSubjectTreeBasics server/stree/stree_test.go:33
    [Fact]
    public void TestSubjectTreeBasics()
    {
        var st = new SubjectTree<int>();
        st.Size.ShouldBe(0);

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

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

        // But it should with a literal. Find with single leaf.
        var (v, found2) = st.Find(B("foo.bar.baz"));
        found2.ShouldBeTrue();
        v.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);

        // Now we have node4 -> leaf*2
        var (v2, found3) = st.Find(B("foo.bar"));
        found3.ShouldBeTrue();
        v2.ShouldBe(22);

        // Make sure we can still retrieve the original after the split.
        var (v3, found4) = st.Find(B("foo.bar.baz"));
        found4.ShouldBeTrue();
        v3.ShouldBe(33);
    }

    // Go: TestSubjectTreeNoPrefix server/stree/stree_test.go:432
    [Fact]
    public void TestSubjectTreeNoPrefix()
    {
        var st = new SubjectTree<int>();
        for (int i = 0; i < 26; i++)
        {
            var subj = B($"{(char)('A' + i)}");
            var (old, updated) = st.Insert(subj, 22);
            old.ShouldBe(default);
            updated.ShouldBeFalse();
        }

        st.Root.ShouldBeOfType<Node48>();
        var n = (Node48)st.Root!;
        n.NumChildren.ShouldBe((ushort)26);

        var (v, found) = st.Delete(B("B"));
        found.ShouldBeTrue();
        v.ShouldBe(22);
        n.NumChildren.ShouldBe((ushort)25);

        var (v2, found2) = st.Delete(B("Z"));
        found2.ShouldBeTrue();
        v2.ShouldBe(22);
        n.NumChildren.ShouldBe((ushort)24);
    }

    // Go: TestSubjectTreeEmpty server/stree/stree_test.go:1330
    [Fact]
    public void TestSubjectTreeEmpty()
    {
        // Test Empty on new tree
        var st = new SubjectTree<int>();
        st.Size.ShouldBe(0);
        var st2 = st.Empty();
        st2.ShouldBeSameAs(st); // Should return same instance
        st2.Size.ShouldBe(0);

        // Test Empty on tree with data
        st.Insert(B("foo.bar"), 1);
        st.Insert(B("foo.baz"), 2);
        st.Insert(B("bar.baz"), 3);
        st.Size.ShouldBe(3);

        // Empty should clear everything
        st2 = st.Empty();
        st2.ShouldBeSameAs(st); // Should return same instance
        st.Size.ShouldBe(0);
        st.Root.ShouldBeNull();

        // Verify we can't find old entries
        st.Find(B("foo.bar")).Found.ShouldBeFalse();
        st.Find(B("foo.baz")).Found.ShouldBeFalse();
        st.Find(B("bar.baz")).Found.ShouldBeFalse();

        // Verify we can insert new entries after Empty
        var (old, updated) = st.Insert(B("new.entry"), 42);
        old.ShouldBe(default);
        updated.ShouldBeFalse();
        st.Size.ShouldBe(1);

        var (v, found) = st.Find(B("new.entry"));
        found.ShouldBeTrue();
        v.ShouldBe(42);
    }

    // Go: TestSizeOnNilTree server/stree/stree_test.go:1667
    [Fact]
    public void TestSizeOnNilTree()
    {
        // In C# we can't have a null reference call Size, but we test a new tree
        var st = new SubjectTree<int>();
        st.Size.ShouldBe(0);
    }

    // Go: TestFindEdgeCases server/stree/stree_test.go:1672
    [Fact]
    public void TestFindEdgeCases()
    {
        var st = new SubjectTree<int>();

        // Test Find with empty subject at root level
        st.Insert(B("foo.bar.baz"), 1);
        st.Insert(B("foo"), 2);

        // This should create a tree structure, now test finding with edge cases
        var (v, found) = st.Find(B(""));
        found.ShouldBeFalse();
    }

    #endregion

    #region Node Growth/Shrink

    // Go: TestSubjectTreeNodeGrow server/stree/stree_test.go:69
    [Fact]
    public void TestSubjectTreeNodeGrow()
    {
        var st = new SubjectTree<int>();
        for (int i = 0; i < 4; i++)
        {
            var subj = B($"foo.bar.{(char)('A' + i)}");
            var (old, updated) = st.Insert(subj, 22);
            old.ShouldBe(default);
            updated.ShouldBeFalse();
        }

        // We have filled a node4.
        st.Root.ShouldBeOfType<Node4>();

        // This one will trigger us to grow.
        var (old2, updated2) = st.Insert(B("foo.bar.E"), 22);
        old2.ShouldBe(default);
        updated2.ShouldBeFalse();
        st.Root.ShouldBeOfType<Node10>();

        for (int i = 5; i < 10; i++)
        {
            var subj = B($"foo.bar.{(char)('A' + i)}");
            var (old3, updated3) = st.Insert(subj, 22);
            old3.ShouldBe(default);
            updated3.ShouldBeFalse();
        }

        // This one will trigger us to grow.
        var (old4, updated4) = st.Insert(B("foo.bar.K"), 22);
        old4.ShouldBe(default);
        updated4.ShouldBeFalse();
        // We have filled a node10.
        st.Root.ShouldBeOfType<Node16>();

        for (int i = 11; i < 16; i++)
        {
            var subj = B($"foo.bar.{(char)('A' + i)}");
            var (old5, updated5) = st.Insert(subj, 22);
            old5.ShouldBe(default);
            updated5.ShouldBeFalse();
        }

        // This one will trigger us to grow.
        var (old6, updated6) = st.Insert(B("foo.bar.Q"), 22);
        old6.ShouldBe(default);
        updated6.ShouldBeFalse();
        st.Root.ShouldBeOfType<Node48>();

        // Fill the node48.
        for (int i = 17; i < 48; i++)
        {
            var subj = B($"foo.bar.{(char)('A' + i)}");
            var (old7, updated7) = st.Insert(subj, 22);
            old7.ShouldBe(default);
            updated7.ShouldBeFalse();
        }

        // This one will trigger us to grow.
        var subj8 = B($"foo.bar.{(char)('A' + 49)}");
        var (old8, updated8) = st.Insert(subj8, 22);
        old8.ShouldBe(default);
        updated8.ShouldBeFalse();
        st.Root.ShouldBeOfType<Node256>();
    }

    // Go: TestSubjectTreeNodePrefixMismatch server/stree/stree_test.go:127
    [Fact]
    public void TestSubjectTreeNodePrefixMismatch()
    {
        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);
        // Grab current root. Split below will cause update.
        var or = st.Root;
        // This one will force a split of the node
        st.Insert(B("foo.foo.A"), 44);
        st.Root.ShouldNotBeSameAs(or);

        // Now make sure we can retrieve correctly.
        st.Find(B("foo.bar.A")).Value.ShouldBe(11);
        st.Find(B("foo.bar.B")).Value.ShouldBe(22);
        st.Find(B("foo.bar.C")).Value.ShouldBe(33);
        st.Find(B("foo.foo.A")).Value.ShouldBe(44);
    }

    // Go: TestNode256Operations server/stree/stree_test.go:1493
    [Fact]
    public void TestNode256Operations()
    {
        // Test node256 creation and basic operations
        var n = new Node256(B("prefix"));
        n.IsFull.ShouldBeFalse(); // node256 is never full

        // Test findChild when child doesn't exist
        var child = n.FindChild((byte)'a');
        child.ShouldBeNull();

        // Add a child and find it
        var leaf = new Leaf<int>(B("suffix"), 42);
        n.AddChild((byte)'a', leaf);
        child = n.FindChild((byte)'a');
        child.ShouldNotBeNull();
        n.Meta.Size.ShouldBe((ushort)1);

        // Test iter function
        int iterCount = 0;
        n.Iter((_) => { iterCount++; return true; });
        iterCount.ShouldBe(1);

        // Test iter with early termination
        n.AddChild((byte)'b', new Leaf<int>(B("suffix2"), 43));
        n.AddChild((byte)'c', new Leaf<int>(B("suffix3"), 44));
        iterCount = 0;
        n.Iter((_) => { iterCount++; return false; });
        iterCount.ShouldBe(1);

        // Test children() method
        var children = n.Children();
        children.Length.ShouldBe(256);

        // Test that grow() panics
        Should.Throw<InvalidOperationException>(() => n.Grow())
            .Message.ShouldBe("grow can not be called on node256");
    }

    // Go: TestNode256Shrink server/stree/stree_test.go:1542
    [Fact]
    public void TestNode256Shrink()
    {
        var n256 = new Node256(B("prefix"));

        // Add 49 children
        for (int i = 0; i < 49; i++)
        {
            n256.AddChild((byte)i, new Leaf<int>([(byte)i], i));
        }
        n256.Meta.Size.ShouldBe((ushort)49);

        // Shrink should not happen yet (> 48 children)
        var shrunk = n256.Shrink();
        shrunk.ShouldBeNull();

        // Delete one to get to 48 children
        n256.DeleteChild(0);
        n256.Meta.Size.ShouldBe((ushort)48);

        // Now shrink should return a node48
        shrunk = n256.Shrink();
        shrunk.ShouldNotBeNull();
        shrunk.ShouldBeOfType<Node48>();

        // Verify the shrunk node has all remaining children
        for (int i = 1; i < 49; i++)
        {
            var child = shrunk.FindChild((byte)i);
            child.ShouldNotBeNull();
        }
    }

    // Go: TestNodeShrinkNotNeeded server/stree/stree_test.go:1850
    [Fact]
    public void TestNodeShrinkNotNeeded()
    {
        // Test node10 shrink when not needed (has more than 4 children)
        var n10 = new Node10(B("prefix"));
        for (int i = 0; i < 5; i++)
        {
            n10.AddChild((byte)('a' + i), new Leaf<int>([(byte)('0' + i)], i));
        }
        var shrunk = n10.Shrink();
        shrunk.ShouldBeNull(); // Should not shrink

        // Test node16 shrink when not needed (has more than 10 children)
        var n16 = new Node16(B("prefix"));
        for (int i = 0; i < 11; i++)
        {
            n16.AddChild((byte)i, new Leaf<int>([(byte)i], i));
        }
        shrunk = n16.Shrink();
        shrunk.ShouldBeNull(); // Should not shrink
    }

    #endregion

    #region Delete

    // Go: TestSubjectTreeNodeDelete server/stree/stree_test.go:152
    [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();

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

        var (v3, found3) = st.Find(B("foo.foo.A"));
        found3.ShouldBeFalse();
        v3.ShouldBe(default);

        // Kick to a node4.
        st.Insert(B("foo.bar.A"), 11);
        st.Insert(B("foo.bar.B"), 22);
        st.Insert(B("foo.bar.C"), 33);

        // Make sure we can delete and that we shrink back to leaf.
        var (v4, found4) = st.Delete(B("foo.bar.C"));
        found4.ShouldBeTrue();
        v4.ShouldBe(33);
        var (v5, found5) = st.Delete(B("foo.bar.B"));
        found5.ShouldBeTrue();
        v5.ShouldBe(22);
        // We should have shrunk here.
        st.Root!.IsLeaf.ShouldBeTrue();
        var (v6, found6) = st.Delete(B("foo.bar.A"));
        found6.ShouldBeTrue();
        v6.ShouldBe(11);
        st.Root.ShouldBeNull();

        // Now pop up to a node10 and make sure we can shrink back down.
        for (int i = 0; i < 5; i++)
        {
            var subj = $"foo.bar.{(char)('A' + i)}";
            st.Insert(B(subj), 22);
        }
        st.Root.ShouldBeOfType<Node10>();
        var (v7, found7) = st.Delete(B("foo.bar.A"));
        found7.ShouldBeTrue();
        v7.ShouldBe(22);
        st.Root.ShouldBeOfType<Node4>();

        // Now pop up to node16
        for (int i = 0; i < 11; i++)
        {
            var subj = $"foo.bar.{(char)('A' + i)}";
            st.Insert(B(subj), 22);
        }
        st.Root.ShouldBeOfType<Node16>();
        var (v8, found8) = st.Delete(B("foo.bar.A"));
        found8.ShouldBeTrue();
        v8.ShouldBe(22);
        st.Root.ShouldBeOfType<Node10>();
        st.Find(B("foo.bar.B")).Found.ShouldBeTrue();
        st.Find(B("foo.bar.B")).Value.ShouldBe(22);

        // Now pop up to node48
        st = new SubjectTree<int>();
        for (int i = 0; i < 17; i++)
        {
            var subj = $"foo.bar.{(char)('A' + i)}";
            st.Insert(B(subj), 22);
        }
        st.Root.ShouldBeOfType<Node48>();
        var (v9, found9) = st.Delete(B("foo.bar.A"));
        found9.ShouldBeTrue();
        v9.ShouldBe(22);
        st.Root.ShouldBeOfType<Node16>();
        st.Find(B("foo.bar.B")).Found.ShouldBeTrue();

        // Now pop up to node256
        st = new SubjectTree<int>();
        for (int i = 0; i < 49; i++)
        {
            var subj = $"foo.bar.{(char)('A' + i)}";
            st.Insert(B(subj), 22);
        }
        st.Root.ShouldBeOfType<Node256>();
        var (v10, found10) = st.Delete(B("foo.bar.A"));
        found10.ShouldBeTrue();
        v10.ShouldBe(22);
        st.Root.ShouldBeOfType<Node48>();
        st.Find(B("foo.bar.B")).Found.ShouldBeTrue();
    }

    // Go: TestSubjectTreeNodesAndPaths server/stree/stree_test.go:243
    [Fact]
    public void TestSubjectTreeNodesAndPaths()
    {
        var st = new SubjectTree<int>();
        void Check(string subj)
        {
            var (v, found) = st.Find(B(subj));
            found.ShouldBeTrue();
            v.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");

        // This will do several things in terms of shrinking and pruning
        st.Delete(B("foo.bar"));
        Check("foo.bar.A");
        Check("foo.bar.B");
        Check("foo.bar.C");
    }

    // Go: TestSubjectTreeDeleteShortSubjectNoPanic server/stree/stree_test.go:1308
    [Fact]
    public void TestSubjectTreeDeleteShortSubjectNoPanic()
    {
        var st = new SubjectTree<int>();
        st.Insert(B("foo.bar.baz"), 1);
        st.Insert(B("foo.bar.qux"), 2);

        var (v, found) = st.Delete(B("foo.bar"));
        found.ShouldBeFalse();
        v.ShouldBe(default);

        st.Find(B("foo.bar.baz")).Value.ShouldBe(1);
        st.Find(B("foo.bar.qux")).Value.ShouldBe(2);
    }

    // Go: TestDeleteEdgeCases server/stree/stree_test.go:1947
    [Fact]
    public void TestDeleteEdgeCases()
    {
        var st = new SubjectTree<int>();

        // Test delete on empty tree
        var (val, deleted) = st.Delete(B("foo"));
        deleted.ShouldBeFalse();
        val.ShouldBe(default);

        // Test delete with empty subject
        st.Insert(B("foo"), 1);
        var (val2, deleted2) = st.Delete(B(""));
        deleted2.ShouldBeFalse();
        val2.ShouldBe(default);

        // Test delete with subject shorter than prefix
        st = new SubjectTree<int>();
        st.Insert(B("verylongprefix.suffix"), 1);
        st.Insert(B("verylongprefix.suffix2"), 2);
        var (val3, deleted3) = st.Delete(B("very"));
        deleted3.ShouldBeFalse();
        val3.ShouldBe(default);
    }

    // Go: TestDeleteNilNodePointer server/stree/stree_test.go:2095
    [Fact]
    public void TestDeleteNilNodePointer()
    {
        var st = new SubjectTree<int>();
        // Test delete on empty tree (no root)
        var (val, deleted) = st.Delete(B("foo"));
        deleted.ShouldBeFalse();
        val.ShouldBe(default);
    }

    // Go: TestDeleteChildEdgeCasesMore server/stree/stree_test.go:2036
    [Fact]
    public void TestDeleteChildEdgeCasesMore()
    {
        // Test the edge case in node10 deleteChild where we don't swap (last element)
        var n10 = new Node10(B("prefix"));
        n10.AddChild((byte)'a', new Leaf<int>(B("1"), 1));
        n10.AddChild((byte)'b', new Leaf<int>(B("2"), 2));
        n10.AddChild((byte)'c', new Leaf<int>(B("3"), 3));

        // Delete the last child
        n10.DeleteChild((byte)'c');
        n10.Meta.Size.ShouldBe((ushort)2);

        // Test the edge case in node16 deleteChild where we don't swap (last element)
        var n16 = new Node16(B("prefix"));
        n16.AddChild((byte)'a', new Leaf<int>(B("1"), 1));
        n16.AddChild((byte)'b', new Leaf<int>(B("2"), 2));
        n16.AddChild((byte)'c', new Leaf<int>(B("3"), 3));

        // Delete the last child
        n16.DeleteChild((byte)'c');
        n16.Meta.Size.ShouldBe((ushort)2);
    }

    #endregion

    #region Construction/Structure

    // Go: TestSubjectTreeConstruction server/stree/stree_test.go:268
    [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);

        void CheckNode(INode? n, string kind, string pathStr, ushort numChildren)
        {
            n.ShouldNotBeNull();
            n.Kind.ShouldBe(kind);
            Encoding.UTF8.GetString(n.Path()).ShouldBe(pathStr);
            n.NumChildren.ShouldBe(numChildren);
        }

        CheckNode(st.Root, "NODE4", "foo.ba", 2);
        var nn = st.Root!.FindChild((byte)'r');
        CheckNode(nn!.Node, "NODE4", "r", 2);
        CheckNode(nn.Node!.FindChild(Parts.NoPivot)!.Node, "LEAF", "", 0);
        var rnn = nn.Node!.FindChild((byte)'.');
        CheckNode(rnn!.Node, "NODE4", ".", 3);
        CheckNode(rnn.Node!.FindChild((byte)'A')!.Node, "LEAF", "A", 0);
        CheckNode(rnn.Node!.FindChild((byte)'B')!.Node, "LEAF", "B", 0);
        CheckNode(rnn.Node!.FindChild((byte)'C')!.Node, "LEAF", "C", 0);
        var znn = st.Root!.FindChild((byte)'z');
        CheckNode(znn!.Node, "NODE4", "z.", 3);
        CheckNode(znn.Node!.FindChild((byte)'A')!.Node, "LEAF", "A", 0);
        CheckNode(znn.Node!.FindChild((byte)'B')!.Node, "LEAF", "B", 0);
        CheckNode(znn.Node!.FindChild((byte)'C')!.Node, "LEAF", "C", 0);

        // Now delete "foo.bar" and make sure put ourselves back together properly.
        var (v, found) = st.Delete(B("foo.bar"));
        found.ShouldBeTrue();
        v.ShouldBe(42);

        CheckNode(st.Root, "NODE4", "foo.ba", 2);
        nn = st.Root!.FindChild((byte)'r');
        CheckNode(nn!.Node, "NODE4", "r.", 3);
        CheckNode(nn.Node!.FindChild((byte)'A')!.Node, "LEAF", "A", 0);
        CheckNode(nn.Node!.FindChild((byte)'B')!.Node, "LEAF", "B", 0);
        CheckNode(nn.Node!.FindChild((byte)'C')!.Node, "LEAF", "C", 0);
        znn = st.Root!.FindChild((byte)'z');
        CheckNode(znn!.Node, "NODE4", "z.", 3);
        CheckNode(znn.Node!.FindChild((byte)'A')!.Node, "LEAF", "A", 0);
        CheckNode(znn.Node!.FindChild((byte)'B')!.Node, "LEAF", "B", 0);
        CheckNode(znn.Node!.FindChild((byte)'C')!.Node, "LEAF", "C", 0);
    }

    #endregion

    #region Matching

    // Go: TestSubjectTreeMatchLeafOnly server/stree/stree_test.go:331
    [Fact]
    public void TestSubjectTreeMatchLeafOnly()
    {
        var st = new SubjectTree<int>();
        st.Insert(B("foo.bar.baz.A"), 1);

        // Check all placements of pwc in token space.
        MatchCount(st, "foo.bar.*.A", 1);
        MatchCount(st, "foo.*.baz.A", 1);
        MatchCount(st, "foo.*.*.A", 1);
        MatchCount(st, "foo.*.*.*", 1);
        MatchCount(st, "*.*.*.*", 1);

        // Now check fwc.
        MatchCount(st, ">", 1);
        MatchCount(st, "foo.>", 1);
        MatchCount(st, "foo.*.>", 1);
        MatchCount(st, "foo.bar.>", 1);
        MatchCount(st, "foo.bar.*.>", 1);

        // Check partials so they do not trigger on leafs.
        MatchCount(st, "foo.bar.baz", 0);
    }

    // Go: TestSubjectTreeMatchNodes server/stree/stree_test.go:352
    [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);

        // Test literals.
        MatchCount(st, "foo.bar.A", 1);
        MatchCount(st, "foo.baz.A", 1);
        MatchCount(st, "foo.bar", 0);
        // Test internal pwc
        MatchCount(st, "foo.*.A", 2);
        // Test terminal pwc
        MatchCount(st, "foo.bar.*", 3);
        MatchCount(st, "foo.baz.*", 3);
        // Check fwc
        MatchCount(st, ">", 6);
        MatchCount(st, "foo.>", 6);
        MatchCount(st, "foo.bar.>", 3);
        MatchCount(st, "foo.baz.>", 3);
        // Make sure we do not have false positives on prefix matches.
        MatchCount(st, "foo.ba", 0);

        // Now add in "foo.bar" to make a more complex tree construction and re-test.
        st.Insert(B("foo.bar"), 42);

        // Test literals.
        MatchCount(st, "foo.bar.A", 1);
        MatchCount(st, "foo.baz.A", 1);
        MatchCount(st, "foo.bar", 1);
        // Test internal pwc
        MatchCount(st, "foo.*.A", 2);
        // Test terminal pwc
        MatchCount(st, "foo.bar.*", 3);
        MatchCount(st, "foo.baz.*", 3);
        // Check fwc
        MatchCount(st, ">", 7);
        MatchCount(st, "foo.>", 7);
        MatchCount(st, "foo.bar.>", 3);
        MatchCount(st, "foo.baz.>", 3);
    }

    // Go: TestSubjectTreeMatchUntil server/stree/stree_test.go:407
    [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);

        // Ensure early stop terminates traversal.
        var (count, completed) = MatchUntilCount(st, "foo.>", 3);
        count.ShouldBe(3);
        completed.ShouldBeFalse();

        // Match completes
        (count, completed) = MatchUntilCount(st, "foo.bar", 3);
        count.ShouldBe(1);
        completed.ShouldBeTrue();

        (count, completed) = MatchUntilCount(st, "foo.baz.*", 4);
        count.ShouldBe(3);
        completed.ShouldBeTrue();
    }

    // Go: TestSubjectTreePartialTerminalWildcardBugMatch server/stree/stree_test.go:453
    [Fact]
    public void TestSubjectTreePartialTerminalWildcardBugMatch()
    {
        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.*", 3);
    }

    // Go: TestSubjectTreeMatchSubjectParam server/stree/stree_test.go:461
    [Fact]
    public void TestSubjectTreeMatchSubjectParam()
    {
        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,
        };

        // Make sure we get a proper subject parameter and it matches our value properly.
        st.Match(B(">"), (subject, v) =>
        {
            var key = Encoding.UTF8.GetString(subject);
            checkValMap.ShouldContainKey(key);
            v.ShouldBe(checkValMap[key]);
        });
    }

    // Go: TestSubjectTreeMatchRandomDoublePWC server/stree/stree_test.go:490
    [Fact]
    public void TestSubjectTreeMatchRandomDoublePWC()
    {
        var st = new SubjectTree<int>();
        var rng = new Random(42);
        for (int i = 1; i <= 10_000; i++)
        {
            var subj = $"foo.{rng.Next(20) + 1}.{i}";
            st.Insert(B(subj), 42);
        }
        MatchCount(st, "foo.*.*", 10_000);

        // Check with pwc and short interior token.
        int seen = 0;
        st.Match(B("*.2.*"), (_, _) => seen++);

        // Now check via walk to make sure we are right.
        int verified = 0;
        st.IterOrdered((subject, _) =>
        {
            var tokens = Encoding.UTF8.GetString(subject).Split('.');
            tokens.Length.ShouldBe(3);
            if (tokens[1] == "2") verified++;
            return true;
        });
        seen.ShouldBe(verified);

        seen = 0;
        verified = 0;
        st.Match(B("*.*.222"), (_, _) => seen++);
        st.IterOrdered((subject, _) =>
        {
            var tokens = Encoding.UTF8.GetString(subject).Split('.');
            tokens.Length.ShouldBe(3);
            if (tokens[2] == "222") verified++;
            return true;
        });
        seen.ShouldBe(verified);
    }

    // Go: TestSubjectTreeMatchTsepSecondThenPartialPartBug server/stree/stree_test.go:643
    [Fact]
    public void TestSubjectTreeMatchTsepSecondThenPartialPartBug()
    {
        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.*", 1);
        MatchCount(st, "foo.*.*.zzz.foo.>", 0);
    }

    // Go: TestSubjectTreeMatchMultipleWildcardBasic server/stree/stree_test.go:655
    [Fact]
    public void TestSubjectTreeMatchMultipleWildcardBasic()
    {
        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", 1);
    }

    // Go: TestSubjectTreeMatchInvalidWildcard server/stree/stree_test.go:662
    [Fact]
    public void TestSubjectTreeMatchInvalidWildcard()
    {
        var st = new SubjectTree<int>();
        st.Insert(B("foo.123"), 22);
        st.Insert(B("one.two.three.four.five"), 22);
        st.Insert(B("'*.123"), 22);
        st.Insert(B("bar"), 22);
        MatchCount(st, "invalid.>", 0);
        MatchCount(st, "foo.>.bar", 0);
        MatchCount(st, ">", 4);
        MatchCount(st, "'*.*", 1);
        MatchCount(st, "'*.*.*'", 0);
        // None of these should match.
        MatchCount(st, "`>`", 0);
        MatchCount(st, "\">\u0022", 0);
        MatchCount(st, "'>'", 0);
        MatchCount(st, "'*.>'", 0);
        MatchCount(st, "'*.>.", 0);
        MatchCount(st, "`invalid.>`", 0);
        MatchCount(st, "'*.*'", 0);
    }

    // Go: TestSubjectTreeMatchNoCallbackDupe server/stree/stree_test.go:881
    [Fact]
    public void TestSubjectTreeMatchNoCallbackDupe()
    {
        var st = new SubjectTree<int>();
        st.Insert(B("foo.bar.A"), 1);
        st.Insert(B("foo.bar.B"), 1);
        st.Insert(B("foo.bar.C"), 1);
        st.Insert(B("foo.bar.>"), 1);

        foreach (var f in new[] { ">", "foo.>", "foo.bar.>" })
        {
            var seen = new HashSet<string>();
            st.Match(B(f), (bsubj, _) =>
            {
                var subj = Encoding.UTF8.GetString(bsubj);
                seen.Contains(subj).ShouldBeFalse($"Match callback was called twice for {subj}");
                seen.Add(subj);
            });
        }
    }

    // Go: TestSubjectTreeMatchHasFWCNoPanic server/stree/stree_test.go:954
    [Fact]
    public void TestSubjectTreeMatchHasFWCNoPanic()
    {
        var st = new SubjectTree<int>();
        var subj = B("foo");
        st.Insert(subj, 1);
        // Should not throw
        st.Match(B("."), (_, _) => { });
    }

    // Go: TestMatchEdgeCases server/stree/stree_test.go:1970
    [Fact]
    public void TestMatchEdgeCases()
    {
        var st = new SubjectTree<int>();

        // Test match with null callback
        st.Insert(B("foo.bar"), 1);
        st.Match(B("foo.*"), null); // Should not panic

        // Test match with empty filter
        int count = 0;
        st.Match(B(""), (_, _) => count++);
        count.ShouldBe(0);
    }

    // Go: TestMatchComplexEdgeCases server/stree/stree_test.go:2104
    [Fact]
    public void TestMatchComplexEdgeCases()
    {
        var st = new SubjectTree<int>();

        // Build a complex tree to test match coverage
        st.Insert(B("foo.bar.baz"), 1);
        st.Insert(B("foo.bar.qux"), 2);
        st.Insert(B("foo.baz.bar"), 3);
        st.Insert(B("bar.foo.baz"), 4);

        // Test with terminal fwc but no remaining parts
        int count = 0;
        st.Match(B("foo.bar.>"), (_, _) => count++);
        count.ShouldBe(2);
    }

    // Go: TestMatchPartsEdgeCases server/stree/stree_test.go:1912
    [Fact]
    public void TestMatchPartsEdgeCases()
    {
        // Test the edge case in matchParts
        var filter = B("foo.*.bar.>");
        var parts = Parts.GenParts(filter);

        // Test with a fragment that will cause partial matching
        var frag = B("foo.test");
        var (remaining, matched) = Parts.MatchPartsAgainstFragment(parts, frag);
        matched.ShouldBeTrue();
        remaining.Length.ShouldBeGreaterThan(0);
    }

    // Go: TestMatchPartsMoreEdgeCases server/stree/stree_test.go:2058
    [Fact]
    public void TestMatchPartsMoreEdgeCases()
    {
        // Case where frag is empty
        var parts = Parts.GenParts(B("foo.*"));
        var (remaining, matched) = Parts.MatchPartsAgainstFragment(parts, ReadOnlySpan<byte>.Empty);
        matched.ShouldBeTrue();
        remaining.Length.ShouldBe(parts.Length);
    }

    #endregion

    #region Iteration

    // Go: TestSubjectTreeIterOrdered server/stree/stree_test.go:529
    [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.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 checkOrder = new[]
        {
            "foo.bar",
            "foo.bar.A",
            "foo.bar.B",
            "foo.bar.C",
            "foo.baz.A",
            "foo.baz.B",
            "foo.baz.C",
        };

        int received = 0;
        st.IterOrdered((subject, v) =>
        {
            var subj = Encoding.UTF8.GetString(subject);
            subj.ShouldBe(checkOrder[received]);
            received++;
            v.ShouldBe(checkValMap[subj]);
            return true;
        });
        received.ShouldBe(checkOrder.Length);

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

    // Go: TestSubjectTreeIterFast server/stree/stree_test.go:582
    [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,
        };

        int received = 0;
        st.IterFast((subject, v) =>
        {
            received++;
            var subj = Encoding.UTF8.GetString(subject);
            v.ShouldBe(checkValMap[subj]);
            return true;
        });
        received.ShouldBe(checkValMap.Count);

        // Make sure we can terminate properly.
        received = 0;
        st.IterFast((_, _) =>
        {
            received++;
            return received != 4;
        });
        received.ShouldBe(4);
    }

    // Go: TestIterOrderedAndIterFastNilRoot server/stree/stree_test.go:1733
    [Fact]
    public void TestIterOrderedAndIterFastNilRoot()
    {
        // Test IterOrdered with nil root
        var st = new SubjectTree<int>();
        int count = 0;
        st.IterOrdered((_, _) => { count++; return true; });
        count.ShouldBe(0);

        // Test IterFast with nil root
        count = 0;
        st.IterFast((_, _) => { count++; return true; });
        count.ShouldBe(0);
    }

    // Go: TestIterEdgeCases server/stree/stree_test.go:1985
    [Fact]
    public void TestIterEdgeCases()
    {
        var st = new SubjectTree<int>();

        // Add multiple subjects to create a complex tree
        st.Insert(B("a.b.c"), 1);
        st.Insert(B("a.b.d"), 2);
        st.Insert(B("a.c.d"), 3);
        st.Insert(B("b.c.d"), 4);

        // Test iter with early termination at different points
        int count = 0;
        st.IterInternal(st.Root!, [], false, (_, _) =>
        {
            count++;
            return count < 2;
        });
        count.ShouldBe(2);
    }

    // Go: TestIterComplexTree server/stree/stree_test.go:2121
    [Fact]
    public void TestIterComplexTree()
    {
        var st = new SubjectTree<int>();

        // Build a deeper tree to test the remaining iter cases
        for (int i = 0; i < 20; i++)
        {
            st.Insert(B($"level1.level2.level3.item{i}"), i);
        }

        // This should create multiple node types and test more paths
        int count = 0;
        st.IterOrdered((_, _) => { count++; return true; });
        count.ShouldBe(20);
    }

    #endregion

    #region Insert Edge Cases

    // Go: TestSubjectTreeInsertSamePivotBug server/stree/stree_test.go:623
    [Fact]
    public void TestSubjectTreeInsertSamePivotBug()
    {
        byte[][] testSubjects =
        [
            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, updated) = st.Insert(subj, 22);
            old.ShouldBe(default);
            updated.ShouldBeFalse();
            st.Find(subj).Found.ShouldBeTrue($"Could not find subject which should be findable");
        }
    }

    // Go: TestSubjectTreeInsertLongerLeafSuffixWithTrailingNulls server/stree/stree_test.go:917
    [Fact]
    public void TestSubjectTreeInsertLongerLeafSuffixWithTrailingNulls()
    {
        var st = new SubjectTree<int>();
        var subj = new List<byte>(B("foo.bar.baz_"));
        // add in 10 nulls.
        for (int i = 0; i < 10; i++) subj.Add(0);
        var subjArr = subj.ToArray();

        st.Insert(subjArr, 1);
        // add in 10 more nulls.
        var subj2 = new List<byte>(subjArr);
        for (int i = 0; i < 10; i++) subj2.Add(0);
        var subj2Arr = subj2.ToArray();
        st.Insert(subj2Arr, 2);

        // Make sure we can look them up.
        var (v, found) = st.Find(subjArr);
        found.ShouldBeTrue();
        v.ShouldBe(1);
        var (v2, found2) = st.Find(subj2Arr);
        found2.ShouldBeTrue();
        v2.ShouldBe(2);
    }

    // Go: TestSubjectTreeInsertWithNoPivot server/stree/stree_test.go:943
    [Fact]
    public void TestSubjectTreeInsertWithNoPivot()
    {
        var st = new SubjectTree<int>();
        var subj = new List<byte>(B("foo.bar.baz."));
        subj.Add(Parts.NoPivot);
        var (old, updated) = st.Insert(subj.ToArray(), 22);
        old.ShouldBe(default);
        updated.ShouldBeFalse();
        st.Size.ShouldBe(0);
    }

    // Go: TestInsertEdgeCases server/stree/stree_test.go:1927
    [Fact]
    public void TestInsertEdgeCases()
    {
        var st = new SubjectTree<int>();

        // Test inserting with noPivot byte (should fail)
        var noPivotSubj = new byte[] { (byte)'f', (byte)'o', (byte)'o', 0x7F, (byte)'b', (byte)'a', (byte)'r' };
        var (old, updated) = st.Insert(noPivotSubj, 1);
        old.ShouldBe(default);
        updated.ShouldBeFalse();
        st.Size.ShouldBe(0); // Should not insert

        // Test the edge case where we need to split with same pivot
        st = new SubjectTree<int>();
        st.Insert(B("a.b"), 1);
        st.Insert(B("a.c"), 2);
        st.Size.ShouldBe(2);
    }

    // Go: TestInsertComplexEdgeCases server/stree/stree_test.go:2067
    [Fact]
    public void TestInsertComplexEdgeCases()
    {
        var st = new SubjectTree<int>();

        // Test the recursive insert case with same pivot
        st.Insert(B("a"), 1);
        st.Insert(B("aa"), 2); // This will create a split

        // Now insert something that has the same pivot after split
        st.Insert(B("aaa"), 3); // This should trigger the recursive insert path

        st.Size.ShouldBe(3);

        // Verify all values can be found
        st.Find(B("a")).Value.ShouldBe(1);
        st.Find(B("aa")).Value.ShouldBe(2);
        st.Find(B("aaa")).Value.ShouldBe(3);
    }

    #endregion

    #region Random/Stress Tests

    // Go: TestSubjectTreeRandomTrackEntries server/stree/stree_test.go:683
    [Fact]
    public void TestSubjectTreeRandomTrackEntries()
    {
        var st = new SubjectTree<int>();
        var smap = new HashSet<string>();
        var rng = new Random(42);
        var buf = new byte[10];

        for (int i = 0; i < 1000; i++)
        {
            var sb = new StringBuilder();
            int numTokens = rng.Next(6) + 1;
            for (int t = 0; t < numTokens; t++)
            {
                int tlen = rng.Next(4) + 2;
                var tok = new byte[tlen];
                RandomNumberGenerator.Fill(tok);
                sb.Append(Convert.ToHexString(tok).ToLowerInvariant());
                if (t != numTokens - 1) sb.Append('.');
            }
            var subj = sb.ToString();
            // Avoid dupes
            if (smap.Contains(subj)) continue;
            smap.Add(subj);

            var (old, updated) = st.Insert(B(subj), 22);
            old.ShouldBe(default);
            updated.ShouldBeFalse();
            st.Size.ShouldBe(smap.Count);

            // Make sure all added items can be found.
            foreach (var s in smap)
            {
                st.Find(B(s)).Found.ShouldBeTrue($"Could not find subject {s} which should be findable");
            }
        }
    }

    // Go: TestSubjectTreeLongTokens server/stree/stree_test.go:726
    [Fact]
    public void TestSubjectTreeLongTokens()
    {
        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);
        st.Find(B("a1.aaaaaaaaaaaaaaaaaaaaaa0")).Value.ShouldBe(1);
        st.Find(B("a1.aaaaaaaaaaaaaaaaaaaaaa1")).Value.ShouldBe(3);
    }

    #endregion

    #region Nil/Panic Safety

    // Go: TestSubjectTreeNilNoPanic server/stree/stree_test.go:904
    [Fact]
    public void TestSubjectTreeNilNoPanic()
    {
        // In C# we use a fresh empty tree instead of null
        var st = new SubjectTree<int>();
        st.Match(B("foo"), (_, _) => { });
        st.Find(B("foo")).Found.ShouldBeFalse();
        st.Delete(B("foo")).Found.ShouldBeFalse();
        // Insert on a fresh tree should work
        st.Insert(B("foo"), 22);
        st.Size.ShouldBe(1);
    }

    #endregion

    #region Node-specific Tests

    // Go: TestSubjectTreeNode48 server/stree/stree_test.go:799
    [Fact]
    public void TestSubjectTreeNode48()
    {
        var a = new Leaf<int>(B("a"), 1);
        var b = new Leaf<int>(B("b"), 2);
        var c = new Leaf<int>(B("c"), 3);
        var n = new Node48([]);

        n.AddChild((byte)'A', a);
        n.Key[(byte)'A'].ShouldBe((byte)1);
        n.Child[0].ShouldNotBeNull();
        n.Child[0].ShouldBeSameAs(a);
        n.Children().Length.ShouldBe(1);

        var child = n.FindChild((byte)'A');
        child.ShouldNotBeNull();
        child!.Node.ShouldBeSameAs(a);

        n.AddChild((byte)'B', b);
        n.Key[(byte)'B'].ShouldBe((byte)2);
        n.Child[1].ShouldNotBeNull();
        n.Child[1].ShouldBeSameAs(b);
        n.Children().Length.ShouldBe(2);

        child = n.FindChild((byte)'B');
        child.ShouldNotBeNull();
        child!.Node.ShouldBeSameAs(b);

        n.AddChild((byte)'C', c);
        n.Key[(byte)'C'].ShouldBe((byte)3);
        n.Child[2].ShouldNotBeNull();
        n.Child[2].ShouldBeSameAs(c);
        n.Children().Length.ShouldBe(3);

        child = n.FindChild((byte)'C');
        child.ShouldNotBeNull();
        child!.Node.ShouldBeSameAs(c);

        n.DeleteChild((byte)'A');
        n.Children().Length.ShouldBe(2);
        n.Key[(byte)'A'].ShouldBe((byte)0); // Now deleted
        n.Key[(byte)'B'].ShouldBe((byte)2); // Untouched
        n.Key[(byte)'C'].ShouldBe((byte)1); // Where A was

        child = n.FindChild((byte)'A');
        child.ShouldBeNull();
        n.Child[0].ShouldNotBeNull();
        n.Child[0].ShouldBeSameAs(c);

        child = n.FindChild((byte)'B');
        child.ShouldNotBeNull();
        child!.Node.ShouldBeSameAs(b);
        n.Child[1].ShouldNotBeNull();
        n.Child[1].ShouldBeSameAs(b);

        child = n.FindChild((byte)'C');
        child.ShouldNotBeNull();
        child!.Node.ShouldBeSameAs(c);
        n.Child[2].ShouldBeNull();

        bool gotB = false, gotC = false;
        int iterations = 0;
        n.Iter(nd =>
        {
            iterations++;
            if (ReferenceEquals(nd, b)) gotB = true;
            if (ReferenceEquals(nd, c)) gotC = true;
            return true;
        });
        iterations.ShouldBe(2);
        gotB.ShouldBeTrue();
        gotC.ShouldBeTrue();

        // Check for off-by-one on byte 255
        n.AddChild(255, c);
        n.Key[255].ShouldBe((byte)3);
        var grown = (Node256)n.Grow();
        grown.FindChild(255).ShouldNotBeNull();
        var shrunk = (Node16)n.Shrink()!;
        shrunk.FindChild(255).ShouldNotBeNull();
    }

    // Go: TestNode48IterEarlyTermination server/stree/stree_test.go:1870
    [Fact]
    public void TestNode48IterEarlyTermination()
    {
        var n48 = new Node48(B("prefix"));
        for (int i = 0; i < 10; i++)
        {
            n48.AddChild((byte)i, new Leaf<int>([(byte)i], i));
        }

        int count = 0;
        n48.Iter(_ => { count++; return false; }); // Stop immediately
        count.ShouldBe(1);
    }

    // Go: TestNode10And16IterEarlyTermination server/stree/stree_test.go:1884
    [Fact]
    public void TestNode10And16IterEarlyTermination()
    {
        // Test node10 early termination
        var n10 = new Node10(B("prefix"));
        for (int i = 0; i < 5; i++)
        {
            n10.AddChild((byte)('a' + i), new Leaf<int>([(byte)('0' + i)], i));
        }

        int count = 0;
        n10.Iter(_ => { count++; return count < 2; }); // Stop after 2
        count.ShouldBe(2);

        // Test node16 early termination
        var n16 = new Node16(B("prefix"));
        for (int i = 0; i < 8; i++)
        {
            n16.AddChild((byte)i, new Leaf<int>([(byte)i], i));
        }

        count = 0;
        n16.Iter(_ => { count++; return count < 3; }); // Stop after 3
        count.ShouldBe(3);
    }

    // Go: TestLeafPanicMethods server/stree/stree_test.go:1577
    [Fact]
    public void TestLeafPanicMethods()
    {
        var leaf = new Leaf<int>(B("test"), 42);

        // Test setPrefix panic
        Should.Throw<InvalidOperationException>(() => leaf.SetPrefix(B("prefix")))
            .Message.ShouldBe("setPrefix called on leaf");

        // Test addChild panic
        Should.Throw<InvalidOperationException>(() => leaf.AddChild((byte)'a', null!))
            .Message.ShouldBe("addChild called on leaf");

        // Test findChild panic
        Should.Throw<InvalidOperationException>(() => leaf.FindChild((byte)'a'))
            .Message.ShouldBe("findChild called on leaf");

        // Test grow panic
        Should.Throw<InvalidOperationException>(() => leaf.Grow())
            .Message.ShouldBe("grow called on leaf");

        // Test deleteChild panic
        Should.Throw<InvalidOperationException>(() => leaf.DeleteChild((byte)'a'))
            .Message.ShouldBe("deleteChild called on leaf");

        // Test shrink panic
        Should.Throw<InvalidOperationException>(() => leaf.Shrink())
            .Message.ShouldBe("shrink called on leaf");

        // Test other leaf methods that should work
        leaf.IsFull.ShouldBeTrue();
        leaf.Base.ShouldBeNull();
        leaf.NumChildren.ShouldBe((ushort)0);
        leaf.Children().ShouldBeEmpty();

        // Test iter (should do nothing)
        bool called = false;
        leaf.Iter(n => { called = true; return true; });
        called.ShouldBeFalse();
    }

    // Go: TestLeafIter server/stree/stree_test.go:2003
    [Fact]
    public void TestLeafIter()
    {
        // Test that leaf iter does nothing (it's a no-op)
        var leaf = new Leaf<int>(B("test"), 42);
        bool called = false;

        leaf.Iter(n => { called = true; return true; });
        called.ShouldBeFalse();

        leaf.Iter(n => { called = true; return false; });
        called.ShouldBeFalse();

        // Verify the leaf itself is not affected
        leaf.Match(B("test")).ShouldBeTrue();
        leaf.Value.ShouldBe(42);

        // Also test through the node interface
        INode n2 = leaf;
        called = false;
        n2.Iter(child => { called = true; return true; });
        called.ShouldBeFalse();
    }

    // Go: TestNodeIterMethods server/stree/stree_test.go:1685
    [Fact]
    public void TestNodeIterMethods()
    {
        // Test node4 iter
        var n4 = new Node4(B("prefix"));
        n4.AddChild((byte)'a', new Leaf<int>(B("1"), 1));
        n4.AddChild((byte)'b', new Leaf<int>(B("2"), 2));

        int count = 0;
        n4.Iter(n => { count++; return true; });
        count.ShouldBe(2);

        // Test early termination
        count = 0;
        n4.Iter(n => { count++; return false; });
        count.ShouldBe(1);

        // Test node10 iter
        var n10 = new Node10(B("prefix"));
        for (int i = 0; i < 5; i++)
        {
            n10.AddChild((byte)('a' + i), new Leaf<int>([(byte)('0' + i)], i));
        }

        count = 0;
        n10.Iter(n => { count++; return true; });
        count.ShouldBe(5);

        // Test node16 iter
        var n16 = new Node16(B("prefix"));
        for (int i = 0; i < 8; i++)
        {
            n16.AddChild((byte)('a' + i), new Leaf<int>([(byte)('0' + i)], i));
        }

        count = 0;
        n16.Iter(n => { count++; return true; });
        count.ShouldBe(8);
    }

    // Go: TestNodeAddChildPanic server/stree/stree_test.go:1752
    [Fact]
    public void TestNodeAddChildPanic()
    {
        // Test node4 addChild panic when full
        var n4 = new Node4(B("prefix"));
        n4.AddChild((byte)'a', new Leaf<int>(B("1"), 1));
        n4.AddChild((byte)'b', new Leaf<int>(B("2"), 2));
        n4.AddChild((byte)'c', new Leaf<int>(B("3"), 3));
        n4.AddChild((byte)'d', new Leaf<int>(B("4"), 4));

        Should.Throw<InvalidOperationException>(() =>
            n4.AddChild((byte)'e', new Leaf<int>(B("5"), 5)))
            .Message.ShouldBe("node4 full!");
    }

    // Go: TestNodeAddChildPanicOthers server/stree/stree_test.go:1770
    [Fact]
    public void TestNodeAddChildPanicOthers()
    {
        // Test node10 addChild panic when full
        var n10 = new Node10(B("prefix"));
        for (int i = 0; i < 10; i++)
        {
            n10.AddChild((byte)('a' + i), new Leaf<int>([(byte)('0' + i)], i));
        }
        Should.Throw<InvalidOperationException>(() =>
            n10.AddChild((byte)'k', new Leaf<int>(B("11"), 11)))
            .Message.ShouldBe("node10 full!");

        // Test node16 addChild panic when full
        var n16 = new Node16(B("prefix"));
        for (int i = 0; i < 16; i++)
        {
            n16.AddChild((byte)i, new Leaf<int>([(byte)i], i));
        }
        Should.Throw<InvalidOperationException>(() =>
            n16.AddChild(16, new Leaf<int>(B("16"), 16)))
            .Message.ShouldBe("node16 full!");

        // Test node48 addChild panic when full
        var n48 = new Node48(B("prefix"));
        for (int i = 0; i < 48; i++)
        {
            n48.AddChild((byte)i, new Leaf<int>([(byte)i], i));
        }
        Should.Throw<InvalidOperationException>(() =>
            n48.AddChild(48, new Leaf<int>(B("48"), 48)))
            .Message.ShouldBe("node48 full!");
    }

    // Go: TestNodeDeleteChildNotFound server/stree/stree_test.go:1823
    [Fact]
    public void TestNodeDeleteChildNotFound()
    {
        // Test node10 deleteChild when child doesn't exist
        var n10 = new Node10(B("prefix"));
        n10.AddChild((byte)'a', new Leaf<int>(B("1"), 1));
        n10.AddChild((byte)'b', new Leaf<int>(B("2"), 2));
        n10.DeleteChild((byte)'z');
        n10.Meta.Size.ShouldBe((ushort)2);

        // Test node16 deleteChild when child doesn't exist
        var n16 = new Node16(B("prefix"));
        n16.AddChild((byte)'a', new Leaf<int>(B("1"), 1));
        n16.AddChild((byte)'b', new Leaf<int>(B("2"), 2));
        n16.DeleteChild((byte)'z');
        n16.Meta.Size.ShouldBe((ushort)2);

        // Test node48 deleteChild when child doesn't exist
        var n48 = new Node48(B("prefix"));
        n48.AddChild(0, new Leaf<int>(B("1"), 1));
        n48.AddChild(1, new Leaf<int>(B("2"), 2));
        n48.DeleteChild(255);
        n48.Meta.Size.ShouldBe((ushort)2);
    }

    #endregion

    #region LazyIntersect

    // Go: TestSubjectTreeLazyIntersect server/stree/stree_test.go:965
    [Fact]
    public void TestSubjectTreeLazyIntersect()
    {
        var st1 = new SubjectTree<int>();
        var st2 = new SubjectTree<int>();

        // Should cause an intersection.
        st1.Insert(B("foo.bar"), 1);
        st2.Insert(B("foo.bar"), 1);

        // Should cause an intersection.
        st1.Insert(B("foo.bar.baz.qux"), 1);
        st2.Insert(B("foo.bar.baz.qux"), 1);

        // Should not cause any intersections.
        st1.Insert(B("bar"), 1);
        st2.Insert(B("baz"), 1);
        st1.Insert(B("a.b.c"), 1);
        st2.Insert(B("a.b.d"), 1);
        st1.Insert(B("a.b.ee"), 1);
        st2.Insert(B("a.b.e"), 1);
        st1.Insert(B("bb.c.d"), 1);
        st2.Insert(B("b.c.d"), 1);
        st2.Insert(B("foo.bar.baz.qux.alice"), 1);
        st2.Insert(B("foo.bar.baz.qux.bob"), 1);

        var intersected = new Dictionary<string, int>();
        SubjectTreeHelper.LazyIntersect(st1, st2, (key, _, _) =>
        {
            var k = Encoding.UTF8.GetString(key);
            intersected.TryGetValue(k, out var c);
            intersected[k] = c + 1;
        });
        intersected.Count.ShouldBe(2);
        intersected["foo.bar"].ShouldBe(1);
        intersected["foo.bar.baz.qux"].ShouldBe(1);
    }

    // Go: TestSubjectTreeLazyIntersectComprehensive server/stree/stree_test.go:1375
    [Fact]
    public void TestSubjectTreeLazyIntersectComprehensive()
    {
        // Test with empty trees
        var st1 = new SubjectTree<int>();
        var st2 = new SubjectTree<string>();
        int count = 0;
        SubjectTreeHelper.LazyIntersect(st1, st2, (_, _, _) => count++);
        count.ShouldBe(0);

        // Test with one having data and one empty
        st1.Insert(B("foo"), 1);
        SubjectTreeHelper.LazyIntersect(st1, st2, (_, _, _) => count++);
        count.ShouldBe(0);

        // Test with different value types
        st1 = new SubjectTree<int>();
        st2 = new SubjectTree<string>();

        // Add some intersecting keys
        st1.Insert(B("foo.bar"), 42);
        st2.Insert(B("foo.bar"), "hello");
        st1.Insert(B("baz.qux"), 100);
        st2.Insert(B("baz.qux"), "world");

        // Add non-intersecting keys
        st1.Insert(B("only.in.st1"), 1);
        st2.Insert(B("only.in.st2"), "two");

        var results = new Dictionary<string, (int V1, string V2)>();
        SubjectTreeHelper.LazyIntersect(st1, st2, (key, v1, v2) =>
        {
            results[Encoding.UTF8.GetString(key)] = (v1, v2);
        });

        results.Count.ShouldBe(2);
        results["foo.bar"].V1.ShouldBe(42);
        results["foo.bar"].V2.ShouldBe("hello");
        results["baz.qux"].V1.ShouldBe(100);
        results["baz.qux"].V2.ShouldBe("world");

        // Test that it iterates over smaller tree
        var large = new SubjectTree<int>();
        var small = new SubjectTree<int>();

        for (int i = 0; i < 100; i++)
        {
            large.Insert(B($"large.{i}"), i);
        }
        small.Insert(B("large.5"), 500);
        small.Insert(B("large.10"), 1000);
        small.Insert(B("large.50"), 5000);
        small.Insert(B("small.only"), 999);

        int intersectCount = 0;
        SubjectTreeHelper.LazyIntersect(large, small, (key, v1, v2) =>
        {
            intersectCount++;
            var k = Encoding.UTF8.GetString(key);
            switch (k)
            {
                case "large.5": v1.ShouldBe(5); v2.ShouldBe(500); break;
                case "large.10": v1.ShouldBe(10); v2.ShouldBe(1000); break;
                case "large.50": v1.ShouldBe(50); v2.ShouldBe(5000); break;
                default: throw new Exception($"Unexpected key: {k}");
            }
        });
        intersectCount.ShouldBe(3);

        // Test with complex subjects (multiple levels)
        var st3 = new SubjectTree<int>();
        var st4 = new SubjectTree<int>();

        st3.Insert(B("a.b.c.d.e.f.g"), 1);
        st4.Insert(B("a.b.c.d.e.f.g"), 2);

        // Partial matches (should not intersect)
        st3.Insert(B("a.b.c.d"), 3);
        st4.Insert(B("a.b.c.d.e"), 4);

        // Same prefix different suffix
        st3.Insert(B("prefix.suffix1"), 5);
        st4.Insert(B("prefix.suffix2"), 6);

        int intersections = 0;
        SubjectTreeHelper.LazyIntersect(st3, st4, (key, v1, v2) =>
        {
            intersections++;
            Encoding.UTF8.GetString(key).ShouldBe("a.b.c.d.e.f.g");
            v1.ShouldBe(1);
            v2.ShouldBe(2);
        });
        intersections.ShouldBe(1);
    }

    #endregion

    #region GSL Intersection (Skipped - GSL not yet implemented)

    // Go: TestSubjectTreeGSLIntersection server/stree/stree_test.go:998
    [Fact(Skip = "GSL (GenericSubjectList) not yet implemented")]
    public void TestSubjectTreeGSLIntersection()
    {
        // This test requires the GSL (GenericSubjectList) which is not yet ported.
        // The test has 20+ subtests covering literals, PWC, FWC, and overlapping patterns.
    }

    #endregion

    #region Performance Tests (Skipped)

    // Go: TestSubjectTreeMatchAllPerf server/stree/stree_test.go:749
    [Fact(Skip = "Performance test - enable with --results flag")]
    public void TestSubjectTreeMatchAllPerf()
    {
        // Performance test - skipped by default, same as Go version
    }

    // Go: TestSubjectTreeIterPerf server/stree/stree_test.go:779
    [Fact(Skip = "Performance test - enable with --results flag")]
    public void TestSubjectTreeIterPerf()
    {
        // Performance test - skipped by default, same as Go version
    }

    #endregion
}
