using System.Net;
using System.Net.Sockets;
using System.Text;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using NATS.Client.Core;
using NATS.Server.Auth;
using NATS.Server.Configuration;
using NATS.Server.Gateways;
using NATS.Server.LeafNodes;
using NATS.Server.Routes;
using NATS.Server.Subscriptions;
using NATS.Server.TestUtilities;

namespace NATS.Server.Transport.Tests.Networking;

/// <summary>
/// Ported Go networking tests for gateway interest mode, route pool accounting,
/// and leaf node connections. Each test references the Go function name and file.
/// </summary>
public class NetworkingGoParityTests
{
    // ════════════════════════════════════════════════════════════════════
    // GATEWAY INTEREST MODE (~20 tests from gateway_test.go)
    // ════════════════════════════════════════════════════════════════════

    // Go: TestGatewayDontSendSubInterest server/gateway_test.go:1755
    [Fact]
    public void Tracker_starts_in_optimistic_mode()
    {
        var tracker = new GatewayInterestTracker();
        tracker.GetMode("$G").ShouldBe(GatewayInterestMode.Optimistic);
    }

    // Go: TestGatewayAccountInterest server/gateway_test.go:1794
    [Fact]
    public void Tracker_no_interest_accumulates_in_optimistic_mode()
    {
        var tracker = new GatewayInterestTracker(noInterestThreshold: 5);
        for (var i = 0; i < 4; i++)
            tracker.TrackNoInterest("$G", $"subj.{i}");

        tracker.GetMode("$G").ShouldBe(GatewayInterestMode.Optimistic);
        tracker.ShouldForward("$G", "subj.0").ShouldBeFalse();
        tracker.ShouldForward("$G", "other").ShouldBeTrue();
    }

    // Go: TestGatewaySwitchToInterestOnlyModeImmediately server/gateway_test.go:6934
    [Fact]
    public void Tracker_switches_to_interest_only_at_threshold()
    {
        var tracker = new GatewayInterestTracker(noInterestThreshold: 3);
        tracker.TrackNoInterest("$G", "a");
        tracker.TrackNoInterest("$G", "b");
        tracker.TrackNoInterest("$G", "c");

        tracker.GetMode("$G").ShouldBe(GatewayInterestMode.InterestOnly);
    }

    // Go: TestGatewayAccountInterest server/gateway_test.go:1794
    [Fact]
    public void Tracker_interest_only_blocks_unknown_subjects()
    {
        var tracker = new GatewayInterestTracker(noInterestThreshold: 1);
        tracker.TrackNoInterest("$G", "trigger");

        tracker.GetMode("$G").ShouldBe(GatewayInterestMode.InterestOnly);
        tracker.ShouldForward("$G", "unknown.subject").ShouldBeFalse();
    }

    // Go: TestGatewayAccountInterest server/gateway_test.go:1794
    [Fact]
    public void Tracker_interest_only_forwards_tracked_subjects()
    {
        var tracker = new GatewayInterestTracker(noInterestThreshold: 1);
        tracker.TrackNoInterest("$G", "trigger");
        tracker.TrackInterest("$G", "orders.>");

        tracker.ShouldForward("$G", "orders.created").ShouldBeTrue();
        tracker.ShouldForward("$G", "users.created").ShouldBeFalse();
    }

    // Go: TestGatewayAccountUnsub server/gateway_test.go:1912
    [Fact]
    public void Tracker_removing_interest_in_io_mode_stops_forwarding()
    {
        var tracker = new GatewayInterestTracker(noInterestThreshold: 1);
        tracker.TrackNoInterest("$G", "trigger");
        tracker.TrackInterest("$G", "foo");
        tracker.ShouldForward("$G", "foo").ShouldBeTrue();

        tracker.TrackNoInterest("$G", "foo");
        tracker.ShouldForward("$G", "foo").ShouldBeFalse();
    }

    // Go: TestGatewayAccountInterest server/gateway_test.go:1794
    [Fact]
    public void Tracker_accounts_are_independent()
    {
        var tracker = new GatewayInterestTracker(noInterestThreshold: 1);
        tracker.TrackNoInterest("ACCT_A", "trigger");

        tracker.GetMode("ACCT_A").ShouldBe(GatewayInterestMode.InterestOnly);
        tracker.GetMode("ACCT_B").ShouldBe(GatewayInterestMode.Optimistic);
        tracker.ShouldForward("ACCT_B", "any.subject").ShouldBeTrue();
    }

    // Go: TestGatewaySwitchToInterestOnlyModeImmediately server/gateway_test.go:6934
    [Fact]
    public void Tracker_explicit_switch_to_interest_only()
    {
        var tracker = new GatewayInterestTracker();
        tracker.SwitchToInterestOnly("$G");
        tracker.GetMode("$G").ShouldBe(GatewayInterestMode.InterestOnly);
        tracker.ShouldForward("$G", "anything").ShouldBeFalse();
    }

    // Go: TestGatewayAccountInterest server/gateway_test.go:1794
    [Fact]
    public void Tracker_optimistic_mode_interest_add_removes_from_no_interest()
    {
        var tracker = new GatewayInterestTracker();
        tracker.TrackNoInterest("$G", "foo");
        tracker.ShouldForward("$G", "foo").ShouldBeFalse();

        tracker.TrackInterest("$G", "foo");
        tracker.ShouldForward("$G", "foo").ShouldBeTrue();
    }

    // Go: TestGatewaySubjectInterest server/gateway_test.go:1972
    [Fact]
    public void Tracker_wildcard_interest_matches_in_io_mode()
    {
        var tracker = new GatewayInterestTracker(noInterestThreshold: 1);
        tracker.TrackNoInterest("$G", "trigger");
        tracker.TrackInterest("$G", "events.>");

        tracker.ShouldForward("$G", "events.created").ShouldBeTrue();
        tracker.ShouldForward("$G", "events.a.b.c").ShouldBeTrue();
        tracker.ShouldForward("$G", "other").ShouldBeFalse();
    }

