//! `is-json` matcher: the string value must parse as JSON.
//!
//! Why this exists: MCP tools often hand back a JSON document encoded inside a
//! text content block. Asserting the block is well-formed JSON (and optionally
//! that it validates against an inline schema) is a common, deterministic
//! check that does not belong in a regex.

use serde_json::Value;

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

/// Evaluate `is-json`.
///
/// `argument` is either null/absent (just check parseability) or an object
/// with a `schema` key whose value is an inline JSON Schema. The actual value
/// must be a string; it is parsed, and when a schema is given the parsed
/// document is validated through the shared [`schema`] matcher path so the two
/// matchers cannot diverge.
pub fn evaluate(argument: &Value, actual: &Value) -> Result<AssertionResult> {
    let text = actual.as_str().ok_or_else(|| {
        Error::InvalidMatcherInput("is-json requires a string-typed actual value".into())
    })?;
    let parsed: Value = match serde_json::from_str(text) {
        Ok(v) => v,
        Err(e) => {
            return Ok(AssertionResult::failed(
                MatcherKind::IsJson,
                AssertionDiff {
                    path: String::new(),
                    expected: None,
                    actual: Some(actual.clone()),
                    note: Some(format!("value is not valid JSON: {e}")),
                },
            ));
        }
    };
    match inline_schema(argument)? {
        None => Ok(AssertionResult::passed(MatcherKind::IsJson)),
        Some(doc) => {
            // Reuse the existing schema matcher, then relabel its kind so
            // reporters still group the failure under `is-json`.
            let mut result = schema::evaluate(doc, &parsed)?;
            result.kind = MatcherKind::IsJson;
            Ok(result)
        }
    }
}

/// Pull the optional inline schema out of the argument. Accepts null/absent
/// (no schema) or `{ schema: {...} }`; any other shape is an error.
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-json object argument must carry a `schema` key".into(),
            )),
        },
        _ => Err(Error::InvalidMatcherInput(
            "is-json 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_json_passes_on_valid_json_string() {
        let r = evaluate(&Value::Null, &json!("{\"a\": 1}")).unwrap();
        assert!(r.pass, "diffs: {:?}", r.diffs);
        assert_eq!(r.kind, MatcherKind::IsJson);
    }

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

    #[test]
    fn is_json_rejects_non_string_target() {
        let err = evaluate(&Value::Null, &json!(42)).unwrap_err();
        assert!(matches!(err, Error::InvalidMatcherInput(_)));
    }

    #[test]
    fn is_json_validates_against_inline_schema() {
        let arg = json!({"schema": {"type": "object", "required": ["name"]}});
        let r = evaluate(&arg, &json!("{\"name\": \"alice\"}")).unwrap();
        assert!(r.pass, "diffs: {:?}", r.diffs);
        assert_eq!(r.kind, MatcherKind::IsJson);
    }

    #[test]
    fn is_json_fails_when_parsed_value_violates_schema() {
        let arg = json!({"schema": {"type": "object", "required": ["name"]}});
        let r = evaluate(&arg, &json!("{\"other\": 1}")).unwrap();
        assert!(!r.pass);
        assert_eq!(r.kind, MatcherKind::IsJson);
    }

    #[test]
    fn is_json_object_argument_without_schema_key_errors() {
        let err = evaluate(&json!({"nope": 1}), &json!("{}")).unwrap_err();
        assert!(matches!(err, Error::InvalidMatcherInput(_)));
    }
}
