using NATS.Client.Core;
using NATS.E2E.Tests.Infrastructure;

namespace NATS.E2E.Tests;

[Collection("E2E-Auth")]
public class AuthTests(AuthServerFixture fixture)
{
    [Fact]
    public async Task UsernamePassword_ValidCredentials_Connects()
    {
        await using var client = fixture.CreateClient("testuser", "testpass");
        await client.ConnectAsync();
        await client.PingAsync();
        client.ConnectionState.ShouldBe(NatsConnectionState.Open);
    }

    [Fact]
    public async Task UsernamePassword_InvalidPassword_Rejected()
    {
        await using var client = new NatsConnection(new NatsOpts
        {
            Url = $"nats://127.0.0.1:{fixture.Port}",
            AuthOpts = new NatsAuthOpts { Username = "testuser", Password = "wrongpass" },
            MaxReconnectRetry = 0,
        });

        var ex = await Should.ThrowAsync<Exception>(async () =>
        {
            await client.ConnectAsync();
            await client.PingAsync();
        });

        ex.ShouldNotBeNull();
    }

    [Fact]
    public async Task UsernamePassword_NoCredentials_Rejected()
    {
        await using var client = new NatsConnection(new NatsOpts
        {
            Url = $"nats://127.0.0.1:{fixture.Port}",
            MaxReconnectRetry = 0,
        });

        var ex = await Should.ThrowAsync<Exception>(async () =>
        {
            await client.ConnectAsync();
            await client.PingAsync();
        });

        ex.ShouldNotBeNull();
    }

    [Fact]
    public async Task TokenAuth_ValidToken_Connects()
    {
        var config = """
            authorization {
                token: "s3cret"
            }
            """;

        await using var server = NatsServerProcess.WithConfig(config);
        await server.StartAsync();

        await using var client = new NatsConnection(new NatsOpts
        {
            Url = $"nats://127.0.0.1:{server.Port}",
            AuthOpts = new NatsAuthOpts { Token = "s3cret" },
        });

        await client.ConnectAsync();
        await client.PingAsync();
        client.ConnectionState.ShouldBe(NatsConnectionState.Open);
    }

    [Fact]
    public async Task TokenAuth_InvalidToken_Rejected()
    {
        var config = """
            authorization {
                token: "s3cret"
            }
            """;

        await using var server = NatsServerProcess.WithConfig(config);
        await server.StartAsync();

        await using var client = new NatsConnection(new NatsOpts
        {
            Url = $"nats://127.0.0.1:{server.Port}",
            AuthOpts = new NatsAuthOpts { Token = "wrongtoken" },
            MaxReconnectRetry = 0,
        });

        var ex = await Should.ThrowAsync<Exception>(async () =>
        {
            await client.ConnectAsync();
            await client.PingAsync();
        });

        ex.ShouldNotBeNull();
    }

    [Fact]
    public async Task NKeyAuth_ValidSignature_Connects()
    {
        await using var client = new NatsConnection(new NatsOpts
        {
            Url = $"nats://127.0.0.1:{fixture.Port}",
            AuthOpts = new NatsAuthOpts
            {
                NKey = fixture.NKeyPublicKey,
                Seed = fixture.NKeySeed,
            },
        });

        await client.ConnectAsync();
        await client.PingAsync();
        client.ConnectionState.ShouldBe(NatsConnectionState.Open);
    }

    [Fact]
    public async Task NKeyAuth_InvalidSignature_Rejected()
    {
        // Generate a fresh key pair that is NOT registered with the server
        var otherSeed = NATS.Client.Core.NKeys.CreateUserSeed();
        var otherPublicKey = NATS.Client.Core.NKeys.PublicKeyFromSeed(otherSeed);

        await using var client = new NatsConnection(new NatsOpts
        {
            Url = $"nats://127.0.0.1:{fixture.Port}",
            AuthOpts = new NatsAuthOpts
            {
                NKey = otherPublicKey,
                Seed = otherSeed,
            },
            MaxReconnectRetry = 0,
        });

        var ex = await Should.ThrowAsync<Exception>(async () =>
        {
            await client.ConnectAsync();
            await client.PingAsync();
        });

        ex.ShouldNotBeNull();
    }

