//! Export a mcptest agent suite into a neutral, portable eval dataset.
//!
//! The portable shape is mcptest's own interchange model: enough of a case to
//! round-trip into another ecosystem (a prompt, the target servers, and a
//! single output expectation) plus provenance saying mcptest generated it. It
//! is deliberately small; lossy fields are documented, not hidden.

use serde::{Deserialize, Serialize};
use serde_yaml::Value as Yaml;

/// Schema id stamped on every exported dataset.
pub const INTEROP_SCHEMA_VERSION: &str = "mcptest.sh/interop/v1";

/// One output expectation in the portable shape.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PortableExpect {
    /// The matcher kind (`contains`, `regex`, `equals`).
    pub kind: String,
    /// The expected value.
    pub value: String,
}

/// One portable eval case.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PortableCase {
    /// Case name.
    pub name: String,
    /// The user-turn prompt the agent is given.
    pub prompt: String,
    /// The target servers the case runs against.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub servers: Vec<String>,
    /// A single output expectation, when the suite declared one.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub expect: Option<PortableExpect>,
}

/// A portable dataset: provenance plus the cases.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PortableDataset {
    /// Schema id (see [`INTEROP_SCHEMA_VERSION`]).
    pub schema_version: String,
    /// Always `mcptest`: the artifact says where it came from.
    pub generated_by: String,
    /// The source suite path or label.
    pub source: String,
    /// The exported cases.
    pub cases: Vec<PortableCase>,
}

/// Convert a parsed mcptest suite (`agents:` shape) into a portable dataset.
///
/// Reads each agent's name, prompt, servers, and first expectation. Agents with
/// no prompt are skipped. Returns the dataset and a list of skipped agent names.
pub fn export_suite(suite: &Yaml, source: &str) -> (PortableDataset, Vec<String>) {
    let mut cases = Vec::new();
    let mut skipped = Vec::new();
    let agents = suite
        .get("agents")
        .and_then(Yaml::as_sequence)
        .cloned()
        .unwrap_or_default();
    for (idx, agent) in agents.iter().enumerate() {
        match agent_to_case(agent, idx) {
            Some(case) => cases.push(case),
            None => {
                skipped.push(yaml_str(agent, "name").unwrap_or_else(|| format!("#{}", idx + 1)))
            }
        }
    }
    let dataset = PortableDataset {
        schema_version: INTEROP_SCHEMA_VERSION.to_string(),
        generated_by: "mcptest".to_string(),
        source: source.to_string(),
        cases,
    };
    (dataset, skipped)
}

/// Map one agent node to a portable case, or `None` when it has no prompt.
fn agent_to_case(agent: &Yaml, idx: usize) -> Option<PortableCase> {
    let prompt = yaml_str(agent, "prompt")
        .or_else(|| yaml_str(agent, "task"))
        .filter(|p| !p.trim().is_empty())?;
    let name = yaml_str(agent, "name").unwrap_or_else(|| format!("case {}", idx + 1));
    let servers = agent
        .get("servers")
        .and_then(Yaml::as_sequence)
        .map(|seq| {
            seq.iter()
                .filter_map(|s| s.as_str().map(str::to_string))
                .collect()
        })
        .unwrap_or_default();
    Some(PortableCase {
        name,
        prompt,
        servers,
        expect: first_expect(agent),
    })
}

/// Pull the first expectation off an agent's `expect:` list, if any.
fn first_expect(agent: &Yaml) -> Option<PortableExpect> {
    let first = agent
        .get("expect")
        .and_then(Yaml::as_sequence)
        .and_then(|seq| seq.first())?;
    let matcher = first.get("matcher")?;
    for kind in ["contains", "regex", "equals", "exact"] {
        if let Some(value) = matcher.get(kind).and_then(Yaml::as_str) {
            let kind = if kind == "exact" { "equals" } else { kind };
            return Some(PortableExpect {
                kind: kind.to_string(),
                value: value.to_string(),
            });
        }
    }
    None
}

/// Read a string field off a YAML mapping node.
fn yaml_str(node: &Yaml, key: &str) -> Option<String> {
    node.get(key).and_then(Yaml::as_str).map(str::to_string)
}
