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.Gateways;
using NATS.Server.Subscriptions;

namespace NATS.Server.Tests.Gateways;

/// <summary>
/// Gateway message forwarding, reply mapping, queue subscription delivery,
/// and cross-cluster pub/sub tests.
/// Ported from golang/nats-server/server/gateway_test.go.
/// </summary>
public class GatewayForwardingTests
{
    // ── Basic Message Forwarding ────────────────────────────────────────

    // Go: TestGatewayBasic server/gateway_test.go:399
    [Fact]
    public async Task Message_published_on_local_arrives_at_remote_subscriber()
    {
        await using var fixture = await ForwardingFixture.StartAsync();

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

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

        await using var sub = await subscriber.SubscribeCoreAsync<string>("fwd.test");
        await subscriber.PingAsync();
        await fixture.WaitForRemoteInterestOnLocalAsync("fwd.test");

        await publisher.PublishAsync("fwd.test", "hello-world");

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

    // Go: TestGatewayBasic server/gateway_test.go:399
    [Fact]
    public async Task Message_published_on_remote_arrives_at_local_subscriber()
    {
        await using var fixture = await ForwardingFixture.StartAsync();

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

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

        await using var sub = await subscriber.SubscribeCoreAsync<string>("fwd.reverse");
        await subscriber.PingAsync();
        await fixture.WaitForRemoteInterestOnRemoteAsync("fwd.reverse");

        await publisher.PublishAsync("fwd.reverse", "reverse-msg");

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

    // Go: TestGatewayMsgSentOnlyOnce server/gateway_test.go:2993
    [Fact]
    public async Task Message_forwarded_only_once_to_remote_subscriber()
    {
        await using var fixture = await ForwardingFixture.StartAsync();

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

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

        await using var sub = await subscriber.SubscribeCoreAsync<string>("once.test");
        await subscriber.PingAsync();
        await fixture.WaitForRemoteInterestOnLocalAsync("once.test");

        await publisher.PublishAsync("once.test", "exactly-once");
        await publisher.PingAsync();

        using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(3));
        var msg = await sub.Msgs.ReadAsync(timeout.Token);
        msg.Data.ShouldBe("exactly-once");

        // Wait and verify no duplicates
        await Task.Delay(300);
        using var noMoreTimeout = new CancellationTokenSource(TimeSpan.FromMilliseconds(300));
        await Should.ThrowAsync<OperationCanceledException>(async () =>
            await sub.Msgs.ReadAsync(noMoreTimeout.Token));
    }

    // Go: TestGatewaySendsToNonLocalSubs server/gateway_test.go:3140
    [Fact]
    public async Task Message_without_local_subscriber_forwarded_to_remote()
    {
        await using var fixture = await ForwardingFixture.StartAsync();

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

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

        // Subscribe only on remote, no local subscriber
        await using var sub = await subscriber.SubscribeCoreAsync<string>("only.remote");
        await subscriber.PingAsync();
        await fixture.WaitForRemoteInterestOnLocalAsync("only.remote");

        await publisher.PublishAsync("only.remote", "no-local");

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

    // Go: TestGatewayDoesntSendBackToItself server/gateway_test.go:2150
    [Fact]
    public async Task Both_local_and_remote_subscribers_receive_message_published_locally()
    {
        await using var fixture = await ForwardingFixture.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 remoteSub = await remoteConn.SubscribeCoreAsync<string>("both.test");
        await remoteConn.PingAsync();

        await using var localSub = await localConn.SubscribeCoreAsync<string>("both.test");
        await localConn.PingAsync();

        await fixture.WaitForRemoteInterestOnLocalAsync("both.test");

        await localConn.PublishAsync("both.test", "shared");
        await localConn.PingAsync();

        using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
        var localMsg = await localSub.Msgs.ReadAsync(timeout.Token);
        localMsg.Data.ShouldBe("shared");

        var remoteMsg = await remoteSub.Msgs.ReadAsync(timeout.Token);
        remoteMsg.Data.ShouldBe("shared");
    }

    // ── Wildcard Subject Forwarding ─────────────────────────────────────

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

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

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

        await using var sub = await subscriber.SubscribeCoreAsync<string>("wc.>");
        await subscriber.PingAsync();
        await fixture.WaitForRemoteInterestOnLocalAsync("wc.test.one");

        await publisher.PublishAsync("wc.test.one", "wildcard-msg");

        using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
        var msg = await sub.Msgs.ReadAsync(timeout.Token);
        msg.Subject.ShouldBe("wc.test.one");
        msg.Data.ShouldBe("wildcard-msg");
    }

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

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

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

        await using var sub = await subscriber.SubscribeCoreAsync<string>("orders.*");
        await subscriber.PingAsync();
        await fixture.WaitForRemoteInterestOnLocalAsync("orders.created");

        await publisher.PublishAsync("orders.created", "order-1");

        using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
        var msg = await sub.Msgs.ReadAsync(timeout.Token);
        msg.Subject.ShouldBe("orders.created");
        msg.Data.ShouldBe("order-1");
    }