    // Go: TestGatewayAccountInterest server/gateway_test.go:1794
    [Fact]
    public void ShouldForwardInterestOnly_uses_SubList_remote_interest()
    {
        using var subList = new SubList();
        subList.ApplyRemoteSub(new RemoteSubscription("orders.*", null, "gw1", "$G"));

        GatewayManager.ShouldForwardInterestOnly(subList, "$G", "orders.created").ShouldBeTrue();
        GatewayManager.ShouldForwardInterestOnly(subList, "$G", "users.created").ShouldBeFalse();
    }

    // Go: TestGatewayAccountUnsub server/gateway_test.go:1912
    [Fact]
    public void ShouldForwardInterestOnly_respects_removal()
    {
        using var subList = new SubList();
        subList.ApplyRemoteSub(new RemoteSubscription("orders.*", null, "gw1", "$G"));
        GatewayManager.ShouldForwardInterestOnly(subList, "$G", "orders.created").ShouldBeTrue();

        subList.ApplyRemoteSub(RemoteSubscription.Removal("orders.*", null, "gw1", "$G"));
        GatewayManager.ShouldForwardInterestOnly(subList, "$G", "orders.created").ShouldBeFalse();
    }

    // Go: TestGatewaySubjectInterest server/gateway_test.go:1972
    [Fact]
    public async Task Gateway_propagates_subject_interest_end_to_end()
    {
        await using var fixture = await TwoGatewayFixture.StartAsync();

        await using var conn = new NatsConnection(new NatsOpts
        {
            Url = $"nats://127.0.0.1:{fixture.Remote.Port}",
        });
        await conn.ConnectAsync();

        await using var sub = await conn.SubscribeCoreAsync<string>("gw.interest.test");
        await conn.PingAsync();

        await PollHelper.WaitUntilAsync(() => fixture.Local.HasRemoteInterest("gw.interest.test"));

        fixture.Local.HasRemoteInterest("gw.interest.test").ShouldBeTrue();
    }

    // Go: TestGatewayDontSendSubInterest server/gateway_test.go:1755
    [Fact]
    public async Task Gateway_message_forwarded_to_remote_subscriber()
    {
        await using var fixture = await TwoGatewayFixture.StartAsync();

        await using var remoteConn = new NatsConnection(new NatsOpts
        {
            Url = $"nats://127.0.0.1:{fixture.Remote.Port}",
        });
        await remoteConn.ConnectAsync();
        await using var localConn = new NatsConnection(new NatsOpts
        {
            Url = $"nats://127.0.0.1:{fixture.Local.Port}",
        });
        await localConn.ConnectAsync();

        await using var sub = await remoteConn.SubscribeCoreAsync<string>("gw.fwd.test");
        await remoteConn.PingAsync();

        await PollHelper.WaitUntilAsync(() => fixture.Local.HasRemoteInterest("gw.fwd.test"));

        await localConn.PublishAsync("gw.fwd.test", "gateway-msg");

        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
        (await sub.Msgs.ReadAsync(cts.Token)).Data.ShouldBe("gateway-msg");
    }

    // Go: TestGatewayAccountUnsub server/gateway_test.go:1912
    [Fact]
    public async Task Gateway_unsubscribe_removes_remote_interest()
    {
        await using var fixture = await TwoGatewayFixture.StartAsync();

        await using var conn = new NatsConnection(new NatsOpts
        {
            Url = $"nats://127.0.0.1:{fixture.Remote.Port}",
        });
        await conn.ConnectAsync();

        var sub = await conn.SubscribeCoreAsync<string>("gw.unsub.test");
        await conn.PingAsync();

        await PollHelper.WaitUntilAsync(() => fixture.Local.HasRemoteInterest("gw.unsub.test"));

        fixture.Local.HasRemoteInterest("gw.unsub.test").ShouldBeTrue();

        await sub.DisposeAsync();
        await conn.PingAsync();

        await PollHelper.WaitUntilAsync(() => !(fixture.Local.HasRemoteInterest("gw.unsub.test")));

        fixture.Local.HasRemoteInterest("gw.unsub.test").ShouldBeFalse();
    }

    // Go: TestGatewayNoAccInterestThenQSubThenRegularSub server/gateway_test.go:5643
    [Fact]
    public async Task Gateway_wildcard_interest_propagates()
    {
        await using var fixture = await TwoGatewayFixture.StartAsync();

        await using var conn = new NatsConnection(new NatsOpts
        {
            Url = $"nats://127.0.0.1:{fixture.Remote.Port}",
        });
        await conn.ConnectAsync();

        await using var sub = await conn.SubscribeCoreAsync<string>("gw.wild.>");
        await conn.PingAsync();

        await PollHelper.WaitUntilAsync(() => fixture.Local.HasRemoteInterest("gw.wild.test"));

        fixture.Local.HasRemoteInterest("gw.wild.test").ShouldBeTrue();
        fixture.Local.HasRemoteInterest("gw.wild.deep.nested").ShouldBeTrue();
    }

    // Go: TestGatewayNoCrashOnInvalidSubject server/gateway_test.go:6279
    [Fact]
    public void Invalid_subject_does_not_crash_SubList()
    {
        using var subList = new SubList();
        // Should handle gracefully, not throw
        subList.HasRemoteInterest("$G", "valid.subject").ShouldBeFalse();
        subList.HasRemoteInterest("$G", "").ShouldBeFalse();
    }

    // Go: TestGatewayLogAccountInterestModeSwitch server/gateway_test.go:5843
    [Fact]
    public void Tracker_default_threshold_is_1000()
    {
        GatewayInterestTracker.DefaultNoInterestThreshold.ShouldBe(1000);
    }

