// Port of Go server/jwt_test.go — JWT claims, trust chain, signing keys, revocation,
// bearer tokens, permission templates, connection type filtering, and account limits.
// Reference: golang/nats-server/server/jwt_test.go

using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using NATS.NKeys;
using NATS.Server.Auth;
using NATS.Server.Auth.Jwt;
using NATS.Server.Protocol;

namespace NATS.Server.Tests.Auth
{

/// <summary>
/// Parity tests ported from Go server/jwt_test.go covering JWT authentication:
/// trust chain validation, signing key hierarchy, expiration, revocation,
/// bearer tokens, permission claims, account limits, connection type filtering,
/// and permission template expansion.
/// </summary>
public class JwtGoParityTests
{
    // =========================================================================
    // Test helpers — mirror Go's opTrustBasicSetup, buildMemAccResolver, etc.
    // Reference: jwt_test.go:65-170
    // =========================================================================

    /// <summary>Creates an operator key pair (mirrors Go oKp / nkeys.CreateOperator).</summary>
    private static KeyPair CreateOperatorKp()
        => KeyPair.CreatePair(PrefixByte.Operator);

    /// <summary>Creates a fresh account key pair (mirrors nkeys.CreateAccount).</summary>
    private static KeyPair CreateAccountKp()
        => KeyPair.CreatePair(PrefixByte.Account);

    /// <summary>Creates a fresh user key pair (mirrors nkeys.CreateUser).</summary>
    private static KeyPair CreateUserKp()
        => KeyPair.CreatePair(PrefixByte.User);

    /// <summary>
    /// Encodes a JWT payload using Ed25519 signing with the given key pair.
    /// Matches the NATS JWT wire format: base64url(header).base64url(payload).base64url(signature).
    /// Reference: github.com/nats-io/jwt/v2 — Claims.Encode
    /// </summary>
    private static string EncodeJwt(object payload, KeyPair signingKp)
    {
        var header = new JwtEncodingHeader { Algorithm = "ed25519-nkey", Type = "jwt" };
        var headerJson = JsonSerializer.Serialize(header);
        var payloadJson = JsonSerializer.Serialize(payload);

        var headerB64 = Base64UrlEncode(Encoding.UTF8.GetBytes(headerJson));
        var payloadB64 = Base64UrlEncode(Encoding.UTF8.GetBytes(payloadJson));

        var signingInput = $"{headerB64}.{payloadB64}";
        var signingInputBytes = Encoding.UTF8.GetBytes(signingInput);

        var sig = new byte[64];
        signingKp.Sign(signingInputBytes, sig);

        var sigB64 = Base64UrlEncode(sig);
        return $"{headerB64}.{payloadB64}.{sigB64}";
    }

    private static string Base64UrlEncode(byte[] data)
        => Convert.ToBase64String(data).TrimEnd('=').Replace('+', '-').Replace('/', '_');

    private static long Now() => DateTimeOffset.UtcNow.ToUnixTimeSeconds();
    private static long Ago(int seconds) => Now() - seconds;
    private static long InFuture(int seconds) => Now() + seconds;

    /// <summary>
    /// Builds an account JWT signed by the operator key pair.
    /// Reference: jwt_test.go:111-140 setupJWTTestWithClaims
    /// </summary>
    private static string BuildAccountJwt(
        KeyPair operatorKp,
        string accountPub,
        string[]? signingKeys = null,
        long? expiresAt = null,
        long? issuedAt = null,
        Dictionary<string, long>? revocations = null,
        JwtTestAccountLimits? limits = null,
        string? name = null,
        string[]? tags = null)
    {
        var operatorPub = operatorKp.GetPublicKey();
        var payload = new JwtTestAccountClaims
        {
            Subject = accountPub,
            Issuer = operatorPub,
            IssuedAt = issuedAt ?? Now(),
            Expires = expiresAt ?? 0,
            Name = name,
            Nats = new JwtTestAccountNats
            {
                Type = "account",
                Version = 2,
                SigningKeys = signingKeys,
                Revocations = revocations,
                Limits = limits,
                Tags = tags,
            },
        };
        return EncodeJwt(payload, operatorKp);
    }

    /// <summary>
    /// Builds a user JWT signed by the account key pair (or a signing key).
    /// Reference: jwt_test.go:62-109 createClientWithIssuer / createClient
    /// </summary>
    private static string BuildUserJwt(
        KeyPair signingKp,
        string userPub,
        string? issuerAccount = null,
        long? expiresAt = null,
        long? issuedAt = null,
        bool bearerToken = false,
        string[]? pubAllow = null,
        string[]? pubDeny = null,
        string[]? subAllow = null,
        string[]? subDeny = null,
        string[]? allowedConnectionTypes = null,
        string? name = null,
        string[]? tags = null)
    {
        var signingPub = signingKp.GetPublicKey();
        var payload = new JwtTestUserClaims
        {
            Subject = userPub,
            Issuer = signingPub,
            IssuedAt = issuedAt ?? Now(),
            Expires = expiresAt ?? 0,
            Name = name,
            Nats = new JwtTestUserNats
            {
                Type = "user",
                Version = 2,
                IssuerAccount = issuerAccount,
                BearerToken = bearerToken ? true : null,
                AllowedConnectionTypes = allowedConnectionTypes,
                Tags = tags,
                Pub = (pubAllow != null || pubDeny != null)
                    ? new JwtTestSubjectPerm { Allow = pubAllow, Deny = pubDeny }
                    : null,
                Sub = (subAllow != null || subDeny != null)
                    ? new JwtTestSubjectPerm { Allow = subAllow, Deny = subDeny }
                    : null,
            },
        };
        return EncodeJwt(payload, signingKp);
    }

    /// <summary>
    /// Builds a JwtAuthenticator with the given operator key pair and account JWT pre-loaded.
    /// Reference: jwt_test.go:137-141 opTrustBasicSetup / buildMemAccResolver / addAccountToMemResolver
    /// </summary>
    private static (JwtAuthenticator auth, MemAccountResolver resolver) BuildAuthenticator(
        KeyPair operatorKp,
        string? accountPub = null,
        string? accountJwt = null)
    {
        var resolver = new MemAccountResolver();
        if (accountPub != null && accountJwt != null)
            resolver.StoreAsync(accountPub, accountJwt).GetAwaiter().GetResult();

        var auth = new JwtAuthenticator([operatorKp.GetPublicKey()], resolver);
        return (auth, resolver);
    }

    /// <summary>Creates a valid nonce signature for a user key pair.</summary>
    private static (byte[] nonce, string sig) CreateNonceSig(KeyPair userKp)
    {
        var nonce = new byte[16];
        RandomNumberGenerator.Fill(nonce);
        var sig = new byte[64];
        userKp.Sign(nonce, sig);
        var sigStr = Base64UrlEncode(sig);
        return (nonce, sigStr);
    }

    /// <summary>Calls Authenticate with a freshly-signed nonce.</summary>
    private static AuthResult? Authenticate(
        JwtAuthenticator auth,
        string userJwt,
        KeyPair userKp)
    {
        var (nonce, sig) = CreateNonceSig(userKp);
        return auth.Authenticate(new ClientAuthContext
        {
            Opts = new ClientOptions { JWT = userJwt, Sig = sig },
            Nonce = nonce,
        });
    }

    // =========================================================================
    // TestJWTUser — basic trust chain: operator -> account -> user
    // Go reference: jwt_test.go:199 TestJWTUser
    // =========================================================================

    [Fact]
    public void JwtUser_NoJwt_ReturnsNull()
    {
        // Go: TestJWTUser — connecting without jwt field should fail (return null).
        using var operatorKp = CreateOperatorKp();
        using var accountKp = CreateAccountKp();
        var accountPub = accountKp.GetPublicKey();
        var accountJwt = BuildAccountJwt(operatorKp, accountPub);

        var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);

        var result = auth.Authenticate(new ClientAuthContext
        {
            Opts = new ClientOptions(),
            Nonce = [],
        });

        result.ShouldBeNull();
    }

    [Fact]
    public void JwtUser_NoAccountResolver_ReturnsNull()
    {
        // Go: TestJWTUser — connecting with JWT but no account in resolver should fail.
        using var operatorKp = CreateOperatorKp();
        using var accountKp = CreateAccountKp();
        using var userKp = CreateUserKp();
        var accountPub = accountKp.GetPublicKey();
        var userPub = userKp.GetPublicKey();

        // Authenticator with empty resolver
        var resolver = new MemAccountResolver();
        var auth = new JwtAuthenticator([operatorKp.GetPublicKey()], resolver);

        var userJwt = BuildUserJwt(accountKp, userPub);
        var result = Authenticate(auth, userJwt, userKp);

        result.ShouldBeNull();
    }

