using System.Text;
using System.Text.Json;
using NATS.NKeys;
using NATS.Server.Auth.Jwt;

namespace NATS.Server.Auth.Tests;

public class JwtTests
{
    /// <summary>
    /// Helper: base64url-encode a string for constructing test JWTs.
    /// </summary>
    private static string Base64UrlEncode(string value)
    {
        var bytes = Encoding.UTF8.GetBytes(value);
        return Convert.ToBase64String(bytes)
            .TrimEnd('=')
            .Replace('+', '-')
            .Replace('/', '_');
    }

    /// <summary>
    /// Helper: build a minimal unsigned JWT from header and payload JSON strings.
    /// The signature part is a base64url-encoded 64-byte zero array (invalid but structurally correct).
    /// </summary>
    private static string BuildUnsignedToken(string headerJson, string payloadJson)
    {
        var header = Base64UrlEncode(headerJson);
        var payload = Base64UrlEncode(payloadJson);
        var fakeSig = Convert.ToBase64String(new byte[64])
            .TrimEnd('=')
            .Replace('+', '-')
            .Replace('/', '_');
        return $"{header}.{payload}.{fakeSig}";
    }

    /// <summary>
    /// Helper: build a real signed NATS JWT using an NKey keypair.
    /// Signs header.payload with Ed25519.
    /// </summary>
    private static string BuildSignedToken(string payloadJson, KeyPair signingKey)
    {
        var headerJson = """{"typ":"JWT","alg":"ed25519-nkey"}""";
        var header = Base64UrlEncode(headerJson);
        var payload = Base64UrlEncode(payloadJson);
        var signingInput = $"{header}.{payload}";
        var signingInputBytes = Encoding.UTF8.GetBytes(signingInput);

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

        var sigB64 = Convert.ToBase64String(sig)
            .TrimEnd('=')
            .Replace('+', '-')
            .Replace('/', '_');

        return $"{header}.{payload}.{sigB64}";
    }

    // =====================================================================
    // IsJwt tests
    // =====================================================================

    [Fact]
    public void IsJwt_returns_true_for_eyJ_prefix()
    {
        NatsJwt.IsJwt("eyJhbGciOiJlZDI1NTE5LW5rZXkiLCJ0eXAiOiJKV1QifQ.payload.sig").ShouldBeTrue();
    }

    [Fact]
    public void IsJwt_returns_true_for_minimal_eyJ()
    {
        NatsJwt.IsJwt("eyJ").ShouldBeTrue();
    }

    [Fact]
    public void IsJwt_returns_false_for_non_jwt()
    {
        NatsJwt.IsJwt("notajwt").ShouldBeFalse();
    }

    [Fact]
    public void IsJwt_returns_false_for_empty_string()
    {
        NatsJwt.IsJwt("").ShouldBeFalse();
    }

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

    // =====================================================================
    // Decode tests
    // =====================================================================

    [Fact]
    public void Decode_splits_header_payload_signature_correctly()
    {
        var headerJson = """{"typ":"JWT","alg":"ed25519-nkey"}""";
        var payloadJson = """{"sub":"UAXXX","iss":"AAXXX","iat":1700000000}""";
        var token = BuildUnsignedToken(headerJson, payloadJson);

        var result = NatsJwt.Decode(token);

        result.ShouldNotBeNull();
        result.Header.ShouldNotBeNull();
        result.Header.Type.ShouldBe("JWT");
        result.Header.Algorithm.ShouldBe("ed25519-nkey");
    }

    [Fact]
    public void Decode_returns_payload_json()
    {
        var headerJson = """{"typ":"JWT","alg":"ed25519-nkey"}""";
        var payloadJson = """{"sub":"UAXXX","iss":"AAXXX","iat":1700000000}""";
        var token = BuildUnsignedToken(headerJson, payloadJson);

        var result = NatsJwt.Decode(token);

        result.ShouldNotBeNull();
        result.PayloadJson.ShouldNotBeNullOrEmpty();

        // The payload JSON should parse back to matching fields
        using var doc = JsonDocument.Parse(result.PayloadJson);
        doc.RootElement.GetProperty("sub").GetString().ShouldBe("UAXXX");
        doc.RootElement.GetProperty("iss").GetString().ShouldBe("AAXXX");
    }

    [Fact]
    public void Decode_preserves_signature_bytes()
    {
        var headerJson = """{"typ":"JWT","alg":"ed25519-nkey"}""";
        var payloadJson = """{"sub":"test"}""";
        var token = BuildUnsignedToken(headerJson, payloadJson);

        var result = NatsJwt.Decode(token);

        result.ShouldNotBeNull();
        result.Signature.ShouldNotBeNull();
        result.Signature.Length.ShouldBe(64);
    }

    [Fact]
    public void Decode_preserves_signing_input()
    {
        var headerJson = """{"typ":"JWT","alg":"ed25519-nkey"}""";
        var payloadJson = """{"sub":"test"}""";
        var token = BuildUnsignedToken(headerJson, payloadJson);

        var result = NatsJwt.Decode(token);

        result.ShouldNotBeNull();

        // SigningInput should be "header.payload" (the first two parts)
        var parts = token.Split('.');
        var expectedSigningInput = $"{parts[0]}.{parts[1]}";
        result.SigningInput.ShouldBe(expectedSigningInput);
    }

    [Fact]
    public void Decode_returns_null_for_invalid_token_missing_parts()
    {
        NatsJwt.Decode("onlyonepart").ShouldBeNull();
    }

    [Fact]
    public void Decode_returns_null_for_two_parts()
    {
        NatsJwt.Decode("part1.part2").ShouldBeNull();
    }

    [Fact]
    public void Decode_returns_null_for_empty_string()
    {
        NatsJwt.Decode("").ShouldBeNull();
    }

    [Fact]
    public void Decode_returns_null_for_invalid_base64_in_header()
    {
        NatsJwt.Decode("!!!invalid.payload.sig").ShouldBeNull();
    }

    // =====================================================================
    // Verify tests
    // =====================================================================

    [Fact]
    public void Verify_returns_true_for_valid_signed_token()
    {
        var accountKp = KeyPair.CreatePair(PrefixByte.Account);
        var accountPublicKey = accountKp.GetPublicKey();

        var payloadJson = $$"""{"sub":"UAXXX","iss":"{{accountPublicKey}}","iat":1700000000}""";
        var token = BuildSignedToken(payloadJson, accountKp);

        NatsJwt.Verify(token, accountPublicKey).ShouldBeTrue();
    }

