//! `mcptest generate {conformance,redteam,eval}` coverage scaffolders.
//!
//! `generate suite` covers tool behavior. These three cover the other layers an
//! author needs for complete coverage: protocol conformance, behavioral
//! red-team probes, and agent-loop evals. Each reads a tool/capability catalog
//! from a file (deterministic and CI-friendly, like `generate suite`), invokes
//! the matching pure renderer in [`mcptest_core::stub_gen`], and routes the
//! result to stdout or a file. The live `tools/list` path is the agent verbs
//! (`scaffold_conformance`, `scaffold_redteam`, `scaffold_eval`).

use std::fs;
use std::path::{Path, PathBuf};

use anyhow::{Context, Result};
use mcptest_core::stub_gen::{
    render_conformance_suite, render_eval_suite, render_redteam_suite, ConformanceOptions,
    EvalOptions, RedteamOptions, DEFAULT_SPEC_VERSION,
};
use serde_json::{json, Value};

use super::generate_suite::{read_config_value, read_tools};

/// Arguments for `mcptest generate conformance`.
#[derive(Debug, Clone)]
pub struct GenerateConformanceArgs {
    /// Catalog/snapshot file the advertised capabilities are read from.
    pub from_config: PathBuf,
    /// Server key the generated checks reference.
    pub server_name: String,
    /// MCP spec revision to pin. `None` uses [`DEFAULT_SPEC_VERSION`].
    pub spec_version: Option<String>,
    /// Write the suite here instead of stdout.
    pub output: Option<PathBuf>,
    /// Suppress progress lines. Honors the global `--quiet` flag.
    pub quiet: bool,
}

/// Arguments for `mcptest generate eval`.
#[derive(Debug, Clone)]
pub struct GenerateEvalArgs {
    /// Catalog/snapshot file the tool list is read from.
    pub from_config: PathBuf,
    /// Server key the generated agent tests reference.
    pub server_name: String,
    /// Model identifier for each agent test. `None` uses the renderer default.
    pub model: Option<String>,
    /// Write the suite here instead of stdout.
    pub output: Option<PathBuf>,
    /// Suppress progress lines.
    pub quiet: bool,
}

/// Arguments for `mcptest generate redteam`.
#[derive(Debug, Clone)]
pub struct GenerateRedteamArgs {
    /// Catalog/snapshot file the tool list is read from.
    pub from_config: PathBuf,
    /// Server key the generated probes reference.
    pub server_name: String,
    /// Model identifier for each probe. `None` uses the renderer default.
    pub model: Option<String>,
    /// Write the suite here instead of stdout.
    pub output: Option<PathBuf>,
    /// Suppress progress lines.
    pub quiet: bool,
}

/// Render a conformance suite from a server's advertised capabilities.
pub fn run_conformance(args: GenerateConformanceArgs) -> Result<i32> {
    let caps = match read_capabilities(&args.from_config) {
        Ok(c) => c,
        Err(err) => {
            eprintln!("mcptest generate conformance: {err:#}");
            return Ok(1);
        }
    };
    let opts = ConformanceOptions {
        server_name: args.server_name.clone(),
        spec_version: args
            .spec_version
            .clone()
            .unwrap_or_else(|| DEFAULT_SPEC_VERSION.to_string()),
    };
    emit(
        &render_conformance_suite(&caps, &opts),
        args.output.as_deref(),
        args.quiet,
    )
}

/// Render an agent-loop eval suite from a server's tool catalog.
pub fn run_eval(args: GenerateEvalArgs) -> Result<i32> {
    let tools = match read_tools(&args.from_config) {
        Ok(t) => t,
        Err(err) => {
            eprintln!("mcptest generate eval: {err:#}");
            return Ok(1);
        }
    };
    let opts = EvalOptions {
        server_name: args.server_name.clone(),
        model: args
            .model
            .clone()
            .unwrap_or_else(|| mcptest_core::stub_gen::DEFAULT_EVAL_MODEL.to_string()),
    };
    emit(
        &render_eval_suite(&tools, &opts),
        args.output.as_deref(),
        args.quiet,
    )
}

/// Render a behavioral red-team suite from a server's tool catalog.
pub fn run_redteam(args: GenerateRedteamArgs) -> Result<i32> {
    let tools = match read_tools(&args.from_config) {
        Ok(t) => t,
        Err(err) => {
            eprintln!("mcptest generate redteam: {err:#}");
            return Ok(1);
        }
    };
    let opts = RedteamOptions {
        server_name: args.server_name.clone(),
        model: args
            .model
            .clone()
            .unwrap_or_else(|| mcptest_core::stub_gen::DEFAULT_REDTEAM_MODEL.to_string()),
    };
    emit(
        &render_redteam_suite(&tools, &opts),
        args.output.as_deref(),
        args.quiet,
    )
}

/// Write the generated suite to a file or stdout.
fn emit(generated: &str, output: Option<&Path>, quiet: bool) -> Result<i32> {
    if let Some(path) = output {
        fs::write(path, generated)
            .with_context(|| format!("failed to write {}", path.display()))?;
        if !quiet {
            println!("Generated {}", path.display());
        }
    } else {
        print!("{generated}");
    }
    Ok(0)
}