    // Go: TestGatewayAccountInterestModeSwitchOnlyOncePerAccount server/gateway_test.go:5932
    [Fact]
    public void Tracker_switch_is_idempotent()
    {
        var tracker = new GatewayInterestTracker(noInterestThreshold: 1);
        tracker.TrackNoInterest("$G", "a");
        tracker.GetMode("$G").ShouldBe(GatewayInterestMode.InterestOnly);

        // Switching again should not change state
        tracker.SwitchToInterestOnly("$G");
        tracker.GetMode("$G").ShouldBe(GatewayInterestMode.InterestOnly);
    }

    // Go: TestGatewayReplyMappingBasic server/gateway_test.go:3200
    [Fact]
    public void Reply_mapper_round_trips()
    {
        var mapped = ReplyMapper.ToGatewayReply("INBOX.abc123", "SERVERID1");
        mapped.ShouldNotBeNull();
        mapped!.ShouldStartWith("_GR_.");

        ReplyMapper.HasGatewayReplyPrefix(mapped).ShouldBeTrue();
        ReplyMapper.TryRestoreGatewayReply(mapped, out var restored).ShouldBeTrue();
        restored.ShouldBe("INBOX.abc123");
    }

    // Go: TestGatewayReplyMappingBasic server/gateway_test.go:3200
    [Fact]
    public void Reply_mapper_null_input_returns_null()
    {
        var result = ReplyMapper.ToGatewayReply(null, "S1");
        result.ShouldBeNull();
    }

    // ════════════════════════════════════════════════════════════════════
    // ROUTE POOL ACCOUNTING (~15 tests from routes_test.go)
    // ════════════════════════════════════════════════════════════════════

    // Go: TestRoutePool server/routes_test.go:1966
    [Fact]
    public void Route_pool_idx_deterministic_for_same_account()
    {
        var idx1 = RouteManager.ComputeRoutePoolIdx(3, "$G");
        var idx2 = RouteManager.ComputeRoutePoolIdx(3, "$G");
        idx1.ShouldBe(idx2);
    }

    // Go: TestRoutePool server/routes_test.go:1966
    [Fact]
    public void Route_pool_idx_in_range()
    {
        for (var poolSize = 1; poolSize <= 10; poolSize++)
        {
            var idx = RouteManager.ComputeRoutePoolIdx(poolSize, "$G");
            idx.ShouldBeGreaterThanOrEqualTo(0);
            idx.ShouldBeLessThan(poolSize);
        }
    }

    // Go: TestRoutePool server/routes_test.go:1966
    [Fact]
    public void Route_pool_idx_distributes_accounts()
    {
        var accounts = new[] { "$G", "ACCT_A", "ACCT_B", "ACCT_C", "ACCT_D" };
        var poolSize = 3;
        var indices = new HashSet<int>();
        foreach (var account in accounts)
            indices.Add(RouteManager.ComputeRoutePoolIdx(poolSize, account));

        // With 5 accounts and pool of 3, we should use at least 2 different indices
        indices.Count.ShouldBeGreaterThanOrEqualTo(2);
    }

    // Go: TestRoutePool server/routes_test.go:1966
    [Fact]
    public void Route_pool_idx_single_pool_always_zero()
    {
        RouteManager.ComputeRoutePoolIdx(1, "$G").ShouldBe(0);
        RouteManager.ComputeRoutePoolIdx(1, "ACCT_A").ShouldBe(0);
        RouteManager.ComputeRoutePoolIdx(1, "ACCT_B").ShouldBe(0);
    }

    // Go: TestRoutePoolConnectRace server/routes_test.go:2100
    [Fact]
    public async Task Route_pool_default_three_connections_per_peer()
    {
        var optsA = new NatsOptions
        {
            Host = "127.0.0.1",
            Port = 0,
            Cluster = new ClusterOptions
            {
                Name = Guid.NewGuid().ToString("N"),
                Host = "127.0.0.1",
                Port = 0,
            },
        };

        var serverA = new NatsServer(optsA, NullLoggerFactory.Instance);
        var ctsA = new CancellationTokenSource();
        _ = serverA.StartAsync(ctsA.Token);
        await serverA.WaitForReadyAsync();

        try
        {
            var optsB = new NatsOptions
            {
                Host = "127.0.0.1",
                Port = 0,
                Cluster = new ClusterOptions
                {
                    Name = Guid.NewGuid().ToString("N"),
                    Host = "127.0.0.1",
                    Port = 0,
                    Routes = [serverA.ClusterListen!],
                },
            };

            var serverB = new NatsServer(optsB, NullLoggerFactory.Instance);
            var ctsB = new CancellationTokenSource();
            _ = serverB.StartAsync(ctsB.Token);
            await serverB.WaitForReadyAsync();

            try
            {
                await PollHelper.WaitUntilAsync(() => !(serverA.Stats.Routes < 3));

                serverA.Stats.Routes.ShouldBeGreaterThanOrEqualTo(3);
            }
            finally
            {
                await ctsB.CancelAsync();
                serverB.Dispose();
                ctsB.Dispose();
            }
        }
        finally
        {
            await ctsA.CancelAsync();
            serverA.Dispose();
            ctsA.Dispose();
        }
    }

    // Go: TestRoutePoolRouteStoredSameIndexBothSides server/routes_test.go:2180
    [Fact]
    public void Route_pool_idx_uses_FNV1a_hash()
    {
        // Go uses fnv.New32a() — FNV-1a 32-bit
        // Verify we produce the same hash for known inputs
        var idx = RouteManager.ComputeRoutePoolIdx(10, "$G");
        idx.ShouldBeGreaterThanOrEqualTo(0);
        idx.ShouldBeLessThan(10);

        // Same input always produces same output
        RouteManager.ComputeRoutePoolIdx(10, "$G").ShouldBe(idx);
    }

