//! `exact` matcher: deep equality with normalized object key ordering.
//!
//! Why this matcher exists: most MCP responses are small structured objects
//! and the cheapest check is "does the response equal what I expect, ignoring
//! the order keys happened to land in." We walk the value recursively so that
//! when a test fails, the reporter can point at the first JSON Pointer where
//! the values diverged, not just say "values differ."

use serde_json::Value;

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

/// Evaluate the `exact` matcher.
///
/// Returns a passing [`AssertionResult`] when `expected` deep-equals `actual`
/// after recursive object-key normalization. On mismatch, returns a failing
/// result whose single [`AssertionDiff`] points at the deepest JSON Pointer
/// where the two trees first diverged.
pub fn evaluate(expected: &Value, actual: &Value) -> Result<AssertionResult> {
    match diff(expected, actual, String::new()) {
        None => Ok(AssertionResult::passed(MatcherKind::Exact)),
        Some(d) => Ok(AssertionResult::failed(MatcherKind::Exact, d)),
    }
}

/// Walk the two values in parallel and return the first divergence.
///
/// The walk is depth-first so that mismatches deep inside an object surface
/// with their full pointer path, which is what the pretty reporter needs to
/// underline the right line in the YAML.
fn diff(expected: &Value, actual: &Value, path: String) -> Option<AssertionDiff> {
    match (expected, actual) {
        (Value::Object(e), Value::Object(a)) => diff_objects(e, a, path),
        (Value::Array(e), Value::Array(a)) => diff_arrays(e, a, path),
        (e, a) if e == a => None,
        (e, a) => Some(AssertionDiff {
            path,
            expected: Some(e.clone()),
            actual: Some(a.clone()),
            note: Some("values are not equal".into()),
        }),
    }
}

fn diff_objects(
    expected: &serde_json::Map<String, Value>,
    actual: &serde_json::Map<String, Value>,
    path: String,
) -> Option<AssertionDiff> {
    // Iterate expected keys in sorted order so the "first" mismatch is
    // deterministic regardless of how the YAML loader ordered keys.
    let mut keys: Vec<&String> = expected.keys().collect();
    keys.sort();
    for key in keys {
        let child_path = format!("{}/{}", path, escape_pointer(key));
        let exp_val = &expected[key];
        match actual.get(key) {
            None => {
                return Some(AssertionDiff {
                    path: child_path,
                    expected: Some(exp_val.clone()),
                    actual: None,
                    note: Some(format!("missing key `{key}` in actual value")),
                });
            }
            Some(act_val) => {
                if let Some(d) = diff(exp_val, act_val, child_path) {
                    return Some(d);
                }
            }
        }
    }
    // Surface extra keys after we know the shared ones match.
    let mut extras: Vec<&String> = actual
        .keys()
        .filter(|k| !expected.contains_key(*k))
        .collect();
    extras.sort();
    if let Some(extra) = extras.first() {
        let child_path = format!("{}/{}", path, escape_pointer(extra));
        return Some(AssertionDiff {
            path: child_path,
            expected: None,
            actual: Some(actual[*extra].clone()),
            note: Some(format!("unexpected key `{extra}` in actual value")),
        });
    }
    None
}

fn diff_arrays(expected: &[Value], actual: &[Value], path: String) -> Option<AssertionDiff> {
    if expected.len() != actual.len() {
        return Some(AssertionDiff {
            path,
            expected: Some(Value::Array(expected.to_vec())),
            actual: Some(Value::Array(actual.to_vec())),
            note: Some(format!(
                "array length differs: expected {}, actual {}",
                expected.len(),
                actual.len()
            )),
        });
    }
    for (i, (e, a)) in expected.iter().zip(actual.iter()).enumerate() {
        let child_path = format!("{path}/{i}");
        if let Some(d) = diff(e, a, child_path) {
            return Some(d);
        }
    }
    None
}

/// Escape a key for use as a JSON Pointer segment per RFC 6901.
///
/// `~` becomes `~0`, `/` becomes `~1`. Most keys touch neither so this is
/// usually a no-op.
fn escape_pointer(key: &str) -> String {
    key.replace('~', "~0").replace('/', "~1")
}

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

    #[test]
    fn deep_equality_passes_with_reordered_keys() {
        let expected = json!({"a": 1, "b": {"c": 2, "d": [1, 2]}});
        let actual = json!({"b": {"d": [1, 2], "c": 2}, "a": 1});
        let r = evaluate(&expected, &actual).unwrap();
        assert!(r.pass);
        assert!(r.diffs.is_empty());
    }

    #[test]
    fn scalar_mismatch_reports_path_and_note() {
        let expected = json!({"user": {"name": "alice", "age": 30}});
        let actual = json!({"user": {"name": "alice", "age": 31}});
        let r = evaluate(&expected, &actual).unwrap();
        assert!(!r.pass);
        let diff = &r.diffs[0];
        assert_eq!(diff.path, "/user/age");
        assert!(diff.note.as_deref().unwrap_or("").contains("not equal"));
        assert_eq!(diff.expected.as_ref().unwrap(), &json!(30));
        assert_eq!(diff.actual.as_ref().unwrap(), &json!(31));
    }

    #[test]
    fn missing_key_in_actual_is_flagged() {
        let expected = json!({"a": 1, "b": 2});
        let actual = json!({"a": 1});
        let r = evaluate(&expected, &actual).unwrap();
        assert!(!r.pass);
        assert_eq!(r.diffs[0].path, "/b");
        assert!(r.diffs[0].note.as_deref().unwrap().contains("missing key"));
        assert!(r.diffs[0].actual.is_none());
    }

    #[test]
    fn extra_key_in_actual_is_flagged() {
        let expected = json!({"a": 1});
        let actual = json!({"a": 1, "b": 2});
        let r = evaluate(&expected, &actual).unwrap();
        assert!(!r.pass);
        assert_eq!(r.diffs[0].path, "/b");
        assert!(r.diffs[0]
            .note
            .as_deref()
            .unwrap()
            .contains("unexpected key"));
    }

    #[test]
    fn array_length_differences_surface() {
        let r = evaluate(&json!([1, 2, 3]), &json!([1, 2])).unwrap();
        assert!(!r.pass);
        assert_eq!(r.diffs[0].path, "");
        assert!(r.diffs[0].note.as_deref().unwrap().contains("length"));
    }

    #[test]
    fn null_versus_missing_are_different() {
        // Edge case: expecting an explicit null is not the same as a missing key.
        let r = evaluate(&json!({"a": null}), &json!({})).unwrap();
        assert!(!r.pass);
        assert_eq!(r.diffs[0].path, "/a");
        assert!(r.diffs[0].actual.is_none());
    }

    #[test]
    fn json_pointer_escapes_special_chars() {
        let r = evaluate(&json!({"a/b": 1}), &json!({"a/b": 2})).unwrap();
        assert!(!r.pass);
        assert_eq!(r.diffs[0].path, "/a~1b");
    }
}
