//! Integration tests for `mcptest validate`.
//!
//! Covers the four required paths in the ticket: valid file passes, schema
//! violation fails, broken `${VAR}` reference fails, missing import fails.
//! A fifth test pins the "all errors reported in one pass" requirement.

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

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

fn write(path: &std::path::Path, body: &str) {
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent).unwrap();
    }
    std::fs::write(path, body).unwrap();
}

#[test]
fn validate_returns_zero_for_valid_file() {
    let dir = tempdir().unwrap();
    let cfg = dir.path().join("mcptest.yaml");
    write(
        &cfg,
        "servers:\n  main:\n    command: [\"echo\", \"hi\"]\ntools: []\n",
    );
    mcptest()
        .args(["--config", cfg.to_str().unwrap(), "validate"])
        .assert()
        .success()
        .stdout(predicate::str::contains("ok"));
}

#[test]
fn validate_reports_schema_violation_with_exit_two() {
    let dir = tempdir().unwrap();
    let cfg = dir.path().join("mcptest.yaml");
    write(&cfg, "serverz:\n  main:\n    command: [\"echo\", \"hi\"]\n");
    let assertion = mcptest()
        .args(["--config", cfg.to_str().unwrap(), "validate"])
        .assert()
        .code(2);
    let stdout = String::from_utf8(assertion.get_output().stdout.clone()).unwrap();
    assert!(stdout.contains("[schema]"));
}

#[test]
fn validate_reports_unresolved_var_clearly() {
    let dir = tempdir().unwrap();
    let cfg = dir.path().join("mcptest.yaml");
    write(
        &cfg,
        "servers:\n  main:\n    command: [\"echo\", \"${MUST_BE_UNSET:?}\"]\n",
    );
    let assertion = mcptest()
        .args(["--config", cfg.to_str().unwrap(), "validate"])
        .env_remove("MUST_BE_UNSET")
        .assert()
        .code(2);
    let stdout = String::from_utf8(assertion.get_output().stdout.clone()).unwrap();
    assert!(
        stdout.contains("[vars]") || stdout.contains("MUST_BE_UNSET"),
        "expected vars finding in stdout, got: {stdout}"
    );
}

#[test]
fn validate_reports_missing_import_clearly() {
    let dir = tempdir().unwrap();
    let cfg = dir.path().join("mcptest.yaml");
    write(
        &cfg,
        "imports:\n  - missing.yml\nservers:\n  main:\n    command: [\"echo\", \"hi\"]\n",
    );
    let assertion = mcptest()
        .args(["--config", cfg.to_str().unwrap(), "validate"])
        .assert()
        .code(2);
    let stdout = String::from_utf8(assertion.get_output().stdout.clone()).unwrap();
    assert!(
        stdout.contains("[imports]")
            || stdout.contains("missing.yml")
            || stdout.contains("not readable"),
        "expected imports finding in stdout, got: {stdout}"
    );
}

#[test]
fn validate_emits_structured_json_when_machine_format_requested() {
    let dir = tempdir().unwrap();
    let cfg = dir.path().join("mcptest.yaml");
    write(&cfg, "serverz:\n  main:\n    command: [\"echo\", \"hi\"]\n");
    let assertion = mcptest()
        .args([
            "--config",
            cfg.to_str().unwrap(),
            "validate",
            "--format",
            "json",
        ])
        .assert()
        .code(2);
    let stdout = String::from_utf8(assertion.get_output().stdout.clone()).unwrap();
    let doc: serde_json::Value = serde_json::from_str(stdout.trim()).unwrap();
    assert_eq!(doc["valid"], false);
    let errors = doc["errors"].as_array().unwrap();
    assert!(!errors.is_empty());
    // The typoed key carries a did-you-mean hint pointing at the fix
    // (`server` or `servers`; both are within edit distance 1).
    let hinted = errors
        .iter()
        .find(|e| e["hint"].is_string())
        .unwrap_or_else(|| panic!("expected a hinted error, got: {errors:?}"));
    assert!(hinted["hint"].as_str().unwrap().contains("server"));
    assert!(hinted["path"].is_string());
    assert!(hinted["message"].as_str().is_some_and(|m| !m.is_empty()));
}

#[test]
fn validate_json_mode_reports_valid_true_with_empty_errors() {
    let dir = tempdir().unwrap();
    let cfg = dir.path().join("mcptest.yaml");
    write(
        &cfg,
        "servers:\n  main:\n    command: [\"echo\", \"hi\"]\ntools: []\n",
    );
    let assertion = mcptest()
        .args([
            "--config",
            cfg.to_str().unwrap(),
            "validate",
            "--format",
            "json",
        ])
        .assert()
        .success();
    let stdout = String::from_utf8(assertion.get_output().stdout.clone()).unwrap();
    let doc: serde_json::Value = serde_json::from_str(stdout.trim()).unwrap();
    assert_eq!(doc["valid"], true);
    assert!(doc["errors"].as_array().unwrap().is_empty());
}

#[test]
fn validate_human_output_appends_did_you_mean_hint() {
    let dir = tempdir().unwrap();
    let cfg = dir.path().join("mcptest.yaml");
    write(&cfg, "serverz:\n  main:\n    command: [\"echo\", \"hi\"]\n");
    let assertion = mcptest()
        .args(["--config", cfg.to_str().unwrap(), "validate"])
        .assert()
        .code(2);
    let stdout = String::from_utf8(assertion.get_output().stdout.clone()).unwrap();
    assert!(
        stdout.contains("did you mean"),
        "expected a hint in human output, got: {stdout}"
    );
}

#[test]
fn validate_reports_all_errors_in_one_pass() {
    let dir = tempdir().unwrap();
    let cfg = dir.path().join("mcptest.yaml");
    // Two schema mistakes: typoed top-level key plus extra unknown field.
    write(
        &cfg,
        "serverz:\n  main:\n    command: [\"echo\", \"hi\"]\nunknown_top_key: 1\n",
    );
    let assertion = mcptest()
        .args(["--config", cfg.to_str().unwrap(), "validate"])
        .assert()
        .code(2);
    let stdout = String::from_utf8(assertion.get_output().stdout.clone()).unwrap();
    // The first line is "<path>: N issue(s)"; the body lists every finding.
    // We assert on multiple findings (>= 2) rather than a specific count so
    // the test does not break when the loader gains hints.
    let issue_lines = stdout.lines().filter(|l| l.starts_with('[')).count();
    assert!(
        issue_lines >= 2,
        "expected at least 2 findings, got {issue_lines}: {stdout}"
    );
}