    // Go: TestRoutePoolPerAccountSubUnsubProtoParsing server/routes_test.go:3104
    [Fact]
    public async Task Route_subscription_propagation_between_peers()
    {
        var optsA = new NatsOptions
        {
            Host = "127.0.0.1",
            Port = 0,
            Cluster = new ClusterOptions
            {
                Name = Guid.NewGuid().ToString("N"),
                Host = "127.0.0.1",
                Port = 0,
            },
        };

        var serverA = new NatsServer(optsA, NullLoggerFactory.Instance);
        var ctsA = new CancellationTokenSource();
        _ = serverA.StartAsync(ctsA.Token);
        await serverA.WaitForReadyAsync();

        try
        {
            var optsB = new NatsOptions
            {
                Host = "127.0.0.1",
                Port = 0,
                Cluster = new ClusterOptions
                {
                    Name = Guid.NewGuid().ToString("N"),
                    Host = "127.0.0.1",
                    Port = 0,
                    Routes = [serverA.ClusterListen!],
                },
            };

            var serverB = new NatsServer(optsB, NullLoggerFactory.Instance);
            var ctsB = new CancellationTokenSource();
            _ = serverB.StartAsync(ctsB.Token);
            await serverB.WaitForReadyAsync();

            try
            {
                await PollHelper.WaitUntilAsync(() => !(serverA.Stats.Routes < 3));

                await using var conn = new NatsConnection(new NatsOpts
                {
                    Url = $"nats://127.0.0.1:{serverB.Port}",
                });
                await conn.ConnectAsync();

                await using var sub = await conn.SubscribeCoreAsync<string>("route.sub.test");
                await conn.PingAsync();

                await PollHelper.WaitUntilAsync(() => serverA.HasRemoteInterest("route.sub.test"));

                serverA.HasRemoteInterest("route.sub.test").ShouldBeTrue();
            }
            finally
            {
                await ctsB.CancelAsync();
                serverB.Dispose();
                ctsB.Dispose();
            }
        }
        finally
        {
            await ctsA.CancelAsync();
            serverA.Dispose();
            ctsA.Dispose();
        }
    }

    // Go: TestRoutePerAccount server/routes_test.go:2539
    [Fact]
    public void Route_pool_different_accounts_can_get_different_indices()
    {
        // With a large pool, different accounts should hash to different slots
        var indices = new Dictionary<string, int>();
        for (var i = 0; i < 100; i++)
        {
            var acct = $"account_{i}";
            indices[acct] = RouteManager.ComputeRoutePoolIdx(100, acct);
        }

        // With 100 accounts and pool size 100, we should have decent distribution
        var uniqueIndices = indices.Values.Distinct().Count();
        uniqueIndices.ShouldBeGreaterThan(20);
    }

    // Go: TestRouteSendLocalSubsWithLowMaxPending server/routes_test.go:1098
    [Fact]
    public async Task Route_message_forwarded_to_subscriber_on_peer()
    {
        var optsA = new NatsOptions
        {
            Host = "127.0.0.1",
            Port = 0,
            Cluster = new ClusterOptions
            {
                Name = Guid.NewGuid().ToString("N"),
                Host = "127.0.0.1",
                Port = 0,
            },
        };

        var serverA = new NatsServer(optsA, NullLoggerFactory.Instance);
        var ctsA = new CancellationTokenSource();
        _ = serverA.StartAsync(ctsA.Token);
        await serverA.WaitForReadyAsync();

        try
        {
            var optsB = new NatsOptions
            {
                Host = "127.0.0.1",
                Port = 0,
                Cluster = new ClusterOptions
                {
                    Name = Guid.NewGuid().ToString("N"),
                    Host = "127.0.0.1",
                    Port = 0,
                    Routes = [serverA.ClusterListen!],
                },
            };

            var serverB = new NatsServer(optsB, NullLoggerFactory.Instance);
            var ctsB = new CancellationTokenSource();
            _ = serverB.StartAsync(ctsB.Token);
            await serverB.WaitForReadyAsync();

            try
            {
                await PollHelper.WaitUntilAsync(() => !(serverA.Stats.Routes < 3));

                await using var subConn = new NatsConnection(new NatsOpts
                {
                    Url = $"nats://127.0.0.1:{serverB.Port}",
                });
                await subConn.ConnectAsync();

                await using var pubConn = new NatsConnection(new NatsOpts
                {
                    Url = $"nats://127.0.0.1:{serverA.Port}",
                });
                await pubConn.ConnectAsync();

                await using var sub = await subConn.SubscribeCoreAsync<string>("route.fwd.test");
                await subConn.PingAsync();

                await PollHelper.WaitUntilAsync(() => serverA.HasRemoteInterest("route.fwd.test"));

                await pubConn.PublishAsync("route.fwd.test", "routed-msg");

                using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
                (await sub.Msgs.ReadAsync(cts.Token)).Data.ShouldBe("routed-msg");
            }
            finally
            {
                await ctsB.CancelAsync();
                serverB.Dispose();
                ctsB.Dispose();
            }
        }
        finally
        {
            await ctsA.CancelAsync();
            serverA.Dispose();
            ctsA.Dispose();
        }
    }

    // Go: TestRoutePoolAndPerAccountErrors server/routes_test.go:1906
    [Fact]
    public void Route_pool_idx_zero_pool_returns_zero()
    {
        RouteManager.ComputeRoutePoolIdx(0, "$G").ShouldBe(0);
    }

    // Go: TestRoutePoolSizeDifferentOnEachServer server/routes_test.go:2254
    [Fact]
    public void Route_pool_idx_consistent_across_sizes()
    {
        // The hash should be deterministic regardless of pool size
        var hashSmall = RouteManager.ComputeRoutePoolIdx(3, "test");
        var hashLarge = RouteManager.ComputeRoutePoolIdx(100, "test");

        hashSmall.ShouldBeGreaterThanOrEqualTo(0);
        hashLarge.ShouldBeGreaterThanOrEqualTo(0);
    }

    // ════════════════════════════════════════════════════════════════════
    // LEAF NODE CONNECTIONS (~20 tests from leafnode_test.go)
    // ════════════════════════════════════════════════════════════════════

