//! First-run integration tests for WOR-1350 and WOR-1352.
//!
//! `mcptest init` scaffolds `mcptest.yml`, but the run-shaped commands
//! historically resolved only `./mcptest.yaml` (WOR-1350), and the
//! scaffolded `mcptest.yml` was a settings-only file the suite schema
//! rejected (WOR-1352). These tests pin the shared default resolution
//! (`mcptest.yaml` first, then `mcptest.yml`, hard error when both exist)
//! and that the init-then-bare-run first contact passes offline.

use assert_cmd::Command;
use tempfile::tempdir;

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

/// Stdout and stderr of a finished assert, concatenated, so assertions do
/// not depend on which stream a diagnostic lands on.
fn combined_output(assertion: &assert_cmd::assert::Assert) -> String {
    let output = assertion.get_output();
    format!(
        "{}{}",
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    )
}

/// PATH with the directory of the compiled `mcptest` binary prepended, so a
/// scaffolded suite whose server command is `["mcptest", "mock", ...]` can
/// spawn the binary under test instead of whatever (if anything) is
/// installed on the host.
fn path_with_mcptest_bin() -> std::ffi::OsString {
    let bin_dir = std::path::Path::new(env!("CARGO_BIN_EXE_mcptest"))
        .parent()
        .expect("binary has a parent dir")
        .to_path_buf();
    let mut paths = vec![bin_dir];
    if let Some(existing) = std::env::var_os("PATH") {
        paths.extend(std::env::split_paths(&existing));
    }
    std::env::join_paths(paths).expect("joinable PATH")
}

#[test]
fn bare_run_after_init_passes_offline() {
    let dir = tempdir().unwrap();
    mcptest()
        .current_dir(dir.path())
        .arg("init")
        .assert()
        .success();
    // WOR-1352: the scaffolded mcptest.yml is a schema-valid suite whose
    // server is `mcptest mock` fed from the scaffolded catalog, so the
    // advertised init-then-run first contact is hermetic: no npx, no
    // network, exit 0 with one passing test.
    let assertion = mcptest()
        .current_dir(dir.path())
        .env("PATH", path_with_mcptest_bin())
        .arg("run")
        .assert()
        .success();
    let output = combined_output(&assertion);
    assert!(
        output.contains("1 passed"),
        "bare run after init should execute the scaffolded suite green:\n{output}"
    );
}

#[test]
fn explicit_run_of_the_scaffolded_example_suite_passes_offline() {
    let dir = tempdir().unwrap();
    mcptest()
        .current_dir(dir.path())
        .arg("init")
        .assert()
        .success();
    // The documented explicit-path variant of first contact must stay as
    // green as the bare run.
    let assertion = mcptest()
        .current_dir(dir.path())
        .env("PATH", path_with_mcptest_bin())
        .args(["run", "tests/example.yaml"])
        .assert()
        .success();
    let output = combined_output(&assertion);
    assert!(
        output.contains("1 passed"),
        "explicit run of tests/example.yaml should pass green:\n{output}"
    );
}

#[test]
fn bare_run_loads_a_suite_named_mcptest_yml() {
    let dir = tempdir().unwrap();
    // A schema-valid suite with zero tests: the run resolves and loads
    // the yml end to end, then reports the empty selection without ever
    // connecting to the (inert) server.
    std::fs::write(
        dir.path().join("mcptest.yml"),
        "servers:\n  inert:\n    command: [\"true\"]\n",
    )
    .unwrap();
    let assertion = mcptest()
        .current_dir(dir.path())
        .args(["run", "--pass-with-no-tests"])
        .assert()
        .success();
    let output = combined_output(&assertion);
    assert!(
        output.contains("0 tests discovered") && output.contains("mcptest.yml"),
        "run should load mcptest.yml and report the empty suite:\n{output}"
    );
}

