using Microsoft.Extensions.Logging.Abstractions;
using NATS.Server;
using NATS.Server.Auth;
using NATS.Server.Imports;
using NATS.Server.Subscriptions;

namespace NATS.Server.Tests.Accounts;

/// <summary>
/// Tests for cross-account stream/service export/import delivery, authorization, and mapping.
/// Reference: Go accounts_test.go TestAccountIsolationExportImport, TestMultiAccountsIsolation,
/// TestImportAuthorized, TestSimpleMapping, TestAddServiceExport, TestAddStreamExport,
/// TestServiceExportWithWildcards, TestServiceImportWithWildcards, etc.
/// </summary>
public class AccountImportExportTests
{
    // Go: TestAccountIsolationExportImport server/accounts_test.go:111
    [Fact]
    public void Stream_export_import_delivers_cross_account()
    {
        using var server = CreateTestServer();

        var exporter = server.GetOrCreateAccount("acct-a");
        var importer = server.GetOrCreateAccount("acct-b");

        // Account A exports "events.>"
        exporter.AddStreamExport("events.>", null);
        exporter.Exports.Streams.ShouldContainKey("events.>");

        // Account B imports "events.>" from Account A
        importer.AddStreamImport(exporter, "events.>", "imported.events.>");
        importer.Imports.Streams.Count.ShouldBe(1);
        importer.Imports.Streams[0].From.ShouldBe("events.>");
        importer.Imports.Streams[0].To.ShouldBe("imported.events.>");
        importer.Imports.Streams[0].SourceAccount.ShouldBe(exporter);

        // Also set up a service export/import to verify cross-account message delivery
        exporter.AddServiceExport("svc.>", ServiceResponseType.Singleton, null);
        importer.AddServiceImport(exporter, "requests.>", "svc.>");

        var received = new List<(string Subject, string Sid)>();
        var mockClient = new TestNatsClient(1, exporter);
        mockClient.OnMessage = (subject, sid, _, _, _) =>
            received.Add((subject, sid));

        var exportSub = new Subscription { Subject = "svc.order", Sid = "s1", Client = mockClient };
        exporter.SubList.Insert(exportSub);

        var si = importer.Imports.Services["requests.>"][0];
        server.ProcessServiceImport(si, "requests.order", null,
            ReadOnlyMemory<byte>.Empty, ReadOnlyMemory<byte>.Empty);

        received.Count.ShouldBe(1);
        received[0].Subject.ShouldBe("svc.order");
        received[0].Sid.ShouldBe("s1");
    }

