// Gated behind the `slow` feature (WOR-982): this suite spins up a local
// wiremock token endpoint and drives blocking HTTP round trips, so it is
// excluded from the default fast gate. Run with
// `cargo test -p mcptest-core --features slow`.
#![cfg(feature = "slow")]

//! End-to-end OAuth 2.1 + PKCE token-exchange and refresh test (
//! follow-up).
//!
//! The OAuth/PKCE building blocks (`oauth_pkce`, `token`, `refresh`,
//! `reqwest_refresher`) each have unit coverage, but nothing exercised the
//! full code->token->cache->refresh round trip against a real HTTP token
//! endpoint. This test stands up a `wiremock` token endpoint and drives:
//!
//! 1. A PKCE authorization-code exchange (`code` -> access + refresh token).
//! 2. Writing the resulting token to a `TokenCache`.
//! 3. A refresh against the same endpoint via `maybe_refresh`, asserting the
//!    new access token lands in the cache and is the one the runner would
//!    present as a bearer header.
//!
//! The exchange and refresh calls are blocking (they use
//! `reqwest::blocking`), so we run the mock server on a current-thread
//! runtime and call the blocking helpers directly, mirroring the
//! `oauth_pkce` unit-test style.

use mcptest_core::auth::{
    exchange_with_client, maybe_refresh, PkceParams, ReqwestRefresher, ReqwestRefresherConfig,
    Token, TokenCache, TokenExchangeRequest,
};
use tempfile::TempDir;
use wiremock::matchers::{body_string_contains, method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};

const SERVER_URL: &str = "https://mcp.example.com/v1";

fn runtime() -> tokio::runtime::Runtime {
    tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()
        .expect("runtime")
}

#[test]
fn pkce_code_exchange_then_refresh_round_trips_through_the_cache() {
    let rt = runtime();

    // Stand up a token endpoint that answers both the authorization-code
    // exchange and the later refresh_token grant.
    let (uri, _guard) = rt.block_on(async {
        let server = MockServer::start().await;

        Mock::given(method("POST"))
            .and(path("/token"))
            .and(body_string_contains("grant_type=authorization_code"))
            .and(body_string_contains("code=auth-code-xyz"))
            .and(body_string_contains("code_verifier="))
            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
                "access_token": "AT.initial",
                "refresh_token": "RT.initial",
                "expires_in": 5,
                "token_type": "Bearer",
                "scope": "mcp:read"
            })))
            .up_to_n_times(1)
            .mount(&server)
            .await;

        Mock::given(method("POST"))
            .and(path("/token"))
            .and(body_string_contains("grant_type=refresh_token"))
            .and(body_string_contains("refresh_token=RT.initial"))
            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
                "access_token": "AT.refreshed",
                "refresh_token": "RT.refreshed",
                "expires_in": 3600,
                "token_type": "Bearer",
                "scope": "mcp:read"
            })))
            .mount(&server)
            .await;

        (server.uri(), server)
    });

    // Step 1: PKCE authorization-code exchange.
    let client = reqwest::blocking::Client::new();
    let pkce = PkceParams::generate();
    let now: u64 = 1_700_000_000;
    let exchange = TokenExchangeRequest {
        token_endpoint: format!("{uri}/token"),
        client_id: "mcptest-client".into(),
        client_secret: None,
        code: "auth-code-xyz".into(),
        redirect_uri: "http://127.0.0.1:9000/callback".into(),
        code_verifier: pkce.code_verifier.clone(),
    };
    let token: Token = exchange_with_client(&client, &exchange, now).expect("code exchange");
    assert_eq!(token.access_token, "AT.initial");
    assert_eq!(token.refresh_token.as_deref(), Some("RT.initial"));
    assert_eq!(token.expires_at_unix, now + 5);

    // Step 2: persist to the per-server cache (what `mcptest login` does).
    let dir = TempDir::new().expect("tempdir");
    let cache = TokenCache::with_root(dir.path());
    cache.write(SERVER_URL, &token).expect("write token");

    // Step 3: refresh. The cached token is near expiry (5s), so a 60s
    // proactive window forces a refresh against the mock endpoint.
    let refresher = ReqwestRefresher::with_client(
        ReqwestRefresherConfig {
            token_endpoint: format!("{uri}/token"),
            client_id: "mcptest-client".into(),
            ..ReqwestRefresherConfig::default()
        },
        reqwest::blocking::Client::new(),
    );
    let refreshed =
        maybe_refresh(&cache, SERVER_URL, &refresher, now, 60).expect("refresh succeeds");

    // The new access token is obtained and is what the runner would present.
    assert_eq!(refreshed.access_token, "AT.refreshed");
    assert_eq!(refreshed.refresh_token.as_deref(), Some("RT.refreshed"));

    // And it is durably applied: re-reading the cache yields the new token.
    let on_disk = cache.read(SERVER_URL).expect("read").expect("present");
    assert_eq!(on_disk.access_token, "AT.refreshed");
    assert!(on_disk.expires_at_unix > now + 5, "refresh extended expiry");
}

#[test]
fn refresh_is_skipped_when_the_cached_token_is_still_fresh() {
    let rt = runtime();
    let (uri, _guard) = rt.block_on(async {
        let server = MockServer::start().await;
        // No refresh mock mounted: if the code calls the endpoint, the
        // request 404s and the refresh fails, which this test would catch.
        (server.uri(), server)
    });

    let dir = TempDir::new().expect("tempdir");
    let cache = TokenCache::with_root(dir.path());
    let now: u64 = 1_700_000_000;
    let fresh = Token {
        access_token: "AT.fresh".into(),
        refresh_token: Some("RT.fresh".into()),
        expires_at_unix: now + 3600,
        token_type: "Bearer".into(),
        scope: Some("mcp:read".into()),
        last_refreshed: "2026-05-23T00:00:00Z".into(),
        token_endpoint: None,
        client_id: None,
    };
    cache.write(SERVER_URL, &fresh).expect("write");

    let refresher = ReqwestRefresher::with_client(
        ReqwestRefresherConfig {
            token_endpoint: format!("{uri}/token"),
            client_id: "mcptest-client".into(),
            ..ReqwestRefresherConfig::default()
        },
        reqwest::blocking::Client::new(),
    );
    let got = maybe_refresh(&cache, SERVER_URL, &refresher, now, 60).expect("no refresh needed");
    assert_eq!(got.access_token, "AT.fresh");
}
