//! End-to-end tests for `mcptest doctor` dotenv discovery.
//!
//! Covers the happy path (a `.env` exists, doctor lists it) and the
//! null case (no `.env` present, doctor reports it as such), plus the
//! credential-state-specific AUTH layer hints (WOR-1274) against a
//! wiremock endpoint that rejects the probe.

use assert_cmd::Command;
use predicates::prelude::*;
use tempfile::tempdir;
use wiremock::{matchers::method, Mock, MockServer, ResponseTemplate};

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

#[test]
fn doctor_lists_discovered_env_file() {
    let dir = tempdir().unwrap();
    std::fs::write(dir.path().join(".env"), "FOO=bar\n").unwrap();

    mcptest()
        .current_dir(dir.path())
        .arg("doctor")
        .assert()
        .success()
        .stdout(predicate::str::contains("mcptest doctor"))
        .stdout(predicate::str::contains(".env"))
        .stdout(predicate::str::contains("EnvDefault"))
        .stdout(predicate::str::contains("FOO"));
}

#[test]
fn doctor_redacts_values_by_default() {
    let dir = tempdir().unwrap();
    std::fs::write(
        dir.path().join(".env"),
        "MCPTEST_FAKE_TOKEN_FOR_TEST=supersecretvalue\n",
    )
    .unwrap();

    mcptest()
        .current_dir(dir.path())
        .arg("doctor")
        .assert()
        .success()
        .stdout(predicate::str::contains("supersecretvalue").not())
        .stdout(predicate::str::contains("***"));
}

#[test]
fn doctor_reports_no_dotenv_when_directory_is_empty() {
    let dir = tempdir().unwrap();

    mcptest()
        .current_dir(dir.path())
        .arg("doctor")
        .assert()
        .success()
        .stdout(predicate::str::contains("no .env files"));
}

#[test]
fn doctor_token_check_says_skipped_without_a_url_target() {
    // WOR-1422: with no --url the live tools/list check cannot run, so doctor
    // must say it was skipped, not that the feature is unwired.
    let dir = tempdir().unwrap();
    mcptest()
        .current_dir(dir.path())
        .arg("doctor")
        .assert()
        .success()
        .stdout(predicate::str::contains(
            "tool catalog token check skipped: no --url target",
        ))
        .stdout(predicate::str::contains("not yet wired").not());
}

#[test]
fn doctor_token_check_can_be_disabled() {
    let dir = tempdir().unwrap();
    mcptest()
        .current_dir(dir.path())
        .args(["doctor", "--no-tool-tokens"])
        .assert()
        .success()
        .stdout(predicate::str::contains(
            "tool catalog token check disabled",
        ));
}

/// Stand up an HTTP stub that answers every POST with `status`, mimicking
/// a server that rejects the doctor's `initialize` probe at the auth layer.
async fn start_rejecting_server(status: u16) -> MockServer {
    let server = MockServer::start().await;
    Mock::given(method("POST"))
        .respond_with(
            ResponseTemplate::new(status)
                .insert_header("WWW-Authenticate", "Bearer realm=\"mcp\"")
                .set_body_string("rejected"),
        )
        .mount(&server)
        .await;
    server
}

#[tokio::test(flavor = "multi_thread")]
async fn doctor_url_401_without_credentials_hints_the_flag_and_login() {
    let server = start_rejecting_server(401).await;
    let dir = tempdir().unwrap();
    mcptest()
        .current_dir(dir.path())
        .args(["doctor", "--url", &server.uri()])
        .assert()
        .code(1)
        .stdout(
            predicate::str::contains("[AUTH] FAIL")
                .and(predicate::str::contains("no credentials were supplied"))
                .and(predicate::str::contains("--bearer-token-env"))
                .and(predicate::str::contains("mcptest login")),
        );
}

#[tokio::test(flavor = "multi_thread")]
async fn doctor_url_401_with_an_unset_env_var_names_the_var() {
    let server = start_rejecting_server(401).await;
    let dir = tempdir().unwrap();
    mcptest()
        .current_dir(dir.path())
        .env_remove("WOR1274_DOCTOR_UNSET")
        .args([
            "doctor",
            "--url",
            &server.uri(),
            "--bearer-token-env",
            "WOR1274_DOCTOR_UNSET",
        ])
        .assert()
        .code(1)
        .stdout(
            predicate::str::contains("`WOR1274_DOCTOR_UNSET`")
                .and(predicate::str::contains("not set or is empty")),
        );
}

#[tokio::test(flavor = "multi_thread")]
async fn doctor_url_401_with_a_rejected_token_hints_re_authentication() {
    let server = start_rejecting_server(401).await;
    let dir = tempdir().unwrap();
    mcptest()
        .current_dir(dir.path())
        .env("WOR1274_DOCTOR_REJECTED", "wrong-token")
        .args([
            "doctor",
            "--url",
            &server.uri(),
            "--bearer-token-env",
            "WOR1274_DOCTOR_REJECTED",
        ])
        .assert()
        .code(1)
        .stdout(
            predicate::str::contains("`WOR1274_DOCTOR_REJECTED`")
                .and(predicate::str::contains("rejected (HTTP 401)"))
                .and(predicate::str::contains("mcptest login")),
        );
}

#[tokio::test(flavor = "multi_thread")]
async fn doctor_url_403_with_a_token_hints_insufficient_scope() {
    let server = start_rejecting_server(403).await;
    let dir = tempdir().unwrap();
    mcptest()
        .current_dir(dir.path())
        .env("WOR1274_DOCTOR_FORBIDDEN", "low-priv-token")
        .args([
            "doctor",
            "--url",
            &server.uri(),
            "--bearer-token-env",
            "WOR1274_DOCTOR_FORBIDDEN",
        ])
        .assert()
        .code(1)
        .stdout(
            predicate::str::contains("lacks permission (HTTP 403)")
                .and(predicate::str::contains("scope or audience")),
        );
}

#[tokio::test(flavor = "multi_thread")]
async fn doctor_url_401_with_an_expired_cached_login_hints_refresh() {
    use mcptest_core::auth::{Token, TokenCache};
    let server = start_rejecting_server(401).await;
    let dir = tempdir().unwrap();
    let auth_dir = tempdir().unwrap();
    // Seed the login cache with a token for this exact URL that is long
    // expired, the state a stale `mcptest login` leaves behind.
    TokenCache::with_root(auth_dir.path())
        .write(
            &server.uri(),
            &Token {
                access_token: "stale".into(),
                refresh_token: None,
                expires_at_unix: 1,
                token_type: "Bearer".into(),
                scope: None,
                last_refreshed: "2020-01-01T00:00:00Z".into(),
                token_endpoint: None,
                client_id: None,
            },
        )
        .unwrap();
    mcptest()
        .current_dir(dir.path())
        .env("MCPTEST_AUTH_DIR", auth_dir.path())
        .args(["doctor", "--url", &server.uri()])
        .assert()
        .code(1)
        .stdout(
            predicate::str::contains("cached OAuth token")
                .and(predicate::str::contains("has expired"))
                .and(predicate::str::contains("mcptest login")),
        );
}
