//! Live candidate source for the top-level `evals:` test type.
//!
//! When an eval declares no deterministic `response:`, its candidate is the
//! trace of a live tool-using agent run: the eval's `prompt` is driven against
//! its connected `server`, and the whole run envelope is what the rubric
//! grades (the same target the agent-side `eval.rubric` matcher uses). This is
//! its own module so `commands/eval.rs` stays focused on parsing and grading
//! and the agent-driver dependency is contained here.

use std::sync::Arc;

use anyhow::Result;
use mcptest_agent::{run_agent, AgentConfig, Budget, ServerHandle};
use mcptest_core::eval::provider::LlmProvider;
use serde_json::Value;

use crate::commands::eval::EvalSpec;

/// Drive the eval's `prompt` as a tool-using agent run against `handles` and
/// return the run envelope to grade. Reuses the same agent driver the live
/// `agents:` runner uses, so a top-level eval and an agent test grade an
/// identically shaped candidate. Secrets in a driver error are redacted before
/// the message reaches the caller.
pub(crate) async fn run_live_candidate(
    spec: &EvalSpec,
    handles: &[ServerHandle<'_>],
    provider: &dyn LlmProvider,
    model: &str,
    budget: Arc<Budget>,
) -> Result<Value> {
    let config = AgentConfig {
        model: model.to_string(),
        user_prompt: spec.prompt.clone(),
        ..AgentConfig::default()
    };
    let trace = run_agent(&config, provider, handles, budget)
        .await
        .map_err(|e| {
            anyhow::anyhow!(
                "live agent run for eval `{}` failed: {}",
                spec.name,
                mcptest_agent::redact_secrets(&e.to_string())
            )
        })?;
    Ok(trace.to_envelope())
}

#[cfg(test)]
mod tests {
    use super::*;
    use mcptest_agent::stub::StubProvider;

    fn spec(name: &str, prompt: &str) -> EvalSpec {
        EvalSpec {
            name: name.into(),
            server: "fix".into(),
            prompt: prompt.into(),
            rubric: String::new(),
            rubric_config: None,
            response: None,
            threshold: 0.7,
            judge: None,
            matrix: None,
        }
    }

    #[tokio::test]
    async fn a_run_with_no_servers_is_a_clear_error() {
        // The agent driver requires at least one server; the wrapper surfaces
        // that as an error that names the eval rather than panicking.
        let provider = StubProvider::text("hello");
        let budget = Arc::new(Budget::new(None, None));
        let err = run_live_candidate(
            &spec("needs-a-server", "say hi"),
            &[],
            &provider,
            "stub",
            budget,
        )
        .await
        .expect_err("no servers is an error");
        assert!(
            err.to_string().contains("needs-a-server"),
            "error names the eval: {err}"
        );
    }

    // Builtins-only stdio fixture (no forks) so the handshake stays robust
    // under the gate's parallel load, per the lesson from the matrix-assert
    // flake. Handles initialize and an empty tools/list; the stub model answers
    // in one turn with no tool call, so tools/call is never reached.
    #[cfg(unix)]
    const MINIMAL_SERVER: &str = r##"#!/bin/sh
while IFS= read -r line; do
  case "$line" in
    *'"id":'*) tmp=${line#*'"id":'}; id=${tmp%%[!0-9]*} ;;
    *) id= ;;
  esac
  case "$line" in
    *'"method":"initialize"'*)
      printf '{"jsonrpc":"2.0","id":%s,"result":{"protocolVersion":"2025-03-26","serverInfo":{"name":"fix","version":"0.0.1"},"capabilities":{}}}\n' "$id" ;;
    *'"method":"notifications/initialized"'*) : ;;
    *'"method":"tools/list"'*)
      printf '{"jsonrpc":"2.0","id":%s,"result":{"tools":[]}}\n' "$id" ;;
    *)
      [ -n "$id" ] && printf '{"jsonrpc":"2.0","id":%s,"error":{"code":-32601,"message":"method not found"}}\n' "$id" ;;
  esac
done
"##;

    #[cfg(unix)]
    #[tokio::test]
    async fn drives_the_prompt_against_a_server_and_returns_the_run_envelope() {
        use mcptest_core::protocol::{Client, ProtocolVersion};
        use mcptest_core::transport::{StdioConfig, StdioTransport};
        use std::os::unix::fs::PermissionsExt;
        use std::time::Duration;

        let tmp = tempfile::TempDir::new().unwrap();
        let script = tmp.path().join("server.sh");
        std::fs::write(&script, MINIMAL_SERVER).unwrap();
        let mut perms = std::fs::metadata(&script).unwrap().permissions();
        perms.set_mode(0o755);
        std::fs::set_permissions(&script, perms).unwrap();

        let cfg = StdioConfig {
            command: script.to_string_lossy().into(),
            args: vec![],
            env: Default::default(),
            working_dir: None,
            shutdown_timeout: Duration::from_secs(2),
        };
        let (transport, handle) = StdioTransport::spawn(cfg).expect("spawn fixture");
        let client = Client::builder(transport).build();
        client
            .initialize(ProtocolVersion::V2025_03_26)
            .await
            .expect("initialize");
        // Leak the child handle for the test's duration: dropping it would kill
        // the server before the client finishes calling.
        Box::leak(Box::new(handle));

        let handles = [ServerHandle {
            name: "fix",
            client: &client,
        }];
        let provider = StubProvider::text("hello there");
        let budget = Arc::new(Budget::new(None, None));
        let envelope =
            run_live_candidate(&spec("live", "say hi"), &handles, &provider, "stub", budget)
                .await
                .expect("the run produces an envelope");
        assert_eq!(
            envelope.get("final_response").and_then(Value::as_str),
            Some("hello there")
        );
    }
}