/// Resolve the server's advertised capabilities from a catalog/snapshot file.
///
/// Prefers an explicit `capabilities` object (top-level or under `result`, as
/// an `initialize` snapshot carries it). When none is present, infers the set
/// from which catalogs the file declares: a `tools[]`, `resources[]`, or
/// `prompts[]` array (top-level or under `mock_server`) advertises that
/// capability. The conformance renderer needs only which capabilities exist.
fn read_capabilities(path: &Path) -> Result<Value> {
    let value = read_config_value(path)?;
    if let Some(caps) = explicit_capabilities(&value) {
        return Ok(caps);
    }
    let mut caps = serde_json::Map::new();
    for key in ["tools", "resources", "prompts"] {
        if declares_catalog(&value, key) {
            caps.insert(key.to_string(), json!({}));
        }
    }
    Ok(Value::Object(caps))
}

/// Read an explicit `capabilities` object from the snapshot, if present.
fn explicit_capabilities(value: &Value) -> Option<Value> {
    if let Some(caps) = value.get("capabilities").filter(|c| c.is_object()) {
        return Some(caps.clone());
    }
    value
        .get("result")
        .and_then(|r| r.get("capabilities"))
        .filter(|c| c.is_object())
        .cloned()
}

/// True when the file declares a `<key>[]` catalog at the top level or under
/// `mock_server`, in either case signaling the server has that capability.
fn declares_catalog(value: &Value, key: &str) -> bool {
    value.get(key).and_then(Value::as_array).is_some()
        || value
            .get("mock_server")
            .and_then(|m| m.get(key))
            .and_then(Value::as_array)
            .is_some()
}

#[cfg(test)]
mod tests {
    use super::*;
    use tempfile::tempdir;

    fn write(dir: &Path, name: &str, body: &str) -> PathBuf {
        let p = dir.join(name);
        fs::write(&p, body).unwrap();
        p
    }

    #[test]
    fn infers_capabilities_from_declared_catalogs() {
        let dir = tempdir().unwrap();
        let p = write(
            dir.path(),
            "snap.json",
            r#"{"tools":[{"name":"a"}],"resources":[{"uri":"x"}]}"#,
        );
        let caps = read_capabilities(&p).unwrap();
        assert!(caps.get("tools").is_some());
        assert!(caps.get("resources").is_some());
        assert!(caps.get("prompts").is_none());
    }

    #[test]
    fn prefers_explicit_capabilities_object() {
        let dir = tempdir().unwrap();
        let p = write(
            dir.path(),
            "init.json",
            r#"{"capabilities":{"prompts":{}},"tools":[{"name":"a"}]}"#,
        );
        let caps = read_capabilities(&p).unwrap();
        // The explicit block wins, so only prompts is reported.
        assert!(caps.get("prompts").is_some());
        assert!(caps.get("tools").is_none());
    }

    #[test]
    fn infers_from_mock_manifest() {
        let dir = tempdir().unwrap();
        let p = write(
            dir.path(),
            "mock.yaml",
            "mock_server:\n  tools:\n    - name: search\n  prompts:\n    - name: greet\n",
        );
        let caps = read_capabilities(&p).unwrap();
        assert!(caps.get("tools").is_some());
        assert!(caps.get("prompts").is_some());
    }

    #[test]
    fn conformance_writes_to_file() {
        let dir = tempdir().unwrap();
        let src = write(dir.path(), "snap.json", r#"{"tools":[{"name":"a"}]}"#);
        let out = dir.path().join("conf.yml");
        let code = run_conformance(GenerateConformanceArgs {
            from_config: src,
            server_name: "server".to_string(),
            spec_version: None,
            output: Some(out.clone()),
            quiet: true,
        })
        .unwrap();
        assert_eq!(code, 0);
        let body = fs::read_to_string(&out).unwrap();
        assert!(body.contains("check: \"tools/list\""));
    }

    #[test]
    fn eval_writes_to_file() {
        let dir = tempdir().unwrap();
        let src = write(
            dir.path(),
            "snap.json",
            r#"{"tools":[{"name":"get_weather","description":"weather"}]}"#,
        );
        let out = dir.path().join("eval.yml");
        let code = run_eval(GenerateEvalArgs {
            from_config: src,
            server_name: "server".to_string(),
            model: None,
            output: Some(out.clone()),
            quiet: true,
        })
        .unwrap();
        assert_eq!(code, 0);
        let body = fs::read_to_string(&out).unwrap();
        assert!(body.contains("mcp_task_completion:"));
    }

    #[test]
    fn redteam_writes_to_file() {
        let dir = tempdir().unwrap();
        let src = write(dir.path(), "snap.json", r#"{"tools":[{"name":"run_sql"}]}"#);
        let out = dir.path().join("rt.yml");
        let code = run_redteam(GenerateRedteamArgs {
            from_config: src,
            server_name: "server".to_string(),
            model: None,
            output: Some(out.clone()),
            quiet: true,
        })
        .unwrap();
        assert_eq!(code, 0);
        let body = fs::read_to_string(&out).unwrap();
        assert!(body.contains("resists prompt injection"));
    }

    #[test]
    fn missing_tools_for_eval_is_a_clean_error_exit() {
        let dir = tempdir().unwrap();
        let src = write(dir.path(), "empty.json", r#"{"servers":{}}"#);
        let code = run_eval(GenerateEvalArgs {
            from_config: src,
            server_name: "server".to_string(),
            model: None,
            output: None,
            quiet: true,
        })
        .unwrap();
        assert_eq!(code, 1);
    }
}
