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

namespace NATS.Server.Tests.Routes;

/// <summary>
/// Tests for route subscription propagation: RS+/RS-, wildcard subs, queue subs,
/// unsubscribe propagation, and account-scoped interest.
/// Ported from Go: server/routes_test.go.
/// </summary>
public class RouteSubscriptionTests
{
    // -- Helpers --

    private static async Task<(NatsServer Server, CancellationTokenSource Cts)> StartServerAsync(
        NatsOptions opts)
    {
        var server = new NatsServer(opts, NullLoggerFactory.Instance);
        var cts = new CancellationTokenSource();
        _ = server.StartAsync(cts.Token);
        await server.WaitForReadyAsync();
        return (server, cts);
    }

    private static NatsOptions MakeClusterOpts(string? clusterName = null, string? seed = null)
    {
        return new NatsOptions
        {
            Host = "127.0.0.1",
            Port = 0,
            Cluster = new ClusterOptions
            {
                Name = clusterName ?? Guid.NewGuid().ToString("N"),
                Host = "127.0.0.1",
                Port = 0,
                Routes = seed is null ? [] : [seed],
            },
        };
    }

    private static async Task WaitForRouteFormation(NatsServer a, NatsServer b, int timeoutMs = 5000)
    {
        using var timeout = new CancellationTokenSource(timeoutMs);
        while (!timeout.IsCancellationRequested &&
               (Interlocked.Read(ref a.Stats.Routes) == 0 ||
                Interlocked.Read(ref b.Stats.Routes) == 0))
        {
            await Task.Delay(50, timeout.Token).ContinueWith(_ => { }, TaskScheduler.Default);
        }
    }

    private static async Task WaitForCondition(Func<bool> predicate, int timeoutMs = 5000)
    {
        using var cts = new CancellationTokenSource(timeoutMs);
        while (!cts.IsCancellationRequested)
        {
            if (predicate()) return;
            await Task.Delay(20, cts.Token).ContinueWith(_ => { }, TaskScheduler.Default);
        }

        throw new TimeoutException("Condition not met.");
    }

    private static async Task DisposeServers(params (NatsServer Server, CancellationTokenSource Cts)[] servers)
    {
        foreach (var (server, cts) in servers)
        {
            await cts.CancelAsync();
            server.Dispose();
            cts.Dispose();
        }
    }

    // -- Tests: RS+ propagation --

    // Go: TestRoutePoolPerAccountSubUnsubProtoParsing server/routes_test.go:3104 (plain sub)
    [Fact]
    public async Task Plain_subscription_propagates_remote_interest()
    {
        var cluster = Guid.NewGuid().ToString("N");
        var a = await StartServerAsync(MakeClusterOpts(cluster));
        var b = await StartServerAsync(MakeClusterOpts(cluster, a.Server.ClusterListen!));

        try
        {
            await WaitForRouteFormation(a.Server, b.Server);

            await using var nc = new NatsConnection(new NatsOpts
            {
                Url = $"nats://127.0.0.1:{a.Server.Port}",
            });
            await nc.ConnectAsync();
            await using var sub = await nc.SubscribeCoreAsync<string>("sub.test");
            await nc.PingAsync();

            await WaitForCondition(() => b.Server.HasRemoteInterest("sub.test"));
            b.Server.HasRemoteInterest("sub.test").ShouldBeTrue();
        }
        finally
        {
            await DisposeServers(a, b);
        }
    }

