//! `is-valid-tools-call` matcher: the value is a well-formed MCP
//! tool call.
//!
//! Why this exists: the most MCP-relevant deterministic check is "did the
//! model emit a structurally valid tool call?" Per the MCP spec a tool call is
//! an object with a string `name` and an `arguments` object. When the test
//! supplies a JSON Schema for the arguments, we validate `arguments` against
//! it through the shared schema matcher path so a tool's input contract is
//! enforced in one place.

use serde_json::Value;

use super::{schema, AssertionDiff, AssertionResult, MatcherKind};
use crate::{Error, Result};

/// Evaluate `is-valid-tools-call`.
///
/// `argument` is null/absent (shape check only) or `{ schema: {...} }` to also
/// validate the call's `arguments` object against an inline JSON Schema. A
/// missing `name`, a non-string `name`, or a missing/non-object `arguments`
/// each produce a failing result with a pointed note rather than an error,
/// since a malformed call is a legitimate test failure.
pub fn evaluate(argument: &Value, actual: &Value) -> Result<AssertionResult> {
    let Value::Object(call) = actual else {
        return Ok(fail("tool call must be an object", actual));
    };
    match call.get("name") {
        Some(Value::String(_)) => {}
        _ => return Ok(fail("tool call is missing a string `name`", actual)),
    }
    let Some(Value::Object(_)) = call.get("arguments") else {
        return Ok(fail("tool call is missing an `arguments` object", actual));
    };
    match inline_schema(argument)? {
        None => Ok(AssertionResult::passed(MatcherKind::IsValidToolsCall)),
        Some(doc) => {
            let arguments = &call["arguments"];
            let mut result = schema::evaluate(doc, arguments)?;
            result.kind = MatcherKind::IsValidToolsCall;
            Ok(result)
        }
    }
}

/// Build a failing result for a structurally wrong tool call.
fn fail(note: &str, actual: &Value) -> AssertionResult {
    AssertionResult::failed(
        MatcherKind::IsValidToolsCall,
        AssertionDiff {
            path: String::new(),
            expected: None,
            actual: Some(actual.clone()),
            note: Some(note.into()),
        },
    )
}

/// Pull the optional inline arguments schema out of the matcher argument.
fn inline_schema(argument: &Value) -> Result<Option<&Value>> {
    match argument {
        Value::Null => Ok(None),
        Value::Object(map) => match map.get("schema") {
            Some(schema) => Ok(Some(schema)),
            None => Err(Error::InvalidMatcherInput(
                "is-valid-tools-call object argument must carry a `schema` key".into(),
            )),
        },
        _ => Err(Error::InvalidMatcherInput(
            "is-valid-tools-call argument must be null or an object with a `schema` key".into(),
        )),
    }
}

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

    #[test]
    fn is_valid_tools_call_passes_on_well_formed_call() {
        let call = json!({"name": "search", "arguments": {"query": "rust"}});
        let r = evaluate(&Value::Null, &call).unwrap();
        assert!(r.pass, "diffs: {:?}", r.diffs);
        assert_eq!(r.kind, MatcherKind::IsValidToolsCall);
    }

    #[test]
    fn is_valid_tools_call_fails_without_name() {
        let r = evaluate(&Value::Null, &json!({"arguments": {}})).unwrap();
        assert!(!r.pass);
        assert!(r.diffs[0].note.as_deref().unwrap().contains("name"));
    }

    #[test]
    fn is_valid_tools_call_fails_with_non_string_name() {
        let r = evaluate(&Value::Null, &json!({"name": 7, "arguments": {}})).unwrap();
        assert!(!r.pass);
    }

    #[test]
    fn is_valid_tools_call_fails_without_arguments_object() {
        let r = evaluate(&Value::Null, &json!({"name": "x", "arguments": []})).unwrap();
        assert!(!r.pass);
        assert!(r.diffs[0].note.as_deref().unwrap().contains("arguments"));
    }

    #[test]
    fn is_valid_tools_call_fails_when_not_an_object() {
        let r = evaluate(&Value::Null, &json!("search")).unwrap();
        assert!(!r.pass);
    }

    #[test]
    fn is_valid_tools_call_validates_arguments_against_schema() {
        let arg = json!({"schema": {"type": "object", "required": ["query"]}});
        let call = json!({"name": "search", "arguments": {"query": "rust"}});
        let r = evaluate(&arg, &call).unwrap();
        assert!(r.pass, "diffs: {:?}", r.diffs);
    }

    #[test]
    fn is_valid_tools_call_fails_when_arguments_violate_schema() {
        let arg = json!({"schema": {"type": "object", "required": ["query"]}});
        let call = json!({"name": "search", "arguments": {"other": 1}});
        let r = evaluate(&arg, &call).unwrap();
        assert!(!r.pass);
        assert_eq!(r.kind, MatcherKind::IsValidToolsCall);
    }
}
