//! Doc-contract test for the troubleshooting anchors (WOR-1268).
//!
//! Runtime errors append `see: https://mcptest.sh/docs/troubleshooting#<anchor>`
//! lines; the anchors live in `mcptest_core::error_anchors`. These tests pin
//! both ends of that contract: every constant must resolve to a real anchor
//! in docs/troubleshooting.md (an explicit `<a id="...">` tag or a heading
//! under the mdBook anchor transform), and representative error surfaces
//! must actually emit the constant, so a renamed heading, a deleted anchor
//! tag, or a dropped `see:` line fails the gate.

use std::collections::BTreeSet;
use std::path::Path;

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

/// Read docs/troubleshooting.md from the workspace root.
fn troubleshooting_doc() -> String {
    // CARGO_MANIFEST_DIR points at crates/mcptest; two parents up is root.
    let path = Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("../../docs/troubleshooting.md")
        .canonicalize()
        .expect("docs/troubleshooting.md exists");
    std::fs::read_to_string(path).expect("troubleshooting doc is readable")
}

/// Collect explicit `<a id="...">` anchor tags. These are the doc's stable
/// anchor contract (the page says ids survive heading renames).
fn explicit_ids(doc: &str) -> BTreeSet<String> {
    let mut ids = BTreeSet::new();
    for line in doc.lines() {
        let mut rest = line;
        while let Some(start) = rest.find("<a id=\"") {
            let tail = &rest[start + "<a id=\"".len()..];
            if let Some(end) = tail.find('"') {
                ids.insert(tail[..end].to_string());
                rest = &tail[end..];
            } else {
                break;
            }
        }
    }
    ids
}

/// Collect every heading, transformed with the mdBook anchor convention:
/// lowercase, spaces to hyphens, punctuation dropped.
fn heading_anchors(doc: &str) -> BTreeSet<String> {
    doc.lines()
        .filter(|l| l.starts_with('#'))
        .map(|l| mdbook_anchor(l.trim_start_matches('#').trim()))
        .collect()
}

/// Apply the mdBook anchor transform to one heading.
fn mdbook_anchor(heading: &str) -> String {
    let mut out = String::new();
    for c in heading.chars() {
        if c.is_alphanumeric() {
            out.extend(c.to_lowercase());
        } else if c.is_whitespace() {
            out.push('-');
        } else if c == '-' || c == '_' {
            out.push(c);
        }
        // Other punctuation is dropped.
    }
    out
}

#[test]
fn every_error_anchor_resolves_to_a_troubleshooting_entry() {
    let doc = troubleshooting_doc();
    let ids = explicit_ids(&doc);
    let headings = heading_anchors(&doc);
    assert!(!ids.is_empty(), "no <a id> anchors found; wrong file?");
    for url in mcptest_core::error_anchors::ALL {
        let anchor = url
            .strip_prefix(mcptest_core::error_anchors::TROUBLESHOOTING_BASE)
            .and_then(|rest| rest.strip_prefix('#'))
            .unwrap_or_else(|| panic!("`{url}` is not base#fragment shaped"));
        assert!(
            ids.contains(anchor) || headings.contains(anchor),
            "anchor `#{anchor}` has no <a id> tag and no matching heading in \
             docs/troubleshooting.md; the error message would link a dead anchor"
        );
    }
}

// ----- end-to-end reachability for three representative families ----------

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

#[test]
fn validate_failure_ends_with_the_config_validation_link() {
    let dir = tempfile::tempdir().unwrap();
    let cfg = dir.path().join("mcptest.yaml");
    std::fs::write(&cfg, "serverz:\n  main:\n    command: [\"echo\", \"hi\"]\n").unwrap();
    mcptest()
        .args(["--config", cfg.to_str().unwrap(), "validate"])
        .assert()
        .code(2)
        .stdout(predicate::str::contains(format!(
            "see: {}",
            mcptest_core::error_anchors::CONFIG_VALIDATION_FAILED
        )));
}

#[test]
fn unknown_matcher_finding_links_its_troubleshooting_entry() {
    let dir = tempfile::tempdir().unwrap();
    let cfg = dir.path().join("mcptest.yaml");
    std::fs::write(
        &cfg,
        "servers:\n  main:\n    command: [\"echo\", \"hi\"]\ntools:\n  - name: t\n    server: main\n    tool: echo\n    expect:\n      - target: result\n        matcher: { exxact: \"hi\" }\n",
    )
    .unwrap();
    mcptest()
        .args(["--config", cfg.to_str().unwrap(), "validate"])
        .assert()
        .code(2)
        .stdout(predicate::str::contains(format!(
            "see: {}",
            mcptest_core::error_anchors::UNKNOWN_MATCHER
        )));
}

#[test]
fn stdio_spawn_failure_links_its_troubleshooting_entry() {
    let dir = tempfile::tempdir().unwrap();
    let cfg = dir.path().join("mcptest.yaml");
    std::fs::write(
        &cfg,
        "servers:\n  ghost:\n    command: [\"/nonexistent/wor1268-no-such-binary\"]\ntools:\n  - name: t\n    server: ghost\n    tool: echo\n    args: {}\n    expect:\n      - target: result.isError\n        matcher: { exact: false }\n",
    )
    .unwrap();
    mcptest()
        .args(["run", "--config", cfg.to_str().unwrap()])
        .assert()
        .failure()
        .stderr(predicate::str::contains(format!(
            "see: {}",
            mcptest_core::error_anchors::STDIO_SPAWN_FAILED
        )));
}
