//! Build the advisory-lane judge closure for `mcptest security --model`.
//!
//! The advisory lane (ADR 0041) takes a `Fn(&str) -> Option<String>` judge: it
//! hands the lane's prompt to a model and returns the raw response, or `None`
//! when the call fails. This module resolves a provider from the environment
//! the same way agent and llm-judge runs do (model-as-mechanism, the OSS
//! pattern) and wraps a single-shot completion in that closure.
//!
//! The judge is advisory only. Its output lands in the advisory section of the
//! report and never touches the deterministic verdict or the exit code, so a
//! judge outage or a missing key degrades the run to fewer advisory findings,
//! never to a changed grade.

use std::sync::Arc;

use anyhow::{anyhow, Result};
use mcptest_agent::provider::{from_env, ProviderLookup};
use mcptest_core::eval::provider::{
    CompletionOptions, LlmProvider, Message, ResponseContent, Role,
};
use mcptest_core::network::ProxyConfig;
use tokio::runtime::Runtime;

/// Build a judge closure for `model`.
///
/// Resolves the provider from the process environment (the credential lookup
/// mirrors agent runs). A recognized family with no key, or a family that is
/// not wired up, is an error here rather than a silent stub: the user asked for
/// the advisory lane explicitly with `--model`, so a missing key should be
/// reported, not papered over. The deterministic lanes have already produced
/// the verdict by the time this is called, so this never blocks the gate.
pub(super) fn build_judge(model: &str) -> Result<impl Fn(&str) -> Option<String>> {
    let provider = resolve_provider(model)?;
    let runtime = Runtime::new().map_err(|e| anyhow!("could not build tokio runtime: {e}"))?;
    let model = model.to_string();
    Ok(move |prompt: &str| run_once(&runtime, provider.as_ref(), &model, prompt))
}

/// Resolve a real provider for `model`, or explain why the advisory lane cannot
/// run. Uses the default proxy policy, which still honors `HTTPS_PROXY` from the
/// environment.
fn resolve_provider(model: &str) -> Result<Arc<dyn LlmProvider>> {
    match from_env(model, &ProxyConfig::default())? {
        ProviderLookup::Resolved { provider, .. } => Ok(provider),
        ProviderLookup::MissingCredential { env_var, label } => Err(anyhow!(
            "no credential for {label}: set {env_var} to enable the advisory lane"
        )),
        ProviderLookup::UnsupportedFamily { model } => Err(anyhow!(
            "model `{model}` is not a recognized provider family"
        )),
    }
}

/// Run one judge completion. Returns the model's text, or `None` on any failure
/// (transport error, a tool-use response instead of text). A `None` is recorded
/// by the advisory lane as a judge error and contributes no finding.
fn run_once(
    runtime: &Runtime,
    provider: &dyn LlmProvider,
    model: &str,
    prompt: &str,
) -> Option<String> {
    let messages = vec![Message {
        role: Role::User,
        content: prompt.to_string(),
    }];
    let options = CompletionOptions {
        model: model.to_string(),
        temperature: Some(0.0),
        max_tokens: Some(512),
        ..Default::default()
    };
    let response = runtime
        .block_on(provider.chat_completion(&messages, &options))
        .ok()?;
    match response.content {
        ResponseContent::Text { text } => Some(text),
        ResponseContent::ToolUse { .. } => None,
    }
}

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

    #[test]
    fn unsupported_family_is_an_error_not_a_silent_stub() {
        // The advisory lane is opt-in; an unrecognized family is reported rather
        // than papered over with a stub. This also documents that no judge is
        // built (and so no model is consulted) for an unknown model, which keeps
        // the verdict path model-free. `Arc<dyn LlmProvider>` is not `Debug`, so
        // we match instead of `expect_err`.
        match resolve_provider("acme-llama-99b") {
            Ok(_) => unreachable!("an unsupported family must not resolve a provider"),
            Err(e) => assert!(e.to_string().contains("not a recognized provider family")),
        }
    }
}