#[test]
fn zero_test_suite_without_pass_flag_exits_no_tests_selected() {
    let dir = tempdir().unwrap();
    std::fs::write(
        dir.path().join("mcptest.yml"),
        "servers:\n  inert:\n    command: [\"true\"]\n",
    )
    .unwrap();
    // WOR-1351: exit 7 is the pinned "zero tests selected" contract,
    // distinct from a test failure (1).
    mcptest()
        .current_dir(dir.path())
        .arg("run")
        .assert()
        .code(7);
}

#[test]
fn bare_run_without_any_config_names_both_candidates() {
    let dir = tempdir().unwrap();
    let assertion = mcptest()
        .current_dir(dir.path())
        .arg("run")
        .assert()
        .success();
    let output = combined_output(&assertion);
    assert!(
        output.contains("no config at")
            && output.contains("mcptest.yaml")
            && output.contains("mcptest.yml"),
        "the no-config notice should name both accepted file names:\n{output}"
    );
}

#[test]
fn bare_run_errors_when_both_default_configs_exist() {
    let dir = tempdir().unwrap();
    std::fs::write(
        dir.path().join("mcptest.yaml"),
        "servers:\n  a:\n    command: [\"true\"]\n",
    )
    .unwrap();
    std::fs::write(
        dir.path().join("mcptest.yml"),
        "servers:\n  b:\n    command: [\"true\"]\n",
    )
    .unwrap();
    let assertion = mcptest()
        .current_dir(dir.path())
        .arg("run")
        .assert()
        .code(2);
    let stderr = String::from_utf8_lossy(&assertion.get_output().stderr).to_string();
    assert!(
        stderr.contains("mcptest.yaml") && stderr.contains("mcptest.yml"),
        "the ambiguity error must name both files:\n{stderr}"
    );
    assert!(
        stderr.contains("--config"),
        "the ambiguity error should point at the way out:\n{stderr}"
    );
}

#[test]
fn bare_validate_falls_back_to_mcptest_yml() {
    let dir = tempdir().unwrap();
    std::fs::write(
        dir.path().join("mcptest.yml"),
        "servers:\n  inert:\n    command: [\"true\"]\n",
    )
    .unwrap();
    mcptest()
        .current_dir(dir.path())
        .arg("validate")
        .assert()
        .success();
}

#[test]
fn bare_validate_errors_when_both_default_configs_exist() {
    let dir = tempdir().unwrap();
    std::fs::write(
        dir.path().join("mcptest.yaml"),
        "servers:\n  a:\n    command: [\"true\"]\n",
    )
    .unwrap();
    std::fs::write(
        dir.path().join("mcptest.yml"),
        "servers:\n  b:\n    command: [\"true\"]\n",
    )
    .unwrap();
    let assertion = mcptest()
        .current_dir(dir.path())
        .arg("validate")
        .assert()
        .code(2);
    let stderr = String::from_utf8_lossy(&assertion.get_output().stderr).to_string();
    assert!(
        stderr.contains("mcptest.yaml") && stderr.contains("mcptest.yml"),
        "the ambiguity error must name both files:\n{stderr}"
    );
}

#[test]
fn run_against_a_directory_names_the_fix_instead_of_the_os_error() {
    let dir = tempdir().unwrap();
    mcptest()
        .current_dir(dir.path())
        .arg("init")
        .assert()
        .success();
    // WOR-1354: `mcptest run tests/` is not supported. The loader refuses
    // the directory with an actionable message (pass a suite file or
    // --config) instead of the raw "Is a directory (os error 21)" read
    // failure, keeping the invalid-invocation exit code.
    let assertion = mcptest()
        .current_dir(dir.path())
        .args(["run", "tests/"])
        .assert()
        .code(2);
    let output = combined_output(&assertion);
    assert!(
        output.contains("is a directory"),
        "the error must say the path is a directory:\n{output}"
    );
    assert!(
        output.contains("mcptest run tests/example.yaml"),
        "the error must show a working invocation:\n{output}"
    );
    assert!(
        output.contains("--config"),
        "the error must mention --config:\n{output}"
    );
    assert!(
        !output.contains("os error"),
        "the raw OS error must not leak through:\n{output}"
    );
}
