//! Server pre-connect for the live runner.
//!
//! Pulled out of `run_live` so that module stays under the size cap. Connects
//! every server a suite references (tool tests, agent tests, tool-quality
//! checks) into the pool before the run drives, surfacing a connect failure as
//! a top-level error rather than a per-test failure.

use anyhow::Result;

use crate::cli::args::RunArgs;

/// Pre-resolve every server the suite needs (tool tests plus agent tests),
/// surfacing connect failures as a top-level run error rather than per-test
/// failures. Failing fast here lines up with the operator expectation that a
/// misconfigured server is a setup problem, not a test result.
pub(crate) async fn connect_referenced_servers(
    suite: &mcptest_core::suite::ResolvedSuite,
    proxy: &mcptest_core::network::ProxyConfig,
    args: &RunArgs,
    pool: &mcptest_core::connector::ServerPool,
) -> Result<()> {
    let mut referenced_servers: std::collections::BTreeSet<String> =
        std::collections::BTreeSet::new();
    for t in &suite.tests {
        referenced_servers.insert(t.server.clone());
    }
    for a in &suite.agents {
        for s in &a.servers {
            referenced_servers.insert(s.clone());
        }
    }
    // `tool_quality` checks connect to a server to score its catalog,
    // so their server must be in the pool too.
    for c in &suite.tool_quality {
        referenced_servers.insert(c.server.clone());
    }
    for server_name in &referenced_servers {
        let Some(spec) = suite.servers.get(server_name) else {
            anyhow::bail!(
                "test references unknown server `{server_name}`; declare it under `servers:`"
            );
        };
        let live = mcptest_core::connector::connect_server(
            server_name,
            spec,
            "mcptest",
            mcptest_core::version::semver(),
            proxy,
            &args.server_passthrough,
        )
        .await
        .map_err(|e| connect_failure(server_name, e))?;
        pool.insert(server_name.clone(), live).await;
    }
    Ok(())
}

/// Wrap a connect failure for the top-level run error. A stdio failure gets a
/// trailing `see:` line pointing at the troubleshooting entry for its family
/// (WOR-1268): a server that started and then died during the handshake links
/// the startup-exit entry (WOR-1326), every other stdio failure (missing
/// binary, bad permissions) links the spawn entry. Other transports keep the
/// plain context because their own diagnostic either already carries the
/// right link or belongs to no linked family.
fn connect_failure(server_name: &str, err: mcptest_core::connector::ConnectError) -> anyhow::Error {
    use mcptest_core::connector::ConnectError;
    use mcptest_core::transport::StdioError;
    let anchor = match &err {
        ConnectError::Stdio(StdioError::StartupExit { .. }) => {
            Some(mcptest_core::error_anchors::SERVER_EXITED_DURING_STARTUP)
        }
        ConnectError::Stdio(_) => Some(mcptest_core::error_anchors::STDIO_SPAWN_FAILED),
        _ => None,
    };
    match anchor {
        Some(anchor) => anyhow::anyhow!("connect to `{server_name}` failed: {err}\nsee: {anchor}"),
        None => anyhow::Error::new(err).context(format!("connect to `{server_name}` failed")),
    }
}

#[cfg(test)]
mod tests {
    use super::connect_failure;
    use mcptest_core::connector::ConnectError;
    use mcptest_core::transport::StdioError;

    #[test]
    fn stdio_connect_failure_links_the_spawn_troubleshooting_entry() {
        let err = connect_failure("ghost", ConnectError::Stdio(StdioError::EmptyCommand));
        let rendered = format!("{err:#}");
        assert!(rendered.contains("connect to `ghost` failed"), "{rendered}");
        assert!(
            rendered.contains(&format!(
                "see: {}",
                mcptest_core::error_anchors::STDIO_SPAWN_FAILED
            )),
            "{rendered}"
        );
    }

    #[cfg(unix)]
    #[test]
    fn startup_exit_connect_failure_links_the_startup_exit_entry() {
        use std::os::unix::process::ExitStatusExt;
        let err = connect_failure(
            "dying",
            ConnectError::Stdio(StdioError::StartupExit {
                status: std::process::ExitStatus::from_raw(1 << 8),
                stderr_tail: vec!["Error: Cannot find module '/nope.js'".to_string()],
            }),
        );
        let rendered = format!("{err:#}");
        assert!(rendered.contains("connect to `dying` failed"), "{rendered}");
        assert!(rendered.contains("exit status: 1"), "{rendered}");
        assert!(rendered.contains("Cannot find module"), "{rendered}");
        assert!(
            rendered.contains(&format!(
                "see: {}",
                mcptest_core::error_anchors::SERVER_EXITED_DURING_STARTUP
            )),
            "{rendered}"
        );
    }

    #[test]
    fn non_stdio_connect_failure_carries_no_spawn_link() {
        let err = connect_failure("api", ConnectError::Cassette("unreadable".into()));
        let rendered = format!("{err:#}");
        assert!(rendered.contains("connect to `api` failed"), "{rendered}");
        assert!(
            !rendered.contains(mcptest_core::error_anchors::STDIO_SPAWN_FAILED),
            "{rendered}"
        );
    }
}
