// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// Unit tests for the OIDC RS256 ID-token verifier (services/oidc). Vectors are
// real RSA-2048 tokens signed with openssl (scratchpad/gen_oidc.sh); the JWKS
// carries the matching public key (n, e). Exercises the happy path, claim
// extraction, aud-as-array, and every rejection (alg, signature, iss, aud, exp,
// missing key, malformed).

#include "network_drivers/presentation/ssh/crypto/ssh_rsa.h" // in-test RS256 signing
#include "network_drivers/session/scratch.h"
#include "services/oidc/oidc.h"
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unity.h>

// The native RSA sign fixture (defined in ssh_rsa.cpp) lets us mint RS256 tokens with
// arbitrary claims so the post-signature-verify guards can be exercised.
extern uint8_t _test_rsa_n[256];
extern uint8_t _test_rsa_d[256];
extern uint8_t _test_rsa_e[4];

// A known RSA-2048 keypair (same as test_ssh_crypto's round-trip fixture).
static const char RT_N[] = "a855616bbe1ed8ff73463006a1c2e9fcb67d8b3f39e19df514bd17f444697402"
                           "52d7e059497714b436fdabfc56a75a09ce85f9946f2896cd57e705a7d432a89c"
                           "c2296ad8e04b12e26648279c203cbfe4e0784a0dc4be8b370abf02c126f87aa3"
                           "7860267f4b11b49fcc00d830b45fc2fb5ded85ddcbe684201ed7c614de313ead"
                           "2410bc0ef689cc32909159f6c29279075c4bbf43deea939b32901a93c1bcb38c"
                           "528253951433076c1a3b9864e9d95f04c3757634a8626cae7826f501a2f20288"
                           "6eb4b63fc27b1bff84bafb4be5b4d1bc603e1c711112dc318a5213e7c2a71d49"
                           "6935767aec1dcd5e46f3d159f51daf8525e3b78833f561af2e34a2698539c58f";
static const char RT_D[] = "0568149fe10fdd01f242187e5ce4b3435f90d88f98611ccce2fdb4aa2edd2ec8"
                           "792dfb1f7c2bc999850d352991045c95530d254159dea2f0be78614b7abaa617"
                           "511f279206aac4e74a7efea4fa705fec97ea82dcac34888492d7ffdc93e15a7e"
                           "749eacd26f6706adeb8441ffa02ee80ff37c4996c508b4921bd259a322f26264"
                           "204e74612f1e58b1b5c54507a01916751dfcac4ed30aade8a0e458d2d636a35a"
                           "1f309e7bec81b348aa603b14be4c8dfc4dc2b5693fdfa1c1227ad33999c2f4d1"
                           "5f6ecea9d3fdb3337c31a11eb498f30c2b47ad9fcff3a68240e23595e406ceb4"
                           "ba73c3d29a97a67517ca45ae871673ca769cdf627a72ccd8b7bb3d57a195c3b5";

static void hex2bytes(uint8_t *out, const char *hex, size_t n)
{
    for (size_t i = 0; i < n; i++)
    {
        char b[3] = {hex[2 * i], hex[2 * i + 1], 0};
        out[i] = (uint8_t)strtol(b, nullptr, 16);
    }
}

