//! Named string-containment matchers.
//!
//! Why these exist: promptfoo ships a vocabulary of small deterministic
//! checks (`contains-all`, `contains-any`, `icontains`, `starts-with`) that
//! read better in a YAML test than a hand-rolled regex. They are grouped in
//! one module because each is a few lines of pure string or array work and
//! splitting them into separate files would add ceremony without clarity.
//!
//! Every matcher here is pure: no clock, no network, no randomness. A
//! malformed argument (for example `contains-all` handed a number instead of
//! an array) is an evaluation error, never a panic.

use serde_json::Value;

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

/// Evaluate `contains-all`: the value must contain every needle.
///
/// Strings: each needle (which must itself be a string) must appear as a
/// substring. Arrays: each needle element must be present (deep equality).
/// The first missing needle becomes the failure diff so the user sees which
/// one was absent.
pub fn contains_all(needles: &Value, actual: &Value) -> Result<AssertionResult> {
    let needles = as_needle_array(needles, "contains-all")?;
    for needle in needles {
        if !value_contains(actual, needle)? {
            return Ok(AssertionResult::failed(
                MatcherKind::ContainsAll,
                missing_diff(needle, actual, "value does not contain this needle"),
            ));
        }
    }
    Ok(AssertionResult::passed(MatcherKind::ContainsAll))
}

/// Evaluate `contains-any`: the value must contain at least one needle.
///
/// Same containment semantics as `contains-all`. An empty needle list can
/// never be satisfied, so it fails with a clear note rather than vacuously
/// passing.
pub fn contains_any(needles: &Value, actual: &Value) -> Result<AssertionResult> {
    let needles = as_needle_array(needles, "contains-any")?;
    for needle in needles {
        if value_contains(actual, needle)? {
            return Ok(AssertionResult::passed(MatcherKind::ContainsAny));
        }
    }
    Ok(AssertionResult::failed(
        MatcherKind::ContainsAny,
        AssertionDiff {
            path: String::new(),
            expected: Some(Value::Array(needles.to_vec())),
            actual: Some(actual.clone()),
            note: Some("value contains none of the needles".into()),
        },
    ))
}

/// Evaluate `icontains`: case-insensitive substring containment.
///
/// Both the needle argument and the actual value must be strings; anything
/// else is an evaluation error so the user fixes the test rather than seeing
/// a silent failure.
pub fn icontains(needle: &Value, actual: &Value) -> Result<AssertionResult> {
    let needle = as_string(needle, "icontains")?;
    let haystack = string_at_target(actual, "icontains")?;
    if haystack.to_lowercase().contains(&needle.to_lowercase()) {
        Ok(AssertionResult::passed(MatcherKind::IContains))
    } else {
        Ok(AssertionResult::failed(
            MatcherKind::IContains,
            AssertionDiff {
                path: String::new(),
                expected: Some(Value::String(needle.to_string())),
                actual: Some(Value::String(haystack.to_string())),
                note: Some("value does not contain the substring (case-insensitive)".into()),
            },
        ))
    }
}

/// Evaluate `starts-with`: the value string must start with the prefix.
pub fn starts_with(prefix: &Value, actual: &Value) -> Result<AssertionResult> {
    let prefix = as_string(prefix, "starts-with")?;
    let haystack = string_at_target(actual, "starts-with")?;
    if haystack.starts_with(prefix) {
        Ok(AssertionResult::passed(MatcherKind::StartsWith))
    } else {
        Ok(AssertionResult::failed(
            MatcherKind::StartsWith,
            AssertionDiff {
                path: String::new(),
                expected: Some(Value::String(prefix.to_string())),
                actual: Some(Value::String(haystack.to_string())),
                note: Some("value does not start with the prefix".into()),
            },
        ))
    }
}

/// Does `actual` contain `needle`? Strings use substring containment (needle
/// must be a string); arrays use element membership (deep equality).
fn value_contains(actual: &Value, needle: &Value) -> Result<bool> {
    match actual {
        Value::String(haystack) => {
            let n = needle.as_str().ok_or_else(|| {
                Error::InvalidMatcherInput(
                    "needle for a string target must itself be a string".into(),
                )
            })?;
            Ok(haystack.contains(n))
        }
        Value::Array(items) => Ok(items.iter().any(|item| item == needle)),
        other => Err(Error::InvalidMatcherInput(format!(
            "contains-all/contains-any need a string or array target, got {}",
            type_name(other)
        ))),
    }
}

