//! Handler for `mcptest fuzz`: fuzz every tool a server exposes.
//!
//! Connects to the server named by the global `--server-command` or
//! `--server-url` flags, lists its tools, and runs the schema-driven fuzzer
//! ([`mcptest_core::executor::fuzz_tool`]) against each. Exits 1 if any tool
//! crashed, hung, violated the protocol, or leaked.

use std::collections::BTreeMap;

use anyhow::{anyhow, Context, Result};
use serde_json::{json, Value};

use crate::cli::args::FuzzArgs;
use crate::cli::global::GlobalArgs;

/// Entry point for `mcptest fuzz`.
pub(crate) fn fuzz_command(global: &GlobalArgs, args: &FuzzArgs) -> Result<i32> {
    let spec = build_spec(global)?;
    let runtime = tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()
        .context("failed to build tokio runtime")?;
    runtime.block_on(async move { run(spec, args).await })
}

/// Build a one-server spec from the global server-target flags.
fn build_spec(global: &GlobalArgs) -> Result<mcptest_core::suite::ServerSpec> {
    use mcptest_core::suite::ServerSpec;
    if let Some(raw) = &global.server_command {
        let argv = shell_words::split(raw)
            .with_context(|| format!("failed to parse --server-command: {raw}"))?;
        let mut iter = argv.into_iter();
        let command = iter
            .next()
            .ok_or_else(|| anyhow!("--server-command is empty"))?;
        return Ok(ServerSpec::Stdio {
            command,
            args: iter.collect(),
            env: BTreeMap::new(),
        });
    }
    if let Some(url) = &global.server_url {
        let mut headers = BTreeMap::new();
        if let Some(env_name) = &global.server_auth_bearer_env {
            if let Ok(token) = std::env::var(env_name) {
                headers.insert("Authorization".to_string(), format!("Bearer {token}"));
            }
        }
        return Ok(ServerSpec::StreamableHttp {
            url: url.clone(),
            headers,
            oauth: None,
            stateless: false,
        });
    }
    Err(anyhow!(
        "mcptest fuzz needs a target: pass --server-command \"<cmd>\" or --server-url <URL>"
    ))
}

/// Connect, list tools, fuzz each, and return the process exit code.
async fn run(spec: mcptest_core::suite::ServerSpec, args: &FuzzArgs) -> Result<i32> {
    let live = mcptest_core::connector::connect_server(
        "target",
        &spec,
        "mcptest",
        mcptest_core::version::semver(),
        &mcptest_core::network::ProxyConfig::default(),
        &[],
    )
    .await
    .context("connect to server failed")?;
    let client = &live.client;

    let listing = client
        .request("tools/list".to_string(), json!({}))
        .await
        .context("tools/list failed");
    let listing = match listing {
        Ok(value) => value,
        Err(e) => {
            live.shutdown.shutdown().await.ok();
            return Err(e);
        }
    };
    let tools = listing
        .get("tools")
        .and_then(Value::as_array)
        .cloned()
        .unwrap_or_default();

    let mut any_fail = false;
    let mut fuzzed = 0usize;
    for tool in &tools {
        let name = tool.get("name").and_then(Value::as_str).unwrap_or_default();
        if name.is_empty() {
            continue;
        }
        if let Some(filter) = &args.tool {
            if !name.contains(filter.as_str()) {
                continue;
            }
        }
        fuzzed += 1;
        let report = mcptest_core::executor::fuzz_tool(
            client,
            name,
            &json!({}),
            args.seed,
            args.cases,
            args.max_call_ms,
        )
        .await;
        if report.clean() {
            println!("  [ok]   {name}  ({} cases)", report.cases_run);
        } else {
            any_fail = true;
            println!(
                "  [FAIL] {name}  ({} cases): {} crash, {} hang, {} protocol-violation, {} leak",
                report.cases_run,
                report.crashes,
                report.hangs,
                report.protocol_violations,
                report.leaks
            );
            for finding in &report.findings {
                println!("           {finding}");
            }
        }
    }
    live.shutdown.shutdown().await.ok();

    if fuzzed == 0 {
        eprintln!("mcptest fuzz: no tools matched");
        return Ok(2);
    }
    println!(
        "\nfuzzed {fuzzed} tool(s): {}",
        if any_fail { "FAIL" } else { "ok" }
    );
    Ok(if any_fail { 1 } else { 0 })
}
