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

namespace NATS.Server.Tests.Accounts;

/// <summary>
/// Tests for account creation, registration, isolation, and basic account lifecycle.
/// Reference: Go accounts_test.go — TestRegisterDuplicateAccounts, TestAccountIsolation,
/// TestAccountFromOptions, TestAccountSimpleConfig, TestAccountParseConfig,
/// TestMultiAccountsIsolation, TestNewAccountAndRequireNewAlwaysError, etc.
/// </summary>
public class AccountIsolationTests
{
    private static int GetFreePort()
    {
        using var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        sock.Bind(new IPEndPoint(IPAddress.Loopback, 0));
        return ((IPEndPoint)sock.LocalEndPoint!).Port;
    }

    private static NatsServer CreateTestServer(NatsOptions? options = null)
    {
        var port = GetFreePort();
        options ??= new NatsOptions();
        options.Port = port;
        return new NatsServer(options, NullLoggerFactory.Instance);
    }

    private static async Task<(NatsServer server, int port, CancellationTokenSource cts)> StartServerAsync(NatsOptions options)
    {
        var port = GetFreePort();
        options.Port = port;
        var server = new NatsServer(options, NullLoggerFactory.Instance);
        var cts = new CancellationTokenSource();
        _ = server.StartAsync(cts.Token);
        await server.WaitForReadyAsync();
        return (server, port, cts);
    }

    private static bool ExceptionChainContains(Exception ex, string substring)
    {
        Exception? current = ex;
        while (current != null)
        {
            if (current.Message.Contains(substring, StringComparison.OrdinalIgnoreCase))
                return true;
            current = current.InnerException;
        }
        return false;
    }

    // Go: TestRegisterDuplicateAccounts server/accounts_test.go:50
    [Fact]
    public void Register_duplicate_account_returns_existing()
    {
        using var server = CreateTestServer();

        var foo = server.GetOrCreateAccount("$foo");
        var foo2 = server.GetOrCreateAccount("$foo");

        // GetOrCreateAccount returns the same instance if already registered
        foo.ShouldBeSameAs(foo2);
    }

    // Go: TestAccountIsolation server/accounts_test.go:57
    [Fact]
    public void Account_isolation_separate_sublists()
    {
        using var server = CreateTestServer();

        var fooAcc = server.GetOrCreateAccount("$foo");
        var barAcc = server.GetOrCreateAccount("$bar");

        // Accounts must have different SubLists
        fooAcc.SubList.ShouldNotBeSameAs(barAcc.SubList);
        fooAcc.Name.ShouldBe("$foo");
        barAcc.Name.ShouldBe("$bar");
    }

    // Go: TestAccountIsolation server/accounts_test.go:57
    [Fact]
    public void Account_isolation_messages_do_not_cross()
    {
        using var server = CreateTestServer();

        var fooAcc = server.GetOrCreateAccount("$foo");
        var barAcc = server.GetOrCreateAccount("$bar");

        var receivedFoo = new List<string>();
        var receivedBar = new List<string>();

        var clientFoo = new TestNatsClient(1, fooAcc);
        clientFoo.OnMessage = (subject, _, _, _, _) => receivedFoo.Add(subject);
        var clientBar = new TestNatsClient(2, barAcc);
        clientBar.OnMessage = (subject, _, _, _, _) => receivedBar.Add(subject);

        // Subscribe to "foo" in both accounts
        barAcc.SubList.Insert(new Subscription { Subject = "foo", Sid = "1", Client = clientBar });
        fooAcc.SubList.Insert(new Subscription { Subject = "foo", Sid = "1", Client = clientFoo });

        // Publish to foo account's SubList
        var result = fooAcc.SubList.Match("foo");
        foreach (var sub in result.PlainSubs)
            sub.Client?.SendMessage("foo", sub.Sid, null, default, default);

        // Only foo account subscriber should receive
        receivedFoo.Count.ShouldBe(1);
        receivedBar.Count.ShouldBe(0);
    }

    // Go: TestAccountFromOptions server/accounts_test.go:386
    [Fact]
    public void Account_from_options_creates_accounts()
    {
        using var server = CreateTestServer(new NatsOptions
        {
            Accounts = new Dictionary<string, AccountConfig>
            {
                ["foo"] = new AccountConfig(),
                ["bar"] = new AccountConfig(),
            },
        });

        var fooAcc = server.GetOrCreateAccount("foo");
        var barAcc = server.GetOrCreateAccount("bar");

        fooAcc.ShouldNotBeNull();
        barAcc.ShouldNotBeNull();
        fooAcc.SubList.ShouldNotBeNull();
        barAcc.SubList.ShouldNotBeNull();
    }

