//! Integration tests for the transport-level auth CLI flags and the
//! `mcptest doctor` auth pre-flight.
//!
//! Covers: the new `mcptest run` flags parse and override the YAML auth
//! config (CLI beats YAML); `--profile` selects the named-profile SigV4
//! credential source; an mTLS override that produces an invalid
//! block is rejected up front; and the doctor pre-flight never prints any
//! secret material.

use assert_cmd::Command;
use predicates::prelude::*;

fn mcptest() -> Command {
    Command::cargo_bin("mcptest").expect("binary builds")
}

/// Write a minimal config with one HTTP server carrying an mTLS block and
/// return the temp dir so the file outlives the command.
fn config_with_mtls(dir: &std::path::Path, cert: &str, key: &str) {
    let body = format!(
        "servers:\n  api:\n    url: https://mcp.example.com/v1\n    http:\n      tls:\n        mtls:\n          client_cert_path: {cert}\n          client_key_path: {key}\n"
    );
    std::fs::write(dir.join("mcptest.yaml"), body).unwrap();
}

#[test]
fn run_parses_all_mtls_flags() {
    // A nonexistent config means the run path no-ops after flag parsing, so
    // a successful exit confirms clap accepted every flag.
    let dir = tempfile::tempdir().unwrap();
    mcptest()
        .current_dir(dir.path())
        .args([
            "run",
            "--client-cert",
            "/tmp/client.pem",
            "--client-key",
            "/tmp/client.key",
            "--ca-bundle",
            "/tmp/ca.pem",
        ])
        .assert()
        .success();
}

#[test]
fn run_parses_sigv4_and_webbot_flags() {
    let dir = tempfile::tempdir().unwrap();
    mcptest()
        .current_dir(dir.path())
        .args([
            "run",
            "--aws-sigv4-region",
            "us-east-1",
            "--service",
            "execute-api",
            "--webbot-key-env",
            "MCPTEST_BOT_KEY",
            "--webbot-agent",
            "https://mcptest.sh",
            "--webbot-algorithm",
            "ed25519",
        ])
        .assert()
        .success();
}

#[test]
fn profile_flag_selects_named_profile_without_error() {
    // `--profile` now wires the named-profile credential source.
    // The override applies cleanly; with `--collect-only` the run reaches the
    // short-circuit before any AWS file read, so no profile error fires.
    let dir = tempfile::tempdir().unwrap();
    std::fs::write(
        dir.path().join("mcptest.yaml"),
        "servers:\n  api:\n    url: https://mcp.example.com/v1\n",
    )
    .unwrap();
    mcptest()
        .current_dir(dir.path())
        .args([
            "run",
            "--collect-only",
            "--profile",
            "prod",
            "--aws-sigv4-region",
            "us-east-1",
            "--service",
            "execute-api",
        ])
        .assert()
        .stderr(predicate::str::contains("auth override is invalid").not())
        .stderr(predicate::str::contains("not yet supported").not());
}

#[test]
fn unknown_webbot_algorithm_is_rejected() {
    let dir = tempfile::tempdir().unwrap();
    std::fs::write(
        dir.path().join("mcptest.yaml"),
        "servers:\n  api:\n    url: https://mcp.example.com/v1\n",
    )
    .unwrap();
    mcptest()
        .current_dir(dir.path())
        .args(["run", "--webbot-algorithm", "nonsense"])
        .assert()
        .failure()
        .stderr(predicate::str::contains("is not recognized"));
}

#[test]
fn mtls_cert_flag_without_key_is_rejected() {
    // Overriding only the cert (no key, no YAML key) produces an invalid
    // mTLS block, which the override validator catches before the run.
    let dir = tempfile::tempdir().unwrap();
    std::fs::write(
        dir.path().join("mcptest.yaml"),
        "servers:\n  api:\n    url: https://mcp.example.com/v1\n",
    )
    .unwrap();
    mcptest()
        .current_dir(dir.path())
        .args(["run", "--client-cert", "/tmp/only-cert.pem"])
        .assert()
        .failure()
        .stderr(predicate::str::contains("auth override is invalid"));
}

#[test]
fn cli_cert_override_beats_yaml_and_validates() {
    // A YAML cert+key pair plus a CLI cert override. The override swaps the
    // cert path but keeps the YAML key, so the block stays valid. The
    // auth-override gate runs before `--collect-only` short-circuits, so
    // this exercises the override path without any network connect.
    let dir = tempfile::tempdir().unwrap();
    config_with_mtls(dir.path(), "/etc/yaml-cert.pem", "/etc/yaml-key.pem");
    mcptest()
        .current_dir(dir.path())
        .args([
            "run",
            "--collect-only",
            "--client-cert",
            "/etc/cli-cert.pem",
        ])
        .assert()
        // The override is valid (cert+key both present), so no auth-override
        // error fires and the run reaches the collect-only short-circuit.
        .stderr(predicate::str::contains("auth override is invalid").not())
        .stderr(predicate::str::contains("named-profile").not());
}

