//! `is-sql` matcher: the string value must parse as SQL.
//!
//! Why this exists: an MCP tool that turns a natural-language request into a
//! query (a "text to SQL" tool) should hand back something a database would
//! accept. Asserting the output parses as SQL is a deterministic check that a
//! regex cannot do without re-implementing a parser.
//!
//! The value is parsed with `sqlparser` against the permissive
//! [`GenericDialect`], which accepts the broadest set of statements rather than
//! pinning one engine's grammar. A parse error fails the assertion; a malformed
//! matcher argument is an evaluation error, never a panic.

use serde_json::Value;
use sqlparser::dialect::GenericDialect;
use sqlparser::parser::Parser;

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

/// Evaluate `is-sql`.
///
/// `argument` must be null/absent: the matcher has no options, so any other
/// shape is rejected to catch typos rather than ignoring them. The actual value
/// must be a string; it is parsed against the generic dialect and the first
/// parse error becomes the failure diff.
pub fn evaluate(argument: &Value, actual: &Value) -> Result<AssertionResult> {
    if !argument.is_null() {
        return Err(Error::InvalidMatcherInput(
            "is-sql takes no argument; pass null".into(),
        ));
    }
    let text = actual.as_str().ok_or_else(|| {
        Error::InvalidMatcherInput("is-sql requires a string-typed actual value".into())
    })?;
    match Parser::parse_sql(&GenericDialect {}, text) {
        Ok(statements) if statements.is_empty() => Ok(AssertionResult::failed(
            MatcherKind::IsSql,
            diff(actual, "value contains no SQL statement".into()),
        )),
        Ok(_) => Ok(AssertionResult::passed(MatcherKind::IsSql)),
        Err(e) => Ok(AssertionResult::failed(
            MatcherKind::IsSql,
            diff(
                actual,
                format!("value is not valid SQL (generic dialect): {e}"),
            ),
        )),
    }
}

/// Build the single-diff failure shape shared by every `is-sql` failure path.
fn diff(actual: &Value, note: String) -> AssertionDiff {
    AssertionDiff {
        path: String::new(),
        expected: None,
        actual: Some(actual.clone()),
        note: Some(note),
    }
}

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

    #[test]
    fn is_sql_passes_on_a_select() {
        let r = evaluate(
            &Value::Null,
            &json!("SELECT id, name FROM users WHERE id = 1"),
        )
        .unwrap();
        assert!(r.pass, "diffs: {:?}", r.diffs);
        assert_eq!(r.kind, MatcherKind::IsSql);
    }

    #[test]
    fn is_sql_passes_on_an_insert() {
        let r = evaluate(&Value::Null, &json!("INSERT INTO t (a, b) VALUES (1, 'x')")).unwrap();
        assert!(r.pass, "diffs: {:?}", r.diffs);
    }

    #[test]
    fn is_sql_fails_on_garbage() {
        let r = evaluate(&Value::Null, &json!("this is not sql at all ;;;")).unwrap();
        assert!(!r.pass);
        assert!(r.diffs[0]
            .note
            .as_deref()
            .unwrap()
            .contains("not valid SQL"));
    }

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

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

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