    // Go: TestAccountFromOptions server/accounts_test.go:386
    [Fact]
    public void Account_from_options_applies_config()
    {
        using var server = CreateTestServer(new NatsOptions
        {
            Accounts = new Dictionary<string, AccountConfig>
            {
                ["limited"] = new AccountConfig { MaxConnections = 5, MaxSubscriptions = 10 },
            },
        });

        var acc = server.GetOrCreateAccount("limited");
        acc.MaxConnections.ShouldBe(5);
        acc.MaxSubscriptions.ShouldBe(10);
    }

    // Go: TestMultiAccountsIsolation server/accounts_test.go:304
    [Fact]
    public async Task Multi_accounts_isolation_only_correct_importer_receives()
    {
        var (server, port, cts) = await StartServerAsync(new NatsOptions
        {
            Users =
            [
                new User { Username = "public", Password = "public", Account = "PUBLIC" },
                new User { Username = "client", Password = "client", Account = "CLIENT" },
                new User { Username = "client2", Password = "client2", Account = "CLIENT2" },
            ],
        });

        try
        {
            await using var publicNc = new NatsConnection(new NatsOpts
            {
                Url = $"nats://public:public@127.0.0.1:{port}",
            });
            await using var clientNc = new NatsConnection(new NatsOpts
            {
                Url = $"nats://client:client@127.0.0.1:{port}",
            });
            await using var client2Nc = new NatsConnection(new NatsOpts
            {
                Url = $"nats://client2:client2@127.0.0.1:{port}",
            });

            await publicNc.ConnectAsync();
            await clientNc.ConnectAsync();
            await client2Nc.ConnectAsync();

            // Subscribe on both client accounts
            await using var clientSub = await clientNc.SubscribeCoreAsync<string>("orders.>");
            await using var client2Sub = await client2Nc.SubscribeCoreAsync<string>("orders.>");
            await clientNc.PingAsync();
            await client2Nc.PingAsync();

            // Publish from the same account as client - CLIENT should get it
            await clientNc.PublishAsync("orders.entry", "test1");

            using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(3));
            var msg = await clientSub.Msgs.ReadAsync(timeout.Token);
            msg.Data.ShouldBe("test1");

            // CLIENT2 should NOT receive messages from CLIENT account
            using var shortTimeout = new CancellationTokenSource(TimeSpan.FromMilliseconds(300));
            try
            {
                await client2Sub.Msgs.ReadAsync(shortTimeout.Token);
                throw new Exception("CLIENT2 should not have received a message from CLIENT account");
            }
            catch (OperationCanceledException)
            {
                // Expected
            }
        }
        finally
        {
            await cts.CancelAsync();
            server.Dispose();
        }
    }

    // Go: TestAccountIsolation server/accounts_test.go:57 (integration variant)
    [Fact]
    public async Task Same_account_receives_messages_integration()
    {
        var (server, port, cts) = await StartServerAsync(new NatsOptions
        {
            Users =
            [
                new User { Username = "alice", Password = "pass", Account = "acct-a" },
                new User { Username = "charlie", Password = "pass", Account = "acct-a" },
            ],
        });

        try
        {
            await using var alice = new NatsConnection(new NatsOpts
            {
                Url = $"nats://alice:pass@127.0.0.1:{port}",
            });
            await using var charlie = new NatsConnection(new NatsOpts
            {
                Url = $"nats://charlie:pass@127.0.0.1:{port}",
            });

            await alice.ConnectAsync();
            await charlie.ConnectAsync();

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

            await alice.PublishAsync("test.subject", "from-alice");

            using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
            var msg = await sub.Msgs.ReadAsync(timeout.Token);
            msg.Data.ShouldBe("from-alice");
        }
        finally
        {
            await cts.CancelAsync();
            server.Dispose();
        }
    }

    // Go: TestAccountIsolation server/accounts_test.go:57 (integration variant)
    [Fact]
    public async Task Different_account_does_not_receive_messages_integration()
    {
        var (server, port, cts) = await StartServerAsync(new NatsOptions
        {
            Users =
            [
                new User { Username = "alice", Password = "pass", Account = "acct-a" },
                new User { Username = "bob", Password = "pass", Account = "acct-b" },
            ],
        });

        try
        {
            await using var alice = new NatsConnection(new NatsOpts
            {
                Url = $"nats://alice:pass@127.0.0.1:{port}",
            });
            await using var bob = new NatsConnection(new NatsOpts
            {
                Url = $"nats://bob:pass@127.0.0.1:{port}",
            });

            await alice.ConnectAsync();
            await bob.ConnectAsync();

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

            await alice.PublishAsync("test.subject", "from-alice");

            using var timeout = new CancellationTokenSource(TimeSpan.FromMilliseconds(500));
            try
            {
                await sub.Msgs.ReadAsync(timeout.Token);
                throw new Exception("Bob should not have received a message from a different account");
            }
            catch (OperationCanceledException)
            {
                // Expected
            }
        }
        finally
        {
            await cts.CancelAsync();
            server.Dispose();
        }
    }

    // Go: TestMultiAccountsIsolation server/accounts_test.go:304
    [Fact]
    public void Three_account_isolation_with_wildcard_subs()
    {
        using var server = CreateTestServer();

        var accountA = server.GetOrCreateAccount("acct-a");
        var accountB = server.GetOrCreateAccount("acct-b");
        var accountC = server.GetOrCreateAccount("acct-c");

        accountA.SubList.ShouldNotBeSameAs(accountB.SubList);
        accountB.SubList.ShouldNotBeSameAs(accountC.SubList);

        var receivedA = new List<string>();
        var receivedB = new List<string>();
        var receivedC = new List<string>();

        var clientA = new TestNatsClient(1, accountA);
        clientA.OnMessage = (subject, _, _, _, _) => receivedA.Add(subject);
        var clientB = new TestNatsClient(2, accountB);
        clientB.OnMessage = (subject, _, _, _, _) => receivedB.Add(subject);
        var clientC = new TestNatsClient(3, accountC);
        clientC.OnMessage = (subject, _, _, _, _) => receivedC.Add(subject);

        accountA.SubList.Insert(new Subscription { Subject = "orders.>", Sid = "a1", Client = clientA });
        accountB.SubList.Insert(new Subscription { Subject = "orders.>", Sid = "b1", Client = clientB });
        accountC.SubList.Insert(new Subscription { Subject = "orders.>", Sid = "c1", Client = clientC });

        // Publish in Account A's subject space
        var resultA = accountA.SubList.Match("orders.client.stream.entry");
        foreach (var sub in resultA.PlainSubs)
            sub.Client?.SendMessage("orders.client.stream.entry", sub.Sid, null, default, default);

        receivedA.Count.ShouldBe(1);
        receivedB.Count.ShouldBe(0);
        receivedC.Count.ShouldBe(0);

        // Publish in Account B
        var resultB = accountB.SubList.Match("orders.other.stream.entry");
        foreach (var sub in resultB.PlainSubs)
            sub.Client?.SendMessage("orders.other.stream.entry", sub.Sid, null, default, default);

        receivedA.Count.ShouldBe(1); // unchanged
        receivedB.Count.ShouldBe(1);
        receivedC.Count.ShouldBe(0);
    }

    // Go: TestAccountGlobalDefault server/accounts_test.go:2254
    [Fact]
    public void Global_account_has_default_name()
    {
        Account.GlobalAccountName.ShouldBe("$G");
    }

    // Go: TestRegisterDuplicateAccounts server/accounts_test.go:50
    [Fact]
    public void GetOrCreateAccount_returns_same_instance()
    {
        using var server = CreateTestServer();

        var acc1 = server.GetOrCreateAccount("test-acc");
        var acc2 = server.GetOrCreateAccount("test-acc");

        acc1.ShouldBeSameAs(acc2);
    }

    // Go: TestAccountIsolation server/accounts_test.go:57 — verifies accounts are different objects
    [Fact]
    public void Accounts_are_distinct_objects()
    {
        using var server = CreateTestServer();

        var foo = server.GetOrCreateAccount("foo");
        var bar = server.GetOrCreateAccount("bar");

        foo.ShouldNotBeSameAs(bar);
        foo.Name.ShouldNotBe(bar.Name);
    }

    // Go: TestAccountMapsUsers server/accounts_test.go:2138
    [Fact]
    public async Task Users_mapped_to_correct_accounts()
    {
        var (server, port, cts) = await StartServerAsync(new NatsOptions
        {
            Users =
            [
                new User { Username = "alice", Password = "pass", Account = "acct-a" },
                new User { Username = "bob", Password = "pass", Account = "acct-b" },
            ],
        });

        try
        {
            // Both should connect successfully to their respective accounts
            await using var alice = new NatsConnection(new NatsOpts
            {
                Url = $"nats://alice:pass@127.0.0.1:{port}",
            });
            await using var bob = new NatsConnection(new NatsOpts
            {
                Url = $"nats://bob:pass@127.0.0.1:{port}",
            });

            await alice.ConnectAsync();
            await alice.PingAsync();
            await bob.ConnectAsync();
            await bob.PingAsync();

            // Verify isolation: publish in A, subscribe in B should not receive
            await using var bobSub = await bob.SubscribeCoreAsync<string>("mapped.test");
            await bob.PingAsync();

            await alice.PublishAsync("mapped.test", "hello");

            using var timeout = new CancellationTokenSource(TimeSpan.FromMilliseconds(300));
            try
            {
                await bobSub.Msgs.ReadAsync(timeout.Token);
                throw new Exception("Bob should not receive messages from Alice's account");
            }
            catch (OperationCanceledException)
            {
                // Expected — accounts are isolated
            }
        }
        finally
        {
            await cts.CancelAsync();
            server.Dispose();
        }
    }

    // Go: TestAccountIsolation server/accounts_test.go:57 — wildcard subscriber in isolated account
    [Fact]
    public void Wildcard_subscriber_in_isolated_account_no_cross_delivery()
    {
        using var server = CreateTestServer();

        var fooAcc = server.GetOrCreateAccount("$foo");
        var barAcc = server.GetOrCreateAccount("$bar");

        var receivedBar = new List<string>();
        var clientBar = new TestNatsClient(2, barAcc);
        clientBar.OnMessage = (subject, _, _, _, _) => receivedBar.Add(subject);

        // Bar subscribes to wildcard
        barAcc.SubList.Insert(new Subscription { Subject = ">", Sid = "1", Client = clientBar });

        // Publish in foo account
        var result = fooAcc.SubList.Match("anything.goes.here");
        result.PlainSubs.Length.ShouldBe(0); // No subscribers in foo

        // Bar should still have no messages
        receivedBar.Count.ShouldBe(0);
    }

    // Go: TestAccountIsolation server/accounts_test.go:57 — multiple subs same account
    [Fact]
    public void Multiple_subscribers_same_account_all_receive()
    {
        using var server = CreateTestServer();

        var acc = server.GetOrCreateAccount("test");

        var received1 = new List<string>();
        var received2 = new List<string>();

        var client1 = new TestNatsClient(1, acc);
        client1.OnMessage = (subject, _, _, _, _) => received1.Add(subject);
        var client2 = new TestNatsClient(2, acc);
        client2.OnMessage = (subject, _, _, _, _) => received2.Add(subject);

        acc.SubList.Insert(new Subscription { Subject = "events.>", Sid = "s1", Client = client1 });
        acc.SubList.Insert(new Subscription { Subject = "events.>", Sid = "s2", Client = client2 });

        var result = acc.SubList.Match("events.order.created");
        result.PlainSubs.Length.ShouldBe(2);

        foreach (var sub in result.PlainSubs)
            sub.Client?.SendMessage("events.order.created", sub.Sid, null, default, default);

        received1.Count.ShouldBe(1);
        received2.Count.ShouldBe(1);
    }

    /// <summary>
    /// Minimal test double for INatsClient used in isolation tests.
    /// </summary>
    private sealed class TestNatsClient(ulong id, Account account) : INatsClient
    {
        public ulong Id => id;
        public ClientKind Kind => ClientKind.Client;
        public Account? Account => account;
        public Protocol.ClientOptions? ClientOpts => null;
        public ClientPermissions? Permissions => null;
        public Action<string, string, string?, ReadOnlyMemory<byte>, ReadOnlyMemory<byte>>? OnMessage { get; set; }

        public void SendMessage(string subject, string sid, string? replyTo,
            ReadOnlyMemory<byte> headers, ReadOnlyMemory<byte> payload)
        {
            OnMessage?.Invoke(subject, sid, replyTo, headers, payload);
        }

        public bool QueueOutbound(ReadOnlyMemory<byte> data) => true;
        public void RemoveSubscription(string sid) { }
    }
}