    // ── Reply Subject Mapping (_GR_. Prefix) ────────────────────────────

    // Go: TestGatewaySendReplyAcrossGateways server/gateway_test.go:5165
    [Fact]
    public void Reply_mapper_adds_gr_prefix_with_cluster_id()
    {
        var mapped = ReplyMapper.ToGatewayReply("_INBOX.abc", "CLUSTER-A");
        mapped.ShouldNotBeNull();
        mapped.ShouldStartWith("_GR_.");
        mapped.ShouldContain("CLUSTER-A");
    }

    // Go: TestGatewaySendReplyAcrossGateways server/gateway_test.go:5165
    [Fact]
    public void Reply_mapper_restores_original_reply()
    {
        var original = "_INBOX.abc123";
        var mapped = ReplyMapper.ToGatewayReply(original, "C1");
        mapped.ShouldNotBeNull();

        ReplyMapper.TryRestoreGatewayReply(mapped, out var restored).ShouldBeTrue();
        restored.ShouldBe(original);
    }

    // Go: TestGatewaySendReplyAcrossGateways server/gateway_test.go:5165
    [Fact]
    public void Reply_mapper_handles_nested_gr_prefixes()
    {
        var original = "_INBOX.reply1";
        var once = ReplyMapper.ToGatewayReply(original, "CLUSTER-A");
        var twice = ReplyMapper.ToGatewayReply(once, "CLUSTER-B");

        ReplyMapper.TryRestoreGatewayReply(twice!, out var restored).ShouldBeTrue();
        restored.ShouldBe(original);
    }

    // Go: TestGatewayClientsDontReceiveMsgsOnGWPrefix server/gateway_test.go:5586
    [Fact]
    public void Reply_mapper_returns_null_for_null_input()
    {
        var result = ReplyMapper.ToGatewayReply(null, "CLUSTER");
        result.ShouldBeNull();
    }

    // Go: TestGatewayClientsDontReceiveMsgsOnGWPrefix server/gateway_test.go:5586
    [Fact]
    public void Reply_mapper_returns_empty_for_empty_input()
    {
        var result = ReplyMapper.ToGatewayReply("", "CLUSTER");
        result.ShouldBe("");
    }

    // Go: TestGatewayClientsDontReceiveMsgsOnGWPrefix server/gateway_test.go:5586
    [Fact]
    public void Has_gateway_reply_prefix_detects_gr_prefix()
    {
        ReplyMapper.HasGatewayReplyPrefix("_GR_.CLUSTER.inbox").ShouldBeTrue();
        ReplyMapper.HasGatewayReplyPrefix("_INBOX.abc").ShouldBeFalse();
        ReplyMapper.HasGatewayReplyPrefix(null).ShouldBeFalse();
        ReplyMapper.HasGatewayReplyPrefix("").ShouldBeFalse();
    }

    // Go: TestGatewaySendReplyAcrossGateways server/gateway_test.go:5165
    [Fact]
    public void Restore_returns_false_for_non_gr_subject()
    {
        ReplyMapper.TryRestoreGatewayReply("_INBOX.abc", out _).ShouldBeFalse();
    }

    // Go: TestGatewayReplyMapTracking server/gateway_test.go:6017
    [Fact]
    public void Restore_returns_false_for_malformed_gr_subject()
    {
        // _GR_. with no cluster separator
        ReplyMapper.TryRestoreGatewayReply("_GR_.nodot", out _).ShouldBeFalse();
    }

    // Go: TestGatewayReplyMapTracking server/gateway_test.go:6017
    [Fact]
    public void Restore_returns_false_for_gr_prefix_with_nothing_after_separator()
    {
        ReplyMapper.TryRestoreGatewayReply("_GR_.CLUSTER.", out _).ShouldBeFalse();
    }

    // ── Queue Subscription Forwarding ───────────────────────────────────

    // Go: TestGatewayQueueSub server/gateway_test.go:2265
    [Fact]
    public async Task Queue_subscription_interest_tracked_on_remote()
    {
        using var subList = new SubList();
        subList.ApplyRemoteSub(new RemoteSubscription("foo", "bar", "gw1", "$G"));

        subList.HasRemoteInterest("$G", "foo").ShouldBeTrue();
        subList.MatchRemote("$G", "foo").Count.ShouldBe(1);
    }

