package dev.ulfrx.recipe.auth

import com.auth0.jwk.Jwk
import com.auth0.jwk.JwkProvider
import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import java.security.KeyPairGenerator
import java.security.interfaces.RSAPrivateKey
import java.security.interfaces.RSAPublicKey
import java.util.Base64
import java.util.Date

/**
 * Test helper for the Phase 2 server JWT validation surface (D-21..D-23).
 *
 * Generates an RS256 RSA-2048 keypair in-process, exposes a [JwkProvider] backed
 * by the public key (so Ktor's `verifier(jwkProvider, issuer)` can resolve the
 * `kid` without HTTP), and mints RS256 tokens with configurable claims so tests
 * can hand-craft missing/expired/wrong-issuer/wrong-audience/blank-sub variants.
 *
 * Tests must NOT spin up a real JWKS endpoint — `JwkProviderBuilder` requires a
 * URL, and using a network port from `testApplication` is fragile. The [provider]
 * surface lets `configureAuthentication(authConfig, jwkProvider)` bypass the URL
 * fetch entirely.
 */
internal class JwtTestSupport(
    val issuer: String = "https://test-authentik.invalid/application/o/recipe/",
    val audience: String = "recipe-app",
    val keyId: String = "test-key-1",
) {
    private val keyPair =
        KeyPairGenerator.getInstance("RSA").apply { initialize(2048) }.genKeyPair()
    private val publicKey = keyPair.public as RSAPublicKey
    private val privateKey = keyPair.private as RSAPrivateKey
    private val algorithm = Algorithm.RSA256(publicKey, privateKey)

    val provider: JwkProvider =
        JwkProvider {
            // Use the documented Jwk.fromValues factory — the 9-arg constructors are
            // both deprecated in jwks-rsa, and `allWarningsAsErrors` forbids them.
            // RS256 = RSA + SHA-256; jwks-rsa needs n + e modulus/exponent (base64url).
            Jwk.fromValues(
                mapOf(
                    "kid" to keyId,
                    "kty" to "RSA",
                    "alg" to "RS256",
                    "use" to "sig",
                    "n" to publicKey.modulus.toBase64UrlNoPad(),
                    "e" to publicKey.publicExponent.toBase64UrlNoPad(),
                ),
            )
        }

    /**
     * Mint a signed JWT.
     *
     * @param iss override issuer (defaults to the verifier's expected [issuer]).
     * @param aud override audience (defaults to the verifier's expected [audience]).
     * @param sub subject claim; pass `""` or `null` to test the blank-sub gate.
     * @param email optional email claim.
     * @param name optional name claim (preferred_username uses the same input).
     * @param expiresInSeconds expiry offset from now; pass a negative value for
     *   the expired-token negative case (default 600s = 10 min).
     */
    fun mint(
        iss: String = issuer,
        aud: String = audience,
        sub: String? = "auth0|test-user",
        email: String? = "test@example.invalid",
        name: String? = "Test User",
        expiresInSeconds: Long = 600,
    ): String {
        val now = System.currentTimeMillis()
        val builder =
            JWT
                .create()
                .withKeyId(keyId)
                .withIssuer(iss)
                .withAudience(aud)
                .withIssuedAt(Date(now))
                .withExpiresAt(Date(now + expiresInSeconds * 1000L))
        if (sub != null) builder.withSubject(sub)
        if (email != null) builder.withClaim("email", email)
        if (name != null) {
            builder.withClaim("name", name)
            builder.withClaim("preferred_username", name)
        }
        return builder.sign(algorithm)
    }
}

private fun java.math.BigInteger.toBase64UrlNoPad(): String {
    // Strip the leading sign byte that BigInteger.toByteArray() prepends when the
    // high bit is set (jwks-rsa expects unsigned big-endian per RFC 7518).
    val raw = this.toByteArray()
    val unsigned =
        if (raw.size > 1 && raw[0] == 0.toByte()) raw.copyOfRange(1, raw.size) else raw
    return Base64.getUrlEncoder().withoutPadding().encodeToString(unsigned)
}
