//! Integration tests for the `mcptest` binary.
//!
//! These tests exercise the compiled binary through `assert_cmd` so they
//! catch regressions clap's derive macro might introduce. Unit-level parser
//! checks live next to the parser in `src/cli.rs`.

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

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

#[test]
fn prints_version_for_version_flag() {
    mcptest()
        .arg("--version")
        .assert()
        .success()
        .stdout(predicate::str::contains(env!("CARGO_PKG_VERSION")));
}

#[test]
fn help_lists_every_global_flag_and_subcommand() {
    let output = mcptest().arg("--help").assert().success();
    let stdout = String::from_utf8(output.get_output().stdout.clone()).expect("utf8 help");

    for flag in [
        "--no-color",
        "--debug",
        "--verbose",
        "--config",
        "--env-file",
        "--no-env-file",
        "--var",
        "--show-secrets",
        "--filter",
        "--reporter",
        "--output",
        "--parallel",
        "--timeout",
        "--retry",
        "--watch",
        "--server-url",
        "--server-command",
        "--server-auth-bearer-env",
        "--server-config",
        "--wait-for-ready",
        "--quiet",
    ] {
        assert!(stdout.contains(flag), "help missing flag {flag}:\n{stdout}");
    }

    for sub in [
        "run",
        "init",
        "doctor",
        "validate",
        "schema",
        "coverage",
        "report",
        "eval",
        "completions",
    ] {
        assert!(
            stdout.contains(sub),
            "help missing subcommand {sub}:\n{stdout}"
        );
    }
}

#[test]
fn help_groups_subcommands_with_start_here_first_and_hides_exec() {
    // WOR-1332: the 40-command flat list is rendered as task-oriented
    // groups. "Start here" leads, every heading appears in order, and the
    // internal `exec` co-process mode is not listed (it still parses).
    let output = mcptest().arg("--help").assert().success();
    let stdout = String::from_utf8(output.get_output().stdout.clone()).expect("utf8 help");

    let mut last = 0;
    for heading in [
        "Start here:",
        "Agent integration:",
        "Explore a server:",
        "Quality and security:",
        "Reports and artifacts:",
        "Plumbing:",
    ] {
        let pos = stdout
            .find(heading)
            .unwrap_or_else(|| panic!("heading {heading:?} missing:\n{stdout}"));
        assert!(pos > last, "heading {heading:?} out of order:\n{stdout}");
        last = pos;
    }
    assert!(
        !stdout.contains("\n    exec "),
        "exec must not be listed:\n{stdout}"
    );
}

#[test]
fn run_with_no_config_prints_no_config_message_and_exits_zero() {
    //  lit up the runner end to end; absent a config the command
    // tells the operator and exits 0 so scaffolding scripts that probe
    // an empty repo do not spuriously fail. Runs in a tempdir because
    // the bare default would otherwise resolve the repo's checked-in
    // mcptest.yml (WOR-1350).
    let dir = tempfile::tempdir().unwrap();
    mcptest()
        .current_dir(dir.path())
        .arg("run")
        .assert()
        .success()
        .stdout(predicate::str::contains("no config at"));
}

#[test]
fn init_creates_starter_files() {
    let dir = tempfile::tempdir().unwrap();
    mcptest()
        .current_dir(dir.path())
        .arg("init")
        .assert()
        .success()
        .stdout(predicate::str::contains("Next: mcptest run"));
}

#[test]
fn completions_bash_emits_script_mentioning_binary_name() {
    let assertion = mcptest().args(["completions", "bash"]).assert().success();
    let stdout = String::from_utf8(assertion.get_output().stdout.clone()).expect("utf8 script");
    assert!(!stdout.is_empty(), "completions output should not be empty");
    assert!(
        stdout.contains("mcptest"),
        "bash completion should reference the binary name"
    );
}

#[test]
fn unknown_flag_exits_with_clap_error_code() {
    mcptest()
        .arg("--invalid-flag")
        .assert()
        .failure()
        .stderr(predicate::str::contains("--invalid-flag"));
}

#[test]
fn repeatable_var_flag_is_accepted() {
    // Same  transition: with no config present the run command
    // exits 0 and prints the no-config notice.
    let dir = tempfile::tempdir().unwrap();
    mcptest()
        .current_dir(dir.path())
        .args(["--var", "FOO=bar", "--var", "BAZ=qux", "run"])
        .assert()
        .success()
        .stdout(predicate::str::contains("no config at"));
}

#[test]
fn no_color_flag_is_accepted_before_subcommand() {
    let dir = tempfile::tempdir().unwrap();
    mcptest()
        .current_dir(dir.path())
        .args(["--no-color", "run"])
        .assert()
        .success();
}

#[test]
fn timeout_flag_accepts_seconds() {
    let dir = tempfile::tempdir().unwrap();
    mcptest()
        .current_dir(dir.path())
        .args(["--timeout", "5", "run"])
        .assert()
        .success();
}

#[test]
fn timeout_flag_rejects_garbage() {
    mcptest()
        .args(["--timeout", "soon", "run"])
        .assert()
        .failure()
        .stderr(predicate::str::contains("whole number of seconds"));
}