    // Go: TestGatewayQueueSub server/gateway_test.go:2265
    [Fact]
    public async Task Queue_subscription_with_multiple_groups_all_tracked()
    {
        using var subList = new SubList();
        subList.ApplyRemoteSub(new RemoteSubscription("foo", "bar", "gw1", "$G"));
        subList.ApplyRemoteSub(new RemoteSubscription("foo", "baz", "gw1", "$G"));

        subList.MatchRemote("$G", "foo").Count.ShouldBe(2);
    }

    // Go: TestGatewayQueueSub server/gateway_test.go:2265
    [Fact]
    public async Task Queue_sub_removal_clears_remote_interest()
    {
        using var subList = new SubList();
        subList.ApplyRemoteSub(new RemoteSubscription("foo", "bar", "gw1", "$G"));
        subList.HasRemoteInterest("$G", "foo").ShouldBeTrue();

        subList.ApplyRemoteSub(RemoteSubscription.Removal("foo", "bar", "gw1", "$G"));
        subList.HasRemoteInterest("$G", "foo").ShouldBeFalse();
    }

    // ── GatewayManager Forwarding ───────────────────────────────────────

    // Go: TestGatewayBasic server/gateway_test.go:399
    [Fact]
    public async Task Gateway_manager_forward_message_increments_js_counter()
    {
        var manager = new GatewayManager(
            new GatewayOptions { Name = "GW", Host = "127.0.0.1", Port = 0 },
            new ServerStats(),
            "S1",
            _ => { },
            _ => { },
            NullLogger<GatewayManager>.Instance);

        await manager.ForwardJetStreamClusterMessageAsync(
            new GatewayMessage("$JS.CLUSTER.test", null, "x"u8.ToArray()),
            default);

        manager.ForwardedJetStreamClusterMessages.ShouldBe(1);
    }

    // Go: TestGatewayBasic server/gateway_test.go:399
    [Fact]
    public async Task Gateway_manager_forward_js_message_multiple_times()
    {
        var manager = new GatewayManager(
            new GatewayOptions { Name = "GW", Host = "127.0.0.1", Port = 0 },
            new ServerStats(),
            "S1",
            _ => { },
            _ => { },
            NullLogger<GatewayManager>.Instance);

        for (int i = 0; i < 5; i++)
        {
            await manager.ForwardJetStreamClusterMessageAsync(
                new GatewayMessage("$JS.CLUSTER.test", null, "x"u8.ToArray()),
                default);
        }

        manager.ForwardedJetStreamClusterMessages.ShouldBe(5);
    }

    // ── Multiple Messages ───────────────────────────────────────────────

