//! Execute and evaluate `compositions:` for `mcptest run`.
//!
//! The composition DAG engine lives in `mcptest_core::composition`. This
//! handler connects the suite's servers, runs each composition (once, or once
//! per `cases:` row), evaluates its deterministic `expect:` block through
//! [`mcptest_core::composition::evaluate_expect`], prints one row per
//! composition or case, and folds the result into the run's exit code.
//!
//! Judged `eval:` metrics need a model provider and are reported as deferred
//! rather than gated here, matching how key-free `evals:` defer.

use std::sync::Arc;

use anyhow::Result;
use mcptest_core::composition::{self, CompositionOutcome};
use mcptest_core::connector::ServerPool;
use mcptest_core::suite::ResolvedSuite;

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

/// Run and evaluate every composition in the suite. Returns `0` when every
/// composition's `expect:` block passed, `1` otherwise.
pub(crate) async fn run_compositions(
    suite: &ResolvedSuite,
    proxy: &mcptest_core::network::ProxyConfig,
    args: &RunArgs,
    quiet: bool,
) -> Result<i32> {
    let pool = Arc::new(ServerPool::new());
    // Best-effort connect: mocked nodes short-circuit before any transport, so
    // a connection failure must not abort an otherwise offline composition
    // suite. A live node with no mock will then surface its own error and fail
    // the `composition.ran` expectation, which is the right signal.
    if let Err(e) =
        super::run_live_connect::connect_referenced_servers(suite, proxy, args, &pool).await
    {
        if !quiet {
            eprintln!(
                "note: server connect failed ({e}); composition nodes without a mock will error"
            );
        }
    }
    // Single-server shorthand: when the suite declares exactly one server,
    // nodes omit `server:` and route to it.
    let default_server = if suite.servers.len() == 1 {
        suite.servers.keys().next().cloned()
    } else {
        None
    };

    let mut outcomes: Vec<CompositionOutcome> = Vec::new();
    for comp in &suite.compositions {
        if comp.cases.is_empty() {
            let trace = composition::run(comp, pool.as_ref(), default_server.as_deref()).await;
            outcomes.push(composition::evaluate_expect(
                comp.name.clone(),
                &trace,
                &comp.expect,
            ));
        } else {
            // Each case runs the DAG with its `vars` bound. Both the
            // composition's top-level `expect:` and the case's own `expect:`
            // apply to that case's trace.
            for ct in composition::run_cases(comp, pool.as_ref(), default_server.as_deref()).await {
                let mut merged = comp.expect.clone();
                merged.extend(ct.expect.iter().cloned());
                let name = format!("{} [{}]", comp.name, ct.name);
                outcomes.push(composition::evaluate_expect(name, &ct.trace, &merged));
            }
        }
        if !comp.eval.is_empty() && !quiet {
            println!(
                "composition `{}` eval: deferred (judged metrics need a model provider)",
                comp.name
            );
        }
    }

    let failed = outcomes.iter().filter(|o| !o.passed).count();
    if !quiet {
        for o in &outcomes {
            if o.passed {
                println!("composition [PASS] {}", o.name);
            } else {
                println!("composition [FAIL] {}", o.name);
                for f in &o.failures {
                    println!("    {f}");
                }
            }
        }
        let passed = outcomes.len() - failed;
        println!(
            "ran {} composition(s): {passed} passed, {failed} failed",
            outcomes.len()
        );
    }
    Ok(i32::from(failed != 0))
}