    [Fact]
    public void JwtUser_ValidTrustChain_ReturnsResult()
    {
        // Go: TestJWTUser — valid operator -> account -> user chain allows connection.
        using var operatorKp = CreateOperatorKp();
        using var accountKp = CreateAccountKp();
        using var userKp = CreateUserKp();
        var accountPub = accountKp.GetPublicKey();
        var userPub = userKp.GetPublicKey();

        var accountJwt = BuildAccountJwt(operatorKp, accountPub);
        var userJwt = BuildUserJwt(accountKp, userPub);
        var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);

        var result = Authenticate(auth, userJwt, userKp);

        result.ShouldNotBeNull();
        result.Identity.ShouldBe(userPub);
        result.AccountName.ShouldBe(accountPub);
    }

    // =========================================================================
    // TestJWTUserBadTrusted — bad trusted key rejects valid JWT
    // Go reference: jwt_test.go:255 TestJWTUserBadTrusted
    // =========================================================================

    [Fact]
    public async Task JwtUser_BadTrustedKey_ReturnsNull()
    {
        // Go: TestJWTUserBadTrusted — replacing trusted keys with invalid key rejects connections.
        using var operatorKp = CreateOperatorKp();
        using var accountKp = CreateAccountKp();
        using var userKp = CreateUserKp();
        var accountPub = accountKp.GetPublicKey();
        var userPub = userKp.GetPublicKey();

        var accountJwt = BuildAccountJwt(operatorKp, accountPub);
        var userJwt = BuildUserJwt(accountKp, userPub);

        using var badOperatorKp = CreateOperatorKp();
        var resolver = new MemAccountResolver();
        await resolver.StoreAsync(accountPub, accountJwt);
        var auth = new JwtAuthenticator([badOperatorKp.GetPublicKey()], resolver);

        var result = Authenticate(auth, userJwt, userKp);

        result.ShouldBeNull();
    }

    // =========================================================================
    // TestJWTUserExpired — expired user JWT
    // Go reference: jwt_test.go:292 TestJWTUserExpired
    // =========================================================================

    [Fact]
    public void JwtUser_ExpiredUser_ReturnsNull()
    {
        // Go: TestJWTUserExpired — expired user JWT is rejected.
        using var operatorKp = CreateOperatorKp();
        using var accountKp = CreateAccountKp();
        using var userKp = CreateUserKp();
        var accountPub = accountKp.GetPublicKey();
        var userPub = userKp.GetPublicKey();

        var accountJwt = BuildAccountJwt(operatorKp, accountPub);
        var userJwt = BuildUserJwt(accountKp, userPub,
            issuedAt: Ago(10),
            expiresAt: Ago(2));
        var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);

        var result = Authenticate(auth, userJwt, userKp);

        result.ShouldBeNull();
    }

    // =========================================================================
    // TestJWTUserExpiresAfterConnect — JWT not yet expired returns result
    // Go reference: jwt_test.go:301 TestJWTUserExpiresAfterConnect
    // =========================================================================

    [Fact]
    public void JwtUser_NotYetExpired_ReturnsResult()
    {
        // Go: TestJWTUserExpiresAfterConnect — a JWT that expires in the future should succeed at connect time.
        using var operatorKp = CreateOperatorKp();
        using var accountKp = CreateAccountKp();
        using var userKp = CreateUserKp();
        var accountPub = accountKp.GetPublicKey();
        var userPub = userKp.GetPublicKey();

        var accountJwt = BuildAccountJwt(operatorKp, accountPub);
        var userJwt = BuildUserJwt(accountKp, userPub,
            issuedAt: Now(),
            expiresAt: InFuture(60));
        var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);

        var result = Authenticate(auth, userJwt, userKp);

        result.ShouldNotBeNull();
        result.Expiry.ShouldNotBeNull();
        var expiryUnix = result.Expiry!.Value.ToUnixTimeSeconds();
        (expiryUnix >= InFuture(58) && expiryUnix <= InFuture(62)).ShouldBeTrue("expiry should be approximately 60 seconds in the future");
    }

    // =========================================================================
    // TestJWTAccountExpired — expired account JWT decode test
    // Go reference: jwt_test.go:473 TestJWTAccountExpired
    // =========================================================================

    [Fact]
    public void JwtAccount_ExpiredClaims_DecodedCorrectly()
    {
        // Go: TestJWTAccountExpired — expired account JWT can still be decoded.
        // The .NET JwtAuthenticator checks user expiry; account expiry is a server-level concern.
        using var operatorKp = CreateOperatorKp();
        using var accountKp = CreateAccountKp();
        var accountPub = accountKp.GetPublicKey();

        var accountJwt = BuildAccountJwt(operatorKp, accountPub,
            issuedAt: Ago(10),
            expiresAt: Ago(2));

        var claims = NatsJwt.DecodeAccountClaims(accountJwt);
        claims.ShouldNotBeNull();
        claims.Expires.ShouldBeGreaterThan(0);
        (DateTimeOffset.UtcNow.ToUnixTimeSeconds() > claims.Expires).ShouldBeTrue();
    }

    // =========================================================================
    // TestJWTUserPermissionClaims — user permissions from JWT claims
    // Go reference: jwt_test.go:331 TestJWTUserPermissionClaims
    // =========================================================================

    [Fact]
    public void JwtUser_PermissionClaims_Transferred()
    {
        // Go: TestJWTUserPermissionClaims — pub/sub allow/deny transferred from JWT to AuthResult.
        using var operatorKp = CreateOperatorKp();
        using var accountKp = CreateAccountKp();
        using var userKp = CreateUserKp();
        var accountPub = accountKp.GetPublicKey();
        var userPub = userKp.GetPublicKey();

        var accountJwt = BuildAccountJwt(operatorKp, accountPub);
        var userJwt = BuildUserJwt(accountKp, userPub,
            pubAllow: ["foo", "bar"],
            pubDeny: ["baz"],
            subAllow: ["foo", "bar"],
            subDeny: ["baz"]);
        var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);

        var result = Authenticate(auth, userJwt, userKp);

        result.ShouldNotBeNull();
        result.Permissions.ShouldNotBeNull();
        result.Permissions!.Publish.ShouldNotBeNull();
        result.Permissions!.Subscribe.ShouldNotBeNull();

        result.Permissions.Publish!.Allow.ShouldNotBeNull();
        result.Permissions.Publish.Allow!.Count.ShouldBe(2);
        result.Permissions.Publish.Deny.ShouldNotBeNull();
        result.Permissions.Publish.Deny!.Count.ShouldBe(1);

        result.Permissions.Subscribe!.Allow.ShouldNotBeNull();
        result.Permissions.Subscribe.Allow!.Count.ShouldBe(2);
        result.Permissions.Subscribe.Deny.ShouldNotBeNull();
        result.Permissions.Subscribe.Deny!.Count.ShouldBe(1);

        result.Permissions.Publish.Allow.ShouldContain("foo");
        result.Permissions.Publish.Allow.ShouldContain("bar");
        result.Permissions.Publish.Deny.ShouldContain("baz");
        result.Permissions.Subscribe.Allow.ShouldContain("foo");
        result.Permissions.Subscribe.Allow.ShouldContain("bar");
        result.Permissions.Subscribe.Deny.ShouldContain("baz");
    }

    [Fact]
    public void JwtUser_NoPermissions_NullPermissions()
    {
        // Go: user with no permissions results in null Permissions on AuthResult.
        using var operatorKp = CreateOperatorKp();
        using var accountKp = CreateAccountKp();
        using var userKp = CreateUserKp();
        var accountPub = accountKp.GetPublicKey();
        var userPub = userKp.GetPublicKey();

        var accountJwt = BuildAccountJwt(operatorKp, accountPub);
        var userJwt = BuildUserJwt(accountKp, userPub);
        var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);

        var result = Authenticate(auth, userJwt, userKp);

        result.ShouldNotBeNull();
        result.Permissions.ShouldBeNull();
    }

    // =========================================================================
    // TestJWTUserSigningKey — account signing key hierarchy
    // Go reference: jwt_test.go:2241 TestJWTUserSigningKey
    // =========================================================================

    [Fact]
    public void JwtUser_SigningKey_NotInAccount_ReturnsNull()
    {
        // Go: TestJWTUserSigningKey — signing key not registered in account JWT is rejected.
        using var operatorKp = CreateOperatorKp();
        using var accountKp = CreateAccountKp();
        using var signingKp = CreateAccountKp();
        using var userKp = CreateUserKp();
        var accountPub = accountKp.GetPublicKey();
        var userPub = userKp.GetPublicKey();

        var accountJwt = BuildAccountJwt(operatorKp, accountPub);
        var userJwt = BuildUserJwt(signingKp, userPub, issuerAccount: accountPub);
        var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);

        var result = Authenticate(auth, userJwt, userKp);

        result.ShouldBeNull();
    }

    [Fact]
    public void JwtUser_SigningKey_RegisteredInAccount_ReturnsResult()
    {
        // Go: TestJWTUserSigningKey — signing key listed in account JWT allows user connection.
        using var operatorKp = CreateOperatorKp();
        using var accountKp = CreateAccountKp();
        using var signingKp = CreateAccountKp();
        using var userKp = CreateUserKp();
        var accountPub = accountKp.GetPublicKey();
        var signingPub = signingKp.GetPublicKey();
        var userPub = userKp.GetPublicKey();

        var accountJwt = BuildAccountJwt(operatorKp, accountPub, signingKeys: [signingPub]);
        var userJwt = BuildUserJwt(signingKp, userPub, issuerAccount: accountPub);
        var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);

        var result = Authenticate(auth, userJwt, userKp);

        result.ShouldNotBeNull();
        result.Identity.ShouldBe(userPub);
        result.AccountName.ShouldBe(accountPub);
    }

    [Fact]
    public void JwtUser_MultipleSigningKeys_AnyWorks()
    {
        // Go: account can have multiple signing keys; any one can sign user JWTs.
        using var operatorKp = CreateOperatorKp();
        using var accountKp = CreateAccountKp();
        using var signingKp1 = CreateAccountKp();
        using var signingKp2 = CreateAccountKp();
        using var userKp = CreateUserKp();
        var accountPub = accountKp.GetPublicKey();
        var userPub = userKp.GetPublicKey();

        var accountJwt = BuildAccountJwt(operatorKp, accountPub,
            signingKeys: [signingKp1.GetPublicKey(), signingKp2.GetPublicKey()]);
        var userJwt = BuildUserJwt(signingKp2, userPub, issuerAccount: accountPub);
        var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);

        var result = Authenticate(auth, userJwt, userKp);

        result.ShouldNotBeNull();
        result.Identity.ShouldBe(userPub);
    }

    // =========================================================================
    // TestJWTUserRevoked — revoked user JWT
    // Go reference: jwt_test.go:2673 TestJWTUserRevoked
    // =========================================================================

    [Fact]
    public void JwtUser_Revoked_ReturnsNull()
    {
        // Go: TestJWTUserRevoked — user revoked in account JWT is rejected at connect.
        using var operatorKp = CreateOperatorKp();
        using var accountKp = CreateAccountKp();
        using var userKp = CreateUserKp();
        var accountPub = accountKp.GetPublicKey();
        var userPub = userKp.GetPublicKey();

        // Account with user revoked at "now"; user JWT issued 5 seconds ago
        var accountJwt = BuildAccountJwt(operatorKp, accountPub,
            revocations: new Dictionary<string, long> { [userPub] = Now() });
        var userJwt = BuildUserJwt(accountKp, userPub, issuedAt: Ago(5));
        var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);

        var result = Authenticate(auth, userJwt, userKp);

        result.ShouldBeNull();
    }

    [Fact]
    public void JwtUser_NotRevoked_ReturnsResult()
    {
        // Go: user issued AFTER revocation time is not revoked.
        using var operatorKp = CreateOperatorKp();
        using var accountKp = CreateAccountKp();
        using var userKp = CreateUserKp();
        var accountPub = accountKp.GetPublicKey();
        var userPub = userKp.GetPublicKey();

        // Revocation set to 10 seconds ago
        var accountJwt = BuildAccountJwt(operatorKp, accountPub,
            revocations: new Dictionary<string, long> { [userPub] = Ago(10) });
        var userJwt = BuildUserJwt(accountKp, userPub, issuedAt: Now());
        var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);

        var result = Authenticate(auth, userJwt, userKp);

        result.ShouldNotBeNull();
    }

    [Fact]
    public void JwtUser_WildcardRevocation_RevokesAll()
    {
        // Go: TestJWTUserRevocation — wildcard "*" revocation revokes all users issued before the time.
        using var operatorKp = CreateOperatorKp();
        using var accountKp = CreateAccountKp();
        using var userKp = CreateUserKp();
        var accountPub = accountKp.GetPublicKey();
        var userPub = userKp.GetPublicKey();

        var accountJwt = BuildAccountJwt(operatorKp, accountPub,
            revocations: new Dictionary<string, long> { ["*"] = Now() });
        var userJwt = BuildUserJwt(accountKp, userPub, issuedAt: Ago(5));
        var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);

        var result = Authenticate(auth, userJwt, userKp);

        result.ShouldBeNull();
    }

    [Fact]
    public void JwtUser_WildcardRevocation_NewJwt_NotRevoked()
    {
        // Go: TestJWTUserRevocation — user JWT issued after wildcard revocation time is allowed.
        using var operatorKp = CreateOperatorKp();
        using var accountKp = CreateAccountKp();
        using var userKp = CreateUserKp();
        var accountPub = accountKp.GetPublicKey();
        var userPub = userKp.GetPublicKey();

        var accountJwt = BuildAccountJwt(operatorKp, accountPub,
            revocations: new Dictionary<string, long> { ["*"] = Ago(10) });
        var userJwt = BuildUserJwt(accountKp, userPub, issuedAt: Now());
        var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);

        var result = Authenticate(auth, userJwt, userKp);

        result.ShouldNotBeNull();
    }

    // =========================================================================
    // TestJWTBearerToken — bearer token skips nonce signature
    // Go reference: jwt_test.go:2997 TestJWTBearerToken
    // =========================================================================

    [Fact]
    public void JwtBearerToken_NoSignature_ReturnsResult()
    {
        // Go: TestJWTBearerToken — bearer token does not require nonce signature.
        using var operatorKp = CreateOperatorKp();
        using var accountKp = CreateAccountKp();
        using var userKp = CreateUserKp();
        var accountPub = accountKp.GetPublicKey();
        var userPub = userKp.GetPublicKey();

        var accountJwt = BuildAccountJwt(operatorKp, accountPub);
        var userJwt = BuildUserJwt(accountKp, userPub, bearerToken: true);
        var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);

        var result = auth.Authenticate(new ClientAuthContext
        {
            Opts = new ClientOptions { JWT = userJwt },
            Nonce = [],
        });

        result.ShouldNotBeNull();
        result.Identity.ShouldBe(userPub);
    }

    [Fact]
    public void JwtBearerToken_WithIssuerAccount_SameAsAccount_Succeeds()
    {
        // Go: TestJWTBearerWithIssuerSameAsAccountToken — bearer with IssuerAccount = account pub is OK.
        using var operatorKp = CreateOperatorKp();
        using var accountKp = CreateAccountKp();
        using var userKp = CreateUserKp();
        var accountPub = accountKp.GetPublicKey();
        var userPub = userKp.GetPublicKey();

        var accountJwt = BuildAccountJwt(operatorKp, accountPub);
        var userJwt = BuildUserJwt(accountKp, userPub, issuerAccount: accountPub, bearerToken: true);
        var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);

        var result = auth.Authenticate(new ClientAuthContext
        {
            Opts = new ClientOptions { JWT = userJwt },
            Nonce = [],
        });

        result.ShouldNotBeNull();
    }

    [Fact]
    public void JwtBearerToken_WithBadIssuerAccount_ReturnsNull()
    {
        // Go: TestJWTBearerWithBadIssuerToken — bearer with IssuerAccount pointing to different account is rejected.
        using var operatorKp = CreateOperatorKp();
        using var accountKp = CreateAccountKp();
        using var differentAccountKp = CreateAccountKp();
        using var userKp = CreateUserKp();
        var accountPub = accountKp.GetPublicKey();
        var differentAccountPub = differentAccountKp.GetPublicKey();
        var userPub = userKp.GetPublicKey();

        var accountJwt = BuildAccountJwt(operatorKp, accountPub);
        var userJwt = BuildUserJwt(accountKp, userPub, issuerAccount: differentAccountPub, bearerToken: true);
        var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);

        var result = auth.Authenticate(new ClientAuthContext
        {
            Opts = new ClientOptions { JWT = userJwt },
            Nonce = [],
        });

        result.ShouldBeNull();
    }

    [Fact]
    public void JwtNonBearer_NoSignature_ReturnsNull()
    {
        // Go: non-bearer tokens without nonce signature are rejected.
        using var operatorKp = CreateOperatorKp();
        using var accountKp = CreateAccountKp();
        using var userKp = CreateUserKp();
        var accountPub = accountKp.GetPublicKey();
        var userPub = userKp.GetPublicKey();

        var accountJwt = BuildAccountJwt(operatorKp, accountPub);
        var userJwt = BuildUserJwt(accountKp, userPub);
        var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);

        var result = auth.Authenticate(new ClientAuthContext
        {
            Opts = new ClientOptions { JWT = userJwt },
            Nonce = [],
        });

        result.ShouldBeNull();
    }

    [Fact]
    public void JwtNonBearer_BadSignature_ReturnsNull()
    {
        // Go: non-bearer tokens with incorrect nonce signature are rejected.
        using var operatorKp = CreateOperatorKp();
        using var accountKp = CreateAccountKp();
        using var userKp = CreateUserKp();
        using var wrongKp = CreateUserKp();
        var accountPub = accountKp.GetPublicKey();
        var userPub = userKp.GetPublicKey();

        var accountJwt = BuildAccountJwt(operatorKp, accountPub);
        var userJwt = BuildUserJwt(accountKp, userPub);
        var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);

        var nonce = new byte[16];
        RandomNumberGenerator.Fill(nonce);
        var (_, wrongSig) = CreateNonceSig(wrongKp);

        var result = auth.Authenticate(new ClientAuthContext
        {
            Opts = new ClientOptions { JWT = userJwt, Sig = wrongSig },
            Nonce = nonce,
        });

        result.ShouldBeNull();
    }

    // =========================================================================
    // TestJWTNoOperatorMode — non-operator mode
    // Go reference: jwt_test.go:4483 TestJWTNoOperatorMode
    // =========================================================================

    [Fact]
    public void JwtAuth_NoTrustedKeys_AuthNotRequired()
    {
        // Go: TestJWTNoOperatorMode — without operator mode, auth is not required (no TrustedKeys).
        var service = AuthService.Build(new NatsOptions());

        service.IsAuthRequired.ShouldBeFalse();
    }

    [Fact]
    public void JwtAuth_TrustedKeysConfigured_AuthRequired()
    {
        // Go: TestJWTNoOperatorMode — operator mode with TrustedKeys sets auth required.
        using var operatorKp = CreateOperatorKp();
        var service = AuthService.Build(new NatsOptions
        {
            TrustedKeys = [operatorKp.GetPublicKey()],
            AccountResolver = new MemAccountResolver(),
        });

        service.IsAuthRequired.ShouldBeTrue();
        service.NonceRequired.ShouldBeTrue();
    }

    // =========================================================================
    // Connection types filtering
    // Go reference: jwt_test.go — allowed_connection_types claim enforcement
    // =========================================================================

    [Fact]
    public void JwtConnectionTypes_StandardAllowed_Succeeds()
    {
        // Go: when allowed_connection_types includes STANDARD, standard client connects.
        using var operatorKp = CreateOperatorKp();
        using var accountKp = CreateAccountKp();
        using var userKp = CreateUserKp();
        var accountPub = accountKp.GetPublicKey();
        var userPub = userKp.GetPublicKey();

        var accountJwt = BuildAccountJwt(operatorKp, accountPub);
        var userJwt = BuildUserJwt(accountKp, userPub, allowedConnectionTypes: ["STANDARD"]);
        var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);

        var (nonce, sig) = CreateNonceSig(userKp);
        var result = auth.Authenticate(new ClientAuthContext
        {
            Opts = new ClientOptions { JWT = userJwt, Sig = sig },
            Nonce = nonce,
            ConnectionType = "STANDARD",
        });

        result.ShouldNotBeNull();
    }

    [Fact]
    public void JwtConnectionTypes_WebsocketOnly_StandardRejected()
    {
        // Go: when allowed_connection_types only includes WEBSOCKET, standard client is rejected.
        using var operatorKp = CreateOperatorKp();
        using var accountKp = CreateAccountKp();
        using var userKp = CreateUserKp();
        var accountPub = accountKp.GetPublicKey();
        var userPub = userKp.GetPublicKey();

        var accountJwt = BuildAccountJwt(operatorKp, accountPub);
        var userJwt = BuildUserJwt(accountKp, userPub, allowedConnectionTypes: ["WEBSOCKET"]);
        var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);

        var (nonce, sig) = CreateNonceSig(userKp);
        var result = auth.Authenticate(new ClientAuthContext
        {
            Opts = new ClientOptions { JWT = userJwt, Sig = sig },
            Nonce = nonce,
            ConnectionType = "STANDARD",
        });

        result.ShouldBeNull();
    }

    [Fact]
    public void JwtConnectionTypes_EmptyList_AllConnectionsAllowed()
    {
        // Go: empty allowed_connection_types means all types are allowed.
        using var operatorKp = CreateOperatorKp();
        using var accountKp = CreateAccountKp();
        using var userKp = CreateUserKp();
        var accountPub = accountKp.GetPublicKey();
        var userPub = userKp.GetPublicKey();

        var accountJwt = BuildAccountJwt(operatorKp, accountPub);
        var userJwt = BuildUserJwt(accountKp, userPub);
        var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);

        var (nonce, sig) = CreateNonceSig(userKp);
        var result = auth.Authenticate(new ClientAuthContext
        {
            Opts = new ClientOptions { JWT = userJwt, Sig = sig },
            Nonce = nonce,
            ConnectionType = "STANDARD",
        });

        result.ShouldNotBeNull();
    }

    [Fact]
    public void JwtConnectionTypes_MultipleTypes_AnyMatchingAllowed()
    {
        // Go: multiple allowed connection types — any matching type is allowed.
        using var operatorKp = CreateOperatorKp();
        using var accountKp = CreateAccountKp();
        using var userKp = CreateUserKp();
        var accountPub = accountKp.GetPublicKey();
        var userPub = userKp.GetPublicKey();

        var accountJwt = BuildAccountJwt(operatorKp, accountPub);
        var userJwt = BuildUserJwt(accountKp, userPub,
            allowedConnectionTypes: ["STANDARD", "WEBSOCKET", "LEAFNODE"]);
        var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);

        foreach (var connType in new[] { "STANDARD", "WEBSOCKET", "LEAFNODE" })
        {
            var (nonce, sig) = CreateNonceSig(userKp);
            var result = auth.Authenticate(new ClientAuthContext
            {
                Opts = new ClientOptions { JWT = userJwt, Sig = sig },
                Nonce = nonce,
                ConnectionType = connType,
            });
            result.ShouldNotBeNull($"expected connection type {connType} to be allowed");
        }
    }

    [Fact]
    public void JwtConnectionTypes_UnknownTypeOnly_ReturnsNull()
    {
        // Go: unknown connection type values cause rejection.
        using var operatorKp = CreateOperatorKp();
        using var accountKp = CreateAccountKp();
        using var userKp = CreateUserKp();
        var accountPub = accountKp.GetPublicKey();
        var userPub = userKp.GetPublicKey();

        var accountJwt = BuildAccountJwt(operatorKp, accountPub);
        var userJwt = BuildUserJwt(accountKp, userPub, allowedConnectionTypes: ["UNKNOWN_TYPE_XYZ"]);
        var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);

        var (nonce, sig) = CreateNonceSig(userKp);
        var result = auth.Authenticate(new ClientAuthContext
        {
            Opts = new ClientOptions { JWT = userJwt, Sig = sig },
            Nonce = nonce,
            ConnectionType = "STANDARD",
        });

        result.ShouldBeNull();
    }

    // =========================================================================
    // NatsJwt.IsJwt — JWT string detection
    // Go reference: jwt_test.go:4951 TestJWTHeader
    // =========================================================================

    [Fact]
    public void NatsJwt_IsJwt_ValidJwt_ReturnsTrue()
    {
        // Go: TestJWTHeader — valid JWT starts with "eyJ".
        using var accountKp = CreateAccountKp();
        using var userKp = CreateUserKp();
        var userJwt = BuildUserJwt(accountKp, userKp.GetPublicKey());

        NatsJwt.IsJwt(userJwt).ShouldBeTrue();
        userJwt.ShouldStartWith("eyJ");
    }

    [Fact]
    public void NatsJwt_IsJwt_EmptyString_ReturnsFalse()
    {
        NatsJwt.IsJwt("").ShouldBeFalse();
        NatsJwt.IsJwt(null!).ShouldBeFalse();
    }

    [Fact]
    public void NatsJwt_IsJwt_PlainString_ReturnsFalse()
    {
        NatsJwt.IsJwt("not-a-jwt").ShouldBeFalse();
        NatsJwt.IsJwt("Bearer token").ShouldBeFalse();
    }

    // =========================================================================
    // NatsJwt.Decode — structural JWT parsing
    // Go reference: jwt_test.go:4951 TestJWTHeader
    // =========================================================================

    [Fact]
    public void NatsJwt_Decode_ValidToken_ReturnsToken()
    {
        // Go: TestJWTHeader — JWT decoded into header+payload+signature.
        using var accountKp = CreateAccountKp();
        using var userKp = CreateUserKp();
        var userJwt = BuildUserJwt(accountKp, userKp.GetPublicKey());

        var decoded = NatsJwt.Decode(userJwt);

        decoded.ShouldNotBeNull();
        decoded.Header.ShouldNotBeNull();
        decoded.Header.Algorithm.ShouldBe("ed25519-nkey");
        decoded.Header.Type.ShouldBe("jwt");
        decoded.PayloadJson.ShouldNotBeNullOrEmpty();
        decoded.Signature.ShouldNotBeNull();
        decoded.Signature.Length.ShouldBe(64);
    }

    [Fact]
    public void NatsJwt_Decode_MalformedToken_ReturnsNull()
    {
        NatsJwt.Decode("not.valid").ShouldBeNull();
        NatsJwt.Decode("").ShouldBeNull();
        NatsJwt.Decode("a.b.c.d").ShouldBeNull();
    }

    // =========================================================================
    // NatsJwt.DecodeUserClaims — user claims deserialization
    // Go reference: jwt_test.go — user claims structures
    // =========================================================================

    [Fact]
    public void NatsJwt_DecodeUserClaims_AllFields_Populated()
    {
        // Go: user claims decoded from JWT with all standard fields.
        using var accountKp = CreateAccountKp();
        using var userKp = CreateUserKp();
        var accountPub = accountKp.GetPublicKey();
        var userPub = userKp.GetPublicKey();
        var iat = Now();
        var exp = InFuture(3600);

        var userJwt = BuildUserJwt(accountKp, userPub,
            issuedAt: iat,
            expiresAt: exp,
            name: "testuser",
            pubAllow: ["foo", "bar"]);

        var claims = NatsJwt.DecodeUserClaims(userJwt);

        claims.ShouldNotBeNull();
        claims.Subject.ShouldBe(userPub);
        claims.Issuer.ShouldBe(accountPub);
        claims.IssuedAt.ShouldBe(iat);
        claims.Expires.ShouldBe(exp);
        claims.Name.ShouldBe("testuser");
        claims.Nats.ShouldNotBeNull();
        claims.Nats!.Pub.ShouldNotBeNull();
        claims.Nats.Pub!.Allow.ShouldNotBeNull();
        claims.Nats.Pub.Allow!.Length.ShouldBe(2);
    }

    [Fact]
    public void NatsJwt_DecodeUserClaims_BearerToken_Parsed()
    {
        // Go: TestJWTBearerToken — bearer_token field parsed correctly.
        using var accountKp = CreateAccountKp();
        using var userKp = CreateUserKp();

        var userJwt = BuildUserJwt(accountKp, userKp.GetPublicKey(), bearerToken: true);
        var claims = NatsJwt.DecodeUserClaims(userJwt);

        claims.ShouldNotBeNull();
        claims.BearerToken.ShouldBeTrue();
    }

    [Fact]
    public void NatsJwt_DecodeUserClaims_IssuerAccount_Parsed()
    {
        // Go: TestJWTUserSigningKey — issuer_account field preserved in claims.
        using var accountKp = CreateAccountKp();
        using var signingKp = CreateAccountKp();
        using var userKp = CreateUserKp();
        var accountPub = accountKp.GetPublicKey();

        var userJwt = BuildUserJwt(signingKp, userKp.GetPublicKey(), issuerAccount: accountPub);
        var claims = NatsJwt.DecodeUserClaims(userJwt);

        claims.ShouldNotBeNull();
        claims.IssuerAccount.ShouldBe(accountPub);
    }

    // =========================================================================
    // NatsJwt.DecodeAccountClaims — account claims deserialization
    // Go reference: jwt_test.go:629 TestJWTAccountBasicImportExport
    // =========================================================================

    [Fact]
    public void NatsJwt_DecodeAccountClaims_AllFields_Populated()
    {
        // Go: TestJWTAccountBasicImportExport — account claims decoded with signing keys and revocations.
        using var operatorKp = CreateOperatorKp();
        using var accountKp = CreateAccountKp();
        using var signingKp = CreateAccountKp();
        var accountPub = accountKp.GetPublicKey();
        var signingPub = signingKp.GetPublicKey();
        var revokedUser = "UREVOKED123";
        var revocationTime = Ago(100);

        var accountJwt = BuildAccountJwt(operatorKp, accountPub,
            signingKeys: [signingPub],
            revocations: new Dictionary<string, long> { [revokedUser] = revocationTime },
            name: "TestAccount");

        var claims = NatsJwt.DecodeAccountClaims(accountJwt);

        claims.ShouldNotBeNull();
        claims.Subject.ShouldBe(accountPub);
        claims.Issuer.ShouldBe(operatorKp.GetPublicKey());
        claims.Name.ShouldBe("TestAccount");
        claims.Nats.ShouldNotBeNull();
        claims.Nats!.SigningKeys.ShouldNotBeNull();
        claims.Nats.SigningKeys!.ShouldContain(signingPub);
        claims.Nats.Revocations.ShouldNotBeNull();
        claims.Nats.Revocations!.ShouldContainKey(revokedUser);
        claims.Nats.Revocations[revokedUser].ShouldBe(revocationTime);
    }

    // =========================================================================
    // NatsJwt.Verify — JWT signature verification
    // Go reference: jwt_test.go — trust chain verification
    // =========================================================================

    [Fact]
    public void NatsJwt_Verify_ValidSignature_ReturnsTrue()
    {
        using var accountKp = CreateAccountKp();
        using var userKp = CreateUserKp();
        var accountPub = accountKp.GetPublicKey();
        var userJwt = BuildUserJwt(accountKp, userKp.GetPublicKey());

        NatsJwt.Verify(userJwt, accountPub).ShouldBeTrue();
    }

    [Fact]
    public void NatsJwt_Verify_WrongKey_ReturnsFalse()
    {
        using var accountKp = CreateAccountKp();
        using var wrongKp = CreateAccountKp();
        using var userKp = CreateUserKp();
        var userJwt = BuildUserJwt(accountKp, userKp.GetPublicKey());

        NatsJwt.Verify(userJwt, wrongKp.GetPublicKey()).ShouldBeFalse();
    }

    [Fact]
    public void NatsJwt_Verify_TamperedPayload_ReturnsFalse()
    {
        using var accountKp = CreateAccountKp();
        using var userKp = CreateUserKp();
        var accountPub = accountKp.GetPublicKey();
        var userJwt = BuildUserJwt(accountKp, userKp.GetPublicKey());

        var parts = userJwt.Split('.');
        var tamperedJwt = $"{parts[0]}.{parts[1]}X.{parts[2]}";

        NatsJwt.Verify(tamperedJwt, accountPub).ShouldBeFalse();
    }

    // =========================================================================
    // MemAccountResolver — in-memory resolver store/fetch
    // Go reference: jwt_test.go:65-90 buildMemAccResolver / addAccountToMemResolver
    // =========================================================================

    [Fact]
    public async Task MemAccountResolver_StoreAndFetch_ReturnsJwt()
    {
        var resolver = new MemAccountResolver();
        resolver.IsReadOnly.ShouldBeFalse();

        await resolver.StoreAsync("ABCDEF", "jwt-value");
        var jwt = await resolver.FetchAsync("ABCDEF");

        jwt.ShouldBe("jwt-value");
    }

    [Fact]
    public async Task MemAccountResolver_FetchUnknown_ReturnsNull()
    {
        var resolver = new MemAccountResolver();
        var jwt = await resolver.FetchAsync("NONEXISTENT");

        jwt.ShouldBeNull();
    }

    [Fact]
    public async Task MemAccountResolver_UpdateExisting_ReplacesJwt()
    {
        var resolver = new MemAccountResolver();
        await resolver.StoreAsync("ACCT1", "old-jwt");
        await resolver.StoreAsync("ACCT1", "new-jwt");

        var jwt = await resolver.FetchAsync("ACCT1");
        jwt.ShouldBe("new-jwt");
    }

    [Fact]
    public async Task MemAccountResolver_MultipleAccounts_IndependentStorage()
    {
        var resolver = new MemAccountResolver();
        await resolver.StoreAsync("ACCT1", "jwt-for-acct1");
        await resolver.StoreAsync("ACCT2", "jwt-for-acct2");

        (await resolver.FetchAsync("ACCT1")).ShouldBe("jwt-for-acct1");
        (await resolver.FetchAsync("ACCT2")).ShouldBe("jwt-for-acct2");
    }

    // =========================================================================
    // Permission templates
    // Go reference: jwt_test.go:4315 TestJWTTemplates
    // =========================================================================

    [Fact]
    public void PermissionTemplates_NameSubjectAccountExpansion()
    {
        // Go: TestJWTTemplates — name/subject/account-name/account-subject expand correctly.
        var result = PermissionTemplates.Expand(
            "foo.{{name()}}.{{subject()}}.{{account-name()}}.{{account-subject()}}.bar",
            name: "myname", subject: "UABC123",
            accountName: "accname", accountSubject: "AABC456",
            userTags: [], accountTags: []);

        result.Count.ShouldBe(1);
        result[0].ShouldBe("foo.myname.UABC123.accname.AABC456.bar");
    }

    [Fact]
    public void PermissionTemplates_TagExpansion_SingleTag()
    {
        // Go: TestJWTInLineTemplates — {{tag(bucket)}} expands to tag value.
        var result = PermissionTemplates.Expand(
            "$JS.API.STREAM.INFO.KV_{{tag(bucket)}}",
            name: "myname", subject: "UABC123",
            accountName: "accname", accountSubject: "AABC456",
            userTags: ["bucket:a"], accountTags: []);

        result.Count.ShouldBe(1);
        result[0].ShouldBe("$JS.API.STREAM.INFO.KV_a");
    }

    [Fact]
    public void PermissionTemplates_TagExpansion_MultipleValues_CartesianProduct()
    {
        // Go: TestJWTTemplates — multiple tag values produce cartesian product.
        var result = PermissionTemplates.Expand(
            "{{tag(foo)}}.none.{{tag(bar)}}",
            name: "myname", subject: "UABC123",
            accountName: "accname", accountSubject: "AABC456",
            userTags: ["foo:foo1", "foo:foo2", "bar:bar1", "bar:bar2", "bar:bar3"],
            accountTags: []);

        result.Count.ShouldBe(6);
        result.ShouldContain("foo1.none.bar1");
        result.ShouldContain("foo2.none.bar3");
    }

    [Fact]
    public void PermissionTemplates_AccountTagExpansion()
    {
        // Go: TestJWTTemplates — {{account-tag(acc)}} uses account tags.
        var result = PermissionTemplates.Expand(
            "{{tag(foo)}}.{{account-tag(acc)}}",
            name: "myname", subject: "UABC123",
            accountName: "accname", accountSubject: "AABC456",
            userTags: ["foo:foo1", "foo:foo2"],
            accountTags: ["acc:acc1", "acc:acc2"]);

        result.Count.ShouldBe(4);
        result.ShouldContain("foo1.acc1");
        result.ShouldContain("foo2.acc2");
    }

    [Fact]
    public void PermissionTemplates_MissingTag_EmptyResult()
    {
        // Go: TestJWTTemplates — missing tag produces empty result (subject dropped).
        var result = PermissionTemplates.Expand(
            "{{tag(NOT_THERE)}}",
            name: "myname", subject: "UABC123",
            accountName: "accname", accountSubject: "AABC456",
            userTags: ["foo:foo1"], accountTags: []);

        result.Count.ShouldBe(0);
    }

    [Fact]
    public void PermissionTemplates_NoTemplates_ReturnsOriginal()
    {
        var result = PermissionTemplates.Expand(
            "plain.subject.>",
            name: "myname", subject: "UABC123",
            accountName: "accname", accountSubject: "AABC456",
            userTags: [], accountTags: []);

        result.Count.ShouldBe(1);
        result[0].ShouldBe("plain.subject.>");
    }

    [Fact]
    public void PermissionTemplates_ExpandAll_MultiplePatterns_Flattened()
    {
        // Go: TestJWTTemplates — ExpandAll processes multiple patterns, dropping empties.
        var patterns = new[] { "{{name()}}", "plain.sub", "{{tag(NOT_THERE)}}" };
        var result = PermissionTemplates.ExpandAll(patterns,
            name: "myname", subject: "UABC123",
            accountName: "accname", accountSubject: "AABC456",
            userTags: [], accountTags: []);

        result.Count.ShouldBe(2);
        result.ShouldContain("myname");
        result.ShouldContain("plain.sub");
    }

    // =========================================================================
    // JwtConnectionTypes static conversion
    // Go reference: jwt_test.go — allowed_connection_types claim
    // =========================================================================

    [Fact]
    public void JwtConnectionTypesConvert_KnownTypes_AllRecognized()
    {
        var knownTypes = new[]
        {
            "STANDARD", "WEBSOCKET", "LEAFNODE", "LEAFNODE_WS", "MQTT", "MQTT_WS", "INPROCESS"
        };

        foreach (var type in knownTypes)
        {
            var (valid, hasUnknown) = JwtConnectionTypesTestHelper.Convert([type]);
            valid.ShouldContain(type);
            hasUnknown.ShouldBeFalse();
        }
    }

    [Fact]
    public void JwtConnectionTypesConvert_CaseInsensitive_Normalized()
    {
        var (valid, _) = JwtConnectionTypesTestHelper.Convert(["standard", "WebSocket"]);
        valid.ShouldContain("STANDARD");
        valid.ShouldContain("WEBSOCKET");
    }

    [Fact]
    public void JwtConnectionTypesConvert_UnknownType_HasUnknownFlag()
    {
        var (valid, hasUnknown) = JwtConnectionTypesTestHelper.Convert(["UNKNOWN_XYZ"]);
        valid.Count.ShouldBe(0);
        hasUnknown.ShouldBeTrue();
    }

    [Fact]
    public void JwtConnectionTypesConvert_MixedKnownUnknown_PartialValid()
    {
        var (valid, hasUnknown) = JwtConnectionTypesTestHelper.Convert(["STANDARD", "UNKNOWN_TYPE"]);
        valid.ShouldContain("STANDARD");
        hasUnknown.ShouldBeTrue();
    }

    [Fact]
    public void JwtConnectionTypesConvert_EmptyList_BothEmpty()
    {
        var (valid, hasUnknown) = JwtConnectionTypesTestHelper.Convert([]);
        valid.Count.ShouldBe(0);
        hasUnknown.ShouldBeFalse();
    }

    // =========================================================================
    // UserClaims helpers
    // Go reference: jwt_test.go:292 TestJWTUserExpired — IsExpired()
    // =========================================================================

    [Fact]
    public void UserClaims_IsExpired_ZeroExpiry_ReturnsFalse()
    {
        new UserClaims { Expires = 0 }.IsExpired().ShouldBeFalse();
    }

    [Fact]
    public void UserClaims_IsExpired_FutureExpiry_ReturnsFalse()
    {
        new UserClaims { Expires = InFuture(3600) }.IsExpired().ShouldBeFalse();
    }

    [Fact]
    public void UserClaims_IsExpired_PastExpiry_ReturnsTrue()
    {
        new UserClaims { Expires = Ago(10) }.IsExpired().ShouldBeTrue();
    }

    [Fact]
    public void UserClaims_GetExpiry_ZeroExpires_ReturnsNull()
    {
        new UserClaims { Expires = 0 }.GetExpiry().ShouldBeNull();
    }

    [Fact]
    public void UserClaims_GetExpiry_NonZeroExpires_ReturnsDateTimeOffset()
    {
        var exp = InFuture(3600);
        var expiry = new UserClaims { Expires = exp }.GetExpiry();
        expiry.ShouldNotBeNull();
        expiry!.Value.ToUnixTimeSeconds().ShouldBe(exp);
    }

    // =========================================================================
    // Trust chain: user issuer validation
    // Go reference: jwt_test.go:2241 TestJWTUserSigningKey
    // =========================================================================

    [Fact]
    public void JwtAuth_UserIssuedByWrongAccount_ReturnsNull()
    {
        using var operatorKp = CreateOperatorKp();
        using var accountKp = CreateAccountKp();
        using var wrongAccountKp = CreateAccountKp();
        using var userKp = CreateUserKp();
        var accountPub = accountKp.GetPublicKey();
        var userPub = userKp.GetPublicKey();

        var accountJwt = BuildAccountJwt(operatorKp, accountPub);
        var userJwt = BuildUserJwt(wrongAccountKp, userPub);
        var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);

        Authenticate(auth, userJwt, userKp).ShouldBeNull();
    }

    [Fact]
    public async Task JwtAuth_AccountIssuedByWrongOperator_ReturnsNull()
    {
        using var trustedOpKp = CreateOperatorKp();
        using var untrustedOpKp = CreateOperatorKp();
        using var accountKp = CreateAccountKp();
        using var userKp = CreateUserKp();
        var accountPub = accountKp.GetPublicKey();
        var userPub = userKp.GetPublicKey();

        var accountJwt = BuildAccountJwt(untrustedOpKp, accountPub);
        var userJwt = BuildUserJwt(accountKp, userPub);

        var resolver = new MemAccountResolver();
        await resolver.StoreAsync(accountPub, accountJwt);
        var auth = new JwtAuthenticator([trustedOpKp.GetPublicKey()], resolver);

        Authenticate(auth, userJwt, userKp).ShouldBeNull();
    }

    // =========================================================================
    // Account limits from JWT claims
    // Go reference: jwt_test.go:1088 TestJWTAccountLimitsSubs
    // =========================================================================

    [Fact]
    public void JwtAuth_AccountLimits_Parsed_FromClaims()
    {
        // Go: TestJWTAccountLimitsSubs — account limits decoded correctly from JWT.
        using var operatorKp = CreateOperatorKp();
        using var accountKp = CreateAccountKp();
        var accountPub = accountKp.GetPublicKey();

        var accountJwt = BuildAccountJwt(operatorKp, accountPub,
            limits: new JwtTestAccountLimits
            {
                MaxSubscriptions = 10,
                MaxPayload = 8,
                MaxConnections = 5,
                MaxData = 1024 * 1024,
            });

        var claims = NatsJwt.DecodeAccountClaims(accountJwt);
        claims.ShouldNotBeNull();
        claims.Nats!.Limits.ShouldNotBeNull();
        claims.Nats.Limits!.MaxSubscriptions.ShouldBe(10);
        claims.Nats.Limits.MaxPayload.ShouldBe(8);
        claims.Nats.Limits.MaxConnections.ShouldBe(5);
        claims.Nats.Limits.MaxData.ShouldBe(1024 * 1024);
    }

    // =========================================================================
    // JetStream limits from JWT
    // Go reference: jwt_test.go:5400 TestJWTJetStreamTiers
    // =========================================================================

    [Fact]
    public void JwtAuth_JetStreamLimits_InAuthResult()
    {
        // Go: TestJWTJetStreamTiers — JetStream max_streams from account JWT in AuthResult.
        using var operatorKp = CreateOperatorKp();
        using var accountKp = CreateAccountKp();
        using var userKp = CreateUserKp();
        var accountPub = accountKp.GetPublicKey();
        var userPub = userKp.GetPublicKey();

        var accountJwt = BuildAccountJwtWithJetStream(operatorKp, accountPub, maxStreams: 10);
        var userJwt = BuildUserJwt(accountKp, userPub);
        var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);

        var result = Authenticate(auth, userJwt, userKp);

        result.ShouldNotBeNull();
        result.MaxJetStreamStreams.ShouldBe(10);
    }

    // =========================================================================
    // Invalid JWT token edge cases
    // =========================================================================

    [Fact]
    public void JwtAuth_MalformedToken_ReturnsNull()
    {
        using var operatorKp = CreateOperatorKp();
        var auth = new JwtAuthenticator([operatorKp.GetPublicKey()], new MemAccountResolver());

        auth.Authenticate(new ClientAuthContext
        {
            Opts = new ClientOptions { JWT = "not-a-jwt-at-all" },
            Nonce = [],
        }).ShouldBeNull();
    }

    [Fact]
    public void JwtAuth_EmptyJwtField_ReturnsNull()
    {
        using var operatorKp = CreateOperatorKp();
        var auth = new JwtAuthenticator([operatorKp.GetPublicKey()], new MemAccountResolver());

        auth.Authenticate(new ClientAuthContext
        {
            Opts = new ClientOptions { JWT = "" },
            Nonce = [],
        }).ShouldBeNull();
    }

    // =========================================================================
    // Signing key removed blocks new connections
    // Go reference: jwt_test.go:2338 TestJWTAccountImportSignerRemoved
    // =========================================================================

    [Fact]
    public async Task JwtAuth_SigningKeyRemoved_NewConnectionFails()
    {
        // Go: TestJWTAccountImportSignerRemoved — removing signing key blocks new connections.
        using var operatorKp = CreateOperatorKp();
        using var accountKp = CreateAccountKp();
        using var signingKp = CreateAccountKp();
        using var userKp = CreateUserKp();
        var accountPub = accountKp.GetPublicKey();
        var signingPub = signingKp.GetPublicKey();
        var userPub = userKp.GetPublicKey();

        var resolver = new MemAccountResolver();

        // Step 1: signing key registered — succeeds
        var accountJwt = BuildAccountJwt(operatorKp, accountPub, signingKeys: [signingPub]);
        await resolver.StoreAsync(accountPub, accountJwt);
        var auth = new JwtAuthenticator([operatorKp.GetPublicKey()], resolver);
        var userJwt = BuildUserJwt(signingKp, userPub, issuerAccount: accountPub);

        Authenticate(auth, userJwt, userKp).ShouldNotBeNull();

        // Step 2: signing key removed — new connection fails
        var accountJwtNoKey = BuildAccountJwt(operatorKp, accountPub, signingKeys: null);
        await resolver.StoreAsync(accountPub, accountJwtNoKey);
        var auth2 = new JwtAuthenticator([operatorKp.GetPublicKey()], resolver);

        Authenticate(auth2, userJwt, userKp).ShouldBeNull();
    }

    // =========================================================================
    // AuthResult identity and account name
    // =========================================================================

    [Fact]
    public void JwtAuth_AuthResult_IdentityIsUserPublicKey()
    {
        using var operatorKp = CreateOperatorKp();
        using var accountKp = CreateAccountKp();
        using var userKp = CreateUserKp();
        var accountPub = accountKp.GetPublicKey();
        var userPub = userKp.GetPublicKey();

        var (auth, _) = BuildAuthenticator(operatorKp, accountPub,
            BuildAccountJwt(operatorKp, accountPub));
        var result = Authenticate(auth, BuildUserJwt(accountKp, userPub), userKp);

        result.ShouldNotBeNull();
        result.Identity.ShouldBe(userPub);
        result.Identity.ShouldStartWith("U");
    }

    [Fact]
    public void JwtAuth_AuthResult_AccountNameIsAccountPublicKey()
    {
        using var operatorKp = CreateOperatorKp();
        using var accountKp = CreateAccountKp();
        using var userKp = CreateUserKp();
        var accountPub = accountKp.GetPublicKey();
        var userPub = userKp.GetPublicKey();

        var (auth, _) = BuildAuthenticator(operatorKp, accountPub,
            BuildAccountJwt(operatorKp, accountPub));
        var result = Authenticate(auth, BuildUserJwt(accountKp, userPub), userKp);

        result.ShouldNotBeNull();
        result.AccountName.ShouldBe(accountPub);
        result.AccountName!.ShouldStartWith("A");
    }

    [Fact]
    public void JwtAuth_AuthResult_WithSigningKey_AccountNameIsAccountPub()
    {
        // Go: TestJWTUserSigningKey — AccountName is account pub even when signing key used.
        using var operatorKp = CreateOperatorKp();
        using var accountKp = CreateAccountKp();
        using var signingKp = CreateAccountKp();
        using var userKp = CreateUserKp();
        var accountPub = accountKp.GetPublicKey();
        var signingPub = signingKp.GetPublicKey();
        var userPub = userKp.GetPublicKey();

        var accountJwt = BuildAccountJwt(operatorKp, accountPub, signingKeys: [signingPub]);
        var userJwt = BuildUserJwt(signingKp, userPub, issuerAccount: accountPub);
        var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);

        var result = Authenticate(auth, userJwt, userKp);

        result.ShouldNotBeNull();
        result.AccountName.ShouldBe(accountPub);
    }

    // =========================================================================
    // Permission template integration in JwtAuthenticator
    // Go reference: jwt_test.go:4315 TestJWTTemplates
    // =========================================================================

    [Fact]
    public void JwtAuth_PermissionTemplates_Expanded_InAuthResult()
    {
        // Go: TestJWTTemplates — permission templates in JWT are expanded during authentication.
        using var operatorKp = CreateOperatorKp();
        using var accountKp = CreateAccountKp();
        using var userKp = CreateUserKp();
        var accountPub = accountKp.GetPublicKey();
        var userPub = userKp.GetPublicKey();

        var accountJwt = BuildAccountJwt(operatorKp, accountPub, name: "accname");
        var userJwt = BuildUserJwt(accountKp, userPub,
            name: "myname",
            pubAllow: ["foo.{{name()}}"],
            subAllow: ["bar.{{subject()}}"]);
        var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);

        var result = Authenticate(auth, userJwt, userKp);

        result.ShouldNotBeNull();
        result.Permissions.ShouldNotBeNull();
        result.Permissions!.Publish!.Allow.ShouldNotBeNull();
        result.Permissions.Publish.Allow!.ShouldContain("foo.myname");
        result.Permissions.Subscribe!.Allow.ShouldNotBeNull();
        result.Permissions.Subscribe.Allow!.ShouldContain($"bar.{userPub}");
    }

    // =========================================================================
    // NatsJwt.VerifyNonce — nonce signature verification
    // Go reference: jwt_test.go — nonce signing in createClientWithIssuer
    // =========================================================================

    [Fact]
    public void NatsJwt_VerifyNonce_ValidSignature_ReturnsTrue()
    {
        using var userKp = CreateUserKp();
        var userPub = userKp.GetPublicKey();

        var nonce = new byte[16];
        RandomNumberGenerator.Fill(nonce);
        var sig = new byte[64];
        userKp.Sign(nonce, sig);

        NatsJwt.VerifyNonce(nonce, Base64UrlEncode(sig), userPub).ShouldBeTrue();
    }

    [Fact]
    public void NatsJwt_VerifyNonce_WrongKey_ReturnsFalse()
    {
        using var userKp = CreateUserKp();
        using var wrongKp = CreateUserKp();

        var nonce = new byte[16];
        RandomNumberGenerator.Fill(nonce);
        var sig = new byte[64];
        userKp.Sign(nonce, sig);

        NatsJwt.VerifyNonce(nonce, Base64UrlEncode(sig), wrongKp.GetPublicKey()).ShouldBeFalse();
    }

    [Fact]
    public void NatsJwt_VerifyNonce_ModifiedNonce_ReturnsFalse()
    {
        using var userKp = CreateUserKp();
        var userPub = userKp.GetPublicKey();

        var nonce = new byte[16];
        RandomNumberGenerator.Fill(nonce);
        var sig = new byte[64];
        userKp.Sign(nonce, sig);
        var sigStr = Base64UrlEncode(sig);

        nonce[0] ^= 0xFF; // tamper after signing

        NatsJwt.VerifyNonce(nonce, sigStr, userPub).ShouldBeFalse();
    }

    // =========================================================================
    // Helper: build account JWT with JetStream limits
    // =========================================================================

    private static string BuildAccountJwtWithJetStream(
        KeyPair operatorKp,
        string accountPub,
        int maxStreams)
    {
        var payload = new JwtTestAccountClaimsJs
        {
            Subject = accountPub,
            Issuer = operatorKp.GetPublicKey(),
            IssuedAt = Now(),
            Expires = 0,
            Nats = new JwtTestAccountNatsJs
            {
                Type = "account",
                Version = 2,
                JetStream = new JwtTestJetStreamLimits { MaxStreams = maxStreams },
            },
        };
        return EncodeJwt(payload, operatorKp);
    }
}