    // Go: TestGatewayMsgSentOnlyOnce server/gateway_test.go:2993
    [Fact]
    public async Task Multiple_messages_forwarded_across_gateway()
    {
        await using var fixture = await ForwardingFixture.StartAsync();

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

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

        await using var sub = await subscriber.SubscribeCoreAsync<string>("multi.test");
        await subscriber.PingAsync();
        await fixture.WaitForRemoteInterestOnLocalAsync("multi.test");

        const int count = 10;
        for (int i = 0; i < count; i++)
        {
            await publisher.PublishAsync("multi.test", $"msg-{i}");
        }

        await publisher.PingAsync();

        using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10));
        var received = new List<string>();
        for (int i = 0; i < count; i++)
        {
            var msg = await sub.Msgs.ReadAsync(timeout.Token);
            received.Add(msg.Data!);
        }

        received.Count.ShouldBe(count);
        for (int i = 0; i < count; i++)
        {
            received.ShouldContain($"msg-{i}");
        }
    }

    // Go: TestGatewayAccountUnsub server/gateway_test.go:1912
    // Verifies that a message published on local with a reply-to subject is forwarded
    // to the remote with the reply-to intact, allowing manual request-reply across gateway.
    [Fact]
    public async Task Message_with_reply_to_forwarded_across_gateway()
    {
        await using var fixture = await ForwardingFixture.StartAsync();

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

        // Subscribe on remote for requests
        await using var sub = await remoteConn.SubscribeCoreAsync<string>("svc.request");
        await remoteConn.PingAsync();
        await fixture.WaitForRemoteInterestOnLocalAsync("svc.request");

        // Publish from local with a reply-to subject via raw socket
        using var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        await sock.ConnectAsync(IPAddress.Loopback, fixture.Local.Port);
        var infoBuf = new byte[4096];
        _ = await sock.ReceiveAsync(infoBuf); // read INFO
        await sock.SendAsync(Encoding.ASCII.GetBytes(
            "CONNECT {}\r\nPUB svc.request _INBOX.reply123 12\r\nrequest-data\r\nPING\r\n"));

        // Wait for PONG to confirm the message was processed
        var pongBuf = new byte[4096];
        var pongTotal = new StringBuilder();
        using var pongCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
        while (!pongTotal.ToString().Contains("PONG"))
        {
            var n = await sock.ReceiveAsync(pongBuf, SocketFlags.None, pongCts.Token);
            if (n == 0) break;
            pongTotal.Append(Encoding.ASCII.GetString(pongBuf, 0, n));
        }

        using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10));
        var msg = await sub.Msgs.ReadAsync(timeout.Token);
        msg.Data.ShouldBe("request-data");
        msg.ReplyTo.ShouldNotBeNullOrEmpty();
    }

    // ── Account Scoped Forwarding ───────────────────────────────────────

    // Go: TestGatewayAccountInterest server/gateway_test.go:1794
    [Fact]
    public async Task Messages_forwarded_within_same_account_only()
    {
        var users = new User[]
        {
            new() { Username = "user_a", Password = "pass", Account = "ACCT_A" },
            new() { Username = "user_b", Password = "pass", Account = "ACCT_B" },
        };

        await using var fixture = await ForwardingFixture.StartWithUsersAsync(users);

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

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

        await using var publisherA = new NatsConnection(new NatsOpts
        {
            Url = $"nats://user_a:pass@127.0.0.1:{fixture.Local.Port}",
        });
        await publisherA.ConnectAsync();

        await using var subA = await remoteA.SubscribeCoreAsync<string>("acct.test");
        await using var subB = await remoteB.SubscribeCoreAsync<string>("acct.test");
        await remoteA.PingAsync();
        await remoteB.PingAsync();
        await fixture.WaitForRemoteInterestOnLocalAsync("ACCT_A", "acct.test");

        await publisherA.PublishAsync("acct.test", "for-account-a");

        using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
        var msgA = await subA.Msgs.ReadAsync(timeout.Token);
        msgA.Data.ShouldBe("for-account-a");

        // Account B should not receive
        using var noMsgTimeout = new CancellationTokenSource(TimeSpan.FromMilliseconds(500));
        await Should.ThrowAsync<OperationCanceledException>(async () =>
            await subB.Msgs.ReadAsync(noMsgTimeout.Token));
    }

    // Go: TestGatewayAccountInterest server/gateway_test.go:1794
    [Fact]
    public async Task Non_matching_subject_not_forwarded_after_interest_established()
    {
        await using var fixture = await ForwardingFixture.StartAsync();

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

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

        // Subscribe to a specific subject
        await using var sub = await subscriber.SubscribeCoreAsync<string>("specific.topic");
        await subscriber.PingAsync();
        await fixture.WaitForRemoteInterestOnLocalAsync("specific.topic");

        // Publish to a different subject
        await publisher.PublishAsync("other.topic", "should-not-arrive");
        await publisher.PingAsync();

        await Task.Delay(300);
        using var noMsgTimeout = new CancellationTokenSource(TimeSpan.FromMilliseconds(300));
        await Should.ThrowAsync<OperationCanceledException>(async () =>
            await sub.Msgs.ReadAsync(noMsgTimeout.Token));
    }

    // Go: TestGatewayNoCrashOnInvalidSubject server/gateway_test.go:6279
    [Fact]
    public void GatewayMessage_record_stores_all_fields()
    {
        var payload = new byte[] { 1, 2, 3 };
        var msg = new GatewayMessage("test.subject", "_INBOX.reply", payload, "MYACCT");

        msg.Subject.ShouldBe("test.subject");
        msg.ReplyTo.ShouldBe("_INBOX.reply");
        msg.Payload.Length.ShouldBe(3);
        msg.Account.ShouldBe("MYACCT");
    }

    // Go: TestGatewayBasic server/gateway_test.go:399
    [Fact]
    public void GatewayMessage_defaults_account_to_global()
    {
        var msg = new GatewayMessage("test.subject", null, new byte[] { });
        msg.Account.ShouldBe("$G");
    }

    // ── Interest-Only Mode and ShouldForwardInterestOnly ────────────────

    // Go: TestGatewaySwitchToInterestOnlyModeImmediately server/gateway_test.go:6934
    [Fact]
    public void Should_forward_interest_only_returns_true_when_interest_exists()
    {
        using var subList = new SubList();
        subList.ApplyRemoteSub(new RemoteSubscription("orders.>", null, "gw1", "A"));

        GatewayManager.ShouldForwardInterestOnly(subList, "A", "orders.created").ShouldBeTrue();
    }

    // Go: TestGatewaySwitchToInterestOnlyModeImmediately server/gateway_test.go:6934
    [Fact]
    public void Should_forward_interest_only_returns_false_without_interest()
    {
        using var subList = new SubList();
        subList.ApplyRemoteSub(new RemoteSubscription("orders.>", null, "gw1", "A"));

        GatewayManager.ShouldForwardInterestOnly(subList, "A", "payments.created").ShouldBeFalse();
    }

    // Go: TestGatewaySwitchToInterestOnlyModeImmediately server/gateway_test.go:6934
    [Fact]
    public void Should_forward_interest_only_for_different_account_returns_false()
    {
        using var subList = new SubList();
        subList.ApplyRemoteSub(new RemoteSubscription("orders.>", null, "gw1", "A"));

        GatewayManager.ShouldForwardInterestOnly(subList, "B", "orders.created").ShouldBeFalse();
    }

    // Go: TestGatewaySubjectInterest server/gateway_test.go:1972
    [Fact]
    public void Should_forward_with_wildcard_interest()
    {
        using var subList = new SubList();
        subList.ApplyRemoteSub(new RemoteSubscription("test.*", null, "gw1", "$G"));

        GatewayManager.ShouldForwardInterestOnly(subList, "$G", "test.one").ShouldBeTrue();
        GatewayManager.ShouldForwardInterestOnly(subList, "$G", "test.two").ShouldBeTrue();
        GatewayManager.ShouldForwardInterestOnly(subList, "$G", "other.one").ShouldBeFalse();
    }

    // Go: TestGatewaySubjectInterest server/gateway_test.go:1972
    [Fact]
    public void Should_forward_with_fwc_interest()
    {
        using var subList = new SubList();
        subList.ApplyRemoteSub(new RemoteSubscription("events.>", null, "gw1", "$G"));

        GatewayManager.ShouldForwardInterestOnly(subList, "$G", "events.a.b.c").ShouldBeTrue();
        GatewayManager.ShouldForwardInterestOnly(subList, "$G", "other.x").ShouldBeFalse();
    }
}