#[test]
fn doctor_preflight_reports_sigv4_credentials_without_leaking_secret() {
    let dir = tempfile::tempdir().unwrap();
    std::fs::write(
        dir.path().join("mcptest.yaml"),
        "servers:\n  awsg:\n    url: https://abc.execute-api.us-east-1.amazonaws.com/prod/mcp\n    auth:\n      aws_sigv4:\n        region: us-east-1\n        service: execute-api\n        credentials:\n          source: explicit\n          access_key_id_env: DOCTOR_AWS_AK\n          secret_access_key_env: DOCTOR_AWS_SK\n",
    )
    .unwrap();
    mcptest()
        .current_dir(dir.path())
        .env("DOCTOR_AWS_AK", "AKIDEXAMPLE")
        .env("DOCTOR_AWS_SK", "this-secret-must-not-leak")
        .args(["doctor"])
        .assert()
        .success()
        .stdout(predicate::str::contains("auth pre-flight"))
        .stdout(predicate::str::contains("sigv4"))
        .stdout(predicate::str::contains("present"))
        // The hard rule: the secret value never appears in output.
        .stdout(predicate::str::contains("this-secret-must-not-leak").not());
}

#[test]
fn doctor_preflight_fails_when_webbot_key_cannot_load() {
    // rsa-pss is a supported algorithm, so the pre-flight gets past
    // the algorithm check and fails on the signing key, which cannot load
    // because DOCTOR_WBA_UNUSED is not set.
    let dir = tempfile::tempdir().unwrap();
    std::fs::write(
        dir.path().join("mcptest.yaml"),
        "servers:\n  bot:\n    url: https://mcp.example.com/v1\n    auth:\n      web_bot_auth:\n        key_env: DOCTOR_WBA_UNUSED\n        directory_url: https://mcptest.sh/dir\n        signature_agent: https://mcptest.sh\n        algorithm: rsa-pss\n",
    )
    .unwrap();
    mcptest()
        .current_dir(dir.path())
        .args(["doctor"])
        .assert()
        .failure()
        .stdout(predicate::str::contains("FAIL"))
        .stdout(predicate::str::contains("signing key failed to load"));
}

#[test]
fn web_bot_auth_directory_prints_public_jwk_for_ed25519() {
    // Generate an Ed25519 key, then confirm the directory command prints an
    // OKP JWK and never the private key.
    let dir = tempfile::tempdir().unwrap();
    let key_path = dir.path().join("bot.ed25519");
    let status = std::process::Command::new("openssl")
        .args(["genpkey", "-algorithm", "ed25519", "-out"])
        .arg(&key_path)
        .status();
    // Skip cleanly if openssl is unavailable on the runner.
    if !status.map(|s| s.success()).unwrap_or(false) {
        eprintln!("skipping: openssl not available");
        return;
    }
    mcptest()
        .current_dir(dir.path())
        .args(["web-bot-auth", "directory", "--key"])
        .arg(&key_path)
        .assert()
        .success()
        .stdout(predicate::str::contains("\"kty\": \"OKP\""))
        .stdout(predicate::str::contains("\"crv\": \"Ed25519\""))
        .stdout(predicate::str::contains("\"keys\""))
        // The published directory must never carry the private key.
        .stdout(predicate::str::contains("PRIVATE KEY").not());
}

#[test]
fn web_bot_auth_directory_rejects_both_key_sources() {
    let dir = tempfile::tempdir().unwrap();
    mcptest()
        .current_dir(dir.path())
        .args([
            "web-bot-auth",
            "directory",
            "--key",
            "/tmp/a.key",
            "--key-env",
            "SOME_VAR",
        ])
        .assert()
        .failure()
        .stderr(predicate::str::contains("exactly one"));
}

#[test]
fn doctor_probe_flag_parses_and_runs_offline_default() {
    // With no auth scheme configured, `--probe` is a no-op that still exits
    // cleanly, proving the flag parses and wires through doctor.
    let dir = tempfile::tempdir().unwrap();
    std::fs::write(
        dir.path().join("mcptest.yaml"),
        "servers:\n  api:\n    url: https://mcp.example.com/v1\n",
    )
    .unwrap();
    mcptest()
        .current_dir(dir.path())
        .args(["doctor", "--probe"])
        .assert()
        .success();
}

#[test]
fn doctor_preflight_fails_on_missing_mtls_cert() {
    let dir = tempfile::tempdir().unwrap();
    config_with_mtls(
        dir.path(),
        "/definitely/missing/cert.pem",
        "/definitely/missing/key.pem",
    );
    mcptest()
        .current_dir(dir.path())
        .args(["doctor"])
        .assert()
        .failure()
        .stdout(predicate::str::contains("auth pre-flight"))
        .stdout(predicate::str::contains("FAIL"));
}
