//! Warnings for source constructs mcptest's interop importers do not carry.
//!
//! The honesty rule: a lossy conversion must say what it dropped. These checks
//! scan a source document for high-level constructs that have no mcptest
//! equivalent (a promptfoo provider matrix, DeepEval metric configs, mcp-eval
//! setup/teardown hooks) and return one human-readable line each.

use serde_json::Value;

use crate::benchmark_import::BenchmarkKind;

/// Collect warnings for constructs the importer cannot represent.
pub fn unsupported_warnings(doc: &Value, kind: BenchmarkKind) -> Vec<String> {
    match kind {
        BenchmarkKind::Promptfoo => promptfoo_warnings(doc),
        BenchmarkKind::DeepEval => deepeval_warnings(doc),
        BenchmarkKind::McpEval => mcpeval_warnings(doc),
        BenchmarkKind::Generic => Vec::new(),
    }
}

/// promptfoo carries a provider matrix and shared defaults mcptest does not map.
fn promptfoo_warnings(doc: &Value) -> Vec<String> {
    let mut out = Vec::new();
    if doc.get("providers").is_some() {
        out.push(
            "promptfoo `providers` are not imported; set the agent model and servers in mcptest"
                .to_string(),
        );
    }
    if doc.get("defaultTest").is_some() {
        out.push(
            "promptfoo `defaultTest` shared config is not imported; assertions are per-test only"
                .to_string(),
        );
    }
    if has_unmapped_assert(doc) {
        out.push(
            "some promptfoo assert types (model-graded, javascript, python) map to no mcptest \
             matcher and were dropped"
                .to_string(),
        );
    }
    out
}

/// DeepEval datasets carry metric configs and retrieval context mcptest drops.
fn deepeval_warnings(doc: &Value) -> Vec<String> {
    let mut out = Vec::new();
    if doc.get("metrics").is_some() || mentions(doc, "metric") {
        out.push(
            "DeepEval `metrics` (G-Eval, answer-relevancy, faithfulness) are not imported; \
             add mcptest matchers or an llm-judge block"
                .to_string(),
        );
    }
    if mentions(doc, "retrieval_context") || mentions(doc, "context") {
        out.push(
            "DeepEval retrieval `context` fields are not imported; mcptest drives the live server \
             instead"
                .to_string(),
        );
    }
    out
}

/// mcp-eval suites carry setup/teardown and trace-metric config mcptest drops.
fn mcpeval_warnings(doc: &Value) -> Vec<String> {
    let mut out = Vec::new();
    if mentions(doc, "setup") || mentions(doc, "teardown") {
        out.push(
            "mcp-eval `setup`/`teardown` hooks are not imported; use mcptest fixtures or a mock \
             server"
                .to_string(),
        );
    }
    if mentions(doc, "metrics") || mentions(doc, "otel") {
        out.push(
            "mcp-eval OpenTelemetry trace metrics are not imported; mcptest scores tool calls \
             directly"
                .to_string(),
        );
    }
    out
}

/// Whether any promptfoo assert uses a type mcptest has no matcher for.
fn has_unmapped_assert(doc: &Value) -> bool {
    let mapped = ["contains", "icontains", "regex", "equals"];
    doc.get("tests")
        .and_then(Value::as_array)
        .into_iter()
        .flatten()
        .any(|t| {
            t.get("assert")
                .and_then(Value::as_array)
                .into_iter()
                .flatten()
                .any(|a| {
                    a.get("type")
                        .and_then(Value::as_str)
                        .is_some_and(|ty| !mapped.contains(&ty))
                })
        })
}

/// Whether a key appears anywhere in the document tree.
fn mentions(value: &Value, key: &str) -> bool {
    match value {
        Value::Object(map) => map.contains_key(key) || map.values().any(|v| mentions(v, key)),
        Value::Array(arr) => arr.iter().any(|v| mentions(v, key)),
        _ => false,
    }
}
