//! Provider resolution and the stub-fallback gate for agent cells.
//!
//! Holds the per-cell provider lookup, the WOR-1312 stub gate (a run that
//! silently swaps a real model for the canned stub produces green reports
//! that exercised nothing), and the lookup logging. Pulled out of
//! `run_live_agents.rs` so that module stays under the
//! production-module-size guard; behavior is unchanged.

use anyhow::{Context, Result};

use super::run_live_agents::CellAcc;

/// Gate the cell on provider resolution and mark stub use in the report.
///
/// A non-resolved provider is an error unless the operator opted in; on stub
/// use, make it unmistakable: a `provider [STUB]` stdout line plus a passing
/// `(provider)` report row whose note leads with `stub:` (WOR-1312). Split
/// out of `dispatch_agent_cell` for the function-length lint.
#[allow(clippy::too_many_arguments)]
pub(super) fn gate_and_mark_stub_cell(
    lookup: &mcptest_agent::ProviderLookup,
    allow_stub_provider: bool,
    quiet: bool,
    agent_name: &str,
    case_name: &str,
    model_id: &str,
    acc: &mut CellAcc<'_>,
) -> Result<()> {
    require_provider_or_stub_opt_in(lookup, allow_stub_provider, agent_name, case_name)?;
    if !matches!(lookup, mcptest_agent::ProviderLookup::Resolved { .. }) {
        if !quiet {
            println!("provider [STUB] {case_name}: stub:{model_id} (--allow-stub-provider)");
        }
        acc.eval_outcomes
            .push(stub_provider_marker(case_name, model_id));
    }
    Ok(())
}

/// Gate the stub-provider fallback (WOR-1312).
///
/// A run that silently swaps a real model for the canned stub produces green
/// reports that exercised nothing, so a non-resolved provider is an error by
/// default, naming exactly what to fix (the env var for a missing credential,
/// the model string for an unsupported family). `--allow-stub-provider` opts
/// back into the fallback for offline smoke runs. Free function so the
/// decision is unit-testable without a connected cell.
pub(super) fn require_provider_or_stub_opt_in(
    lookup: &mcptest_agent::ProviderLookup,
    allow_stub: bool,
    agent_name: &str,
    case_name: &str,
) -> Result<()> {
    use mcptest_agent::ProviderLookup;
    match lookup {
        ProviderLookup::Resolved { .. } => Ok(()),
        _ if allow_stub => Ok(()),
        ProviderLookup::MissingCredential { env_var, label } => anyhow::bail!(
            "agent `{agent_name}` (test `{case_name}`): no credential for provider {label}; \
             set {env_var} to dispatch a real model, or pass --allow-stub-provider for an \
             offline smoke run against the canned stub"
        ),
        ProviderLookup::UnsupportedFamily { model } => anyhow::bail!(
            "agent `{agent_name}` (test `{case_name}`): model `{model}` is not a recognized \
             provider family; use a supported model id, or pass --allow-stub-provider for an \
             offline smoke run against the canned stub"
        ),
    }
}

/// Report row marking a cell that ran on the stub provider (WOR-1312).
///
/// Rides the same per-cell outcome channel the `eval:` metrics use, so the
/// row lands in every reporter; a passing check row keeps its note in the
/// message slot. The note leads with `stub:` so a report scan (human or
/// machine) cannot mistake the cell for a real model run.
pub(super) fn stub_provider_marker(
    case_name: &str,
    model_id: &str,
) -> mcptest_core::checks::CheckOutcome {
    mcptest_core::checks::CheckOutcome {
        name: format!("{case_name} (provider)"),
        passed: true,
        failure: Some(format!(
            "stub:{model_id} (no real provider resolved; --allow-stub-provider)"
        )),
    }
}

/// Resolve the LLM provider for an agent model: auto-detect from the env for
/// bare model ids, or look up the named `providers:` entry. The actual
/// provider instance is resolved per run inside `run_agent_passes`.
pub(super) fn resolve_agent_lookup(
    model_ref: &mcptest_core::suite::ModelRef,
    agent: &mcptest_core::suite::AgentTest,
    suite: &mcptest_core::suite::ResolvedSuite,
    proxy: &mcptest_core::network::ProxyConfig,
) -> Result<mcptest_agent::ProviderLookup> {
    use mcptest_agent::from_env;
    use mcptest_agent::provider::from_provider_spec;
    use mcptest_core::suite::ModelRef;
    match model_ref {
        ModelRef::AutoDetect(id) => from_env(id, proxy).with_context(|| {
            format!(
                "agent `{}` provider lookup for model `{id}` failed",
                agent.name
            )
        }),
        ModelRef::Named { provider, .. } => {
            let spec = suite.providers.get(provider).ok_or_else(|| {
                anyhow::anyhow!(
                    "agent `{}` references undeclared provider `{provider}` (add it \
                     under top-level `providers:`)",
                    agent.name
                )
            })?;
            from_provider_spec(spec, proxy).with_context(|| {
                format!("agent `{}` provider `{provider}` lookup failed", agent.name)
            })
        }
    }
}

