//! Per-assertion transform runner.
//!
//! A tool or agent assertion may carry a `transform:` command that rewrites
//! the EXTRACTED value before the matcher compares it. This lets a test
//! normalize a volatile or oddly shaped value (strip an id, lowercase a
//! string, reshape an object) so the matcher asserts on the clean form. It
//! is kept out of [`super`] so the executor module stays under the
//! AGENTS module-size ceiling.
//!
//! The transform reuses the shared one-shot subprocess machinery (the value
//! goes to stdin as JSON, the parsed stdout replaces it) through
//! [`crate::transform::run_transform`] with the [`TransformPhase::Assertion`]
//! marker, so it shares the request/response transform's context shape,
//! timeout, and error type. A spawn failure, non-zero exit, timeout, or
//! unparseable stdout fails the assertion with a clear message.

use serde_json::Value;

use crate::suite::Assertion;
use crate::transform::{run_transform, TransformContext, TransformPhase};

/// Apply an assertion's optional transform to the resolved target value.
///
/// Returns the value unchanged when the assertion declares no transform, so
/// the no-transform path is byte-for-byte the same as before. When a
/// transform is present it runs the command on `target` and returns the
/// replacement; a transform error renders as the assertion failure string
/// the evaluator already reports for any failing assertion.
pub(super) async fn apply_assertion_transform(
    target: Value,
    assertion: &Assertion,
    ctx: &TransformContext,
) -> Result<Value, String> {
    let Some(command) = assertion.transform.as_ref() else {
        return Ok(target);
    };
    run_transform(TransformPhase::Assertion, command, &target, ctx)
        .await
        .map_err(|e| e.to_string())
}

#[cfg(unix)]
#[cfg(test)]
mod tests {
    use super::*;
    use crate::suite::{Matcher, TransformCommand};
    use serde_json::json;

    /// Build an assertion carrying the given optional transform command.
    fn asrt(transform: Option<TransformCommand>) -> Assertion {
        Assertion {
            target: "result.value".into(),
            matcher: Matcher::Exact(json!("anything")),
            weight: 1.0,
            name: None,
            transform,
        }
    }

    /// Build a `/bin/sh -c <script>` transform command for the tests.
    fn sh(script: &str) -> TransformCommand {
        TransformCommand {
            program: "/bin/sh".into(),
            args: vec!["-c".into(), script.into()],
        }
    }

    #[tokio::test]
    async fn no_transform_returns_the_value_unchanged() {
        let value = json!({"id": "vol-1", "name": "Echo"});
        let out =
            apply_assertion_transform(value.clone(), &asrt(None), &TransformContext::default())
                .await
                .expect("no-op transform");
        assert_eq!(out, value);
    }

    #[tokio::test]
    async fn transform_replaces_the_extracted_value() {
        // Lowercase a string before the matcher sees it.
        let cmd = sh(r#"cat >/dev/null; printf '"echo"'"#);
        let out = apply_assertion_transform(
            json!("ECHO"),
            &asrt(Some(cmd)),
            &TransformContext::default(),
        )
        .await
        .expect("transform succeeds");
        assert_eq!(out, json!("echo"));
    }

    #[tokio::test]
    async fn transform_error_renders_a_failure_message() {
        let cmd = sh("exit 5");
        let err =
            apply_assertion_transform(json!("x"), &asrt(Some(cmd)), &TransformContext::default())
                .await
                .expect_err("non-zero exit fails the assertion");
        assert!(err.contains("assertion transform"), "{err}");
        assert!(err.contains("exited with 5"), "{err}");
    }

    #[tokio::test]
    async fn transform_sees_the_assertion_phase_marker() {
        // The script reflects the phase env var back as the new value.
        let cmd = sh(r#"cat >/dev/null; printf '"%s"' "$MCPTEST_TRANSFORM_PHASE""#);
        let out = apply_assertion_transform(
            json!("ignored"),
            &asrt(Some(cmd)),
            &TransformContext::default(),
        )
        .await
        .expect("transform succeeds");
        assert_eq!(out, json!("assertion"));
    }
}