    // Go: TestLeafNodeLoop server/leafnode_test.go:837
    [Fact]
    public void Leaf_loop_detector_marks_and_detects()
    {
        var marked = LeafLoopDetector.Mark("test.subject", "S1");
        LeafLoopDetector.HasLoopMarker(marked).ShouldBeTrue();
        LeafLoopDetector.IsLooped(marked, "S1").ShouldBeTrue();
        LeafLoopDetector.IsLooped(marked, "S2").ShouldBeFalse();
    }

    // Go: TestLeafNodeLoop server/leafnode_test.go:837
    [Fact]
    public void Leaf_loop_detector_unmarks()
    {
        var marked = LeafLoopDetector.Mark("orders.created", "SERVER1");
        LeafLoopDetector.TryUnmark(marked, out var unmarked).ShouldBeTrue();
        unmarked.ShouldBe("orders.created");
    }

    // Go: TestLeafNodeLoop server/leafnode_test.go:837
    [Fact]
    public void Leaf_loop_detector_non_marked_returns_false()
    {
        LeafLoopDetector.HasLoopMarker("plain.subject").ShouldBeFalse();
        LeafLoopDetector.IsLooped("plain.subject", "S1").ShouldBeFalse();
        LeafLoopDetector.TryUnmark("plain.subject", out _).ShouldBeFalse();
    }

    // Go: TestLeafNodeBasicAuthSingleton server/leafnode_test.go:602
    [Fact]
    public async Task Leaf_connection_handshake_succeeds()
    {
        using var listener = new TcpListener(IPAddress.Loopback, 0);
        listener.Start();
        var port = ((IPEndPoint)listener.LocalEndpoint).Port;

        using var remoteSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        await remoteSocket.ConnectAsync(IPAddress.Loopback, port);
        using var leafSocket = await listener.AcceptSocketAsync();
        await using var leaf = new LeafConnection(leafSocket);

        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
        var handshakeTask = leaf.PerformOutboundHandshakeAsync("LOCAL1", cts.Token);
        (await ReadLineAsync(remoteSocket, cts.Token)).ShouldBe("LEAF LOCAL1");
        await WriteLineAsync(remoteSocket, "LEAF REMOTE1", cts.Token);
        await handshakeTask;

        leaf.RemoteId.ShouldBe("REMOTE1");
    }

    // Go: TestLeafNodeRTT server/leafnode_test.go:488
    [Fact]
    public async Task Leaf_connection_inbound_handshake()
    {
        using var listener = new TcpListener(IPAddress.Loopback, 0);
        listener.Start();
        var port = ((IPEndPoint)listener.LocalEndpoint).Port;

        using var remoteSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        await remoteSocket.ConnectAsync(IPAddress.Loopback, port);
        using var leafSocket = await listener.AcceptSocketAsync();
        await using var leaf = new LeafConnection(leafSocket);

        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
        var handshakeTask = leaf.PerformInboundHandshakeAsync("SERVER1", cts.Token);
        await WriteLineAsync(remoteSocket, "LEAF REMOTE2", cts.Token);
        (await ReadLineAsync(remoteSocket, cts.Token)).ShouldBe("LEAF SERVER1");
        await handshakeTask;

        leaf.RemoteId.ShouldBe("REMOTE2");
    }

    // Go: TestLeafNodePermissions server/leafnode_test.go:1267
    [Fact]
    public async Task Leaf_LS_plus_sends_subscription_interest()
    {
        using var listener = new TcpListener(IPAddress.Loopback, 0);
        listener.Start();
        var port = ((IPEndPoint)listener.LocalEndpoint).Port;

        using var remoteSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        await remoteSocket.ConnectAsync(IPAddress.Loopback, port);
        using var leafSocket = await listener.AcceptSocketAsync();
        await using var leaf = new LeafConnection(leafSocket);

        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
        var handshakeTask = leaf.PerformOutboundHandshakeAsync("HUB", cts.Token);
        (await ReadLineAsync(remoteSocket, cts.Token)).ShouldStartWith("LEAF ");
        await WriteLineAsync(remoteSocket, "LEAF SPOKE", cts.Token);
        await handshakeTask;

        await leaf.SendLsPlusAsync("$G", "test.subject", null, cts.Token);
        (await ReadLineAsync(remoteSocket, cts.Token)).ShouldBe("LS+ $G test.subject");
    }

    // Go: TestLeafNodePermissions server/leafnode_test.go:1267
    [Fact]
    public async Task Leaf_LS_minus_sends_unsubscription()
    {
        using var listener = new TcpListener(IPAddress.Loopback, 0);
        listener.Start();
        var port = ((IPEndPoint)listener.LocalEndpoint).Port;

        using var remoteSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        await remoteSocket.ConnectAsync(IPAddress.Loopback, port);
        using var leafSocket = await listener.AcceptSocketAsync();
        await using var leaf = new LeafConnection(leafSocket);

        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
        var handshakeTask = leaf.PerformOutboundHandshakeAsync("HUB", cts.Token);
        (await ReadLineAsync(remoteSocket, cts.Token)).ShouldStartWith("LEAF ");
        await WriteLineAsync(remoteSocket, "LEAF SPOKE", cts.Token);
        await handshakeTask;

        await leaf.SendLsMinusAsync("$G", "test.subject", null, cts.Token);
        (await ReadLineAsync(remoteSocket, cts.Token)).ShouldBe("LS- $G test.subject");
    }

    // Go: TestLeafNodePermissions server/leafnode_test.go:1267
    [Fact]
    public async Task Leaf_LS_plus_with_queue_group()
    {
        using var listener = new TcpListener(IPAddress.Loopback, 0);
        listener.Start();
        var port = ((IPEndPoint)listener.LocalEndpoint).Port;

        using var remoteSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        await remoteSocket.ConnectAsync(IPAddress.Loopback, port);
        using var leafSocket = await listener.AcceptSocketAsync();
        await using var leaf = new LeafConnection(leafSocket);

        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
        var handshakeTask = leaf.PerformOutboundHandshakeAsync("HUB", cts.Token);
        (await ReadLineAsync(remoteSocket, cts.Token)).ShouldStartWith("LEAF ");
        await WriteLineAsync(remoteSocket, "LEAF SPOKE", cts.Token);
        await handshakeTask;

        await leaf.SendLsPlusAsync("$G", "queue.subject", "workers", cts.Token);
        (await ReadLineAsync(remoteSocket, cts.Token)).ShouldBe("LS+ $G queue.subject workers");
    }

