//! Cluster-0.21 OAuth integration on the A2A HTTP transport.
//!
//! Drives [`A2aHttpServer`] end-to-end via [`tower::ServiceExt::oneshot`]
//! with a real [`OAuthAuthenticator`] wired against a wiremock JWKS
//! endpoint serving a signed RSA test key. The three scenarios pin the
//! dispatcher's `authenticate` + `authorize_method` integration on the
//! HTTP path:
//!
//! 1. Valid bearer + correct scope → handler dispatches, no `-32001`
//!    envelope on the wire.
//! 2. Missing bearer → `-32001 UNAUTHENTICATED` envelope, handler never
//!    invoked.
//! 3. Valid bearer + wrong scope → `-32001 UNAUTHENTICATED` envelope
//!    surfaced by `authorize_method`, handler never invoked.
//!
//! The PEM + JWKS + token-mint fixtures are copied verbatim from
//! `klieo-auth-oauth/tests/oauth_integration.rs` — small, single-purpose,
//! and keeping the copy in this crate keeps the cross-crate integration
//! test self-contained.

#![cfg(feature = "http")]

use std::collections::HashMap;
use std::sync::Arc;

use axum::body::Body;
use axum::http::{Request, StatusCode};
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine;
use http_body_util::BodyExt;
use jsonwebtoken::{encode, Algorithm, EncodingKey, Header};
use klieo_a2a::handler::EchoHandler;
use klieo_a2a::http::A2aHttpServer;
use klieo_a2a::server::A2aDispatcher;
use klieo_a2a::task_store::{A2aTaskStore, DEFAULT_BUCKET};
use klieo_auth_common::Authenticator;
use klieo_auth_oauth::OAuthAuthenticator;
use klieo_bus_memory::MemoryBus;
use klieo_core::Pubsub;
use rsa::pkcs8::DecodePrivateKey;
use rsa::traits::PublicKeyParts;
use rsa::RsaPrivateKey;
use serde::Serialize;
use serde_json::json;
use tokio_util::sync::CancellationToken;
use tower::ServiceExt;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};

// Fixture PEM — fresh 2048-bit RSA throwaway key. Copied from
// klieo-auth-oauth/tests/oauth_integration.rs. Committed to source on
// purpose; it protects nothing.
const PEM_A: &str = "-----BEGIN PRIVATE KEY-----\n\
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDbRAbiN8CgEaBd\n\
FKMynC4IJK30RVa9zCYeNo4gs7qZ1cRELDifv+cdWfhXAaTFl8bPCnLC2wXZAaAk\n\
OBz66UOAIEvz2X/tdIelu0LHTGbUMqnr5LV69UQ8KfNRFL+JGkMdEWsIawUfm6ch\n\
YcPV5UAQUAqNrDdGfVW7osSIIYP8gxU3Yr6bM2KANhz56fg7ajPFHp3kUkxQ6Z/9\n\
uE3ZFWYKg9WPGahzSf2q9yO+3zu1xWO0AbP+weNzecLqNhpGB8Od2+4gdAxIC1g/\n\
R2kvjJ3Pa1WCNNGjYdwCX3z/csDqLuMCmemEInnqMlEcJbS6Ltpg2rCZTPUCuQ9+\n\
aX4busPLAgMBAAECggEADHhLc0JxdDOxwj7CDfGflTtPdf9wMJ4eItoTw4dahZTd\n\
3coCtGR2Zo8Z1l+TuTW/Cuz7XC/sSWGpc7XfQWPSa28qx/pRfLXZCxNhzQeAJ5xJ\n\
WU7qBU1Sl/YDeP6BlXCxasNKqjFlRV1q/V/b19vQWhJ7RIBhVQ3GukObrWmejICQ\n\
VVutCepj+a+EyFUMeEI6X6TQi+IV1BQWQ8ahjv/oQIgKNCKB9BLnzXGO6aaz/8mU\n\
uRiW+XlkKdIlqsuq56eoS13CAn80AHdALv/0nuKb5+QfhKhcMe4uIqWITb+Gj1Ow\n\
kGdUR3SBOE0gJmXjZAOXhPogIqCwr4bW45PlmKxvcQKBgQD1dvBlgnF/NEgHXzhs\n\
emiAx7JR6VXt2DKU6Fio+nOAzw84p/f3LUKZh+XReLNcUyg1aT8HrucwlcH8j5CL\n\
At2g0oIILzu9OGajNBXySCvjploPH46VccuUhklLPG73GfikcNZm0C879LBqBnef\n\
Dkf/VElhovkUByeStf83jUHwsQKBgQDkrTnIG6t+Hli4SIAyrtCAEaNHBYIqAgiJ\n\
h8OUmAPnJe/lxudlVjmtTuXvETrzNoBYClACoguDQ6uy1CCVV3rGg7tthSn89anE\n\
pv7NFHcTWQTcU/tnLpLOXu4NLeOFBitsLmJPFAJl3ebiXeBsG09StHJHLvV5DC1I\n\
L/jpI+e7OwKBgA+iKaFtc2kUnVnK2L8f9LPvBA5vfAeP83jEa5Enkl+r0F5dZE+b\n\
1FgO7bYvp8zDhxYPw/HrU9ImV5qMUjpoTlFF1v5Kc9WTWAYg9RzfN9xh5e58pgoi\n\
m0K0Ysql6LNYNhvTC/S0Ya3B4dJ+4vOAZ1l9tsa/KHn8EqsnaD/TgVWhAoGBAMZQ\n\
H9MVg/Tc1m4CmgLaLgFSynm2IjrSBptWvNtptcDI2rI3e8cbLuvIh4l7NNdlRYqU\n\
buzJoRhWIFn2sscw1U3BQlLpcA9q8K6VcFVj0RxbT+bPgi/qaPBFzUZqrGxZfGDW\n\
HPWdUbML7MYVSaIXUQRVbuGirMksMTzwNXniZH2vAoGBALeG/u/alf7cIZxmnemE\n\
sq2N7wJz+JDHKujxwXl40P0dz9D9kYIVRhHA6TVhp/8oxXJTfMuTfWFfzBK5EMco\n\
MTQjW/kWlJQe/nS1TQ+IO+ZY5E4vc35S5hGZJtKUmTAfiBwQmswg/4KefjDRyRgV\n\
bBz+ZVw8j0jxC77vKnnplmPc\n\
-----END PRIVATE KEY-----\n";