// =========================================================================
// Internal helper: expose JwtConnectionTypes.Convert (internal) for testing
// =========================================================================

internal static class JwtConnectionTypesTestHelper
{
    public static (HashSet<string> Valid, bool HasUnknown) Convert(IEnumerable<string> values)
        => JwtConnectionTypes.Convert(values);
}

// =========================================================================
// Internal payload types for JWT encoding in tests.
// Prefixed "JwtTest" to avoid conflicts with production types.
// =========================================================================

internal sealed class JwtEncodingHeader
{
    [JsonPropertyName("alg")] public string? Algorithm { get; set; }
    [JsonPropertyName("typ")] public string? Type { get; set; }
}

internal sealed class JwtTestAccountClaims
{
    [JsonPropertyName("sub")] public string? Subject { get; set; }
    [JsonPropertyName("iss")] public string? Issuer { get; set; }
    [JsonPropertyName("iat")] public long IssuedAt { get; set; }
    [JsonPropertyName("exp")] public long Expires { get; set; }
    [JsonPropertyName("name")] public string? Name { get; set; }
    [JsonPropertyName("nats")] public JwtTestAccountNats? Nats { get; set; }
}

internal sealed class JwtTestAccountNats
{
    [JsonPropertyName("type")] public string? Type { get; set; }
    [JsonPropertyName("version")] public int Version { get; set; }
    [JsonPropertyName("signing_keys")] public string[]? SigningKeys { get; set; }
    [JsonPropertyName("revocations")] public Dictionary<string, long>? Revocations { get; set; }
    [JsonPropertyName("limits")] public JwtTestAccountLimits? Limits { get; set; }
    [JsonPropertyName("tags")] public string[]? Tags { get; set; }
}

