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

namespace NATS.Server.Tests;

public class AuthIntegrationTests
{
    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;
    }

    /// <summary>
    /// Checks whether any exception in the chain contains the given substring.
    /// The NATS client wraps server errors in outer NatsException messages,
    /// so the actual "Authorization Violation" may be in an inner exception.
    /// </summary>
    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;
    }

    private static (NatsServer server, int port, CancellationTokenSource cts) StartServer(NatsOptions options)
    {
        var port = GetFreePort();
        options.Port = port;
        var server = new NatsServer(options, NullLoggerFactory.Instance);
        var cts = new CancellationTokenSource();
        _ = server.StartAsync(cts.Token);
        return (server, port, cts);
    }

    private static async Task<(NatsServer server, int port, CancellationTokenSource cts)> StartServerAsync(NatsOptions options)
    {
        var (server, port, cts) = StartServer(options);
        await server.WaitForReadyAsync();
        return (server, port, cts);
    }

    [Fact]
    public async Task Token_auth_success()
    {
        var (server, port, cts) = await StartServerAsync(new NatsOptions
        {
            Authorization = "s3cr3t",
        });

        try
        {
            await using var client = new NatsConnection(new NatsOpts
            {
                Url = $"nats://s3cr3t@127.0.0.1:{port}",
            });
            await client.ConnectAsync();
            await client.PingAsync();
        }
        finally
        {
            await cts.CancelAsync();
            server.Dispose();
        }
    }

    [Fact]
    public async Task Token_auth_failure_disconnects()
    {
        var (server, port, cts) = await StartServerAsync(new NatsOptions
        {
            Authorization = "s3cr3t",
        });

        try
        {
            await using var client = new NatsConnection(new NatsOpts
            {
                Url = $"nats://wrongtoken@127.0.0.1:{port}",
                MaxReconnectRetry = 0,
            });

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

            ExceptionChainContains(ex, "Authorization Violation").ShouldBeTrue(
                $"Expected 'Authorization Violation' in exception chain, but got: {ex}");
        }
        finally
        {
            await cts.CancelAsync();
            server.Dispose();
        }
    }

    [Fact]
    public async Task UserPassword_auth_success()
    {
        var (server, port, cts) = await StartServerAsync(new NatsOptions
        {
            Username = "admin",
            Password = "secret",
        });

        try
        {
            await using var client = new NatsConnection(new NatsOpts
            {
                Url = $"nats://admin:secret@127.0.0.1:{port}",
            });
            await client.ConnectAsync();
            await client.PingAsync();
        }
        finally
        {
            await cts.CancelAsync();
            server.Dispose();
        }
    }

    [Fact]
    public async Task UserPassword_auth_failure_disconnects()
    {
        var (server, port, cts) = await StartServerAsync(new NatsOptions
        {
            Username = "admin",
            Password = "secret",
        });

        try
        {
            await using var client = new NatsConnection(new NatsOpts
            {
                Url = $"nats://admin:wrong@127.0.0.1:{port}",
                MaxReconnectRetry = 0,
            });

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

            ExceptionChainContains(ex, "Authorization Violation").ShouldBeTrue(
                $"Expected 'Authorization Violation' in exception chain, but got: {ex}");
        }
        finally
        {
            await cts.CancelAsync();
            server.Dispose();
        }
    }

    [Fact]
    public async Task MultiUser_auth_success()
    {
        var (server, port, cts) = await StartServerAsync(new NatsOptions
        {
            Users =
            [
                new User { Username = "alice", Password = "pass1" },
                new User { Username = "bob", Password = "pass2" },
            ],
        });

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

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

            await bob.ConnectAsync();
            await bob.PingAsync();
        }
        finally
        {
            await cts.CancelAsync();
            server.Dispose();
        }
    }

    [Fact]
    public async Task No_credentials_when_auth_required_disconnects()
    {
        var (server, port, cts) = await StartServerAsync(new NatsOptions
        {
            Authorization = "s3cr3t",
        });

        try
        {
            await using var client = new NatsConnection(new NatsOpts
            {
                Url = $"nats://127.0.0.1:{port}",
                MaxReconnectRetry = 0,
            });

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

            ExceptionChainContains(ex, "Authorization Violation").ShouldBeTrue(
                $"Expected 'Authorization Violation' in exception chain, but got: {ex}");
        }
        finally
        {
            await cts.CancelAsync();
            server.Dispose();
        }
    }

    [Fact]
    public async Task No_auth_configured_allows_all()
    {
        var (server, port, cts) = await StartServerAsync(new NatsOptions());

        try
        {
            await using var client = new NatsConnection(new NatsOpts
            {
                Url = $"nats://127.0.0.1:{port}",
            });
            await client.ConnectAsync();
            await client.PingAsync();
        }
        finally
        {
            await cts.CancelAsync();
            server.Dispose();
        }
    }
}