const KID_A: &str = "test-key-a";
const ISSUER: &str = "https://issuer.example.test";
const AUDIENCE: &str = "klieo-a2a-test";
const SUBJECT: &str = "alice@example.test";
const UNAUTHENTICATED_CODE: i32 = -32001;
const TOKEN_TTL_SECS: u64 = 300;
const NBF_SKEW_SECS: u64 = 5;

#[derive(Serialize)]
struct TestClaims {
    iss: String,
    sub: String,
    aud: String,
    exp: u64,
    nbf: u64,
    iat: u64,
    scope: String,
}

fn now_secs() -> u64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .expect("system time before unix epoch")
        .as_secs()
}

fn jwk_from_pem(pem: &str, kid: &str) -> serde_json::Value {
    let private_key = RsaPrivateKey::from_pkcs8_pem(pem).expect("parse RSA PEM");
    let n = URL_SAFE_NO_PAD.encode(private_key.n().to_bytes_be());
    let e = URL_SAFE_NO_PAD.encode(private_key.e().to_bytes_be());
    json!({
        "kty": "RSA",
        "use": "sig",
        "alg": "RS256",
        "kid": kid,
        "n": n,
        "e": e,
    })
}

async fn serve_jwks(jwks: serde_json::Value) -> (MockServer, String) {
    let server = MockServer::start().await;
    Mock::given(method("GET"))
        .and(path("/jwks"))
        .respond_with(ResponseTemplate::new(200).set_body_json(jwks))
        .mount(&server)
        .await;
    let uri = format!("{}/jwks", server.uri());
    (server, uri)
}

fn mint_token(scopes: &[&str]) -> String {
    let now = now_secs();
    let claims = TestClaims {
        iss: ISSUER.into(),
        sub: SUBJECT.into(),
        aud: AUDIENCE.into(),
        exp: now + TOKEN_TTL_SECS,
        nbf: now - NBF_SKEW_SECS,
        iat: now,
        scope: scopes.join(" "),
    };
    let mut header = Header::new(Algorithm::RS256);
    header.kid = Some(KID_A.into());
    let encoding_key =
        EncodingKey::from_rsa_pem(PEM_A.as_bytes()).expect("EncodingKey::from_rsa_pem");
    encode(&header, &claims, &encoding_key).expect("sign JWT")
}

fn scope_map() -> HashMap<String, Vec<String>> {
    HashMap::from([
        ("SendMessage".to_string(), vec!["klieo:write".to_string()]),
        ("GetTask".to_string(), vec!["klieo:read".to_string()]),
    ])
}

async fn build_oauth_authenticator(jwks_uri: String) -> OAuthAuthenticator {
    OAuthAuthenticator::builder()
        .issuer_url(ISSUER)
        .audience(AUDIENCE)
        .jwks_uri(jwks_uri)
        .required_scopes(scope_map())
        .build()
        .await
        .expect("OAuthAuthenticator builds against mock JWKS")
}

