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

namespace NATS.Server.Tests.Accounts;

/// <summary>
/// Tests for authentication mechanisms: username/password, token, NKey-based auth,
/// no-auth-user fallback, multi-user, and AuthService orchestration.
/// Reference: Go auth_test.go — TestUserClone*, TestNoAuthUser, TestUserConnectionDeadline, etc.
/// Reference: Go accounts_test.go — TestAccountMapsUsers.
/// </summary>
public class AuthMechanismTests
{
    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 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: TestUserCloneNilPermissions server/auth_test.go:34
    [Fact]
    public void User_with_nil_permissions()
    {
        var user = new User
        {
            Username = "foo",
            Password = "bar",
        };

        user.Permissions.ShouldBeNull();
    }

    // Go: TestUserClone server/auth_test.go:53
    [Fact]
    public void User_with_permissions_has_correct_fields()
    {
        var user = new User
        {
            Username = "foo",
            Password = "bar",
            Permissions = new Permissions
            {
                Publish = new SubjectPermission { Allow = ["foo"] },
                Subscribe = new SubjectPermission { Allow = ["bar"] },
            },
        };

        user.Username.ShouldBe("foo");
        user.Password.ShouldBe("bar");
        user.Permissions.ShouldNotBeNull();
        user.Permissions.Publish!.Allow![0].ShouldBe("foo");
        user.Permissions.Subscribe!.Allow![0].ShouldBe("bar");
    }

    // Go: TestUserClonePermissionsNoLists server/auth_test.go:80
    [Fact]
    public void User_with_empty_permissions()
    {
        var user = new User
        {
            Username = "foo",
            Password = "bar",
            Permissions = new Permissions(),
        };

        user.Permissions!.Publish.ShouldBeNull();
        user.Permissions!.Subscribe.ShouldBeNull();
    }