/// Build the diff for a missing needle, sharing one shape across the
/// `contains-all` failure path.
fn missing_diff(needle: &Value, actual: &Value, note: &str) -> AssertionDiff {
    AssertionDiff {
        path: String::new(),
        expected: Some(needle.clone()),
        actual: Some(actual.clone()),
        note: Some(note.into()),
    }
}

/// Coerce the matcher argument into a needle slice or fail with a clear note.
fn as_needle_array<'a>(value: &'a Value, matcher: &str) -> Result<&'a [Value]> {
    value.as_array().map(Vec::as_slice).ok_or_else(|| {
        Error::InvalidMatcherInput(format!("{matcher} argument must be an array of needles"))
    })
}

/// Coerce a scalar string argument or fail with a clear note.
fn as_string<'a>(value: &'a Value, matcher: &str) -> Result<&'a str> {
    value
        .as_str()
        .ok_or_else(|| Error::InvalidMatcherInput(format!("{matcher} argument must be a string")))
}

/// Coerce the value at the target into a string, since `icontains` and
/// `starts-with` only make sense on text.
fn string_at_target<'a>(value: &'a Value, matcher: &str) -> Result<&'a str> {
    value.as_str().ok_or_else(|| {
        Error::InvalidMatcherInput(format!("{matcher} requires a string-typed actual value"))
    })
}

/// Human-readable JSON type name for error messages.
fn type_name(value: &Value) -> &'static str {
    match value {
        Value::Null => "null",
        Value::Bool(_) => "bool",
        Value::Number(_) => "number",
        Value::String(_) => "string",
        Value::Array(_) => "array",
        Value::Object(_) => "object",
    }
}

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

    #[test]
    fn contains_all_passes_when_every_substring_present() {
        let r = contains_all(&json!(["foo", "bar"]), &json!("a foo and a bar")).unwrap();
        assert!(r.pass, "diffs: {:?}", r.diffs);
        assert_eq!(r.kind, MatcherKind::ContainsAll);
    }

    #[test]
    fn contains_all_fails_when_one_substring_missing() {
        let r = contains_all(&json!(["foo", "baz"]), &json!("a foo only")).unwrap();
        assert!(!r.pass);
        assert_eq!(r.diffs[0].expected, Some(json!("baz")));
    }

    #[test]
    fn contains_all_matches_array_elements() {
        let r = contains_all(&json!([1, 2]), &json!([3, 2, 1])).unwrap();
        assert!(r.pass, "diffs: {:?}", r.diffs);
    }

    #[test]
    fn contains_all_rejects_non_array_argument() {
        let err = contains_all(&json!("foo"), &json!("foo")).unwrap_err();
        assert!(matches!(err, Error::InvalidMatcherInput(_)));
    }

    #[test]
    fn contains_any_passes_with_one_match() {
        let r = contains_any(&json!(["nope", "bar"]), &json!("only bar here")).unwrap();
        assert!(r.pass, "diffs: {:?}", r.diffs);
    }

    #[test]
    fn contains_any_fails_when_none_match() {
        let r = contains_any(&json!(["xx", "yy"]), &json!("nothing here")).unwrap();
        assert!(!r.pass);
        assert!(r.diffs[0].note.as_deref().unwrap().contains("none"));
    }

    #[test]
    fn contains_any_empty_list_cannot_pass() {
        let r = contains_any(&json!([]), &json!("anything")).unwrap();
        assert!(!r.pass);
    }

    #[test]
    fn icontains_is_case_insensitive() {
        let r = icontains(&json!("WORLD"), &json!("hello world")).unwrap();
        assert!(r.pass, "diffs: {:?}", r.diffs);
    }

    #[test]
    fn icontains_fails_on_absent_substring() {
        let r = icontains(&json!("xyz"), &json!("hello world")).unwrap();
        assert!(!r.pass);
    }

    #[test]
    fn icontains_rejects_non_string_target() {
        let err = icontains(&json!("a"), &json!(42)).unwrap_err();
        assert!(matches!(err, Error::InvalidMatcherInput(_)));
    }

    #[test]
    fn starts_with_passes_on_matching_prefix() {
        let r = starts_with(&json!("hello"), &json!("hello world")).unwrap();
        assert!(r.pass, "diffs: {:?}", r.diffs);
    }

    #[test]
    fn starts_with_fails_when_prefix_absent() {
        let r = starts_with(&json!("world"), &json!("hello world")).unwrap();
        assert!(!r.pass);
        assert_eq!(r.diffs[0].expected, Some(json!("world")));
    }
}