    // Go: TestMultiAccountsIsolation server/accounts_test.go:304
    [Fact]
    public void Account_isolation_prevents_cross_account_delivery()
    {
        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 });

        var resultA = accountA.SubList.Match("orders.client.stream.entry");
        resultA.PlainSubs.Length.ShouldBe(1);

        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);
    }

    // Go: TestAddStreamExport server/accounts_test.go:1560
    [Fact]
    public void Add_stream_export_public()
    {
        using var server = CreateTestServer();
        var foo = server.GetOrCreateAccount("foo");

        foo.AddStreamExport("foo", null);
        foo.Exports.Streams.ShouldContainKey("foo");

        // Public export (no approved list) should be authorized for anyone
        var bar = server.GetOrCreateAccount("bar");
        foo.Exports.Streams["foo"].Auth.IsAuthorized(bar).ShouldBeTrue();
    }

    // Go: TestAddStreamExport server/accounts_test.go:1560
    [Fact]
    public void Add_stream_export_with_approved_accounts()
    {
        using var server = CreateTestServer();
        var foo = server.GetOrCreateAccount("foo");
        var bar = server.GetOrCreateAccount("bar");
        var baz = server.GetOrCreateAccount("baz");

        foo.AddStreamExport("events.>", [bar]);
        foo.Exports.Streams["events.>"].Auth.IsAuthorized(bar).ShouldBeTrue();
        foo.Exports.Streams["events.>"].Auth.IsAuthorized(baz).ShouldBeFalse();
    }

    // Go: TestAddServiceExport server/accounts_test.go:1282
    [Fact]
    public void Add_service_export_singleton()
    {
        using var server = CreateTestServer();
        var foo = server.GetOrCreateAccount("foo");

        foo.AddServiceExport("help", ServiceResponseType.Singleton, null);
        foo.Exports.Services.ShouldContainKey("help");
        foo.Exports.Services["help"].ResponseType.ShouldBe(ServiceResponseType.Singleton);
    }

    // Go: TestAddServiceExport server/accounts_test.go:1282
    [Fact]
    public void Add_service_export_streamed()
    {
        using var server = CreateTestServer();
        var foo = server.GetOrCreateAccount("foo");

        foo.AddServiceExport("data.feed", ServiceResponseType.Streamed, null);
        foo.Exports.Services["data.feed"].ResponseType.ShouldBe(ServiceResponseType.Streamed);
    }

    // Go: TestAddServiceExport server/accounts_test.go:1282
    [Fact]
    public void Add_service_export_chunked()
    {
        using var server = CreateTestServer();
        var foo = server.GetOrCreateAccount("foo");

        foo.AddServiceExport("photos", ServiceResponseType.Chunked, null);
        foo.Exports.Services["photos"].ResponseType.ShouldBe(ServiceResponseType.Chunked);
    }

    // Go: TestServiceExportWithWildcards server/accounts_test.go:1319
    [Fact]
    public void Service_export_with_wildcard_subject()
    {
        using var server = CreateTestServer();
        var foo = server.GetOrCreateAccount("foo");

        foo.AddServiceExport("svc.*", ServiceResponseType.Singleton, null);
        foo.Exports.Services.ShouldContainKey("svc.*");
    }

    // Go: TestImportAuthorized server/accounts_test.go:761
    [Fact]
    public void Stream_import_requires_matching_export()
    {
        using var server = CreateTestServer();
        var foo = server.GetOrCreateAccount("foo");
        var bar = server.GetOrCreateAccount("bar");

        // Without export, import should fail
        Should.Throw<InvalidOperationException>(() =>
            bar.AddStreamImport(foo, "foo", "import"));
    }

    // Go: TestImportAuthorized server/accounts_test.go:761
    [Fact]
    public void Stream_import_with_public_export_succeeds()
    {
        using var server = CreateTestServer();
        var foo = server.GetOrCreateAccount("foo");
        var bar = server.GetOrCreateAccount("bar");

        foo.AddStreamExport("foo", null); // public
        bar.AddStreamImport(foo, "foo", "import");

        bar.Imports.Streams.Count.ShouldBe(1);
        bar.Imports.Streams[0].From.ShouldBe("foo");
    }

    // Go: TestImportAuthorized server/accounts_test.go:761
    [Fact]
    public void Stream_import_from_restricted_export_unauthorized_account_fails()
    {
        using var server = CreateTestServer();
        var foo = server.GetOrCreateAccount("foo");
        var bar = server.GetOrCreateAccount("bar");
        var baz = server.GetOrCreateAccount("baz");

        foo.AddStreamExport("data.>", [bar]); // only bar authorized

        // bar can import
        bar.AddStreamImport(foo, "data.>", "imported");
        bar.Imports.Streams.Count.ShouldBe(1);

        // baz cannot import
        Should.Throw<UnauthorizedAccessException>(() =>
            baz.AddStreamImport(foo, "data.>", "imported"));
    }

    // Go: TestServiceImportWithWildcards server/accounts_test.go:1463
    [Fact]
    public void Service_import_delivers_to_exporter_via_wildcard()
    {
        using var server = CreateTestServer();
        var exporter = server.GetOrCreateAccount("exporter");
        var importer = server.GetOrCreateAccount("importer");

        exporter.AddServiceExport("api.>", ServiceResponseType.Singleton, null);
        importer.AddServiceImport(exporter, "requests.>", "api.>");

        var received = new List<string>();
        var mockClient = new TestNatsClient(1, exporter);
        mockClient.OnMessage = (subject, _, _, _, _) => received.Add(subject);

        exporter.SubList.Insert(new Subscription { Subject = "api.orders", Sid = "s1", Client = mockClient });

        var si = importer.Imports.Services["requests.>"][0];
        server.ProcessServiceImport(si, "requests.orders", null, default, default);

        received.Count.ShouldBe(1);
        received[0].ShouldBe("api.orders");
    }

    // Go: TestSimpleMapping server/accounts_test.go:845
    [Fact]
    public void Service_import_maps_literal_subjects()
    {
        using var server = CreateTestServer();
        var foo = server.GetOrCreateAccount("foo");
        var bar = server.GetOrCreateAccount("bar");

        foo.AddServiceExport("help.request", ServiceResponseType.Singleton, null);
        bar.AddServiceImport(foo, "local.help", "help.request");

        var received = new List<string>();
        var mockClient = new TestNatsClient(1, foo);
        mockClient.OnMessage = (subject, _, _, _, _) => received.Add(subject);

        foo.SubList.Insert(new Subscription { Subject = "help.request", Sid = "s1", Client = mockClient });

        var si = bar.Imports.Services["local.help"][0];
        server.ProcessServiceImport(si, "local.help", null, default, default);

        received.Count.ShouldBe(1);
        received[0].ShouldBe("help.request");
    }

    // Go: TestAccountDuplicateServiceImportSubject server/accounts_test.go:2411
    [Fact]
    public void Duplicate_service_import_same_from_subject_adds_to_list()
    {
        using var server = CreateTestServer();
        var foo = server.GetOrCreateAccount("foo");
        var bar = server.GetOrCreateAccount("bar");
        var baz = server.GetOrCreateAccount("baz");

        foo.AddServiceExport("svc.a", ServiceResponseType.Singleton, null);
        baz.AddServiceExport("svc.a", ServiceResponseType.Singleton, null);

        bar.AddServiceImport(foo, "requests.a", "svc.a");
        bar.AddServiceImport(baz, "requests.a", "svc.a");

        // Both imports should be stored under the same "from" key
        bar.Imports.Services["requests.a"].Count.ShouldBe(2);
    }

    // Go: TestCrossAccountRequestReply server/accounts_test.go:1597
    [Fact]
    public void Service_import_preserves_reply_to()
    {
        using var server = CreateTestServer();
        var exporter = server.GetOrCreateAccount("exporter");
        var importer = server.GetOrCreateAccount("importer");

        exporter.AddServiceExport("api.time", ServiceResponseType.Singleton, null);
        importer.AddServiceImport(exporter, "time.request", "api.time");

        string? capturedReply = null;
        var mockClient = new TestNatsClient(1, exporter);
        mockClient.OnMessage = (_, _, replyTo, _, _) => capturedReply = replyTo;

        exporter.SubList.Insert(new Subscription { Subject = "api.time", Sid = "s1", Client = mockClient });

        var si = importer.Imports.Services["time.request"][0];
        server.ProcessServiceImport(si, "time.request", "_INBOX.abc123", default, default);

        capturedReply.ShouldBe("_INBOX.abc123");
    }

    // Go: TestAccountRemoveServiceImport server/accounts_test.go:2447
    [Fact]
    public void Service_import_invalid_flag_prevents_delivery()
    {
        using var server = CreateTestServer();
        var exporter = server.GetOrCreateAccount("exporter");
        var importer = server.GetOrCreateAccount("importer");

        exporter.AddServiceExport("api.>", ServiceResponseType.Singleton, null);
        importer.AddServiceImport(exporter, "requests.>", "api.>");

        var received = new List<string>();
        var mockClient = new TestNatsClient(1, exporter);
        mockClient.OnMessage = (subject, _, _, _, _) => received.Add(subject);

        exporter.SubList.Insert(new Subscription { Subject = "api.test", Sid = "s1", Client = mockClient });

        // Mark the import as invalid
        var si = importer.Imports.Services["requests.>"][0];
        si.Invalid = true;

        server.ProcessServiceImport(si, "requests.test", null, default, default);

        received.Count.ShouldBe(0);
    }

    // Go: TestAccountCheckStreamImportsEqual server/accounts_test.go:2274
    [Fact]
    public void Stream_import_tracks_source_account()
    {
        using var server = CreateTestServer();
        var foo = server.GetOrCreateAccount("foo");
        var bar = server.GetOrCreateAccount("bar");

        foo.AddStreamExport("data.>", null);
        bar.AddStreamImport(foo, "data.>", "feed.>");

        var si = bar.Imports.Streams[0];
        si.SourceAccount.ShouldBeSameAs(foo);
        si.From.ShouldBe("data.>");
        si.To.ShouldBe("feed.>");
    }

    // Go: TestExportAuth — revoked accounts cannot import
    [Fact]
    public void Revoked_account_cannot_access_export()
    {
        using var server = CreateTestServer();
        var foo = server.GetOrCreateAccount("foo");
        var bar = server.GetOrCreateAccount("bar");

        var auth = new ExportAuth
        {
            RevokedAccounts = new Dictionary<string, long> { [bar.Name] = DateTimeOffset.UtcNow.ToUnixTimeSeconds() },
        };

        auth.IsAuthorized(bar).ShouldBeFalse();
    }

    // Go: TestExportAuth — public export with no restrictions
    [Fact]
    public void Public_export_authorizes_any_account()
    {
        using var server = CreateTestServer();
        var foo = server.GetOrCreateAccount("foo");
        var bar = server.GetOrCreateAccount("bar");

        var auth = new ExportAuth(); // No restrictions
        auth.IsAuthorized(foo).ShouldBeTrue();
        auth.IsAuthorized(bar).ShouldBeTrue();
    }

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

    private static int GetFreePort()
    {
        using var sock = new System.Net.Sockets.Socket(
            System.Net.Sockets.AddressFamily.InterNetwork,
            System.Net.Sockets.SocketType.Stream,
            System.Net.Sockets.ProtocolType.Tcp);
        sock.Bind(new System.Net.IPEndPoint(System.Net.IPAddress.Loopback, 0));
        return ((System.Net.IPEndPoint)sock.LocalEndPoint!).Port;
    }

    /// <summary>
    /// Minimal test double for INatsClient used in import/export 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) { }
    }
}