    // Go: TestNoAuthUser (token auth success) server/auth_test.go:225
    [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();
        }
    }

    // Go: auth mechanism — token auth failure
    [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();
        }
    }

    // Go: auth mechanism — user/password success
    [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();
        }
    }

    // Go: auth mechanism — user/password failure
    [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();
        }
    }

    // Go: TestNoAuthUser server/auth_test.go:225 — multi-user auth
    [Fact]
    public async Task MultiUser_auth_each_user_succeeds()
    {
        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();
        }
    }

    // Go: TestNoAuthUser server/auth_test.go:225 — wrong user password
    [Fact]
    public async Task MultiUser_wrong_password_fails()
    {
        var (server, port, cts) = await StartServerAsync(new NatsOptions
        {
            Users =
            [
                new User { Username = "alice", Password = "pass1" },
            ],
        });

        try
        {
            await using var client = new NatsConnection(new NatsOpts
            {
                Url = $"nats://alice: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', but got: {ex}");
        }
        finally
        {
            await cts.CancelAsync();
            server.Dispose();
        }
    }

    // Go: auth mechanism — no credentials with auth required
    [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', but got: {ex}");
        }
        finally
        {
            await cts.CancelAsync();
            server.Dispose();
        }
    }

    // Go: auth mechanism — no auth configured allows all
    [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();
        }
    }

    // Go: TestNoAuthUser server/auth_test.go:225 — no_auth_user fallback
    [Fact]
    public async Task NoAuthUser_fallback_allows_unauthenticated_connection()
    {
        var (server, port, cts) = await StartServerAsync(new NatsOptions
        {
            Users =
            [
                new User { Username = "foo", Password = "pwd1", Account = "FOO" },
                new User { Username = "bar", Password = "pwd2", Account = "BAR" },
            ],
            NoAuthUser = "foo",
        });

        try
        {
            // Connect without credentials — should use no_auth_user "foo"
            await using var client = new NatsConnection(new NatsOpts
            {
                Url = $"nats://127.0.0.1:{port}",
            });
            await client.ConnectAsync();
            await client.PingAsync();

            // Explicit auth also still works
            await using var bar = new NatsConnection(new NatsOpts
            {
                Url = $"nats://bar:pwd2@127.0.0.1:{port}",
            });
            await bar.ConnectAsync();
            await bar.PingAsync();
        }
        finally
        {
            await cts.CancelAsync();
            server.Dispose();
        }
    }

    // Go: TestNoAuthUser server/auth_test.go:225 — invalid pwd with no_auth_user still fails
    [Fact]
    public async Task NoAuthUser_wrong_password_still_fails()
    {
        var (server, port, cts) = await StartServerAsync(new NatsOptions
        {
            Users =
            [
                new User { Username = "foo", Password = "pwd1", Account = "FOO" },
                new User { Username = "bar", Password = "pwd2", Account = "BAR" },
            ],
            NoAuthUser = "foo",
        });

        try
        {
            await using var client = new NatsConnection(new NatsOpts
            {
                Url = $"nats://bar: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 auth violation, got: {ex}");
        }
        finally
        {
            await cts.CancelAsync();
            server.Dispose();
        }
    }

    // Go: AuthService — tests the build logic for auth service
    [Fact]
    public void AuthService_build_with_no_auth_returns_not_required()
    {
        var authService = AuthService.Build(new NatsOptions());
        authService.IsAuthRequired.ShouldBeFalse();
        authService.NonceRequired.ShouldBeFalse();
    }

    // Go: AuthService — tests the build logic for token auth
    [Fact]
    public void AuthService_build_with_token_marks_auth_required()
    {
        var authService = AuthService.Build(new NatsOptions { Authorization = "secret" });
        authService.IsAuthRequired.ShouldBeTrue();
        authService.NonceRequired.ShouldBeFalse();
    }

    // Go: AuthService — tests the build logic for user/password auth
    [Fact]
    public void AuthService_build_with_user_password_marks_auth_required()
    {
        var authService = AuthService.Build(new NatsOptions
        {
            Username = "admin",
            Password = "secret",
        });
        authService.IsAuthRequired.ShouldBeTrue();
        authService.NonceRequired.ShouldBeFalse();
    }

    // Go: AuthService — tests the build logic for nkey auth
    [Fact]
    public void AuthService_build_with_nkeys_marks_nonce_required()
    {
        var authService = AuthService.Build(new NatsOptions
        {
            NKeys = [new NKeyUser { Nkey = "UABC123" }],
        });
        authService.IsAuthRequired.ShouldBeTrue();
        authService.NonceRequired.ShouldBeTrue();
    }

    // Go: AuthService — tests the build logic for multi-user auth
    [Fact]
    public void AuthService_build_with_users_marks_auth_required()
    {
        var authService = AuthService.Build(new NatsOptions
        {
            Users = [new User { Username = "alice", Password = "pass" }],
        });
        authService.IsAuthRequired.ShouldBeTrue();
    }

    // Go: AuthService.Authenticate — token match
    [Fact]
    public void AuthService_authenticate_token_success()
    {
        var authService = AuthService.Build(new NatsOptions { Authorization = "mytoken" });
        var result = authService.Authenticate(new ClientAuthContext
        {
            Opts = new ClientOptions { Token = "mytoken" },
            Nonce = [],
        });

        result.ShouldNotBeNull();
        result.Identity.ShouldBe("token");
    }

    // Go: AuthService.Authenticate — token mismatch
    [Fact]
    public void AuthService_authenticate_token_failure()
    {
        var authService = AuthService.Build(new NatsOptions { Authorization = "mytoken" });
        var result = authService.Authenticate(new ClientAuthContext
        {
            Opts = new ClientOptions { Token = "wrong" },
            Nonce = [],
        });

        result.ShouldBeNull();
    }

    // Go: AuthService.Authenticate — user/password match
    [Fact]
    public void AuthService_authenticate_user_password_success()
    {
        var authService = AuthService.Build(new NatsOptions
        {
            Users = [new User { Username = "alice", Password = "pass", Account = "acct-a" }],
        });

        var result = authService.Authenticate(new ClientAuthContext
        {
            Opts = new ClientOptions { Username = "alice", Password = "pass" },
            Nonce = [],
        });

        result.ShouldNotBeNull();
        result.Identity.ShouldBe("alice");
        result.AccountName.ShouldBe("acct-a");
    }

    // Go: AuthService.Authenticate — user/password mismatch
    [Fact]
    public void AuthService_authenticate_user_password_failure()
    {
        var authService = AuthService.Build(new NatsOptions
        {
            Users = [new User { Username = "alice", Password = "pass" }],
        });

        var result = authService.Authenticate(new ClientAuthContext
        {
            Opts = new ClientOptions { Username = "alice", Password = "wrong" },
            Nonce = [],
        });

        result.ShouldBeNull();
    }

    // Go: AuthService.Authenticate — no auth user fallback
    [Fact]
    public void AuthService_authenticate_no_auth_user_fallback()
    {
        var authService = AuthService.Build(new NatsOptions
        {
            Users =
            [
                new User { Username = "foo", Password = "pwd1", Account = "FOO" },
            ],
            NoAuthUser = "foo",
        });

        // No credentials provided — should fall back to no_auth_user
        var result = authService.Authenticate(new ClientAuthContext
        {
            Opts = new ClientOptions(),
            Nonce = [],
        });

        result.ShouldNotBeNull();
        result.Identity.ShouldBe("foo");
        result.AccountName.ShouldBe("FOO");
    }

    // Go: AuthService.GenerateNonce — nonce generation
    [Fact]
    public void AuthService_generates_unique_nonces()
    {
        var authService = AuthService.Build(new NatsOptions
        {
            NKeys = [new NKeyUser { Nkey = "UABC" }],
        });

        var nonce1 = authService.GenerateNonce();
        var nonce2 = authService.GenerateNonce();

        nonce1.Length.ShouldBe(11);
        nonce2.Length.ShouldBe(11);
        // Extremely unlikely to be the same
        nonce1.ShouldNotBe(nonce2);
    }

    // Go: AuthService.EncodeNonce — nonce encoding
    [Fact]
    public void AuthService_nonce_encoding_is_url_safe_base64()
    {
        var authService = AuthService.Build(new NatsOptions());
        var nonce = new byte[] { 0xFF, 0xFE, 0xFD, 0xFC, 0xFB, 0xFA, 0xF9, 0xF8, 0xF7, 0xF6, 0xF5 };

        var encoded = authService.EncodeNonce(nonce);

        // Should not contain standard base64 padding or non-URL-safe characters
        encoded.ShouldNotContain("=");
        encoded.ShouldNotContain("+");
        encoded.ShouldNotContain("/");
    }
}