    // Go: TestLeafNodeInterestPropagationDaisychain server/leafnode_test.go:3953
    [Fact]
    public async Task Leaf_receives_remote_subscription()
    {
        using var listener = new TcpListener(IPAddress.Loopback, 0);
        listener.Start();
        var port = ((IPEndPoint)listener.LocalEndpoint).Port;

        using var remoteSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        await remoteSocket.ConnectAsync(IPAddress.Loopback, port);
        using var leafSocket = await listener.AcceptSocketAsync();
        await using var leaf = new LeafConnection(leafSocket);

        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
        var handshakeTask = leaf.PerformOutboundHandshakeAsync("HUB", cts.Token);
        (await ReadLineAsync(remoteSocket, cts.Token)).ShouldStartWith("LEAF ");
        await WriteLineAsync(remoteSocket, "LEAF SPOKE", cts.Token);
        await handshakeTask;

        var received = new TaskCompletionSource<RemoteSubscription>(TaskCreationOptions.RunContinuationsAsynchronously);
        leaf.RemoteSubscriptionReceived = sub =>
        {
            received.TrySetResult(sub);
            return Task.CompletedTask;
        };
        leaf.StartLoop(cts.Token);

        await WriteLineAsync(remoteSocket, "LS+ $G events.>", cts.Token);
        var result = await received.Task.WaitAsync(cts.Token);
        result.Account.ShouldBe("$G");
        result.Subject.ShouldBe("events.>");
        result.IsRemoval.ShouldBeFalse();
    }

    // Go: TestLeafNodeInterestPropagationDaisychain server/leafnode_test.go:3953
    [Fact]
    public async Task Leaf_receives_remote_unsubscription()
    {
        using var listener = new TcpListener(IPAddress.Loopback, 0);
        listener.Start();
        var port = ((IPEndPoint)listener.LocalEndpoint).Port;

        using var remoteSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        await remoteSocket.ConnectAsync(IPAddress.Loopback, port);
        using var leafSocket = await listener.AcceptSocketAsync();
        await using var leaf = new LeafConnection(leafSocket);

        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
        var handshakeTask = leaf.PerformOutboundHandshakeAsync("HUB", cts.Token);
        (await ReadLineAsync(remoteSocket, cts.Token)).ShouldStartWith("LEAF ");
        await WriteLineAsync(remoteSocket, "LEAF SPOKE", cts.Token);
        await handshakeTask;

        var received = new TaskCompletionSource<RemoteSubscription>(TaskCreationOptions.RunContinuationsAsynchronously);
        leaf.RemoteSubscriptionReceived = sub =>
        {
            if (sub.IsRemoval)
                received.TrySetResult(sub);
            return Task.CompletedTask;
        };
        leaf.StartLoop(cts.Token);

        await WriteLineAsync(remoteSocket, "LS+ $G events.>", cts.Token);
        await PollHelper.YieldForAsync(100);
        await WriteLineAsync(remoteSocket, "LS- $G events.>", cts.Token);

        var result = await received.Task.WaitAsync(cts.Token);
        result.IsRemoval.ShouldBeTrue();
        result.Subject.ShouldBe("events.>");
    }

    // Go: TestLeafNodeOriginClusterInfo server/leafnode_test.go:1942
    [Fact]
    public async Task Leaf_handshake_propagates_JetStream_domain()
    {
        using var listener = new TcpListener(IPAddress.Loopback, 0);
        listener.Start();
        var port = ((IPEndPoint)listener.LocalEndpoint).Port;

        using var remoteSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        await remoteSocket.ConnectAsync(IPAddress.Loopback, port);
        using var leafSocket = await listener.AcceptSocketAsync();
        await using var leaf = new LeafConnection(leafSocket) { JetStreamDomain = "hub-domain" };

        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
        var handshakeTask = leaf.PerformOutboundHandshakeAsync("HUB", cts.Token);
        var line = await ReadLineAsync(remoteSocket, cts.Token);
        line.ShouldBe("LEAF HUB domain=hub-domain");
        await WriteLineAsync(remoteSocket, "LEAF SPOKE domain=spoke-domain", cts.Token);
        await handshakeTask;

        leaf.RemoteJetStreamDomain.ShouldBe("spoke-domain");
    }

    // Go: TestLeafNodeRemoteIsHub server/leafnode_test.go:1177
    [Fact]
    public async Task Leaf_manager_solicited_connection_backoff()
    {
        // Verify the exponential backoff computation
        LeafNodeManager.ComputeBackoff(0).ShouldBe(TimeSpan.FromSeconds(1));
        LeafNodeManager.ComputeBackoff(1).ShouldBe(TimeSpan.FromSeconds(2));
        LeafNodeManager.ComputeBackoff(2).ShouldBe(TimeSpan.FromSeconds(4));
        LeafNodeManager.ComputeBackoff(3).ShouldBe(TimeSpan.FromSeconds(8));
        LeafNodeManager.ComputeBackoff(4).ShouldBe(TimeSpan.FromSeconds(16));
        LeafNodeManager.ComputeBackoff(5).ShouldBe(TimeSpan.FromSeconds(32));
        LeafNodeManager.ComputeBackoff(6).ShouldBe(TimeSpan.FromSeconds(60));
        LeafNodeManager.ComputeBackoff(7).ShouldBe(TimeSpan.FromSeconds(60));
        LeafNodeManager.ComputeBackoff(-1).ShouldBe(TimeSpan.FromSeconds(1));
    }