    [Fact]
    public void Verify_returns_false_for_wrong_key()
    {
        var signingKp = KeyPair.CreatePair(PrefixByte.Account);
        var wrongKp = KeyPair.CreatePair(PrefixByte.Account);

        var payloadJson = """{"sub":"UAXXX","iss":"AAXXX","iat":1700000000}""";
        var token = BuildSignedToken(payloadJson, signingKp);

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

    [Fact]
    public void Verify_returns_false_for_tampered_payload()
    {
        var accountKp = KeyPair.CreatePair(PrefixByte.Account);
        var accountPublicKey = accountKp.GetPublicKey();

        var payloadJson = """{"sub":"UAXXX","iss":"AAXXX","iat":1700000000}""";
        var token = BuildSignedToken(payloadJson, accountKp);

        // Tamper with the payload
        var parts = token.Split('.');
        var tamperedPayload = Base64UrlEncode("""{"sub":"HACKED","iss":"AAXXX","iat":1700000000}""");
        var tampered = $"{parts[0]}.{tamperedPayload}.{parts[2]}";

        NatsJwt.Verify(tampered, accountPublicKey).ShouldBeFalse();
    }

    [Fact]
    public void Verify_returns_false_for_invalid_token()
    {
        var kp = KeyPair.CreatePair(PrefixByte.Account);
        NatsJwt.Verify("not.a.jwt", kp.GetPublicKey()).ShouldBeFalse();
    }

    // =====================================================================
    // VerifyNonce tests
    // =====================================================================

    [Fact]
    public void VerifyNonce_accepts_base64url_signature()
    {
        var kp = KeyPair.CreatePair(PrefixByte.User);
        var publicKey = kp.GetPublicKey();
        var nonce = "test-nonce-data"u8.ToArray();

        var sig = new byte[64];
        kp.Sign(nonce, sig);

        // Encode as base64url
        var sigB64Url = Convert.ToBase64String(sig)
            .TrimEnd('=')
            .Replace('+', '-')
            .Replace('/', '_');

        NatsJwt.VerifyNonce(nonce, sigB64Url, publicKey).ShouldBeTrue();
    }

    [Fact]
    public void VerifyNonce_accepts_standard_base64_signature()
    {
        var kp = KeyPair.CreatePair(PrefixByte.User);
        var publicKey = kp.GetPublicKey();
        var nonce = "test-nonce-data"u8.ToArray();

        var sig = new byte[64];
        kp.Sign(nonce, sig);

        // Encode as standard base64
        var sigB64 = Convert.ToBase64String(sig);

        NatsJwt.VerifyNonce(nonce, sigB64, publicKey).ShouldBeTrue();
    }

    [Fact]
    public void VerifyNonce_returns_false_for_wrong_nonce()
    {
        var kp = KeyPair.CreatePair(PrefixByte.User);
        var publicKey = kp.GetPublicKey();
        var nonce = "original-nonce"u8.ToArray();
        var wrongNonce = "different-nonce"u8.ToArray();

        var sig = new byte[64];
        kp.Sign(nonce, sig);
        var sigB64 = Convert.ToBase64String(sig);

        NatsJwt.VerifyNonce(wrongNonce, sigB64, publicKey).ShouldBeFalse();
    }

    [Fact]
    public void VerifyNonce_returns_false_for_invalid_signature()
    {
        var kp = KeyPair.CreatePair(PrefixByte.User);
        var publicKey = kp.GetPublicKey();
        var nonce = "test-nonce"u8.ToArray();

        NatsJwt.VerifyNonce(nonce, "invalid-sig!", publicKey).ShouldBeFalse();
    }

    // =====================================================================
    // DecodeUserClaims tests
    // =====================================================================

    [Fact]
    public void DecodeUserClaims_parses_subject_and_issuer()
    {
        var headerJson = """{"typ":"JWT","alg":"ed25519-nkey"}""";
        var payloadJson = """
        {
            "sub":"UAXXX_USER_KEY",
            "iss":"AAXXX_ISSUER",
            "iat":1700000000,
            "name":"test-user",
            "nats":{
                "type":"user",
                "version":2
            }
        }
        """;
        var token = BuildUnsignedToken(headerJson, payloadJson);

        var claims = NatsJwt.DecodeUserClaims(token);

        claims.ShouldNotBeNull();
        claims.Subject.ShouldBe("UAXXX_USER_KEY");
        claims.Issuer.ShouldBe("AAXXX_ISSUER");
        claims.Name.ShouldBe("test-user");
        claims.IssuedAt.ShouldBe(1700000000);
    }

    [Fact]
    public void DecodeUserClaims_parses_pub_sub_permissions()
    {
        var headerJson = """{"typ":"JWT","alg":"ed25519-nkey"}""";
        var payloadJson = """
        {
            "sub":"UAXXX",
            "iss":"AAXXX",
            "iat":1700000000,
            "nats":{
                "pub":{"allow":["foo.>","bar.*"],"deny":["bar.secret"]},
                "sub":{"allow":[">"],"deny":["_INBOX.private.>"]},
                "type":"user",
                "version":2
            }
        }
        """;
        var token = BuildUnsignedToken(headerJson, payloadJson);

        var claims = NatsJwt.DecodeUserClaims(token);

        claims.ShouldNotBeNull();
        claims.Nats.ShouldNotBeNull();

        claims.Nats.Pub.ShouldNotBeNull();
        claims.Nats.Pub.Allow.ShouldBe(["foo.>", "bar.*"]);
        claims.Nats.Pub.Deny.ShouldBe(["bar.secret"]);

        claims.Nats.Sub.ShouldNotBeNull();
        claims.Nats.Sub.Allow.ShouldBe([">"]);
        claims.Nats.Sub.Deny.ShouldBe(["_INBOX.private.>"]);
    }

    [Fact]
    public void DecodeUserClaims_parses_response_permission()
    {
        var headerJson = """{"typ":"JWT","alg":"ed25519-nkey"}""";
        var payloadJson = """
        {
            "sub":"UAXXX",
            "iss":"AAXXX",
            "iat":1700000000,
            "nats":{
                "resp":{"max":5,"ttl":3000000000},
                "type":"user",
                "version":2
            }
        }
        """;
        var token = BuildUnsignedToken(headerJson, payloadJson);

        var claims = NatsJwt.DecodeUserClaims(token);

        claims.ShouldNotBeNull();
        claims.Nats.ShouldNotBeNull();
        claims.Nats.Resp.ShouldNotBeNull();
        claims.Nats.Resp.MaxMsgs.ShouldBe(5);
        claims.Nats.Resp.TtlNanos.ShouldBe(3000000000L);
        claims.Nats.Resp.Ttl.ShouldBe(TimeSpan.FromSeconds(3));
    }

    [Fact]
    public void DecodeUserClaims_parses_bearer_token_flag()
    {
        var headerJson = """{"typ":"JWT","alg":"ed25519-nkey"}""";
        var payloadJson = """
        {
            "sub":"UAXXX",
            "iss":"AAXXX",
            "iat":1700000000,
            "nats":{
                "bearer_token":true,
                "issuer_account":"AAXXX_ISSUER_ACCOUNT",
                "type":"user",
                "version":2
            }
        }
        """;
        var token = BuildUnsignedToken(headerJson, payloadJson);

        var claims = NatsJwt.DecodeUserClaims(token);

        claims.ShouldNotBeNull();
        claims.Nats.ShouldNotBeNull();
        claims.Nats.BearerToken.ShouldBeTrue();
        claims.Nats.IssuerAccount.ShouldBe("AAXXX_ISSUER_ACCOUNT");
    }

    [Fact]
    public void DecodeUserClaims_parses_tags_src_connection_types()
    {
        var headerJson = """{"typ":"JWT","alg":"ed25519-nkey"}""";
        var payloadJson = """
        {
            "sub":"UAXXX",
            "iss":"AAXXX",
            "iat":1700000000,
            "nats":{
                "tags":["web","mobile"],
                "src":["192.168.1.0/24","10.0.0.0/8"],
                "allowed_connection_types":["STANDARD","WEBSOCKET"],
                "type":"user",
                "version":2
            }
        }
        """;
        var token = BuildUnsignedToken(headerJson, payloadJson);

        var claims = NatsJwt.DecodeUserClaims(token);

        claims.ShouldNotBeNull();
        claims.Nats.ShouldNotBeNull();
        claims.Nats.Tags.ShouldBe(["web", "mobile"]);
        claims.Nats.Src.ShouldBe(["192.168.1.0/24", "10.0.0.0/8"]);
        claims.Nats.AllowedConnectionTypes.ShouldBe(["STANDARD", "WEBSOCKET"]);
    }

    [Fact]
    public void DecodeUserClaims_parses_time_ranges()
    {
        var headerJson = """{"typ":"JWT","alg":"ed25519-nkey"}""";
        var payloadJson = """
        {
            "sub":"UAXXX",
            "iss":"AAXXX",
            "iat":1700000000,
            "nats":{
                "times":[
                    {"start":"08:00:00","end":"17:00:00"},
                    {"start":"20:00:00","end":"22:00:00"}
                ],
                "type":"user",
                "version":2
            }
        }
        """;
        var token = BuildUnsignedToken(headerJson, payloadJson);

        var claims = NatsJwt.DecodeUserClaims(token);

        claims.ShouldNotBeNull();
        claims.Nats.ShouldNotBeNull();
        claims.Nats.Times.ShouldNotBeNull();
        claims.Nats.Times.Length.ShouldBe(2);
        claims.Nats.Times[0].Start.ShouldBe("08:00:00");
        claims.Nats.Times[0].End.ShouldBe("17:00:00");
        claims.Nats.Times[1].Start.ShouldBe("20:00:00");
        claims.Nats.Times[1].End.ShouldBe("22:00:00");
    }

    [Fact]
    public void DecodeUserClaims_convenience_properties_delegate_to_nats()
    {
        var headerJson = """{"typ":"JWT","alg":"ed25519-nkey"}""";
        var payloadJson = """
        {
            "sub":"UAXXX",
            "iss":"AAXXX",
            "iat":1700000000,
            "nats":{
                "bearer_token":true,
                "issuer_account":"AAXXX_ACCOUNT",
                "type":"user",
                "version":2
            }
        }
        """;
        var token = BuildUnsignedToken(headerJson, payloadJson);

        var claims = NatsJwt.DecodeUserClaims(token);

        claims.ShouldNotBeNull();

        // Convenience properties should delegate to Nats sub-object
        claims.BearerToken.ShouldBeTrue();
        claims.IssuerAccount.ShouldBe("AAXXX_ACCOUNT");
    }

    [Fact]
    public void DecodeUserClaims_IsExpired_returns_false_when_no_expiry()
    {
        var headerJson = """{"typ":"JWT","alg":"ed25519-nkey"}""";
        var payloadJson = """
        {
            "sub":"UAXXX",
            "iss":"AAXXX",
            "iat":1700000000,
            "nats":{"type":"user","version":2}
        }
        """;
        var token = BuildUnsignedToken(headerJson, payloadJson);

        var claims = NatsJwt.DecodeUserClaims(token);

        claims.ShouldNotBeNull();
        claims.Expires.ShouldBe(0);
        claims.IsExpired().ShouldBeFalse();
        claims.GetExpiry().ShouldBeNull();
    }

    [Fact]
    public void DecodeUserClaims_IsExpired_returns_true_for_past_expiry()
    {
        var headerJson = """{"typ":"JWT","alg":"ed25519-nkey"}""";
        // Expired in 2020
        var payloadJson = """
        {
            "sub":"UAXXX",
            "iss":"AAXXX",
            "iat":1500000000,
            "exp":1600000000,
            "nats":{"type":"user","version":2}
        }
        """;
        var token = BuildUnsignedToken(headerJson, payloadJson);

        var claims = NatsJwt.DecodeUserClaims(token);

        claims.ShouldNotBeNull();
        claims.Expires.ShouldBe(1600000000);
        claims.IsExpired().ShouldBeTrue();
        claims.GetExpiry().ShouldNotBeNull();
        claims.GetExpiry()!.Value.ToUnixTimeSeconds().ShouldBe(1600000000);
    }

    [Fact]
    public void DecodeUserClaims_IsExpired_returns_false_for_future_expiry()
    {
        var headerJson = """{"typ":"JWT","alg":"ed25519-nkey"}""";
        // Expires far in the future
        var payloadJson = """
        {
            "sub":"UAXXX",
            "iss":"AAXXX",
            "iat":1700000000,
            "exp":4102444800,
            "nats":{"type":"user","version":2}
        }
        """;
        var token = BuildUnsignedToken(headerJson, payloadJson);

        var claims = NatsJwt.DecodeUserClaims(token);

        claims.ShouldNotBeNull();
        claims.IsExpired().ShouldBeFalse();
        claims.GetExpiry().ShouldNotBeNull();
    }

    [Fact]
    public void DecodeUserClaims_returns_null_for_invalid_token()
    {
        NatsJwt.DecodeUserClaims("not-a-jwt").ShouldBeNull();
    }

    // =====================================================================
    // DecodeAccountClaims tests
    // =====================================================================

    [Fact]
    public void DecodeAccountClaims_parses_subject_and_issuer()
    {
        var headerJson = """{"typ":"JWT","alg":"ed25519-nkey"}""";
        var payloadJson = """
        {
            "sub":"AAXXX_ACCOUNT_KEY",
            "iss":"OAXXX_OPERATOR",
            "iat":1700000000,
            "name":"test-account",
            "nats":{
                "type":"account",
                "version":2
            }
        }
        """;
        var token = BuildUnsignedToken(headerJson, payloadJson);

        var claims = NatsJwt.DecodeAccountClaims(token);

        claims.ShouldNotBeNull();
        claims.Subject.ShouldBe("AAXXX_ACCOUNT_KEY");
        claims.Issuer.ShouldBe("OAXXX_OPERATOR");
        claims.Name.ShouldBe("test-account");
        claims.IssuedAt.ShouldBe(1700000000);
    }

    [Fact]
    public void DecodeAccountClaims_parses_limits()
    {
        var headerJson = """{"typ":"JWT","alg":"ed25519-nkey"}""";
        var payloadJson = """
        {
            "sub":"AAXXX",
            "iss":"OAXXX",
            "iat":1700000000,
            "nats":{
                "limits":{
                    "conn":100,
                    "subs":1000,
                    "payload":1048576,
                    "data":10737418240
                },
                "type":"account",
                "version":2
            }
        }
        """;
        var token = BuildUnsignedToken(headerJson, payloadJson);

        var claims = NatsJwt.DecodeAccountClaims(token);

        claims.ShouldNotBeNull();
        claims.Nats.ShouldNotBeNull();
        claims.Nats.Limits.ShouldNotBeNull();
        claims.Nats.Limits.MaxConnections.ShouldBe(100);
        claims.Nats.Limits.MaxSubscriptions.ShouldBe(1000);
        claims.Nats.Limits.MaxPayload.ShouldBe(1048576);
        claims.Nats.Limits.MaxData.ShouldBe(10737418240L);
    }

    [Fact]
    public void DecodeAccountClaims_parses_signing_keys()
    {
        var headerJson = """{"typ":"JWT","alg":"ed25519-nkey"}""";
        var payloadJson = """
        {
            "sub":"AAXXX",
            "iss":"OAXXX",
            "iat":1700000000,
            "nats":{
                "signing_keys":["AAXXX_SIGN_1","AAXXX_SIGN_2","AAXXX_SIGN_3"],
                "type":"account",
                "version":2
            }
        }
        """;
        var token = BuildUnsignedToken(headerJson, payloadJson);

        var claims = NatsJwt.DecodeAccountClaims(token);

        claims.ShouldNotBeNull();
        claims.Nats.ShouldNotBeNull();
        claims.Nats.SigningKeys.ShouldNotBeNull();
        claims.Nats.SigningKeys.ShouldBe(["AAXXX_SIGN_1", "AAXXX_SIGN_2", "AAXXX_SIGN_3"]);
    }

    [Fact]
    public void DecodeAccountClaims_parses_revocations()
    {
        var headerJson = """{"typ":"JWT","alg":"ed25519-nkey"}""";
        var payloadJson = """
        {
            "sub":"AAXXX",
            "iss":"OAXXX",
            "iat":1700000000,
            "nats":{
                "revocations":{
                    "UAXXX_REVOKED_1":1700000000,
                    "UAXXX_REVOKED_2":1700001000
                },
                "type":"account",
                "version":2
            }
        }
        """;
        var token = BuildUnsignedToken(headerJson, payloadJson);

        var claims = NatsJwt.DecodeAccountClaims(token);

        claims.ShouldNotBeNull();
        claims.Nats.ShouldNotBeNull();
        claims.Nats.Revocations.ShouldNotBeNull();
        claims.Nats.Revocations.Count.ShouldBe(2);
        claims.Nats.Revocations["UAXXX_REVOKED_1"].ShouldBe(1700000000);
        claims.Nats.Revocations["UAXXX_REVOKED_2"].ShouldBe(1700001000);
    }

    [Fact]
    public void DecodeAccountClaims_handles_negative_one_unlimited_limits()
    {
        var headerJson = """{"typ":"JWT","alg":"ed25519-nkey"}""";
        var payloadJson = """
        {
            "sub":"AAXXX",
            "iss":"OAXXX",
            "iat":1700000000,
            "nats":{
                "limits":{
                    "conn":-1,
                    "subs":-1,
                    "payload":-1,
                    "data":-1
                },
                "type":"account",
                "version":2
            }
        }
        """;
        var token = BuildUnsignedToken(headerJson, payloadJson);

        var claims = NatsJwt.DecodeAccountClaims(token);

        claims.ShouldNotBeNull();
        claims.Nats.ShouldNotBeNull();
        claims.Nats.Limits.ShouldNotBeNull();
        claims.Nats.Limits.MaxConnections.ShouldBe(-1);
        claims.Nats.Limits.MaxSubscriptions.ShouldBe(-1);
        claims.Nats.Limits.MaxPayload.ShouldBe(-1);
        claims.Nats.Limits.MaxData.ShouldBe(-1);
    }

    [Fact]
    public void DecodeAccountClaims_returns_null_for_invalid_token()
    {
        NatsJwt.DecodeAccountClaims("invalid").ShouldBeNull();
    }

    [Fact]
    public void DecodeAccountClaims_parses_expiry()
    {
        var headerJson = """{"typ":"JWT","alg":"ed25519-nkey"}""";
        var payloadJson = """
        {
            "sub":"AAXXX",
            "iss":"OAXXX",
            "iat":1700000000,
            "exp":1800000000,
            "name":"expiring-account",
            "nats":{
                "type":"account",
                "version":2
            }
        }
        """;
        var token = BuildUnsignedToken(headerJson, payloadJson);

        var claims = NatsJwt.DecodeAccountClaims(token);

        claims.ShouldNotBeNull();
        claims.Expires.ShouldBe(1800000000);
    }

    // =====================================================================
    // Round-trip with real Ed25519 signing tests
    // =====================================================================

    [Fact]
    public void Roundtrip_sign_and_verify_user_claims()
    {
        var accountKp = KeyPair.CreatePair(PrefixByte.Account);
        var accountPublicKey = accountKp.GetPublicKey();

        var payloadJson = $$"""
        {
            "sub":"UAXXX_USER",
            "iss":"{{accountPublicKey}}",
            "iat":1700000000,
            "name":"roundtrip-user",
            "nats":{
                "pub":{"allow":["test.>"]},
                "bearer_token":true,
                "issuer_account":"{{accountPublicKey}}",
                "type":"user",
                "version":2
            }
        }
        """;

        var token = BuildSignedToken(payloadJson, accountKp);

        // Verify signature
        NatsJwt.Verify(token, accountPublicKey).ShouldBeTrue();

        // Decode claims
        var claims = NatsJwt.DecodeUserClaims(token);
        claims.ShouldNotBeNull();
        claims.Subject.ShouldBe("UAXXX_USER");
        claims.Name.ShouldBe("roundtrip-user");
        claims.Nats.ShouldNotBeNull();
        claims.Nats.Pub.ShouldNotBeNull();
        claims.Nats.Pub.Allow.ShouldBe(["test.>"]);
        claims.BearerToken.ShouldBeTrue();
        claims.IssuerAccount.ShouldBe(accountPublicKey);
    }

    [Fact]
    public void Roundtrip_sign_and_verify_account_claims()
    {
        var operatorKp = KeyPair.CreatePair(PrefixByte.Operator);
        var operatorPublicKey = operatorKp.GetPublicKey();

        var payloadJson = $$"""
        {
            "sub":"AAXXX_ACCOUNT",
            "iss":"{{operatorPublicKey}}",
            "iat":1700000000,
            "name":"roundtrip-account",
            "nats":{
                "limits":{"conn":50,"subs":500,"payload":65536,"data":-1},
                "signing_keys":["AAXXX_SK1"],
                "revocations":{"UAXXX_OLD":1699000000},
                "type":"account",
                "version":2
            }
        }
        """;

        var token = BuildSignedToken(payloadJson, operatorKp);

        // Verify signature
        NatsJwt.Verify(token, operatorPublicKey).ShouldBeTrue();

        // Decode claims
        var claims = NatsJwt.DecodeAccountClaims(token);
        claims.ShouldNotBeNull();
        claims.Subject.ShouldBe("AAXXX_ACCOUNT");
        claims.Name.ShouldBe("roundtrip-account");
        claims.Nats.ShouldNotBeNull();
        claims.Nats.Limits.ShouldNotBeNull();
        claims.Nats.Limits.MaxConnections.ShouldBe(50);
        claims.Nats.SigningKeys.ShouldBe(["AAXXX_SK1"]);
        claims.Nats.Revocations.ShouldNotBeNull();
        claims.Nats.Revocations["UAXXX_OLD"].ShouldBe(1699000000);
    }

    // =====================================================================
    // Edge case tests
    // =====================================================================

    [Fact]
    public void DecodeUserClaims_handles_missing_nats_object()
    {
        var headerJson = """{"typ":"JWT","alg":"ed25519-nkey"}""";
        var payloadJson = """
        {
            "sub":"UAXXX",
            "iss":"AAXXX",
            "iat":1700000000
        }
        """;
        var token = BuildUnsignedToken(headerJson, payloadJson);

        var claims = NatsJwt.DecodeUserClaims(token);

        // Should still decode the outer fields even if nats is missing
        claims.ShouldNotBeNull();
        claims.Subject.ShouldBe("UAXXX");
        claims.Issuer.ShouldBe("AAXXX");
    }

    [Fact]
    public void DecodeAccountClaims_handles_empty_nats_object()
    {
        var headerJson = """{"typ":"JWT","alg":"ed25519-nkey"}""";
        var payloadJson = """
        {
            "sub":"AAXXX",
            "iss":"OAXXX",
            "iat":1700000000,
            "nats":{}
        }
        """;
        var token = BuildUnsignedToken(headerJson, payloadJson);

        var claims = NatsJwt.DecodeAccountClaims(token);

        claims.ShouldNotBeNull();
        claims.Subject.ShouldBe("AAXXX");
        claims.Nats.ShouldNotBeNull();
    }

    [Fact]
    public void DecodeUserClaims_handles_empty_pub_sub_permissions()
    {
        var headerJson = """{"typ":"JWT","alg":"ed25519-nkey"}""";
        var payloadJson = """
        {
            "sub":"UAXXX",
            "iss":"AAXXX",
            "iat":1700000000,
            "nats":{
                "pub":{},
                "sub":{},
                "type":"user",
                "version":2
            }
        }
        """;
        var token = BuildUnsignedToken(headerJson, payloadJson);

        var claims = NatsJwt.DecodeUserClaims(token);

        claims.ShouldNotBeNull();
        claims.Nats.ShouldNotBeNull();
        claims.Nats.Pub.ShouldNotBeNull();
        claims.Nats.Sub.ShouldNotBeNull();
        // Allow/Deny should be null when not specified
        claims.Nats.Pub.Allow.ShouldBeNull();
        claims.Nats.Pub.Deny.ShouldBeNull();
    }

    // =====================================================================
    // Response permission edge cases
    // Go reference: TestJWTUserResponsePermissionClaimsDefaultValues,
    //               TestJWTUserResponsePermissionClaimsNegativeValues
    // =====================================================================

    [Fact]
    public void DecodeUserClaims_resp_with_zero_max_and_zero_ttl_is_present_but_zeroed()
    {
        // Go TestJWTUserResponsePermissionClaimsDefaultValues:
        // an empty ResponsePermission{} in the JWT serializes as max=0, ttl=0.
        // The .NET parser must round-trip those zero values rather than
        // treating the object as absent.
        var headerJson = """{"typ":"JWT","alg":"ed25519-nkey"}""";
        var payloadJson = """
        {
            "sub":"UAXXX",
            "iss":"AAXXX",
            "iat":1700000000,
            "nats":{
                "resp":{"max":0,"ttl":0},
                "type":"user",
                "version":2
            }
        }
        """;
        var token = BuildUnsignedToken(headerJson, payloadJson);

        var claims = NatsJwt.DecodeUserClaims(token);

        claims.ShouldNotBeNull();
        claims.Nats.ShouldNotBeNull();
        claims.Nats.Resp.ShouldNotBeNull();
        claims.Nats.Resp.MaxMsgs.ShouldBe(0);
        claims.Nats.Resp.TtlNanos.ShouldBe(0L);
        claims.Nats.Resp.Ttl.ShouldBe(TimeSpan.Zero);
    }

    [Fact]
    public void DecodeUserClaims_resp_with_negative_max_and_negative_ttl_round_trips()
    {
        // Go TestJWTUserResponsePermissionClaimsNegativeValues:
        // MaxMsgs=-1, Expires=-1s (== -1_000_000_000 ns).
        // The .NET parser must preserve negative values verbatim.
        var headerJson = """{"typ":"JWT","alg":"ed25519-nkey"}""";
        var payloadJson = """
        {
            "sub":"UAXXX",
            "iss":"AAXXX",
            "iat":1700000000,
            "nats":{
                "resp":{"max":-1,"ttl":-1000000000},
                "type":"user",
                "version":2
            }
        }
        """;
        var token = BuildUnsignedToken(headerJson, payloadJson);

        var claims = NatsJwt.DecodeUserClaims(token);

        claims.ShouldNotBeNull();
        claims.Nats.ShouldNotBeNull();
        claims.Nats.Resp.ShouldNotBeNull();
        claims.Nats.Resp.MaxMsgs.ShouldBe(-1);
        claims.Nats.Resp.TtlNanos.ShouldBe(-1_000_000_000L);
    }

    // =====================================================================
    // JWT expiration edge cases
    // Go reference: TestJWTUserExpired, TestJWTAccountExpired
    // =====================================================================

    [Fact]
    public void DecodeUserClaims_IsExpired_returns_true_when_expired_by_one_second()
    {
        // Mirrors the Go TestJWTUserExpired / TestJWTAccountExpired pattern:
        // exp is set to "now - 2 seconds" which is definitely past.
        var headerJson = """{"typ":"JWT","alg":"ed25519-nkey"}""";
        var expiredByOneSecond = DateTimeOffset.UtcNow.AddSeconds(-1).ToUnixTimeSeconds();
        var payloadJson = $$"""
        {
            "sub":"UAXXX",
            "iss":"AAXXX",
            "iat":1700000000,
            "exp":{{expiredByOneSecond}},
            "nats":{"type":"user","version":2}
        }
        """;
        var token = BuildUnsignedToken(headerJson, payloadJson);

        var claims = NatsJwt.DecodeUserClaims(token);

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

    [Fact]
    public void DecodeUserClaims_IsExpired_returns_false_when_not_yet_expired_by_one_second()
    {
        // Complementary case: exp is 1 second in the future — token is valid.
        var headerJson = """{"typ":"JWT","alg":"ed25519-nkey"}""";
        var expiresSoon = DateTimeOffset.UtcNow.AddSeconds(1).ToUnixTimeSeconds();
        var payloadJson = $$"""
        {
            "sub":"UAXXX",
            "iss":"AAXXX",
            "iat":1700000000,
            "exp":{{expiresSoon}},
            "nats":{"type":"user","version":2}
        }
        """;
        var token = BuildUnsignedToken(headerJson, payloadJson);

        var claims = NatsJwt.DecodeUserClaims(token);

        claims.ShouldNotBeNull();
        claims.IsExpired().ShouldBeFalse();
    }

    [Fact]
    public void DecodeAccountClaims_IsExpired_returns_true_when_account_is_expired()
    {
        // Mirrors Go TestJWTAccountExpired: iat = now-10s, exp = now-2s.
        var headerJson = """{"typ":"JWT","alg":"ed25519-nkey"}""";
        var issuedAt = DateTimeOffset.UtcNow.AddSeconds(-10).ToUnixTimeSeconds();
        var expires = DateTimeOffset.UtcNow.AddSeconds(-2).ToUnixTimeSeconds();
        var payloadJson = $$"""
        {
            "sub":"AAXXX",
            "iss":"OAXXX",
            "iat":{{issuedAt}},
            "exp":{{expires}},
            "nats":{"type":"account","version":2}
        }
        """;
        var token = BuildUnsignedToken(headerJson, payloadJson);

        var claims = NatsJwt.DecodeAccountClaims(token);

        claims.ShouldNotBeNull();
        claims.Expires.ShouldBe(expires);
        // AccountClaims uses the standard exp field; verify it's in the past
        DateTimeOffset.UtcNow.ToUnixTimeSeconds().ShouldBeGreaterThan(claims.Expires);
    }

    // =====================================================================
    // Signing key chain (multi-level) claim fields
    // Go reference: TestJWTUserSigningKey — user issued by account signing key
    // =====================================================================

    [Fact]
    public void DecodeUserClaims_parses_issuer_account_when_user_signed_by_signing_key()
    {
        // In Go, when a user JWT is signed by an account *signing key* (not the
        // primary account key), the JWT issuer (iss) is the signing key's public key
        // and the issuer_account field carries the primary account public key.
        // This test verifies those two fields are decoded correctly.
        var accountKp = KeyPair.CreatePair(PrefixByte.Account);
        var accountPublicKey = accountKp.GetPublicKey();

        // Simulate a signing key (another account-type keypair acting as delegated signer)
        var signingKp = KeyPair.CreatePair(PrefixByte.Account);
        var signingPublicKey = signingKp.GetPublicKey();

        var payloadJson = $$"""
        {
            "sub":"UAXXX_USER",
            "iss":"{{signingPublicKey}}",
            "iat":1700000000,
            "name":"signing-key-user",
            "nats":{
                "issuer_account":"{{accountPublicKey}}",
                "type":"user",
                "version":2
            }
        }
        """;
        var token = BuildSignedToken(payloadJson, signingKp);

        var claims = NatsJwt.DecodeUserClaims(token);

        claims.ShouldNotBeNull();
        // The issuer is the signing key, not the primary account
        claims.Issuer.ShouldBe(signingPublicKey);
        // The issuer_account carries the primary account key
        claims.IssuerAccount.ShouldBe(accountPublicKey);
        // Convenience property must also reflect the nats sub-object
        claims.Nats.ShouldNotBeNull();
        claims.Nats.IssuerAccount.ShouldBe(accountPublicKey);
    }

    [Fact]
    public void Verify_returns_true_when_signed_by_account_signing_key()
    {
        // JWT is signed by a signing key (not the primary account key).
        // Verify must succeed when checked against the signing key's public key.
        var signingKp = KeyPair.CreatePair(PrefixByte.Account);
        var signingPublicKey = signingKp.GetPublicKey();
        var accountPublicKey = KeyPair.CreatePair(PrefixByte.Account).GetPublicKey();

        var payloadJson = $$"""
        {
            "sub":"UAXXX_USER",
            "iss":"{{signingPublicKey}}",
            "iat":1700000000,
            "nats":{
                "issuer_account":"{{accountPublicKey}}",
                "type":"user",
                "version":2
            }
        }
        """;
        var token = BuildSignedToken(payloadJson, signingKp);

        // Verify against the signing key (not the primary account key)
        NatsJwt.Verify(token, signingPublicKey).ShouldBeTrue();
        // Verify against the primary account key must fail (different key)
        NatsJwt.Verify(token, accountPublicKey).ShouldBeFalse();
    }

    // =====================================================================
    // Account claims — JetStream limits
    // Go reference: TestJWTJetStreamTiers (claims parsing portion)
    // =====================================================================

    [Fact]
    public void DecodeAccountClaims_parses_jetstream_limits()
    {
        var headerJson = """{"typ":"JWT","alg":"ed25519-nkey"}""";
        var payloadJson = """
        {
            "sub":"AAXXX",
            "iss":"OAXXX",
            "iat":1700000000,
            "nats":{
                "jetstream":{
                    "max_streams":10,
                    "tier":"T1"
                },
                "type":"account",
                "version":2
            }
        }
        """;
        var token = BuildUnsignedToken(headerJson, payloadJson);

        var claims = NatsJwt.DecodeAccountClaims(token);

        claims.ShouldNotBeNull();
        claims.Nats.ShouldNotBeNull();
        claims.Nats.JetStream.ShouldNotBeNull();
        claims.Nats.JetStream.MaxStreams.ShouldBe(10);
        claims.Nats.JetStream.Tier.ShouldBe("T1");
    }

    [Fact]
    public void DecodeAccountClaims_absent_jetstream_block_leaves_property_null()
    {
        var headerJson = """{"typ":"JWT","alg":"ed25519-nkey"}""";
        var payloadJson = """
        {
            "sub":"AAXXX",
            "iss":"OAXXX",
            "iat":1700000000,
            "nats":{
                "type":"account",
                "version":2
            }
        }
        """;
        var token = BuildUnsignedToken(headerJson, payloadJson);

        var claims = NatsJwt.DecodeAccountClaims(token);

        claims.ShouldNotBeNull();
        claims.Nats.ShouldNotBeNull();
        claims.Nats.JetStream.ShouldBeNull();
    }

    // =====================================================================
    // Account claims — tags
    // Go reference: Account claims can carry tags just like user claims
    // =====================================================================

    [Fact]
    public void DecodeAccountClaims_parses_tags()
    {
        var headerJson = """{"typ":"JWT","alg":"ed25519-nkey"}""";
        var payloadJson = """
        {
            "sub":"AAXXX",
            "iss":"OAXXX",
            "iat":1700000000,
            "nats":{
                "tags":["env:prod","region:us-east"],
                "type":"account",
                "version":2
            }
        }
        """;
        var token = BuildUnsignedToken(headerJson, payloadJson);

        var claims = NatsJwt.DecodeAccountClaims(token);

        claims.ShouldNotBeNull();
        claims.Nats.ShouldNotBeNull();
        claims.Nats.Tags.ShouldNotBeNull();
        claims.Nats.Tags.ShouldBe(["env:prod", "region:us-east"]);
    }

    // =====================================================================
    // Malformed JWT structural edge cases
    // Go reference: NatsJwt.Decode robustness
    // =====================================================================

    [Fact]
    public void Decode_returns_null_for_four_dot_separated_parts()
    {
        // JWT must have exactly three parts. Four segments is not a valid JWT.
        NatsJwt.Decode("part1.part2.part3.part4").ShouldBeNull();
    }

    [Fact]
    public void Decode_handles_base64_with_standard_padding_in_payload()
    {
        // Some JWT implementations emit standard Base64 with '=' padding instead of
        // URL-safe base64url. Verify the decoder handles padding characters correctly.
        var headerJson = """{"typ":"JWT","alg":"ed25519-nkey"}""";
        var payloadJson = """{"sub":"UAXXX","iss":"AAXXX","iat":1700000000}""";

        // Manually build a token where the payload uses standard base64 WITH padding
        var headerB64 = Base64UrlEncode(headerJson);
        var payloadBytes = Encoding.UTF8.GetBytes(payloadJson);
        // Standard base64 with padding (not base64url)
        var payloadB64WithPadding = Convert.ToBase64String(payloadBytes); // may contain '=' padding
        var fakeSig = Convert.ToBase64String(new byte[64]).TrimEnd('=').Replace('+', '-').Replace('/', '_');
        var token = $"{headerB64}.{payloadB64WithPadding}.{fakeSig}";

        // The decoder should handle the padding transparently
        var result = NatsJwt.Decode(token);
        result.ShouldNotBeNull();
        result.PayloadJson.ShouldContain("UAXXX");
    }

    [Fact]
    public void Decode_returns_null_for_empty_header_segment()
    {
        // An empty header part cannot be valid base64 for a JSON object.
        NatsJwt.Decode(".payload.sig").ShouldBeNull();
    }

    [Fact]
    public void Decode_returns_null_for_invalid_base64_in_payload()
    {
        var headerB64 = Base64UrlEncode("""{"typ":"JWT","alg":"ed25519-nkey"}""");
        NatsJwt.Decode($"{headerB64}.!!!invalid.sig").ShouldBeNull();
    }

    [Fact]
    public void Decode_returns_null_for_non_json_payload()
    {
        // A payload that is valid base64url but does not decode to JSON
        // should return null because the header cannot be deserialized.
        var nonJsonPayload = Base64UrlEncode("this-is-not-json");
        var headerB64 = Base64UrlEncode("""{"typ":"JWT","alg":"ed25519-nkey"}""");
        var fakeSig = Convert.ToBase64String(new byte[64]).TrimEnd('=').Replace('+', '-').Replace('/', '_');
        // Decode does not deserialize the payload (only the header), so this
        // actually succeeds at the Decode level but the payloadJson is "this-is-not-json".
        // DecodeUserClaims should return null because the payload is not valid claims JSON.
        var token = $"{headerB64}.{nonJsonPayload}.{fakeSig}";
        var decoded = NatsJwt.Decode(token);
        decoded.ShouldNotBeNull();
        decoded.PayloadJson.ShouldBe("this-is-not-json");
        // But decoding as UserClaims should fail
        NatsJwt.DecodeUserClaims(token).ShouldBeNull();
    }

    // =====================================================================
    // Verify edge cases
    // =====================================================================

    [Fact]
    public void Verify_returns_false_for_empty_public_key()
    {
        var kp = KeyPair.CreatePair(PrefixByte.Account);
        var payloadJson = """{"sub":"UAXXX","iss":"AAXXX","iat":1700000000}""";
        var token = BuildSignedToken(payloadJson, kp);

        NatsJwt.Verify(token, "").ShouldBeFalse();
    }

    [Fact]
    public void Verify_returns_false_for_malformed_public_key()
    {
        var kp = KeyPair.CreatePair(PrefixByte.Account);
        var payloadJson = """{"sub":"UAXXX","iss":"AAXXX","iat":1700000000}""";
        var token = BuildSignedToken(payloadJson, kp);

        NatsJwt.Verify(token, "NOT_A_VALID_NKEY").ShouldBeFalse();
    }

    [Fact]
    public void Verify_returns_false_when_signature_is_truncated()
    {
        var kp = KeyPair.CreatePair(PrefixByte.Account);
        var accountPublicKey = kp.GetPublicKey();
        var payloadJson = $$"""{"sub":"UAXXX","iss":"{{accountPublicKey}}","iat":1700000000}""";
        var token = BuildSignedToken(payloadJson, kp);

        // Truncate the signature part to only 10 chars — invalid length
        var parts = token.Split('.');
        var truncatedToken = $"{parts[0]}.{parts[1]}.{parts[2][..10]}";

        NatsJwt.Verify(truncatedToken, accountPublicKey).ShouldBeFalse();
    }

    // =====================================================================
    // DecodeUserClaims — sub-permission variations
    // Go reference: TestJWTUserPermissionClaims
    // =====================================================================

    [Fact]
    public void DecodeUserClaims_parses_pub_allow_only_with_no_deny()
    {
        // Permissions with only allow and no deny list.
        var headerJson = """{"typ":"JWT","alg":"ed25519-nkey"}""";
        var payloadJson = """
        {
            "sub":"UAXXX",
            "iss":"AAXXX",
            "iat":1700000000,
            "nats":{
                "pub":{"allow":["foo.>","bar.*"]},
                "type":"user",
                "version":2
            }
        }
        """;
        var token = BuildUnsignedToken(headerJson, payloadJson);

        var claims = NatsJwt.DecodeUserClaims(token);

        claims.ShouldNotBeNull();
        claims.Nats.ShouldNotBeNull();
        claims.Nats.Pub.ShouldNotBeNull();
        claims.Nats.Pub.Allow.ShouldBe(["foo.>", "bar.*"]);
        claims.Nats.Pub.Deny.ShouldBeNull();
        claims.Nats.Sub.ShouldBeNull();
    }

    [Fact]
    public void DecodeUserClaims_parses_sub_deny_only_with_no_allow()
    {
        // Permissions with only deny and no allow list.
        var headerJson = """{"typ":"JWT","alg":"ed25519-nkey"}""";
        var payloadJson = """
        {
            "sub":"UAXXX",
            "iss":"AAXXX",
            "iat":1700000000,
            "nats":{
                "sub":{"deny":["private.>"]},
                "type":"user",
                "version":2
            }
        }
        """;
        var token = BuildUnsignedToken(headerJson, payloadJson);

        var claims = NatsJwt.DecodeUserClaims(token);

        claims.ShouldNotBeNull();
        claims.Nats.ShouldNotBeNull();
        claims.Nats.Pub.ShouldBeNull();
        claims.Nats.Sub.ShouldNotBeNull();
        claims.Nats.Sub.Allow.ShouldBeNull();
        claims.Nats.Sub.Deny.ShouldBe(["private.>"]);
    }

    // =====================================================================
    // DecodeAccountClaims — revocation-only and limits-only splits
    // Go reference: TestJWTUserRevoked, TestJWTAccountLimitsSubs
    // =====================================================================

    [Fact]
    public void DecodeAccountClaims_parses_revocations_without_limits()
    {
        // Account JWT with only revocations defined (no limits block).
        var headerJson = """{"typ":"JWT","alg":"ed25519-nkey"}""";
        var payloadJson = """
        {
            "sub":"AAXXX",
            "iss":"OAXXX",
            "iat":1700000000,
            "nats":{
                "revocations":{
                    "UAXXX_REVOKED":1699000000
                },
                "type":"account",
                "version":2
            }
        }
        """;
        var token = BuildUnsignedToken(headerJson, payloadJson);

        var claims = NatsJwt.DecodeAccountClaims(token);

        claims.ShouldNotBeNull();
        claims.Nats.ShouldNotBeNull();
        claims.Nats.Limits.ShouldBeNull();
        claims.Nats.Revocations.ShouldNotBeNull();
        claims.Nats.Revocations.Count.ShouldBe(1);
        claims.Nats.Revocations["UAXXX_REVOKED"].ShouldBe(1699000000);
    }

    [Fact]
    public void DecodeAccountClaims_parses_limits_without_revocations()
    {
        // Account JWT with only limits defined (no revocations block).
        var headerJson = """{"typ":"JWT","alg":"ed25519-nkey"}""";
        var payloadJson = """
        {
            "sub":"AAXXX",
            "iss":"OAXXX",
            "iat":1700000000,
            "nats":{
                "limits":{
                    "conn":50,
                    "subs":500
                },
                "type":"account",
                "version":2
            }
        }
        """;
        var token = BuildUnsignedToken(headerJson, payloadJson);

        var claims = NatsJwt.DecodeAccountClaims(token);

        claims.ShouldNotBeNull();
        claims.Nats.ShouldNotBeNull();
        claims.Nats.Revocations.ShouldBeNull();
        claims.Nats.Limits.ShouldNotBeNull();
        claims.Nats.Limits.MaxConnections.ShouldBe(50);
        claims.Nats.Limits.MaxSubscriptions.ShouldBe(500);
    }

    // =====================================================================
    // Wildcard revocation sentinel value
    // Go reference: TestJWTUserRevocation — "*" key with timestamp=0 means
    // all users issued before that time are revoked
    // =====================================================================

    [Fact]
    public void DecodeAccountClaims_parses_wildcard_revocation_sentinel()
    {
        // The Go JWT library uses "*" as a key in the revocations map
        // to mean "revoke all users issued before this timestamp".
        var headerJson = """{"typ":"JWT","alg":"ed25519-nkey"}""";
        var payloadJson = """
        {
            "sub":"AAXXX",
            "iss":"OAXXX",
            "iat":1700000000,
            "nats":{
                "revocations":{
                    "*":1699000000,
                    "UAXXX_SPECIFIC":1700000000
                },
                "type":"account",
                "version":2
            }
        }
        """;
        var token = BuildUnsignedToken(headerJson, payloadJson);

        var claims = NatsJwt.DecodeAccountClaims(token);

        claims.ShouldNotBeNull();
        claims.Nats.ShouldNotBeNull();
        claims.Nats.Revocations.ShouldNotBeNull();
        claims.Nats.Revocations.Count.ShouldBe(2);
        claims.Nats.Revocations.ContainsKey("*").ShouldBeTrue();
        claims.Nats.Revocations["*"].ShouldBe(1699000000);
        claims.Nats.Revocations["UAXXX_SPECIFIC"].ShouldBe(1700000000);
    }

    // =====================================================================
    // VerifyNonce edge cases
    // Go reference: nonce verification with user keypair
    // =====================================================================

    [Fact]
    public void VerifyNonce_returns_false_for_empty_nonce_with_wrong_sig()
    {
        var kp = KeyPair.CreatePair(PrefixByte.User);
        var publicKey = kp.GetPublicKey();
        // Sign a non-empty nonce but verify against empty nonce
        var nonce = "real-nonce"u8.ToArray();
        var sig = new byte[64];
        kp.Sign(nonce, sig);
        var sigB64 = Convert.ToBase64String(sig);

        NatsJwt.VerifyNonce([], sigB64, publicKey).ShouldBeFalse();
    }

    [Fact]
    public void VerifyNonce_returns_false_for_zero_length_base64_payload()
    {
        var kp = KeyPair.CreatePair(PrefixByte.User);
        var publicKey = kp.GetPublicKey();
        var nonce = "some-nonce"u8.ToArray();

        // An empty string is not valid base64 for a 64-byte signature
        NatsJwt.VerifyNonce(nonce, "", publicKey).ShouldBeFalse();
    }

    // =====================================================================
    // Roundtrip — operator-signed account, account-signed user (full chain)
    // Go reference: TestJWTUser — full three-tier trust chain
    // =====================================================================

    [Fact]
    public void Roundtrip_three_tier_claims_operator_account_user()
    {
        // Mimics the Go three-tier trust hierarchy:
        // Operator -> signs Account JWT -> signs User JWT
        // This test validates that all three levels decode correctly and
        // the signing key chain fields are properly populated.
        var operatorKp = KeyPair.CreatePair(PrefixByte.Operator);
        var operatorPublicKey = operatorKp.GetPublicKey();

        var accountKp = KeyPair.CreatePair(PrefixByte.Account);
        var accountPublicKey = accountKp.GetPublicKey();

        // Account JWT: issued by operator
        var accountPayload = $$"""
        {
            "sub":"{{accountPublicKey}}",
            "iss":"{{operatorPublicKey}}",
            "iat":1700000000,
            "name":"test-account",
            "nats":{
                "limits":{"conn":100,"subs":-1,"payload":-1,"data":-1},
                "type":"account",
                "version":2
            }
        }
        """;
        var accountToken = BuildSignedToken(accountPayload, operatorKp);

        // User JWT: issued by account key
        var userPublicKey = KeyPair.CreatePair(PrefixByte.User).GetPublicKey();
        var userPayload = $$"""
        {
            "sub":"{{userPublicKey}}",
            "iss":"{{accountPublicKey}}",
            "iat":1700000000,
            "name":"test-user",
            "nats":{
                "pub":{"allow":[">"]},
                "sub":{"allow":[">"]},
                "type":"user",
                "version":2
            }
        }
        """;
        var userToken = BuildSignedToken(userPayload, accountKp);

        // Account JWT: verify and decode
        NatsJwt.Verify(accountToken, operatorPublicKey).ShouldBeTrue();
        var accountClaims = NatsJwt.DecodeAccountClaims(accountToken);
        accountClaims.ShouldNotBeNull();
        accountClaims.Subject.ShouldBe(accountPublicKey);
        accountClaims.Issuer.ShouldBe(operatorPublicKey);
        accountClaims.Name.ShouldBe("test-account");
        accountClaims.Nats.ShouldNotBeNull();
        accountClaims.Nats.Limits.ShouldNotBeNull();
        accountClaims.Nats.Limits.MaxConnections.ShouldBe(100);

        // User JWT: verify and decode
        NatsJwt.Verify(userToken, accountPublicKey).ShouldBeTrue();
        var userClaims = NatsJwt.DecodeUserClaims(userToken);
        userClaims.ShouldNotBeNull();
        userClaims.Subject.ShouldBe(userPublicKey);
        userClaims.Issuer.ShouldBe(accountPublicKey);
        userClaims.Name.ShouldBe("test-user");
        userClaims.Nats.ShouldNotBeNull();
        userClaims.Nats.Pub.ShouldNotBeNull();
        userClaims.Nats.Pub.Allow.ShouldBe([">"]);
    }
}