    // Go: TestRoutePoolPerAccountSubUnsubProtoParsing server/routes_test.go:3104 (wildcard * sub)
    [Fact]
    public async Task Wildcard_star_subscription_propagates_remote_interest()
    {
        var cluster = Guid.NewGuid().ToString("N");
        var a = await StartServerAsync(MakeClusterOpts(cluster));
        var b = await StartServerAsync(MakeClusterOpts(cluster, a.Server.ClusterListen!));

        try
        {
            await WaitForRouteFormation(a.Server, b.Server);

            await using var nc = new NatsConnection(new NatsOpts
            {
                Url = $"nats://127.0.0.1:{a.Server.Port}",
            });
            await nc.ConnectAsync();
            await using var sub = await nc.SubscribeCoreAsync<string>("wildcard.*");
            await nc.PingAsync();

            await WaitForCondition(() => b.Server.HasRemoteInterest("wildcard.test"));
            b.Server.HasRemoteInterest("wildcard.test").ShouldBeTrue();
            b.Server.HasRemoteInterest("wildcard.other").ShouldBeTrue();
            b.Server.HasRemoteInterest("no.match").ShouldBeFalse();
        }
        finally
        {
            await DisposeServers(a, b);
        }
    }

    // Go: TestRoutePoolPerAccountSubUnsubProtoParsing server/routes_test.go:3104 (wildcard > sub)
    [Fact]
    public async Task Wildcard_gt_subscription_propagates_remote_interest()
    {
        var cluster = Guid.NewGuid().ToString("N");
        var a = await StartServerAsync(MakeClusterOpts(cluster));
        var b = await StartServerAsync(MakeClusterOpts(cluster, a.Server.ClusterListen!));

        try
        {
            await WaitForRouteFormation(a.Server, b.Server);

            await using var nc = new NatsConnection(new NatsOpts
            {
                Url = $"nats://127.0.0.1:{a.Server.Port}",
            });
            await nc.ConnectAsync();
            await using var sub = await nc.SubscribeCoreAsync<string>("events.>");
            await nc.PingAsync();

            await WaitForCondition(() => b.Server.HasRemoteInterest("events.a"));
            b.Server.HasRemoteInterest("events.a").ShouldBeTrue();
            b.Server.HasRemoteInterest("events.a.b.c").ShouldBeTrue();
            b.Server.HasRemoteInterest("other.a").ShouldBeFalse();
        }
        finally
        {
            await DisposeServers(a, b);
        }
    }

    // Go: TestRoutePoolPerAccountSubUnsubProtoParsing server/routes_test.go:3104 (unsub)
    [Fact]
    public async Task Unsubscribe_removes_remote_interest()
    {
        var cluster = Guid.NewGuid().ToString("N");
        var a = await StartServerAsync(MakeClusterOpts(cluster));
        var b = await StartServerAsync(MakeClusterOpts(cluster, a.Server.ClusterListen!));

        try
        {
            await WaitForRouteFormation(a.Server, b.Server);

            await using var nc = new NatsConnection(new NatsOpts
            {
                Url = $"nats://127.0.0.1:{a.Server.Port}",
            });
            await nc.ConnectAsync();
            var sub = await nc.SubscribeCoreAsync<string>("unsub.test");
            await nc.PingAsync();

            await WaitForCondition(() => b.Server.HasRemoteInterest("unsub.test"));
            b.Server.HasRemoteInterest("unsub.test").ShouldBeTrue();

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

            // Wait for interest to be removed
            await WaitForCondition(() => !b.Server.HasRemoteInterest("unsub.test"));
            b.Server.HasRemoteInterest("unsub.test").ShouldBeFalse();
        }
        finally
        {
            await DisposeServers(a, b);
        }
    }