static const char B64URL[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
static size_t b64url_enc(const uint8_t *in, size_t n, char *out)
{
    size_t o = 0;
    for (size_t i = 0; i < n; i += 3)
    {
        int rem = (int)(n - i);
        uint32_t v = (uint32_t)in[i] << 16;
        if (rem > 1)
            v |= (uint32_t)in[i + 1] << 8;
        if (rem > 2)
            v |= in[i + 2];
        out[o++] = B64URL[(v >> 18) & 63];
        out[o++] = B64URL[(v >> 12) & 63];
        if (rem > 1)
            out[o++] = B64URL[(v >> 6) & 63];
        if (rem > 2)
            out[o++] = B64URL[v & 63];
    }
    out[o] = '\0';
    return o;
}

// Mint a valid RS256 JWT for the given payload JSON, signed with the RT private key.
static void make_jwt(const char *payload_json, char *tok, size_t tok_cap)
{
    static const char HDR[] = "{\"alg\":\"RS256\"}";
    char seg0[64], seg1[512], signing[600], seg2[400];
    b64url_enc((const uint8_t *)HDR, strlen(HDR), seg0);
    b64url_enc((const uint8_t *)payload_json, strlen(payload_json), seg1);
    int sl = snprintf(signing, sizeof(signing), "%s.%s", seg0, seg1);
    uint8_t sig[256];
    ssh_rsa_sign((const uint8_t *)signing, (size_t)sl, sig);
    b64url_enc(sig, 256, seg2);
    snprintf(tok, tok_cap, "%s.%s.%s", seg0, seg1, seg2);
}

// --- generated by gen_oidc.sh (RSA-2048, RS256) ---
static const char *K_TOK_VALID =
    "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InRlc3Qta2V5LTEifQ.eyJpc3MiOiJodHRwczovL2lzc3Vlci5leGFtcGxlIiwiYXVkIj"
    "oiY2xpZW50LTEyMyIsInN1YiI6InVzZXItNDIiLCJlbWFpbCI6ImFsaWNlQGV4YW1wbGUuY29tIiwiaWF0IjoxNzAwMDAwMDAwLCJleHAiOjQxMDI0"
    "NDQ4MDB9.QBKta3RxQh8K5FiNUL6ZJemBvIGmDsgZFhF5FbEkjcmbgNsbPZsLl70N0UFwGbsAY5sGoUZxPhWdl-_VT2nXQnwbCVqIrztVibBj4wlwL"
    "e_jY28M5OLf9t3mT5YaxWmV2FWUS_2yTYBEY96CweeoEeX_oSUSOJZyxZ_e8vbET9RcQU4zSx1sDwK6X9dUHZvs8QmrVCdqFP3OAUq0aqguj87Wcor"
    "YvCmB8nBVwYqmNupvRPP8bBPh9nTISEkP6uTQKVJKrnp5C6JN2puWS06G-YzX6HJW3nS2lHj3iI7_6xqD_pI0eXh720ZwzYSK6PPbn9EHiiw0WuaBt"
    "6G9QwR-Rg";
static const char *K_TOK_EXPIRED =
    "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InRlc3Qta2V5LTEifQ.eyJpc3MiOiJodHRwczovL2lzc3Vlci5leGFtcGxlIiwiYXVkIj"
    "oiY2xpZW50LTEyMyIsInN1YiI6InVzZXItNDIiLCJpYXQiOjE1MDAwMDAwMDAsImV4cCI6MTYwMDAwMDAwMH0.maHZCcdhtKEyDG_VI55NPIQ9V8FX"
    "up9bm4-2Iylx8UGBSHIEm9AJcivxjU4mjgulmb6KW2mX7wPbEPf24p5r-vysJxNLcdItyZfFUy8opB_cF3HC_XYPibqgTf80CuYPJ5acKEoaVHNO7m"
    "9J66GFpmfFn02yL6EtacfZRkKvjEwXhVKfPsMmklLv5n4zSxNkwXkQSqm7SH9xoz3ohNWBNkglNMF-kNOC9jWWKdL58GTvecPKog43wVa9j_kS0VM8"
    "NAwihLbFAKQjSJF6bcX5yZsyYUVxtKDXP8WZnEQbsG122_xFZ6vrul4ZOtsH72J2OYK-6ilfaSEenE2cVudPvw";
static const char *K_TOK_AUDARR =
    "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InRlc3Qta2V5LTEifQ.eyJpc3MiOiJodHRwczovL2lzc3Vlci5leGFtcGxlIiwiYXVkIj"
    "pbIm90aGVyLWFwcCIsImNsaWVudC0xMjMiXSwic3ViIjoidSIsImlhdCI6MTcwMDAwMDAwMCwiZXhwIjo0MTAyNDQ0ODAwfQ.O0aL542aHac3DGg6"
    "2u_OmNww5Whfu-tmYIFUosW9p0bTFFa9hywtf77qkz039y-Sirj7KjUINUK7o_8U6BuLlaX0EdG0M6iBO4rs7NZo2vEP5t2yd9j9Cp9hwvkBulfCrC"
    "nHiBTEc06HXRva1ocYcQ5xcrEkY1JNPeALgIIpHyAJpwm5mGaMm04cTFEdshbBwoSpKaCnwKGEXZm8gMJDBmMlGbXZ2f3rLmxXPlo53KN2wVMvnSF-"
    "MD9O9Qwton1psUH_IJQsFrqoABXL1wc4cCmEpdeTEJtkV0uWWzgF825wl6v1KhUtxZojGHtZXf9PpH_PtGMuROGqHkid5drHKA";
static const char *K_TOK_HS256HDR =
    "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InRlc3Qta2V5LTEifQ.eyJpc3MiOiJodHRwczovL2lzc3Vlci5leGFtcGxlIiwiYXVkIj"
    "oiY2xpZW50LTEyMyIsInN1YiI6InVzZXItNDIiLCJlbWFpbCI6ImFsaWNlQGV4YW1wbGUuY29tIiwiaWF0IjoxNzAwMDAwMDAwLCJleHAiOjQxMDI0"
    "NDQ4MDB9.AAAA";
static const char *K_JWKS =
    "{\"keys\":[{\"kty\":\"RSA\",\"use\":\"sig\",\"kid\":\"test-key-1\",\"alg\":\"RS256\","
    "\"n\":\"owVI9J0dgUuZi6j45vERbQdj4OuB7vY-o7k6IWGdckzCfaNaeEdbaCvK7j2OpEKyKWHsgYAcqQJXbSCnQQuIvbJPNJu8iWAqwZsakKmS0Y"
    "0aP9HIaPvlMi8pN6ihVH8lutfeWVwhn-5_V_4Y5UWRJSzZXNUplH5TML2fVWlkU1XqkbZVnkyrAqK4N8ic1Is80g4JDJvMWNRR2cltCj1hjudBiLza"
    "xwCMRp6lvXV0YsYUL-Hw3gFSSDp4MiH0zkvivV1t3qq8OG9tBUcvZtrW0D7f5FP2YqV30XJko1PaCfnwl3KsiJwno4wK0hybd9ujjDSLW7DhKE88oC"
    "W0Y8YMRQ\",\"e\":\"AQAB\"}]}";

static const char *ISS = "https://issuer.example";
static const char *AUD = "client-123";
static const uint32_t NOW = 1700000100; // after iat(1700000000), before exp(4102444800)

void setUp()
{
}
void tearDown()
{
}

// JSON-parse edge guards reachable before signature verify (header parse), plus the
// scratch-exhaustion fail-closed and a JWK whose modulus decodes to nothing.
void test_oidc_parse_edge_guards()
{
    DetwsOidcKey key;
    key.loaded = false;
    TEST_ASSERT_TRUE(detws_oidc_jwks_find(K_JWKS, "test-key-1", &key));

    // Crafted headers whose "alg" value trips a find_field edge (before sig verify) -> ERR_ALG:
    // a value missing after ':', an unterminated array value, and a numeric value.
    const char *hdr_colon = "eyJhbGciOg.b.c";      // {"alg":
    const char *hdr_array = "eyJhbGciOlthYmM.b.c"; // {"alg":[abc
    const char *hdr_negnum = "eyJhbGciOi01fQ.b.c"; // {"alg":-5}
    TEST_ASSERT_EQUAL_INT(DETWS_OIDC_ERR_ALG,
                          detws_oidc_verify_with_key(hdr_colon, strlen(hdr_colon), &key, ISS, AUD, NOW, nullptr));
    TEST_ASSERT_EQUAL_INT(DETWS_OIDC_ERR_ALG,
                          detws_oidc_verify_with_key(hdr_array, strlen(hdr_array), &key, ISS, AUD, NOW, nullptr));
    TEST_ASSERT_EQUAL_INT(DETWS_OIDC_ERR_ALG,
                          detws_oidc_verify_with_key(hdr_negnum, strlen(hdr_negnum), &key, ISS, AUD, NOW, nullptr));

    // Scratch arena exhausted before the decode buffers can be borrowed -> fail closed.
    scratch_reset();
    while (scratch_alloc(256, 1))
    {
    }
    TEST_ASSERT_EQUAL_INT(DETWS_OIDC_ERR_FORMAT,
                          detws_oidc_verify_with_key(K_TOK_VALID, strlen(K_TOK_VALID), &key, ISS, AUD, NOW, nullptr));
    scratch_reset();

    // A JWK whose modulus base64url-decodes to nothing -> parse_rsa_jwk fails.
    static const char *K_JWKS_BADN = "{\"keys\":[{\"kty\":\"RSA\",\"kid\":\"bad\",\"n\":\"\",\"e\":\"AQAB\"}]}";
    DetwsOidcKey bad;
    bad.loaded = false;
    TEST_ASSERT_FALSE(detws_oidc_jwks_find(K_JWKS_BADN, "bad", &bad));
}

// Guards that run only after the signature verifies: minted (validly-signed) tokens with
// custom claims exercise the aud-missing / aud-array / not-yet-valid / empty-payload paths.
void test_oidc_signed_claim_guards()
{
    hex2bytes(_test_rsa_n, RT_N, 256);
    hex2bytes(_test_rsa_d, RT_D, 256);
    _test_rsa_e[0] = 0;
    _test_rsa_e[1] = 1;
    _test_rsa_e[2] = 0;
    _test_rsa_e[3] = 1; // e = 65537
    ssh_rsa_load_pubkey();

    DetwsOidcKey key;
    key.loaded = true;
    hex2bytes(key.n, RT_N, 256);
    key.e[0] = 0;
    key.e[1] = 1;
    key.e[2] = 0;
    key.e[3] = 1;

    char tok[2048];
    const char *II = "https://issuer.example";

    // No `aud` member at all -> aud_contains find_field fails -> ERR_AUD.
    char pl[256];
    snprintf(pl, sizeof(pl), "{\"iss\":\"%s\",\"exp\":4102444800}", II);
    make_jwt(pl, tok, sizeof(tok));
    TEST_ASSERT_EQUAL_INT(DETWS_OIDC_ERR_AUD,
                          detws_oidc_verify_with_key(tok, strlen(tok), &key, ISS, AUD, NOW, nullptr));

    // aud is an array with no matching entry -> the array loop finds no match -> ERR_AUD.
    snprintf(pl, sizeof(pl), "{\"iss\":\"%s\",\"aud\":[\"other-app\"],\"exp\":4102444800}", II);
    make_jwt(pl, tok, sizeof(tok));
    TEST_ASSERT_EQUAL_INT(DETWS_OIDC_ERR_AUD,
                          detws_oidc_verify_with_key(tok, strlen(tok), &key, ISS, AUD, NOW, nullptr));

    // aud array whose entry has an unterminated quote -> the inner scan bails -> ERR_AUD.
    snprintf(pl, sizeof(pl), "{\"iss\":\"%s\",\"aud\":[\"x],\"exp\":4102444800}", II);
    make_jwt(pl, tok, sizeof(tok));
    TEST_ASSERT_EQUAL_INT(DETWS_OIDC_ERR_AUD,
                          detws_oidc_verify_with_key(tok, strlen(tok), &key, ISS, AUD, NOW, nullptr));

    // A token whose nbf is in the future -> ERR_NOT_YET.
    snprintf(pl, sizeof(pl), "{\"iss\":\"%s\",\"aud\":\"%s\",\"exp\":4102444800,\"nbf\":4102444800}", II, AUD);
    make_jwt(pl, tok, sizeof(tok));
    TEST_ASSERT_EQUAL_INT(DETWS_OIDC_ERR_NOT_YET,
                          detws_oidc_verify_with_key(tok, strlen(tok), &key, ISS, AUD, NOW, nullptr));

    // A valid signature over a payload segment that base64url-decodes to nothing -> ERR_FORMAT.
    {
        static const char HDR[] = "{\"alg\":\"RS256\"}";
        char seg0[64], signing[128], seg2[400];
        b64url_enc((const uint8_t *)HDR, strlen(HDR), seg0);
        int sl = snprintf(signing, sizeof(signing), "%s.A", seg0); // payload segment "A" decodes to 0 bytes
        uint8_t sig[256];
        ssh_rsa_sign((const uint8_t *)signing, (size_t)sl, sig);
        b64url_enc(sig, 256, seg2);
        snprintf(tok, sizeof(tok), "%s.A.%s", seg0, seg2);
        TEST_ASSERT_EQUAL_INT(DETWS_OIDC_ERR_FORMAT,
                              detws_oidc_verify_with_key(tok, strlen(tok), &key, ISS, AUD, NOW, nullptr));
    }
}

void test_token_kid()
{
    char kid[64];
    TEST_ASSERT_TRUE(detws_oidc_token_kid(K_TOK_VALID, strlen(K_TOK_VALID), kid, sizeof(kid)));
    TEST_ASSERT_EQUAL_STRING("test-key-1", kid);
}

void test_jwks_find()
{
    DetwsOidcKey key;
    key.loaded = false;
    TEST_ASSERT_TRUE(detws_oidc_jwks_find(K_JWKS, "test-key-1", &key));
    TEST_ASSERT_TRUE(key.loaded);
    // e = AQAB = 65537 -> 0x00 0x01 0x00 0x01 right-aligned.
    TEST_ASSERT_EQUAL_HEX8(0x00, key.e[0]);
    TEST_ASSERT_EQUAL_HEX8(0x01, key.e[1]);
    TEST_ASSERT_EQUAL_HEX8(0x00, key.e[2]);
    TEST_ASSERT_EQUAL_HEX8(0x01, key.e[3]);
}

void test_jwks_find_missing_kid_fails()
{
    DetwsOidcKey key;
    key.loaded = false;
    TEST_ASSERT_FALSE(detws_oidc_jwks_find(K_JWKS, "nope", &key));
}

void test_verify_valid_token_and_claims()
{
    DetwsOidcClaims c;
    int rc = detws_oidc_verify(K_TOK_VALID, strlen(K_TOK_VALID), K_JWKS, ISS, AUD, NOW, &c);
    TEST_ASSERT_EQUAL_INT(DETWS_OIDC_OK, rc);
    TEST_ASSERT_EQUAL_STRING("user-42", c.sub);
    TEST_ASSERT_EQUAL_STRING("alice@example.com", c.email);
    TEST_ASSERT_TRUE(c.iat == 1700000000LL);
    TEST_ASSERT_TRUE(c.exp == 4102444800LL); // > 2^31: requires 64-bit claims
}

void test_verify_aud_array()
{
    int rc = detws_oidc_verify(K_TOK_AUDARR, strlen(K_TOK_AUDARR), K_JWKS, ISS, AUD, NOW, nullptr);
    TEST_ASSERT_EQUAL_INT(DETWS_OIDC_OK, rc); // "client-123" is one element of the aud array
}

void test_reject_expired()
{
    int rc = detws_oidc_verify(K_TOK_EXPIRED, strlen(K_TOK_EXPIRED), K_JWKS, ISS, AUD, NOW, nullptr);
    TEST_ASSERT_EQUAL_INT(DETWS_OIDC_ERR_EXPIRED, rc);
}

void test_reject_wrong_issuer()
{
    int rc = detws_oidc_verify(K_TOK_VALID, strlen(K_TOK_VALID), K_JWKS, "https://evil.example", AUD, NOW, nullptr);
    TEST_ASSERT_EQUAL_INT(DETWS_OIDC_ERR_ISS, rc);
}

void test_reject_wrong_audience()
{
    int rc = detws_oidc_verify(K_TOK_VALID, strlen(K_TOK_VALID), K_JWKS, ISS, "other-client", NOW, nullptr);
    TEST_ASSERT_EQUAL_INT(DETWS_OIDC_ERR_AUD, rc);
}

void test_reject_non_rs256_header()
{
    int rc = detws_oidc_verify(K_TOK_HS256HDR, strlen(K_TOK_HS256HDR), K_JWKS, ISS, AUD, NOW, nullptr);
    TEST_ASSERT_EQUAL_INT(DETWS_OIDC_ERR_ALG, rc);
}

void test_reject_tampered_payload()
{
    char tok[1024];
    strcpy(tok, K_TOK_VALID);
    char *dot = strchr(tok, '.'); // end of header
    char *c = dot + 6;            // a byte inside the payload segment
    *c = (*c == 'A') ? 'B' : 'A'; // flip to a different valid base64url char
    int rc = detws_oidc_verify(tok, strlen(tok), K_JWKS, ISS, AUD, NOW, nullptr);
    TEST_ASSERT_EQUAL_INT(DETWS_OIDC_ERR_SIGNATURE, rc);
}

void test_reject_tampered_signature()
{
    char tok[1024];
    strcpy(tok, K_TOK_VALID);
    char *sig = strrchr(tok, '.') + 1; // first char of the signature segment
    *sig = (*sig == 'Q') ? 'R' : 'Q';  // flip a meaningful (non-padding) base64url char
    int rc = detws_oidc_verify(tok, strlen(tok), K_JWKS, ISS, AUD, NOW, nullptr);
    TEST_ASSERT_EQUAL_INT(DETWS_OIDC_ERR_SIGNATURE, rc);
}

void test_reject_unknown_key()
{
    // JWKS whose only key has a different kid than the token's.
    const char *other = "{\"keys\":[{\"kty\":\"RSA\",\"kid\":\"different\",\"n\":\"AQAB\",\"e\":\"AQAB\"}]}";
    int rc = detws_oidc_verify(K_TOK_VALID, strlen(K_TOK_VALID), other, ISS, AUD, NOW, nullptr);
    TEST_ASSERT_EQUAL_INT(DETWS_OIDC_ERR_KEY, rc);
}

void test_reject_malformed()
{
    // No kid extractable -> the sole JWKS key is selected, then the token shape
    // fails to split into three segments.
    int rc = detws_oidc_verify("not-a-jwt", 9, K_JWKS, ISS, AUD, NOW, nullptr);
    TEST_ASSERT_EQUAL_INT(DETWS_OIDC_ERR_FORMAT, rc);
}

// token_kid rejects null args, a zero-cap output and a header segment that does not
// base64url-decode.
void test_token_kid_guards()
{
    char kid[64];
    TEST_ASSERT_FALSE(detws_oidc_token_kid(nullptr, 10, kid, sizeof(kid)));
    TEST_ASSERT_FALSE(detws_oidc_token_kid("a.b.c", 5, nullptr, sizeof(kid)));
    TEST_ASSERT_FALSE(detws_oidc_token_kid("a.b.c", 5, kid, 0));
    TEST_ASSERT_FALSE(detws_oidc_token_kid("A.payload.sig", 13, kid, sizeof(kid))); // 1-char header
}

// jwks_find rejects null args, a document with no keys array, an unterminated key
// object, and a key whose kid matches but is unusable (no modulus).
void test_jwks_find_guards()
{
    DetwsOidcKey key;
    key.loaded = false;
    TEST_ASSERT_FALSE(detws_oidc_jwks_find(nullptr, "k", &key));
    TEST_ASSERT_FALSE(detws_oidc_jwks_find("{\"keys\":[]}", "k", nullptr));
    TEST_ASSERT_FALSE(detws_oidc_jwks_find("{\"foo\":1}", "k", &key));                    // no keys array
    TEST_ASSERT_FALSE(detws_oidc_jwks_find("{\"keys\":[{\"kid\":\"x\"", "x", &key));      // unterminated object
    TEST_ASSERT_FALSE(detws_oidc_jwks_find("{\"keys\":[{\"kid\":\"k1\"}]}", "k1", &key)); // kid matches, no n/e
}

// verify_with_key rejects a null / unloaded key, a zero length, and a header that does
// not decode; verify() rejects tokens with the wrong number of segments and a
// wrong-length signature.
void test_verify_guards_and_malformed()
{
    DetwsOidcKey unloaded;
    unloaded.loaded = false;
    TEST_ASSERT_EQUAL_INT(DETWS_OIDC_ERR_FORMAT,
                          detws_oidc_verify_with_key("a.b.c", 5, &unloaded, ISS, AUD, NOW, nullptr));

    DetwsOidcKey key;
    key.loaded = false;
    TEST_ASSERT_TRUE(detws_oidc_jwks_find(K_JWKS, "test-key-1", &key));
    TEST_ASSERT_EQUAL_INT(DETWS_OIDC_ERR_FORMAT, detws_oidc_verify_with_key(nullptr, 5, &key, ISS, AUD, NOW, nullptr));
    TEST_ASSERT_EQUAL_INT(DETWS_OIDC_ERR_FORMAT, detws_oidc_verify_with_key("a.b.c", 0, &key, ISS, AUD, NOW, nullptr));
    TEST_ASSERT_EQUAL_INT(DETWS_OIDC_ERR_FORMAT,
                          detws_oidc_verify_with_key("A.b.c", 5, &key, ISS, AUD, NOW, nullptr)); // header decode fail

    // Wrong segment counts (one '.' / three '.').
    TEST_ASSERT_EQUAL_INT(DETWS_OIDC_ERR_FORMAT, detws_oidc_verify("aa.bb", 5, K_JWKS, ISS, AUD, NOW, nullptr));
    TEST_ASSERT_EQUAL_INT(DETWS_OIDC_ERR_FORMAT, detws_oidc_verify("a.b.c.d", 7, K_JWKS, ISS, AUD, NOW, nullptr));

    // A valid header/payload but a truncated (wrong-length) signature.
    char buf[2048];
    snprintf(buf, sizeof(buf), "%s", K_TOK_VALID);
    char *last_dot = strrchr(buf, '.');
    last_dot[6] = '\0'; // keep only 5 signature chars -> decodes to != 256 bytes
    TEST_ASSERT_EQUAL_INT(DETWS_OIDC_ERR_FORMAT, detws_oidc_verify(buf, strlen(buf), K_JWKS, ISS, AUD, NOW, nullptr));
}

// Crafted JWKS documents exercise the JSON field scanner and the RSA-JWK loader:
// an empty document, an escaped/over-long/unterminated/non-string kid, a key missing
// its exponent, and exponents that do or do not fit the 4-byte field.
void test_jwks_malformed_keys()
{
    DetwsOidcKey key;
    key.loaded = false;

    TEST_ASSERT_FALSE(detws_oidc_jwks_find("", "k", &key)); // needle longer than the document

    // escaped char inside a string value + a mismatching kid (get_str runs, then rejects).
    TEST_ASSERT_FALSE(detws_oidc_jwks_find("{\"keys\":[{\"kid\":\"a\\\\b\"}]}", "zzz", &key));
    // kid string with no closing quote.
    TEST_ASSERT_FALSE(detws_oidc_jwks_find("{\"keys\":[{\"kid\":\"abc}]}", "abc", &key));
    // kid value is not a string.
    TEST_ASSERT_FALSE(detws_oidc_jwks_find("{\"keys\":[{\"kid\":true}]}", "x", &key));

    // a kid longer than the kid buffer (get_str overflow); the key itself is usable.
    char jwks[400];
    int p = snprintf(jwks, sizeof(jwks), "{\"keys\":[{\"kid\":\"");
    for (int i = 0; i < 200; i++)
        jwks[p++] = 'a';
    snprintf(jwks + p, sizeof(jwks) - p, "\",\"n\":\"AAAA\",\"e\":\"AQAB\"}]}");
    TEST_ASSERT_TRUE(detws_oidc_jwks_find(jwks, nullptr, &key));

    // a key with a modulus but no exponent.
    TEST_ASSERT_FALSE(detws_oidc_jwks_find("{\"keys\":[{\"n\":\"AAAA\"}]}", nullptr, &key));
    // an exponent too wide for the 4-byte field.
    TEST_ASSERT_FALSE(detws_oidc_jwks_find("{\"keys\":[{\"n\":\"AAAA\",\"e\":\"AAAAAAAA\"}]}", nullptr, &key));
    // an exponent of 5 bytes with a leading zero, which is stripped and accepted.
    TEST_ASSERT_TRUE(detws_oidc_jwks_find("{\"keys\":[{\"n\":\"AAAA\",\"e\":\"AAAAAAA\"}]}", nullptr, &key));
}

int main()
{
    UNITY_BEGIN();
    RUN_TEST(test_oidc_parse_edge_guards);
    RUN_TEST(test_oidc_signed_claim_guards);
    RUN_TEST(test_jwks_malformed_keys);
    RUN_TEST(test_token_kid_guards);
    RUN_TEST(test_jwks_find_guards);
    RUN_TEST(test_verify_guards_and_malformed);
    RUN_TEST(test_token_kid);
    RUN_TEST(test_jwks_find);
    RUN_TEST(test_jwks_find_missing_kid_fails);
    RUN_TEST(test_verify_valid_token_and_claims);
    RUN_TEST(test_verify_aud_array);
    RUN_TEST(test_reject_expired);
    RUN_TEST(test_reject_wrong_issuer);
    RUN_TEST(test_reject_wrong_audience);
    RUN_TEST(test_reject_non_rs256_header);
    RUN_TEST(test_reject_tampered_payload);
    RUN_TEST(test_reject_tampered_signature);
    RUN_TEST(test_reject_unknown_key);
    RUN_TEST(test_reject_malformed);
    return UNITY_END();
}
