//! `regex` matcher: regex over a string-typed field with capture-group bindings.
//!
//! Why this matcher exists: pattern-matching against ID strings, timestamps,
//! and free-form text is a common assertion. Named captures double as
//! variable extractors so a later step can reference `${capture.NAME}`
//! without writing JSONPath.

use serde_json::Value;
use std::collections::BTreeMap;

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

/// Evaluate the `regex` matcher.
///
/// Returns a passing result when `pattern` matches `actual` and `actual` is a
/// JSON string. Named capture groups are stored in
/// [`AssertionResult::captures`](super::AssertionResult::captures) so later
/// steps can interpolate them. Non-string `actual` values fail with a
/// structured diff rather than coercing, since pattern-matching a number is
/// almost never what the user meant.
pub fn evaluate(pattern: &str, actual: &Value) -> Result<AssertionResult> {
    let re = regex::Regex::new(pattern)?;
    let text = match actual {
        Value::String(s) => s,
        other => {
            return Ok(AssertionResult::failed(
                MatcherKind::Regex,
                AssertionDiff {
                    path: String::new(),
                    expected: Some(Value::String(pattern.to_string())),
                    actual: Some(other.clone()),
                    note: Some(format!(
                        "regex matcher requires a string-typed actual value, got {}",
                        type_name(other)
                    )),
                },
            ));
        }
    };
    let Some(caps) = re.captures(text) else {
        return Ok(AssertionResult::failed(
            MatcherKind::Regex,
            AssertionDiff {
                path: String::new(),
                expected: Some(Value::String(pattern.to_string())),
                actual: Some(Value::String(text.clone())),
                note: Some(format!("regex `{pattern}` did not match input")),
            },
        ));
    };
    let mut captures: BTreeMap<String, String> = BTreeMap::new();
    for name in re.capture_names().flatten() {
        if let Some(m) = caps.name(name) {
            captures.insert(name.to_string(), m.as_str().to_string());
        }
    }
    Ok(AssertionResult {
        kind: MatcherKind::Regex,
        pass: true,
        diffs: Vec::new(),
        captures,
    })
}

/// Render a short type label for diagnostic notes.
fn type_name(v: &Value) -> &'static str {
    match v {
        Value::Null => "null",
        Value::Bool(_) => "boolean",
        Value::Number(_) => "number",
        Value::String(_) => "string",
        Value::Array(_) => "array",
        Value::Object(_) => "object",
    }
}

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

    #[test]
    fn match_with_named_capture_populates_captures_map() {
        let r = evaluate(r"^user-(?P<id>\d+)$", &json!("user-42")).unwrap();
        assert!(r.pass);
        assert_eq!(r.captures.get("id").map(String::as_str), Some("42"));
    }

    #[test]
    fn no_match_returns_failure_with_pattern_in_note() {
        let r = evaluate(r"^\d+$", &json!("abc")).unwrap();
        assert!(!r.pass);
        let diff = &r.diffs[0];
        assert_eq!(diff.path, "");
        assert!(diff.note.as_deref().unwrap().contains("did not match"));
        assert_eq!(diff.actual.as_ref().unwrap(), &json!("abc"));
    }

    #[test]
    fn non_string_actual_fails_with_typed_note() {
        // Edge case: never coerce non-strings; surface the type instead.
        let r = evaluate(".*", &json!(42)).unwrap();
        assert!(!r.pass);
        assert!(r.diffs[0]
            .note
            .as_deref()
            .unwrap()
            .contains("requires a string-typed actual value"));
        assert!(r.diffs[0].note.as_deref().unwrap().contains("number"));
    }

    #[test]
    fn invalid_pattern_returns_compile_error() {
        let err = evaluate("(", &json!("anything")).unwrap_err();
        assert!(matches!(err, crate::Error::RegexCompile(_)));
    }
}
