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.TestUtilities;

namespace NATS.Server.Auth.Tests.Accounts;

/// <summary>
/// Tests for publish/subscribe permission enforcement, account-level limits,
/// and per-user permission isolation.
/// Reference: Go auth_test.go — TestUserClone* (permission structure tests)
/// Reference: Go accounts_test.go — account limits (max connections, max subscriptions).
/// </summary>
public class PermissionTests
{
    private static async Task<(NatsServer server, int port, CancellationTokenSource cts)> StartServerAsync(NatsOptions options)
    {
        var port = TestPortAllocator.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: Permissions — publish allow list
    [Fact]
    public void Publish_allow_list_only()
    {
        var perms = ClientPermissions.Build(new Permissions
        {
            Publish = new SubjectPermission { Allow = ["foo.>", "bar.*"] },
        });

        perms.ShouldNotBeNull();
        perms.IsPublishAllowed("foo.bar").ShouldBeTrue();
        perms.IsPublishAllowed("foo.bar.baz").ShouldBeTrue();
        perms.IsPublishAllowed("bar.one").ShouldBeTrue();
        perms.IsPublishAllowed("baz.one").ShouldBeFalse();
    }

    // Go: Permissions — publish deny list
    [Fact]
    public void Publish_deny_list_only()
    {
        var perms = ClientPermissions.Build(new Permissions
        {
            Publish = new SubjectPermission { Deny = ["secret.>"] },
        });

        perms.ShouldNotBeNull();
        perms.IsPublishAllowed("foo.bar").ShouldBeTrue();
        perms.IsPublishAllowed("secret.data").ShouldBeFalse();
        perms.IsPublishAllowed("secret.nested.deep").ShouldBeFalse();
    }

    // Go: Permissions — publish allow + deny combined
    [Fact]
    public void Publish_allow_and_deny_combined()
    {
        var perms = ClientPermissions.Build(new Permissions
        {
            Publish = new SubjectPermission
            {
                Allow = ["events.>"],
                Deny = ["events.internal.>"],
            },
        });

        perms.ShouldNotBeNull();
        perms.IsPublishAllowed("events.public.data").ShouldBeTrue();
        perms.IsPublishAllowed("events.internal.secret").ShouldBeFalse();
    }

    // Go: Permissions — subscribe allow list
    [Fact]
    public void Subscribe_allow_list()
    {
        var perms = ClientPermissions.Build(new Permissions
        {
            Subscribe = new SubjectPermission { Allow = ["data.>"] },
        });

        perms.ShouldNotBeNull();
        perms.IsSubscribeAllowed("data.updates").ShouldBeTrue();
        perms.IsSubscribeAllowed("admin.logs").ShouldBeFalse();
    }

    // Go: Permissions — subscribe deny list
    [Fact]
    public void Subscribe_deny_list()
    {
        var perms = ClientPermissions.Build(new Permissions
        {
            Subscribe = new SubjectPermission { Deny = ["admin.>"] },
        });

        perms.ShouldNotBeNull();
        perms.IsSubscribeAllowed("data.updates").ShouldBeTrue();
        perms.IsSubscribeAllowed("admin.logs").ShouldBeFalse();
    }

    // Go: Permissions — null permissions allow everything
    [Fact]
    public void Null_permissions_allows_everything()
    {
        var perms = ClientPermissions.Build(null);
        perms.ShouldBeNull();
    }

    // Go: Permissions — empty permissions allows everything
    [Fact]
    public void Empty_permissions_allows_everything()
    {
        var perms = ClientPermissions.Build(new Permissions());
        perms.ShouldBeNull();
    }

    // Go: Permissions — subscribe allow + deny combined
    [Fact]
    public void Subscribe_allow_and_deny_combined()
    {
        var perms = ClientPermissions.Build(new Permissions
        {
            Subscribe = new SubjectPermission
            {
                Allow = ["data.>"],
                Deny = ["data.secret.>"],
            },
        });

        perms.ShouldNotBeNull();
        perms.IsSubscribeAllowed("data.public").ShouldBeTrue();
        perms.IsSubscribeAllowed("data.secret.key").ShouldBeFalse();
    }

    // Go: Permissions — separate publish and subscribe permissions
    [Fact]
    public void Separate_publish_and_subscribe_permissions()
    {
        var perms = ClientPermissions.Build(new Permissions
        {
            Publish = new SubjectPermission { Allow = ["pub.>"] },
            Subscribe = new SubjectPermission { Allow = ["sub.>"] },
        });

        perms.ShouldNotBeNull();
        perms.IsPublishAllowed("pub.data").ShouldBeTrue();
        perms.IsPublishAllowed("sub.data").ShouldBeFalse();
        perms.IsSubscribeAllowed("sub.data").ShouldBeTrue();
        perms.IsSubscribeAllowed("pub.data").ShouldBeFalse();
    }

    // Go: Account limits — max connections
    [Fact]
    public void Account_enforces_max_connections()
    {
        var acc = new Account("test") { MaxConnections = 2 };
        acc.AddClient(1).ShouldBeTrue();
        acc.AddClient(2).ShouldBeTrue();
        acc.AddClient(3).ShouldBeFalse(); // exceeds limit
        acc.ClientCount.ShouldBe(2);
    }

    // Go: Account limits — unlimited connections
    [Fact]
    public void Account_unlimited_connections_when_zero()
    {
        var acc = new Account("test") { MaxConnections = 0 };
        for (ulong i = 1; i <= 100; i++)
            acc.AddClient(i).ShouldBeTrue();
        acc.ClientCount.ShouldBe(100);
    }

    // Go: Account limits — max subscriptions
    [Fact]
    public void Account_enforces_max_subscriptions()
    {
        var acc = new Account("test") { MaxSubscriptions = 2 };
        acc.IncrementSubscriptions().ShouldBeTrue();
        acc.IncrementSubscriptions().ShouldBeTrue();
        acc.IncrementSubscriptions().ShouldBeFalse();
    }

    // Go: Account limits — subscription decrement frees slot
    [Fact]
    public void Account_decrement_subscriptions_frees_slot()
    {
        var acc = new Account("test") { MaxSubscriptions = 1 };
        acc.IncrementSubscriptions().ShouldBeTrue();
        acc.DecrementSubscriptions();
        acc.IncrementSubscriptions().ShouldBeTrue(); // slot freed
    }

    // Go: Account limits — max connections via integration
    [Fact]
    public void Account_remove_client_frees_slot()
    {
        var acc = new Account("test") { MaxConnections = 1 };
        acc.AddClient(1).ShouldBeTrue();
        acc.AddClient(2).ShouldBeFalse(); // full
        acc.RemoveClient(1);
        acc.AddClient(2).ShouldBeTrue(); // slot freed
    }

    // Go: Account limits — default permissions on account
    [Fact]
    public void Account_default_permissions()
    {
        var acc = new Account("test")
        {
            DefaultPermissions = new Permissions
            {
                Publish = new SubjectPermission { Allow = ["pub.>"] },
            },
        };

        acc.DefaultPermissions.ShouldNotBeNull();
        acc.DefaultPermissions.Publish!.Allow![0].ShouldBe("pub.>");
    }

    // Go: Account stats tracking
    [Fact]
    public void Account_tracks_message_stats()
    {
        var acc = new Account("stats-test");

        acc.InMsgs.ShouldBe(0L);
        acc.OutMsgs.ShouldBe(0L);
        acc.InBytes.ShouldBe(0L);
        acc.OutBytes.ShouldBe(0L);

        acc.IncrementInbound(5, 1024);
        acc.IncrementOutbound(3, 512);

        acc.InMsgs.ShouldBe(5L);
        acc.InBytes.ShouldBe(1024L);
        acc.OutMsgs.ShouldBe(3L);
        acc.OutBytes.ShouldBe(512L);
    }

    // Go: Account — user with publish permission can publish
    [Fact]
    public async Task User_with_publish_permission_can_publish_and_subscribe()
    {
        var (server, port, cts) = await StartServerAsync(new NatsOptions
        {
            Users =
            [
                new User
                {
                    Username = "limited",
                    Password = "pass",
                    Permissions = new Permissions
                    {
                        Publish = new SubjectPermission { Allow = ["allowed.>"] },
                        Subscribe = new SubjectPermission { Allow = [">"] },
                    },
                },
            ],
        });

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

            // Subscribe to allowed subjects
            await using var sub = await client.SubscribeCoreAsync<string>("allowed.test");
            await client.PingAsync();

            // Publish to allowed subject
            await client.PublishAsync("allowed.test", "hello");

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

    // Go: Account — user with publish deny
    [Fact]
    public async Task User_with_publish_deny_blocks_denied_subjects()
    {
        var (server, port, cts) = await StartServerAsync(new NatsOptions
        {
            Users =
            [
                new User
                {
                    Username = "limited",
                    Password = "pass",
                    Permissions = new Permissions
                    {
                        Publish = new SubjectPermission
                        {
                            Allow = [">"],
                            Deny = ["secret.>"],
                        },
                        Subscribe = new SubjectPermission { Allow = [">"] },
                    },
                },
            ],
        });

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

            // Subscribe to catch anything
            await using var sub = await client.SubscribeCoreAsync<string>("secret.data");
            await client.PingAsync();

            // Publish to denied subject — server should silently drop
            await client.PublishAsync("secret.data", "shouldnt-arrive");

            using var timeout = new CancellationTokenSource(TimeSpan.FromMilliseconds(500));
            try
            {
                await sub.Msgs.ReadAsync(timeout.Token);
                throw new Exception("Should not have received message on denied subject");
            }
            catch (OperationCanceledException)
            {
                // Expected — timeout confirms permission denial blocked the message
                return;
            }
        }
        finally
        {
            await cts.CancelAsync();
            server.Dispose();
        }
    }

    // Go: Account — user revocation
    [Fact]
    public void Account_user_revocation()
    {
        var acc = new Account("test");

        acc.IsUserRevoked("user1", 100).ShouldBeFalse();

        acc.RevokeUser("user1", 200);
        acc.IsUserRevoked("user1", 100).ShouldBeTrue();  // issued before revocation
        acc.IsUserRevoked("user1", 200).ShouldBeTrue();  // issued at revocation time
        acc.IsUserRevoked("user1", 300).ShouldBeFalse(); // issued after revocation
    }

    // Go: Account — wildcard user revocation
    [Fact]
    public void Account_wildcard_user_revocation()
    {
        var acc = new Account("test");

        acc.RevokeUser("*", 500);
        acc.IsUserRevoked("anyuser", 400).ShouldBeTrue();
        acc.IsUserRevoked("anyuser", 600).ShouldBeFalse();
    }

    // Go: Account — JetStream stream reservation
    [Fact]
    public void Account_jetstream_stream_reservation()
    {
        var acc = new Account("test") { MaxJetStreamStreams = 2 };

        acc.TryReserveStream().ShouldBeTrue();
        acc.TryReserveStream().ShouldBeTrue();
        acc.TryReserveStream().ShouldBeFalse(); // limit reached
        acc.JetStreamStreamCount.ShouldBe(2);

        acc.ReleaseStream();
        acc.JetStreamStreamCount.ShouldBe(1);
        acc.TryReserveStream().ShouldBeTrue(); // slot freed
    }

    // Go: Account limits — permissions cache behavior
    [Fact]
    public void Permission_cache_returns_consistent_results()
    {
        var perms = ClientPermissions.Build(new Permissions
        {
            Publish = new SubjectPermission { Allow = ["foo.>"] },
        });

        perms.ShouldNotBeNull();
        // First call populates cache
        perms.IsPublishAllowed("foo.bar").ShouldBeTrue();
        // Second call uses cache — should return same result
        perms.IsPublishAllowed("foo.bar").ShouldBeTrue();
        // Different subject also cached
        perms.IsPublishAllowed("baz.bar").ShouldBeFalse();
        perms.IsPublishAllowed("baz.bar").ShouldBeFalse();
    }

    // Go: Permissions — delivery allowed check
    [Fact]
    public void Delivery_allowed_respects_deny_list()
    {
        var perms = ClientPermissions.Build(new Permissions
        {
            Subscribe = new SubjectPermission { Deny = ["blocked.>"] },
        });

        perms.ShouldNotBeNull();
        perms.IsDeliveryAllowed("normal.subject").ShouldBeTrue();
        perms.IsDeliveryAllowed("blocked.secret").ShouldBeFalse();
    }
}