    // Go: TestLeafNodeHubWithGateways server/leafnode_test.go:1584
    [Fact]
    public async Task Leaf_hub_spoke_message_round_trip()
    {
        await using var fixture = await LeafFixture.StartAsync();

        await using var hubConn = new NatsConnection(new NatsOpts
        {
            Url = $"nats://127.0.0.1:{fixture.Hub.Port}",
        });
        await hubConn.ConnectAsync();
        await using var spokeConn = new NatsConnection(new NatsOpts
        {
            Url = $"nats://127.0.0.1:{fixture.Spoke.Port}",
        });
        await spokeConn.ConnectAsync();

        await using var sub = await spokeConn.SubscribeCoreAsync<string>("leaf.roundtrip");
        await spokeConn.PingAsync();
        await fixture.WaitForRemoteInterestOnHubAsync("leaf.roundtrip");

        await hubConn.PublishAsync("leaf.roundtrip", "round-trip-msg");

        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
        (await sub.Msgs.ReadAsync(cts.Token)).Data.ShouldBe("round-trip-msg");
    }

    // Go: TestLeafNodeStreamAndShadowSubs server/leafnode_test.go:6176
    [Fact]
    public async Task Leaf_spoke_to_hub_message_delivery()
    {
        await using var fixture = await LeafFixture.StartAsync();

        await using var hubConn = new NatsConnection(new NatsOpts
        {
            Url = $"nats://127.0.0.1:{fixture.Hub.Port}",
        });
        await hubConn.ConnectAsync();
        await using var spokeConn = new NatsConnection(new NatsOpts
        {
            Url = $"nats://127.0.0.1:{fixture.Spoke.Port}",
        });
        await spokeConn.ConnectAsync();

        await using var sub = await hubConn.SubscribeCoreAsync<string>("leaf.reverse");
        await hubConn.PingAsync();
        await fixture.WaitForRemoteInterestOnSpokeAsync("leaf.reverse");

        await spokeConn.PublishAsync("leaf.reverse", "reverse-msg");

        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
        (await sub.Msgs.ReadAsync(cts.Token)).Data.ShouldBe("reverse-msg");
    }

    // Go: TestLeafNodeQueueGroupDistribution server/leafnode_test.go:4021
    [Fact]
    public async Task Leaf_queue_subscription_delivery()
    {
        await using var fixture = await LeafFixture.StartAsync();

        await using var hubConn = new NatsConnection(new NatsOpts
        {
            Url = $"nats://127.0.0.1:{fixture.Hub.Port}",
        });
        await hubConn.ConnectAsync();
        await using var spokeConn = new NatsConnection(new NatsOpts
        {
            Url = $"nats://127.0.0.1:{fixture.Spoke.Port}",
        });
        await spokeConn.ConnectAsync();

        await using var sub = await spokeConn.SubscribeCoreAsync<string>("leaf.queue", queueGroup: "workers");
        await spokeConn.PingAsync();
        await fixture.WaitForRemoteInterestOnHubAsync("leaf.queue");

        await hubConn.PublishAsync("leaf.queue", "queue-msg");

        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
        (await sub.Msgs.ReadAsync(cts.Token)).Data.ShouldBe("queue-msg");
    }

    // Go: TestLeafNodeDuplicateMsg server/leafnode_test.go:6513
    [Fact]
    public async Task Leaf_no_remote_interest_for_unsubscribed_subject()
    {
        await using var fixture = await LeafFixture.StartAsync();
        fixture.Hub.HasRemoteInterest("nonexistent.leaf.subject").ShouldBeFalse();
        fixture.Spoke.HasRemoteInterest("nonexistent.leaf.subject").ShouldBeFalse();
    }

    // Go: TestLeafNodePermissions server/leafnode_test.go:1267
    [Fact]
    public async Task Leaf_connection_LMSG_sends_message()
    {
        using var listener = new TcpListener(IPAddress.Loopback, 0);
        listener.Start();
        var port = ((IPEndPoint)listener.LocalEndpoint).Port;

        using var remoteSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        await remoteSocket.ConnectAsync(IPAddress.Loopback, port);
        using var leafSocket = await listener.AcceptSocketAsync();
        await using var leaf = new LeafConnection(leafSocket);

        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
        var handshakeTask = leaf.PerformOutboundHandshakeAsync("HUB", cts.Token);
        (await ReadLineAsync(remoteSocket, cts.Token)).ShouldStartWith("LEAF ");
        await WriteLineAsync(remoteSocket, "LEAF SPOKE", cts.Token);
        await handshakeTask;

        var payload = Encoding.UTF8.GetBytes("hello-leaf");
        await leaf.SendMessageAsync("$G", "test.msg", "reply.to", payload, cts.Token);

        var line = await ReadLineAsync(remoteSocket, cts.Token);
        line.ShouldBe("LMSG $G test.msg reply.to 10");

        // Read payload + CRLF
        var buf = new byte[12]; // 10 payload + 2 CRLF
        var offset = 0;
        while (offset < 12)
        {
            var n = await remoteSocket.ReceiveAsync(buf.AsMemory(offset), SocketFlags.None, cts.Token);
            offset += n;
        }

        Encoding.UTF8.GetString(buf, 0, 10).ShouldBe("hello-leaf");
    }

    // Go: TestLeafNodeIsolatedLeafSubjectPropagationGlobal server/leafnode_test.go:10280
    [Fact]
    public async Task Leaf_LMSG_with_no_reply()
    {
        using var listener = new TcpListener(IPAddress.Loopback, 0);
        listener.Start();

        using var remoteSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        await remoteSocket.ConnectAsync(IPAddress.Loopback, ((IPEndPoint)listener.LocalEndpoint).Port);
        using var leafSocket = await listener.AcceptSocketAsync();
        await using var leaf = new LeafConnection(leafSocket);

        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
        var handshakeTask = leaf.PerformOutboundHandshakeAsync("HUB", cts.Token);
        (await ReadLineAsync(remoteSocket, cts.Token)).ShouldStartWith("LEAF ");
        await WriteLineAsync(remoteSocket, "LEAF SPOKE", cts.Token);
        await handshakeTask;

        await leaf.SendMessageAsync("$G", "no.reply", null, "data"u8.ToArray(), cts.Token);
        var line = await ReadLineAsync(remoteSocket, cts.Token);
        line.ShouldBe("LMSG $G no.reply - 4");
    }