/// <summary>
/// Shared fixture for forwarding tests that need two running server clusters.
/// </summary>
internal sealed class ForwardingFixture : IAsyncDisposable
{
    private readonly CancellationTokenSource _localCts;
    private readonly CancellationTokenSource _remoteCts;

    private ForwardingFixture(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 Task<ForwardingFixture> StartAsync()
        => StartWithUsersAsync(null);

    public static async Task<ForwardingFixture> StartWithUsersAsync(IReadOnlyList<User>? users)
    {
        var localOptions = new NatsOptions
        {
            Host = "127.0.0.1",
            Port = 0,
            Users = users,
            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,
            Users = users,
            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();

        using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
        while (!timeout.IsCancellationRequested && (local.Stats.Gateways == 0 || remote.Stats.Gateways == 0))
            await Task.Delay(50, timeout.Token).ContinueWith(_ => { }, TaskScheduler.Default);

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

    public async Task WaitForRemoteInterestOnLocalAsync(string subject)
    {
        using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
        while (!timeout.IsCancellationRequested)
        {
            if (Local.HasRemoteInterest(subject))
                return;
            await Task.Delay(50, timeout.Token).ContinueWith(_ => { }, TaskScheduler.Default);
        }

        throw new TimeoutException($"Timed out waiting for remote interest on '{subject}'.");
    }

    public async Task WaitForRemoteInterestOnLocalAsync(string account, string subject)
    {
        using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
        while (!timeout.IsCancellationRequested)
        {
            if (Local.HasRemoteInterest(account, subject))
                return;
            await Task.Delay(50, timeout.Token).ContinueWith(_ => { }, TaskScheduler.Default);
        }

        throw new TimeoutException($"Timed out waiting for remote interest {account}:{subject}.");
    }

    public async Task WaitForRemoteInterestOnRemoteAsync(string subject)
    {
        using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
        while (!timeout.IsCancellationRequested)
        {
            if (Remote.HasRemoteInterest(subject))
                return;
            await Task.Delay(50, timeout.Token).ContinueWith(_ => { }, TaskScheduler.Default);
        }

        throw new TimeoutException($"Timed out waiting for remote interest on '{subject}'.");
    }

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