internal sealed class JwtTestAccountLimits
{
    [JsonPropertyName("conn")] public long MaxConnections { get; set; }
    [JsonPropertyName("subs")] public long MaxSubscriptions { get; set; }
    [JsonPropertyName("payload")] public long MaxPayload { get; set; }
    [JsonPropertyName("data")] public long MaxData { get; set; }
}

internal sealed class JwtTestAccountClaimsJs
{
    [JsonPropertyName("sub")] public string? Subject { get; set; }
    [JsonPropertyName("iss")] public string? Issuer { get; set; }
    [JsonPropertyName("iat")] public long IssuedAt { get; set; }
    [JsonPropertyName("exp")] public long Expires { get; set; }
    [JsonPropertyName("nats")] public JwtTestAccountNatsJs? Nats { get; set; }
}

internal sealed class JwtTestAccountNatsJs
{
    [JsonPropertyName("type")] public string? Type { get; set; }
    [JsonPropertyName("version")] public int Version { get; set; }
    [JsonPropertyName("jetstream")] public JwtTestJetStreamLimits? JetStream { get; set; }
}

internal sealed class JwtTestJetStreamLimits
{
    [JsonPropertyName("max_streams")] public int MaxStreams { get; set; }
}

internal sealed class JwtTestUserClaims
{
    [JsonPropertyName("sub")] public string? Subject { get; set; }
    [JsonPropertyName("iss")] public string? Issuer { get; set; }
    [JsonPropertyName("iat")] public long IssuedAt { get; set; }
    [JsonPropertyName("exp")] public long Expires { get; set; }
    [JsonPropertyName("name")] public string? Name { get; set; }
    [JsonPropertyName("nats")] public JwtTestUserNats? Nats { get; set; }
}

internal sealed class JwtTestUserNats
{
    [JsonPropertyName("type")] public string? Type { get; set; }
    [JsonPropertyName("version")] public int Version { get; set; }
    [JsonPropertyName("issuer_account")] public string? IssuerAccount { get; set; }
    [JsonPropertyName("bearer_token")]
    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    public bool? BearerToken { get; set; }
    [JsonPropertyName("allowed_connection_types")] public string[]? AllowedConnectionTypes { get; set; }
    [JsonPropertyName("tags")] public string[]? Tags { get; set; }
    [JsonPropertyName("pub")] public JwtTestSubjectPerm? Pub { get; set; }
    [JsonPropertyName("sub")] public JwtTestSubjectPerm? Sub { get; set; }
}

internal sealed class JwtTestSubjectPerm
{
    [JsonPropertyName("allow")] public string[]? Allow { get; set; }
    [JsonPropertyName("deny")] public string[]? Deny { get; set; }
}

} // namespace NATS.Server.Tests.Auth