    // ════════════════════════════════════════════════════════════════════
    // Helpers
    // ════════════════════════════════════════════════════════════════════

    private static async Task<string> ReadLineAsync(Socket socket, CancellationToken ct)
    {
        var bytes = new List<byte>(64);
        var single = new byte[1];
        while (true)
        {
            var read = await socket.ReceiveAsync(single, SocketFlags.None, ct);
            if (read == 0)
                break;
            if (single[0] == (byte)'\n')
                break;
            if (single[0] != (byte)'\r')
                bytes.Add(single[0]);
        }

        return Encoding.ASCII.GetString([.. bytes]);
    }

    private static Task WriteLineAsync(Socket socket, string line, CancellationToken ct)
        => socket.SendAsync(Encoding.ASCII.GetBytes($"{line}\r\n"), SocketFlags.None, ct).AsTask();
}

// ════════════════════════════════════════════════════════════════════════
// Shared Fixtures
// ════════════════════════════════════════════════════════════════════════

internal sealed class TwoGatewayFixture : IAsyncDisposable
{
    private readonly CancellationTokenSource _localCts;
    private readonly CancellationTokenSource _remoteCts;

    private TwoGatewayFixture(NatsServer local, NatsServer remote, CancellationTokenSource localCts, CancellationTokenSource remoteCts)
    {
        Local = local;
        Remote = remote;
        _localCts = localCts;
        _remoteCts = remoteCts;
    }

    public NatsServer Local { get; }
    public NatsServer Remote { get; }

    public static async Task<TwoGatewayFixture> StartAsync()
    {
        var localOptions = new NatsOptions
        {
            Host = "127.0.0.1",
            Port = 0,
            Gateway = new GatewayOptions
            {
                Name = "LOCAL",
                Host = "127.0.0.1",
                Port = 0,
            },
        };

        var local = new NatsServer(localOptions, NullLoggerFactory.Instance);
        var localCts = new CancellationTokenSource();
        _ = local.StartAsync(localCts.Token);
        await local.WaitForReadyAsync();

        var remoteOptions = new NatsOptions
        {
            Host = "127.0.0.1",
            Port = 0,
            Gateway = new GatewayOptions
            {
                Name = "REMOTE",
                Host = "127.0.0.1",
                Port = 0,
                Remotes = [local.GatewayListen!],
            },
        };

        var remote = new NatsServer(remoteOptions, NullLoggerFactory.Instance);
        var remoteCts = new CancellationTokenSource();
        _ = remote.StartAsync(remoteCts.Token);
        await remote.WaitForReadyAsync();

        await PollHelper.WaitUntilAsync(() => !((local.Stats.Gateways == 0 || remote.Stats.Gateways == 0)));

        return new TwoGatewayFixture(local, remote, localCts, remoteCts);
    }

    public async ValueTask DisposeAsync()
    {
        await _localCts.CancelAsync();
        await _remoteCts.CancelAsync();
        Local.Dispose();
        Remote.Dispose();
        _localCts.Dispose();
        _remoteCts.Dispose();
    }
}

/// <summary>
/// Leaf fixture duplicated here to avoid cross-namespace dependencies.
/// Uses hub and spoke servers connected via leaf node protocol.
/// </summary>
internal sealed class LeafFixture : IAsyncDisposable
{
    private readonly CancellationTokenSource _hubCts;
    private readonly CancellationTokenSource _spokeCts;

    private LeafFixture(NatsServer hub, NatsServer spoke, CancellationTokenSource hubCts, CancellationTokenSource spokeCts)
    {
        Hub = hub;
        Spoke = spoke;
        _hubCts = hubCts;
        _spokeCts = spokeCts;
    }

    public NatsServer Hub { get; }
    public NatsServer Spoke { get; }

    public static async Task<LeafFixture> StartAsync()
    {
        var hubOptions = new NatsOptions
        {
            Host = "127.0.0.1",
            Port = 0,
            LeafNode = new LeafNodeOptions
            {
                Host = "127.0.0.1",
                Port = 0,
            },
        };

        var hub = new NatsServer(hubOptions, NullLoggerFactory.Instance);
        var hubCts = new CancellationTokenSource();
        _ = hub.StartAsync(hubCts.Token);
        await hub.WaitForReadyAsync();

        var spokeOptions = new NatsOptions
        {
            Host = "127.0.0.1",
            Port = 0,
            LeafNode = new LeafNodeOptions
            {
                Host = "127.0.0.1",
                Port = 0,
                Remotes = [hub.LeafListen!],
            },
        };

        var spoke = new NatsServer(spokeOptions, NullLoggerFactory.Instance);
        var spokeCts = new CancellationTokenSource();
        _ = spoke.StartAsync(spokeCts.Token);
        await spoke.WaitForReadyAsync();

        await PollHelper.WaitUntilAsync(() => !((hub.Stats.Leafs == 0 || spoke.Stats.Leafs == 0)));

        return new LeafFixture(hub, spoke, hubCts, spokeCts);
    }

    public async Task WaitForRemoteInterestOnHubAsync(string subject)
    {
        await PollHelper.WaitOrThrowAsync(
            () => Hub.HasRemoteInterest(subject),
            $"Timed out waiting for remote interest on hub for '{subject}'.");
    }

    public async Task WaitForRemoteInterestOnSpokeAsync(string subject)
    {
        await PollHelper.WaitOrThrowAsync(
            () => Spoke.HasRemoteInterest(subject),
            $"Timed out waiting for remote interest on spoke for '{subject}'.");
    }

    public async ValueTask DisposeAsync()
    {
        await _spokeCts.CancelAsync();
        await _hubCts.CancelAsync();
        Spoke.Dispose();
        Hub.Dispose();
        _spokeCts.Dispose();
        _hubCts.Dispose();
    }
}