fn send_message_body() -> Vec<u8> {
    serde_json::to_vec(&json!({
        "jsonrpc": "2.0",
        "id": 1,
        "method": "SendMessage",
        "params": {
            "message": {
                "messageId": "m-1",
                "contextId": "c-1",
                "role": "user",
                "parts": [{
                    "type": "text",
                    "content": "hello",
                }],
            }
        }
    }))
    .expect("encode SendMessage body")
}

async fn build_server(auth: OAuthAuthenticator) -> Arc<A2aHttpServer> {
    let bus = MemoryBus::new();
    let pubsub: Arc<dyn Pubsub> = bus.pubsub.clone();
    let handler: Arc<dyn klieo_a2a::handler::A2aHandler> = Arc::new(EchoHandler::default());
    let authenticator: Arc<dyn Authenticator> = Arc::new(auth);
    let dispatcher = Arc::new(A2aDispatcher::new(handler, authenticator, pubsub));
    let task_store = Arc::new(A2aTaskStore::new(bus.kv.clone(), DEFAULT_BUCKET.into()));
    Arc::new(A2aHttpServer::new(
        dispatcher,
        task_store,
        CancellationToken::new(),
    ))
}

async fn collect_body(resp: axum::response::Response) -> String {
    let bytes = resp
        .into_body()
        .collect()
        .await
        .expect("collect body")
        .to_bytes();
    String::from_utf8(bytes.to_vec()).expect("body is utf-8")
}

#[tokio::test]
async fn valid_bearer_with_correct_scope_dispatches_to_handler() {
    let (_server, jwks_uri) = serve_jwks(json!({ "keys": [jwk_from_pem(PEM_A, KID_A)] })).await;
    let authn = build_oauth_authenticator(jwks_uri).await;
    let http_server = build_server(authn).await;

    let token = mint_token(&["klieo:write"]);
    let req = Request::post("/a2a")
        .header("content-type", "application/json")
        .header("authorization", format!("Bearer {token}"))
        .body(Body::from(send_message_body()))
        .expect("build request");

    let resp = http_server.router().oneshot(req).await.expect("oneshot");
    assert_eq!(resp.status(), StatusCode::OK);

    let text = collect_body(resp).await;
    let unauthenticated_marker = format!("\"code\":{UNAUTHENTICATED_CODE}");
    assert!(
        !text.contains(&unauthenticated_marker),
        "expected handler dispatch, got -32001 envelope: {text}"
    );
    assert!(
        text.contains("\"result\""),
        "expected JSON-RPC result field on success; got: {text}"
    );
}

#[tokio::test]
async fn missing_bearer_returns_unauthenticated_envelope() {
    let (_server, jwks_uri) = serve_jwks(json!({ "keys": [jwk_from_pem(PEM_A, KID_A)] })).await;
    let authn = build_oauth_authenticator(jwks_uri).await;
    let http_server = build_server(authn).await;

    let req = Request::post("/a2a")
        .header("content-type", "application/json")
        .body(Body::from(send_message_body()))
        .expect("build request");

    let resp = http_server.router().oneshot(req).await.expect("oneshot");
    // JSON-RPC error envelopes ride a 200 HTTP status; the auth failure
    // shape lives inside the body.
    assert_eq!(resp.status(), StatusCode::OK);

    let text = collect_body(resp).await;
    let unauthenticated_marker = format!("\"code\":{UNAUTHENTICATED_CODE}");
    assert!(
        text.contains(&unauthenticated_marker),
        "expected -32001 UNAUTHENTICATED envelope; got: {text}"
    );
}

#[tokio::test]
async fn valid_bearer_with_wrong_scope_returns_unauthenticated_envelope() {
    let (_server, jwks_uri) = serve_jwks(json!({ "keys": [jwk_from_pem(PEM_A, KID_A)] })).await;
    let authn = build_oauth_authenticator(jwks_uri).await;
    let http_server = build_server(authn).await;

    // Token carries only `klieo:read`; SendMessage requires `klieo:write`.
    let token = mint_token(&["klieo:read"]);
    let req = Request::post("/a2a")
        .header("content-type", "application/json")
        .header("authorization", format!("Bearer {token}"))
        .body(Body::from(send_message_body()))
        .expect("build request");

    let resp = http_server.router().oneshot(req).await.expect("oneshot");
    assert_eq!(resp.status(), StatusCode::OK);

    let text = collect_body(resp).await;
    let unauthenticated_marker = format!("\"code\":{UNAUTHENTICATED_CODE}");
    assert!(
        text.contains(&unauthenticated_marker),
        "expected -32001 UNAUTHENTICATED envelope on scope mismatch; got: {text}"
    );
}