    // Go: RS+ wire protocol parsing (low-level)
    [Fact]
    public async Task RSplus_frame_registers_remote_interest_via_wire()
    {
        using var listener = new TcpListener(IPAddress.Loopback, 0);
        listener.Start();
        var port = ((IPEndPoint)listener.LocalEndpoint).Port;

        using var remote = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        await remote.ConnectAsync(IPAddress.Loopback, port);
        using var routeSock = await listener.AcceptSocketAsync();
        await using var route = new RouteConnection(routeSock);
        using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));

        var handshakeTask = route.PerformOutboundHandshakeAsync("LOCAL", timeout.Token);
        _ = await ReadLineAsync(remote, timeout.Token);
        await WriteLineAsync(remote, "ROUTE REMOTE", timeout.Token);
        await handshakeTask;

        using var subList = new SubList();
        route.RemoteSubscriptionReceived = sub =>
        {
            subList.ApplyRemoteSub(sub);
            return Task.CompletedTask;
        };
        route.StartFrameLoop(timeout.Token);

        await WriteLineAsync(remote, "RS+ $G foo.bar", timeout.Token);
        await WaitForCondition(() => subList.HasRemoteInterest("foo.bar"));
        subList.HasRemoteInterest("foo.bar").ShouldBeTrue();
    }

    // Go: RS- wire protocol parsing (low-level)
    [Fact]
    public async Task RSminus_frame_removes_remote_interest_via_wire()
    {
        using var listener = new TcpListener(IPAddress.Loopback, 0);
        listener.Start();
        var port = ((IPEndPoint)listener.LocalEndpoint).Port;

        using var remote = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        await remote.ConnectAsync(IPAddress.Loopback, port);
        using var routeSock = await listener.AcceptSocketAsync();
        await using var route = new RouteConnection(routeSock);
        using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));

        var handshakeTask = route.PerformOutboundHandshakeAsync("LOCAL", timeout.Token);
        _ = await ReadLineAsync(remote, timeout.Token);
        await WriteLineAsync(remote, "ROUTE REMOTE", timeout.Token);
        await handshakeTask;

        using var subList = new SubList();
        route.RemoteSubscriptionReceived = sub =>
        {
            subList.ApplyRemoteSub(sub);
            return Task.CompletedTask;
        };
        route.StartFrameLoop(timeout.Token);

        await WriteLineAsync(remote, "RS+ $G foo.*", timeout.Token);
        await WaitForCondition(() => subList.HasRemoteInterest("foo.bar"));

        await WriteLineAsync(remote, "RS- $G foo.*", timeout.Token);
        await WaitForCondition(() => !subList.HasRemoteInterest("foo.bar"));
        subList.HasRemoteInterest("foo.bar").ShouldBeFalse();
    }

    // Go: RS+ with queue group
    [Fact]
    public async Task RSplus_with_queue_group_registers_remote_interest()
    {
        using var listener = new TcpListener(IPAddress.Loopback, 0);
        listener.Start();
        var port = ((IPEndPoint)listener.LocalEndpoint).Port;

        using var remote = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        await remote.ConnectAsync(IPAddress.Loopback, port);
        using var routeSock = await listener.AcceptSocketAsync();
        await using var route = new RouteConnection(routeSock);
        using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));

        var handshakeTask = route.PerformOutboundHandshakeAsync("LOCAL", timeout.Token);
        _ = await ReadLineAsync(remote, timeout.Token);
        await WriteLineAsync(remote, "ROUTE REMOTE", timeout.Token);
        await handshakeTask;

        RemoteSubscription? received = null;
        route.RemoteSubscriptionReceived = sub =>
        {
            received = sub;
            return Task.CompletedTask;
        };
        route.StartFrameLoop(timeout.Token);

        await WriteLineAsync(remote, "RS+ $G foo.bar myqueue", timeout.Token);
        await WaitForCondition(() => received != null);

        received.ShouldNotBeNull();
        received!.Subject.ShouldBe("foo.bar");
        received.Queue.ShouldBe("myqueue");
    }

    // Go: RS+ with account scope
    [Fact]
    public async Task RSplus_with_account_scope_registers_interest_in_account()
    {
        using var listener = new TcpListener(IPAddress.Loopback, 0);
        listener.Start();
        var port = ((IPEndPoint)listener.LocalEndpoint).Port;

        using var remote = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        await remote.ConnectAsync(IPAddress.Loopback, port);
        using var routeSock = await listener.AcceptSocketAsync();
        await using var route = new RouteConnection(routeSock);
        using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));

        var handshakeTask = route.PerformOutboundHandshakeAsync("LOCAL", timeout.Token);
        _ = await ReadLineAsync(remote, timeout.Token);
        await WriteLineAsync(remote, "ROUTE REMOTE", timeout.Token);
        await handshakeTask;

        using var subList = new SubList();
        route.RemoteSubscriptionReceived = sub =>
        {
            subList.ApplyRemoteSub(sub);
            return Task.CompletedTask;
        };
        route.StartFrameLoop(timeout.Token);

        await WriteLineAsync(remote, "RS+ ACCT_A orders.created", timeout.Token);
        await WaitForCondition(() => subList.HasRemoteInterest("ACCT_A", "orders.created"));
        subList.HasRemoteInterest("ACCT_A", "orders.created").ShouldBeTrue();
    }

    // Go: TestRoutePoolPerAccountSubUnsubProtoParsing server/routes_test.go:3104
    [Fact]
    public async Task Queue_subscription_propagates_across_route()
    {
        var cluster = Guid.NewGuid().ToString("N");
        var a = await StartServerAsync(MakeClusterOpts(cluster));
        var b = await StartServerAsync(MakeClusterOpts(cluster, a.Server.ClusterListen!));

        try
        {
            await WaitForRouteFormation(a.Server, b.Server);

            using var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            await sock.ConnectAsync(IPAddress.Loopback, a.Server.Port);
            _ = await ReadLineAsync(sock, default);
            await sock.SendAsync(Encoding.ASCII.GetBytes("CONNECT {}\r\nSUB foo queue1 1\r\nPING\r\n"));
            await ReadUntilAsync(sock, "PONG");

            await WaitForCondition(() => b.Server.HasRemoteInterest("foo"));
            b.Server.HasRemoteInterest("foo").ShouldBeTrue();
            sock.Dispose();
        }
        finally
        {
            await DisposeServers(a, b);
        }
    }

    // Go: TestRoutePoolPerAccountSubUnsubProtoParsing server/routes_test.go:3104 (queue unsub)
    [Fact]
    public async Task Queue_subscription_delivery_picks_one_per_group()
    {
        var cluster = Guid.NewGuid().ToString("N");
        var optsA = MakeClusterOpts(cluster);
        optsA.Cluster!.PoolSize = 1;
        var a = await StartServerAsync(optsA);
        var optsB = MakeClusterOpts(cluster, a.Server.ClusterListen!);
        optsB.Cluster!.PoolSize = 1;
        var b = await StartServerAsync(optsB);

        try
        {
            await WaitForRouteFormation(a.Server, b.Server);

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

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

            await using var sub1 = await nc1.SubscribeCoreAsync<string>("queue.test", queueGroup: "grp");
            await using var sub2 = await nc2.SubscribeCoreAsync<string>("queue.test", queueGroup: "grp");
            await nc1.PingAsync();
            await nc2.PingAsync();

            await WaitForCondition(() => b.Server.HasRemoteInterest("queue.test"));

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

            // Send 10 messages. Each should go to exactly one queue member.
            for (var i = 0; i < 10; i++)
                await publisher.PublishAsync("queue.test", $"qmsg-{i}");

            // Collect messages from both subscribers
            var received = 0;
            using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));

            async Task CollectMessages(INatsSub<string> sub)
            {
                try
                {
                    while (!timeout.IsCancellationRequested)
                    {
                        _ = await sub.Msgs.ReadAsync(timeout.Token);
                        Interlocked.Increment(ref received);
                    }
                }
                catch (OperationCanceledException) { }
            }

            var t1 = CollectMessages(sub1);
            var t2 = CollectMessages(sub2);

            // Wait for all messages
            await WaitForCondition(() => Volatile.Read(ref received) >= 10, 5000);

            // Total received should be exactly 10 (one per message)
            Volatile.Read(ref received).ShouldBe(10);
        }
        finally
        {
            await DisposeServers(a, b);
        }
    }

    // Go: Interest propagation for multiple subjects
    [Fact]
    public async Task Multiple_subjects_propagate_independently()
    {
        var cluster = Guid.NewGuid().ToString("N");
        var a = await StartServerAsync(MakeClusterOpts(cluster));
        var b = await StartServerAsync(MakeClusterOpts(cluster, a.Server.ClusterListen!));

        try
        {
            await WaitForRouteFormation(a.Server, b.Server);

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

            await using var sub1 = await nc.SubscribeCoreAsync<string>("alpha");
            await using var sub2 = await nc.SubscribeCoreAsync<string>("beta");
            await nc.PingAsync();

            await WaitForCondition(() => b.Server.HasRemoteInterest("alpha") && b.Server.HasRemoteInterest("beta"));
            b.Server.HasRemoteInterest("alpha").ShouldBeTrue();
            b.Server.HasRemoteInterest("beta").ShouldBeTrue();
            b.Server.HasRemoteInterest("gamma").ShouldBeFalse();
        }
        finally
        {
            await DisposeServers(a, b);
        }
    }

    // Go: RS+ account scope with NatsClient auth
    [Fact]
    public async Task Account_scoped_subscription_propagates_remote_interest()
    {
        var users = new User[]
        {
            new() { Username = "user_a", Password = "pass", Account = "A" },
            new() { Username = "user_b", Password = "pass", Account = "B" },
        };
        var cluster = Guid.NewGuid().ToString("N");

        var optsA = new NatsOptions
        {
            Host = "127.0.0.1",
            Port = 0,
            Users = users,
            Cluster = new ClusterOptions
            {
                Name = cluster,
                Host = "127.0.0.1",
                Port = 0,
            },
        };
        var a = await StartServerAsync(optsA);

        var optsB = new NatsOptions
        {
            Host = "127.0.0.1",
            Port = 0,
            Users = users,
            Cluster = new ClusterOptions
            {
                Name = cluster,
                Host = "127.0.0.1",
                Port = 0,
                Routes = [a.Server.ClusterListen!],
            },
        };
        var b = await StartServerAsync(optsB);

        try
        {
            await WaitForRouteFormation(a.Server, b.Server);

            await using var nc = new NatsConnection(new NatsOpts
            {
                Url = $"nats://user_a:pass@127.0.0.1:{a.Server.Port}",
            });
            await nc.ConnectAsync();
            await using var sub = await nc.SubscribeCoreAsync<string>("acct.sub");
            await nc.PingAsync();

            await WaitForCondition(() => b.Server.HasRemoteInterest("A", "acct.sub"));
            b.Server.HasRemoteInterest("A", "acct.sub").ShouldBeTrue();
            // Account B should NOT have interest
            b.Server.HasRemoteInterest("B", "acct.sub").ShouldBeFalse();
        }
        finally
        {
            await DisposeServers(a, b);
        }
    }

    // Go: TestRoutePerAccount server/routes_test.go:2539
    [Fact]
    public async Task Account_scoped_messages_do_not_leak_to_other_accounts()
    {
        var users = new User[]
        {
            new() { Username = "ua", Password = "p", Account = "A" },
            new() { Username = "ub", Password = "p", Account = "B" },
        };
        var cluster = Guid.NewGuid().ToString("N");

        var optsA = new NatsOptions
        {
            Host = "127.0.0.1",
            Port = 0,
            Users = users,
            Cluster = new ClusterOptions
            {
                Name = cluster,
                Host = "127.0.0.1",
                Port = 0,
            },
        };
        var a = await StartServerAsync(optsA);

        var optsB = new NatsOptions
        {
            Host = "127.0.0.1",
            Port = 0,
            Users = users,
            Cluster = new ClusterOptions
            {
                Name = cluster,
                Host = "127.0.0.1",
                Port = 0,
                Routes = [a.Server.ClusterListen!],
            },
        };
        var b = await StartServerAsync(optsB);

        try
        {
            await WaitForRouteFormation(a.Server, b.Server);

            // Subscribe in account A on server B
            await using var subA = new NatsConnection(new NatsOpts
            {
                Url = $"nats://ua:p@127.0.0.1:{b.Server.Port}",
            });
            await subA.ConnectAsync();
            await using var sub = await subA.SubscribeCoreAsync<string>("isolation.test");
            await subA.PingAsync();

            // Subscribe in account B on server B
            await using var subB = new NatsConnection(new NatsOpts
            {
                Url = $"nats://ub:p@127.0.0.1:{b.Server.Port}",
            });
            await subB.ConnectAsync();
            await using var subBSub = await subB.SubscribeCoreAsync<string>("isolation.test");
            await subB.PingAsync();

            await WaitForCondition(() => a.Server.HasRemoteInterest("A", "isolation.test"));

            // Publish in account A from server A
            await using var pub = new NatsConnection(new NatsOpts
            {
                Url = $"nats://ua:p@127.0.0.1:{a.Server.Port}",
            });
            await pub.ConnectAsync();
            await pub.PublishAsync("isolation.test", "for-account-a");

            // Account A subscriber should receive the message
            using var receiveTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
            var msg = await sub.Msgs.ReadAsync(receiveTimeout.Token);
            msg.Data.ShouldBe("for-account-a");

            // Account B subscriber should NOT receive it
            using var leakTimeout = new CancellationTokenSource(TimeSpan.FromMilliseconds(500));
            await Should.ThrowAsync<OperationCanceledException>(async () =>
                await subBSub.Msgs.ReadAsync(leakTimeout.Token));
        }
        finally
        {
            await DisposeServers(a, b);
        }
    }

    // Go: Subscriber disconnect removes interest
    [Fact]
    public async Task Client_disconnect_removes_remote_interest()
    {
        var cluster = Guid.NewGuid().ToString("N");
        var optsA = MakeClusterOpts(cluster);
        optsA.Cluster!.PoolSize = 1;
        var a = await StartServerAsync(optsA);
        var optsB = MakeClusterOpts(cluster, a.Server.ClusterListen!);
        optsB.Cluster!.PoolSize = 1;
        var b = await StartServerAsync(optsB);

        try
        {
            await WaitForRouteFormation(a.Server, b.Server);

            var nc = new NatsConnection(new NatsOpts
            {
                Url = $"nats://127.0.0.1:{a.Server.Port}",
            });
            await nc.ConnectAsync();
            var sub = await nc.SubscribeCoreAsync<string>("disconnect.test");
            await nc.PingAsync();

            await WaitForCondition(() => b.Server.HasRemoteInterest("disconnect.test"));
            b.Server.HasRemoteInterest("disconnect.test").ShouldBeTrue();

            // Unsubscribe and disconnect the client
            await sub.DisposeAsync();
            await nc.PingAsync();
            await nc.DisposeAsync();

            // Interest should be removed (give extra time for propagation)
            await WaitForCondition(() => !b.Server.HasRemoteInterest("disconnect.test"), 15000);
            b.Server.HasRemoteInterest("disconnect.test").ShouldBeFalse();
        }
        finally
        {
            await DisposeServers(a, b);
        }
    }

    // Go: Interest idempotency
    [Fact]
    public async Task Duplicate_subscription_on_same_subject_does_not_double_count()
    {
        var cluster = Guid.NewGuid().ToString("N");
        var a = await StartServerAsync(MakeClusterOpts(cluster));
        var b = await StartServerAsync(MakeClusterOpts(cluster, a.Server.ClusterListen!));

        try
        {
            await WaitForRouteFormation(a.Server, b.Server);

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

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

            await using var sub1 = await nc1.SubscribeCoreAsync<string>("dup.test");
            await using var sub2 = await nc2.SubscribeCoreAsync<string>("dup.test");
            await nc1.PingAsync();
            await nc2.PingAsync();

            await WaitForCondition(() => b.Server.HasRemoteInterest("dup.test"));

            // Publish from B; should be delivered to both local subscribers on A
            await using var pub = new NatsConnection(new NatsOpts
            {
                Url = $"nats://127.0.0.1:{b.Server.Port}",
            });
            await pub.ConnectAsync();
            await pub.PublishAsync("dup.test", "to-both");

            using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
            var msg1 = await sub1.Msgs.ReadAsync(timeout.Token);
            var msg2 = await sub2.Msgs.ReadAsync(timeout.Token);
            msg1.Data.ShouldBe("to-both");
            msg2.Data.ShouldBe("to-both");
        }
        finally
        {
            await DisposeServers(a, b);
        }
    }

    // Go: Wildcard delivery
    [Fact]
    public async Task Wildcard_subscription_delivers_matching_messages_across_route()
    {
        var cluster = Guid.NewGuid().ToString("N");
        var a = await StartServerAsync(MakeClusterOpts(cluster));
        var b = await StartServerAsync(MakeClusterOpts(cluster, a.Server.ClusterListen!));

        try
        {
            await WaitForRouteFormation(a.Server, b.Server);

            await using var nc = new NatsConnection(new NatsOpts
            {
                Url = $"nats://127.0.0.1:{a.Server.Port}",
            });
            await nc.ConnectAsync();
            await using var sub = await nc.SubscribeCoreAsync<string>("data.>");
            await nc.PingAsync();

            await WaitForCondition(() => b.Server.HasRemoteInterest("data.sensor.1"));

            await using var pub = new NatsConnection(new NatsOpts
            {
                Url = $"nats://127.0.0.1:{b.Server.Port}",
            });
            await pub.ConnectAsync();
            await pub.PublishAsync("data.sensor.1", "reading");

            using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
            var msg = await sub.Msgs.ReadAsync(timeout.Token);
            msg.Subject.ShouldBe("data.sensor.1");
            msg.Data.ShouldBe("reading");
        }
        finally
        {
            await DisposeServers(a, b);
        }
    }

    // Go: No messages for non-matching subjects
    [Fact]
    public async Task Non_matching_subject_not_forwarded_across_route()
    {
        var cluster = Guid.NewGuid().ToString("N");
        var a = await StartServerAsync(MakeClusterOpts(cluster));
        var b = await StartServerAsync(MakeClusterOpts(cluster, a.Server.ClusterListen!));

        try
        {
            await WaitForRouteFormation(a.Server, b.Server);

            await using var nc = new NatsConnection(new NatsOpts
            {
                Url = $"nats://127.0.0.1:{a.Server.Port}",
            });
            await nc.ConnectAsync();
            await using var sub = await nc.SubscribeCoreAsync<string>("specific.topic");
            await nc.PingAsync();

            await WaitForCondition(() => b.Server.HasRemoteInterest("specific.topic"));

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

            // Publish to a non-matching subject
            await pub.PublishAsync("other.topic", "should-not-arrive");
            // Publish to the matching subject
            await pub.PublishAsync("specific.topic", "should-arrive");

            using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
            var msg = await sub.Msgs.ReadAsync(timeout.Token);
            msg.Data.ShouldBe("should-arrive");
        }
        finally
        {
            await DisposeServers(a, b);
        }
    }

    // -- Wire-level helpers --

    private static async Task<string> ReadLineAsync(Socket socket, CancellationToken ct)
    {
        var bytes = new List<byte>(64);
        var single = new byte[1];
        using var cts = ct.CanBeNone() ? new CancellationTokenSource(TimeSpan.FromSeconds(5)) : null;
        var effectiveCt = cts?.Token ?? ct;
        while (true)
        {
            var read = await socket.ReceiveAsync(single, SocketFlags.None, effectiveCt);
            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();

    private static async Task<string> ReadUntilAsync(Socket sock, string expected)
    {
        var sb = new StringBuilder();
        var buf = new byte[4096];
        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
        while (!sb.ToString().Contains(expected, StringComparison.Ordinal))
        {
            var n = await sock.ReceiveAsync(buf, SocketFlags.None, cts.Token);
            if (n == 0) break;
            sb.Append(Encoding.ASCII.GetString(buf, 0, n));
        }

        return sb.ToString();
    }
}

file static class CancellationTokenExtensions
{
    public static bool CanBeNone(this CancellationToken ct) => ct == default;
}