    [Fact]
    public async Task Permission_PublishAllowed_Succeeds()
    {
        await using var pub = fixture.CreateClient("pubonly", "pubpass");
        await using var sub = fixture.CreateClient("testuser", "testpass");

        await pub.ConnectAsync();
        await sub.ConnectAsync();

        await using var subscription = await sub.SubscribeCoreAsync<string>("allowed.foo");
        await sub.PingAsync();

        await pub.PublishAsync("allowed.foo", "hello");

        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
        var msg = await subscription.Msgs.ReadAsync(cts.Token);
        msg.Data.ShouldBe("hello");
    }

    [Fact]
    public async Task Permission_PublishDenied_NoDelivery()
    {
        await using var pub = fixture.CreateClient("pubonly", "pubpass");
        await using var sub = fixture.CreateClient("testuser", "testpass");

        await pub.ConnectAsync();
        await sub.ConnectAsync();

        await using var subscription = await sub.SubscribeCoreAsync<string>("denied.foo");
        await sub.PingAsync();

        await pub.PublishAsync("denied.foo", "should-not-arrive");

        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1));
        var readTask = subscription.Msgs.ReadAsync(cts.Token).AsTask();

        await Should.ThrowAsync<OperationCanceledException>(async () => await readTask);
    }

    [Fact]
    public async Task Permission_SubscribeDenied_Rejected()
    {
        await using var pub = fixture.CreateClient("testuser", "testpass");
        await using var sub = fixture.CreateClient("subonly", "subpass");

        await pub.ConnectAsync();
        await sub.ConnectAsync();

        // subonly may not subscribe to denied.topic — the subscription silently
        // fails or is dropped by the server; no message should arrive
        await using var subscription = await sub.SubscribeCoreAsync<string>("denied.topic");
        await sub.PingAsync();

        await pub.PublishAsync("denied.topic", "should-not-arrive");

        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1));
        var readTask = subscription.Msgs.ReadAsync(cts.Token).AsTask();

        await Should.ThrowAsync<OperationCanceledException>(async () => await readTask);
    }

    [Fact]
    public async Task MaxSubscriptions_ExceedsLimit_Rejected()
    {
        // The server is configured with max_subs: 5 (server-wide).
        // Open a single connection and exhaust the limit.
        await using var client = fixture.CreateClient("limited", "limpass");
        await client.ConnectAsync();

        var subscriptions = new List<IAsyncDisposable>();
        try
        {
            // Create subscriptions up to the server max
            for (var i = 0; i < 5; i++)
                subscriptions.Add(await client.SubscribeCoreAsync<string>($"max.subs.test.{i}"));

            // The 6th subscription should cause the server to close the connection
            var ex = await Should.ThrowAsync<Exception>(async () =>
            {
                subscriptions.Add(await client.SubscribeCoreAsync<string>("max.subs.test.6"));
                // Force a round-trip so the server error is observed
                await client.PingAsync();
            });

            ex.ShouldNotBeNull();
        }
        finally
        {
            foreach (var s in subscriptions)
                await s.DisposeAsync();
        }
    }

    [Fact]
    public async Task MaxPayload_ExceedsLimit_Disconnected()
    {
        // The fixture server has max_payload: 512; send > 512 bytes
        await using var client = fixture.CreateClient("testuser", "testpass");
        await client.ConnectAsync();

        var oversized = new string('x', 600);

        var ex = await Should.ThrowAsync<Exception>(async () =>
        {
            await client.PublishAsync("payload.test", oversized);
            // Force a round-trip to observe the server's error response
            await client.PingAsync();
        });

        ex.ShouldNotBeNull();
    }
}
