//! Named model-graded matchers: `factuality`, `answer-relevance`, and
//! `context-faithfulness`.
//!
//! These are the promptfoo-vocabulary surface over the existing `llm-judge`
//! matcher. Each carries one field (a reference answer, the original query, or
//! the retrieved context) and desugars at parse time into an `llm-judge` block
//! whose rubric embeds that field. The judge plumbing, threshold gating, and
//! calibration all carry over unchanged, so this is sugar, not a second
//! metrics engine. This module owns only the vetted rubric text and the shape
//! of the desugared block; the parser ([`super::test_parse`]) extracts the
//! fields and the executor never sees a distinct variant.

use serde_json::{json, Value};

/// The named model-graded matcher keys this module desugars. Kept in one
/// place so the parser dispatch and any future help text agree.
pub(super) const NAMED_GRADED_KEYS: &[&str] =
    &["factuality", "answer-relevance", "context-faithfulness"];

/// Default pass threshold on the judge's 0..1 score. Matches the `llm-judge`
/// default so a named matcher behaves like the judge a suite author already
/// knows; override with an explicit `threshold:` on the matcher.
pub(super) const DEFAULT_THRESHOLD: f64 = 0.7;

/// Rubric for `factuality`: is the candidate consistent with a reference?
pub(super) fn factuality_rubric(reference: &str) -> String {
    format!(
        "Judge the factual consistency of the candidate against the reference text below. \
The candidate must not contradict the reference and must not invent facts beyond it. \
Score 1.0 when the candidate is fully consistent with the reference, 0.0 when it \
contradicts the reference, and an intermediate value when it is only partially \
consistent.\n\nReference:\n{reference}"
    )
}

/// Rubric for `answer-relevance`: does the candidate answer the query?
pub(super) fn answer_relevance_rubric(query: &str) -> String {
    format!(
        "Judge how directly and completely the candidate answers the query below. \
Score 1.0 when the candidate is a direct, complete answer to the query, and lower \
when it is off-topic, evasive, or only partially responsive.\n\nQuery:\n{query}"
    )
}

/// Rubric for `context-faithfulness`: is every claim grounded in the context?
pub(super) fn context_faithfulness_rubric(context: &str) -> String {
    format!(
        "Judge whether every claim in the candidate is supported by the context below. \
Penalize any claim that is not grounded in the context (a hallucination). Score 1.0 \
when all claims are supported by the context, 0.0 when the candidate is largely \
unsupported, and an intermediate value for partial grounding.\n\nContext:\n{context}"
    )
}

/// Assemble the desugared `llm-judge` block. The shape matches
/// `matchers::llm_judge::JudgeConfig` (`deny_unknown_fields`), so it carries
/// only `rubric`, `threshold`, and an optional `model` override.
pub(super) fn judge_block(rubric: String, threshold: f64, model: Option<&str>) -> Value {
    match model {
        Some(m) => json!({ "rubric": rubric, "threshold": threshold, "model": m }),
        None => json!({ "rubric": rubric, "threshold": threshold }),
    }
}

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

    #[test]
    fn factuality_rubric_embeds_the_reference() {
        let r = factuality_rubric("Paris is the capital of France");
        assert!(r.contains("Paris is the capital of France"));
        assert!(r.contains("factual consistency"));
    }

    #[test]
    fn answer_relevance_rubric_embeds_the_query() {
        let r = answer_relevance_rubric("What is the capital of France?");
        assert!(r.contains("What is the capital of France?"));
    }

    #[test]
    fn context_faithfulness_rubric_embeds_the_context() {
        let r = context_faithfulness_rubric("<retrieved docs>");
        assert!(r.contains("<retrieved docs>"));
        assert!(r.contains("hallucination"));
    }

    #[test]
    fn judge_block_omits_model_when_absent() {
        let b = judge_block("r".into(), 0.7, None);
        assert_eq!(b["rubric"], json!("r"));
        assert_eq!(b["threshold"], json!(0.7));
        assert!(b.get("model").is_none());
    }

    #[test]
    fn judge_block_carries_model_override() {
        let b = judge_block("r".into(), 0.8, Some("gpt-4o"));
        assert_eq!(b["model"], json!("gpt-4o"));
    }
}