/// Log the lookup outcome and remember the last real provider for pricing.
///
/// Only a real provider can price the run's tokens, so the resolved branch
/// stashes the provider and model on the accumulator for the cost summary.
pub(super) fn log_provider_lookup(
    lookup: &mcptest_agent::ProviderLookup,
    agent: &mcptest_core::suite::AgentTest,
    label: &str,
    acc: &mut CellAcc<'_>,
) {
    use mcptest_agent::ProviderLookup;
    use std::sync::Arc;
    match lookup {
        ProviderLookup::Resolved {
            provider,
            label: pl,
        } => {
            tracing::info!(
                target: "mcptest::cli",
                agent = %agent.name,
                provider = %pl,
                model = %label,
                "agent dispatching against real provider"
            );
            *acc.last_real_provider = Some(Arc::clone(provider));
            *acc.last_real_model = Some(label.to_string());
        }
        ProviderLookup::MissingCredential { env_var, label: pl } => {
            tracing::info!(
                target: "mcptest::cli",
                agent = %agent.name,
                provider = %pl,
                env_var = %env_var,
                model = %label,
                "no credential, falling back to stub provider"
            );
        }
        ProviderLookup::UnsupportedFamily { model } => {
            tracing::info!(
                target: "mcptest::cli",
                agent = %agent.name,
                model = %model,
                "model family not yet supported, falling back to stub provider"
            );
        }
    }
}

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

    fn missing_credential() -> ProviderLookup {
        ProviderLookup::MissingCredential {
            env_var: "ANTHROPIC_API_KEY".to_string(),
            label: "anthropic".to_string(),
        }
    }

    fn unsupported_family() -> ProviderLookup {
        ProviderLookup::UnsupportedFamily {
            model: "mystery-model-9000".to_string(),
        }
    }

    #[test]
    fn missing_credential_without_opt_in_errors_naming_the_env_var_and_test() {
        let err = require_provider_or_stub_opt_in(
            &missing_credential(),
            false,
            "weather agent",
            "weather agent [claude]",
        )
        .expect_err("hard error by default");
        let msg = err.to_string();
        assert!(
            msg.contains("ANTHROPIC_API_KEY"),
            "names the env var: {msg}"
        );
        assert!(msg.contains("weather agent"), "names the agent: {msg}");
        assert!(
            msg.contains("--allow-stub-provider"),
            "names the escape hatch: {msg}"
        );
    }

    #[test]
    fn unsupported_family_without_opt_in_errors_naming_the_model() {
        let err = require_provider_or_stub_opt_in(&unsupported_family(), false, "agent", "agent")
            .expect_err("hard error by default");
        let msg = err.to_string();
        assert!(msg.contains("mystery-model-9000"), "names the model: {msg}");
        assert!(
            msg.contains("--allow-stub-provider"),
            "names the escape hatch: {msg}"
        );
    }

    #[test]
    fn allow_stub_provider_permits_both_fallback_cases() {
        assert!(require_provider_or_stub_opt_in(&missing_credential(), true, "a", "a").is_ok());
        assert!(require_provider_or_stub_opt_in(&unsupported_family(), true, "a", "a").is_ok());
    }

    #[test]
    fn resolved_provider_needs_no_opt_in() {
        let lookup = ProviderLookup::Resolved {
            provider: std::sync::Arc::new(mcptest_agent::stub::StubProvider::text("hi")),
            label: "stub".to_string(),
        };
        assert!(require_provider_or_stub_opt_in(&lookup, false, "a", "a").is_ok());
    }

    #[test]
    fn stub_marker_note_leads_with_the_stub_prefix() {
        let outcome = stub_provider_marker("weather agent", "claude-sonnet-4-5");
        assert!(outcome.passed, "the marker never fails the run by itself");
        assert_eq!(outcome.name, "weather agent (provider)");
        let note = outcome.failure.expect("marker carries a note");
        assert!(
            note.starts_with("stub:claude-sonnet-4-5"),
            "note is unmistakable: {note}"
        );
    }
}
