//! Per-run security gates for the live agent runner.
//!
//! Folds the suite's top-level `security:` assertions into the agent run as a
//! PASS/FAIL report row. Today that is the cross-server trust boundary
//! (WOR-1423): after a cell's runs finish, extract every candidate cross-server
//! flow from each run envelope, run the deterministic
//! [`mcptest_core::security::cross_server_trust_findings`] detector against the
//! suite's [`mcptest_core::security::TrustBoundaryAssertion`], and reduce the
//! findings to one [`CheckOutcome`]. Split out of `run_live_agents.rs` so that
//! module stays under the production-module-size guard.

use mcptest_core::checks::CheckOutcome;
use mcptest_core::security::{
    cross_server_trust_findings, extract_cross_server_flows, SecurityFinding,
};
use mcptest_core::suite::SecurityAssertions;

/// Run the cross-server trust-boundary check over a cell's envelopes and record
/// its PASS/FAIL outcome on the same report-row vec the other agent gates use
/// (`acc.f1_outcomes`). No-op when the suite declares no `security.trust_boundary`
/// block. Prints a one-line verdict unless `quiet`. Called from
/// [`super::run_live_agents::dispatch_agent_cell`]; lives here so that module
/// stays under the production-module-size guard.
pub(super) fn push_trust_boundary_outcome(
    suite: &mcptest_core::suite::ResolvedSuite,
    case_name: &str,
    quiet: bool,
    envelopes: &[serde_json::Value],
    acc: &mut super::run_live_agents::CellAcc<'_>,
) {
    let Some(outcome) = trust_boundary_outcome(suite.security.as_ref(), envelopes, case_name)
    else {
        return;
    };
    if !quiet {
        let verdict = if outcome.passed { "PASS" } else { "FAIL" };
        let note = outcome.failure.as_deref().unwrap_or("no unbounded flows");
        println!("trust-boundary [{verdict}] {case_name}: {note}");
    }
    acc.f1_outcomes.push(outcome);
}

/// Build the trust-boundary PASS/FAIL outcome for one cell's run envelopes.
///
/// Returns `None` when the suite declares no `security.trust_boundary` block, so
/// a suite without the assertion gets no extra report row and the legacy path is
/// unchanged. When the block is present, every run envelope is scanned for
/// cross-server flows and the suite's assertion clears the allow-listed or
/// sanitized ones; any remaining SEC-060 flow fails the outcome with a one-line
/// summary of the offending flows' evidence.
pub(super) fn trust_boundary_outcome(
    security: Option<&SecurityAssertions>,
    envelopes: &[serde_json::Value],
    case_name: &str,
) -> Option<CheckOutcome> {
    let assertion = security?.trust_boundary.as_ref()?;
    let mut findings: Vec<SecurityFinding> = Vec::new();
    for envelope in envelopes {
        let flows = extract_cross_server_flows(envelope);
        findings.extend(cross_server_trust_findings(&flows, assertion));
    }
    let name = format!("{case_name} (trust-boundary)");
    if findings.is_empty() {
        return Some(CheckOutcome {
            name,
            passed: true,
            failure: None,
        });
    }
    // One line listing the unbounded flows so the report row says which flow
    // crossed an undeclared boundary without dumping the full payloads.
    let summary = findings
        .iter()
        .map(|f| f.evidence.clone())
        .collect::<Vec<_>>()
        .join("; ");
    Some(CheckOutcome {
        name,
        passed: false,
        failure: Some(format!(
            "{} unbounded cross-server flow(s): {summary}",
            findings.len()
        )),
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use mcptest_core::security::TrustBoundaryAssertion;
    use serde_json::json;

    fn flow_envelope() -> serde_json::Value {
        json!({
            "tool_calls": [
                { "name": "fetch", "server": "alpha", "args": {} },
                { "name": "run_query", "server": "beta", "args": { "query": "payload" } },
            ],
            "tool_results": [
                { "call_index": 0, "server": "alpha", "is_error": false, "result": "payload" },
            ],
        })
    }

    #[test]
    fn no_security_block_yields_no_outcome() {
        assert!(trust_boundary_outcome(None, &[flow_envelope()], "case").is_none());
    }

    #[test]
    fn security_without_trust_boundary_yields_no_outcome() {
        let security = SecurityAssertions {
            trust_boundary: None,
            ..Default::default()
        };
        assert!(trust_boundary_outcome(Some(&security), &[flow_envelope()], "case").is_none());
    }

    #[test]
    fn unbounded_cross_server_flow_fails() {
        let security = SecurityAssertions {
            trust_boundary: Some(TrustBoundaryAssertion::default()),
            ..Default::default()
        };
        let outcome = trust_boundary_outcome(Some(&security), &[flow_envelope()], "case").unwrap();
        assert!(!outcome.passed);
        assert!(outcome.failure.is_some());
        assert_eq!(outcome.name, "case (trust-boundary)");
    }

    #[test]
    fn allow_listed_flow_passes() {
        let security = SecurityAssertions {
            trust_boundary: Some(TrustBoundaryAssertion {
                allowed_flows: vec![("alpha".into(), "beta".into())],
                ..Default::default()
            }),
            ..Default::default()
        };
        let outcome = trust_boundary_outcome(Some(&security), &[flow_envelope()], "case").unwrap();
        assert!(outcome.passed);
        assert!(outcome.failure.is_none());
    }
}
