//! Plan building for the live runner.
//!
//! Turns the resolved suite into executable plans: tool/resource/prompt
//! plans for every selected test, plus one agent-matrix cell per
//! (agent, model) pair, with the post-run accumulators (floors, recorded
//! cells, matrix cells) the gating pass consumes. Pulled out of
//! [`super::run_live`] so that module stays under the
//! production-module-size guard; behavior is unchanged.

use anyhow::Result;

/// Borrowed inputs the [`build_all_plans`] preamble needs.
/// Bundled so the helper has one parameter instead of nine, and so
/// the caller stays under the function-length lint.
pub(super) struct BuildPlansCtx<'a> {
    pub(super) suite: &'a mcptest_core::suite::ResolvedSuite,
    pub(super) selected_set: &'a std::collections::BTreeSet<String>,
    pub(super) config_path: &'a std::path::Path,
    pub(super) record: bool,
    pub(super) quiet: bool,
    pub(super) proxy: &'a mcptest_core::network::ProxyConfig,
    pub(super) journal: Option<&'a mcptest_core::runner::resume::RunJournal>,
    pub(super) pool: &'a std::sync::Arc<mcptest_core::connector::ServerPool>,
    pub(super) budget: &'a std::sync::Arc<mcptest_agent::Budget>,
    /// WOR-1312: `--allow-stub-provider` opt-in. Without it, a cell whose
    /// real provider cannot be resolved is a hard error instead of a silent
    /// stub fallback.
    pub(super) allow_stub_provider: bool,
    /// WOR-1320: the run-wide LLM-judge verdict cache, reborrowed into each
    /// cell's accumulators so every agent `eval:` metric shares one cache.
    pub(super) verdict_cache: &'a mut mcptest_core::eval::VerdictCache,
}

/// The specs, plans, and post-run accumulators [`build_all_plans`] produces.
pub(super) struct PlanBundle {
    pub(super) specs: Vec<mcptest_core::runner::TestSpec>,
    pub(super) plans:
        std::collections::HashMap<String, std::sync::Arc<mcptest_core::executor::PlannedTest>>,
    pub(super) last_real_provider:
        Option<std::sync::Arc<dyn mcptest_core::eval::provider::LlmProvider>>,
    pub(super) last_real_model: Option<String>,
    pub(super) floor_outcomes: Vec<(String, mcptest_core::eval::SelectionReport, bool)>,
    pub(super) recorded_cells: Vec<RecordedCell>,
    pub(super) matrix_cells: Vec<super::run_live_matrix::MatrixCell>,
    /// Per-cell agent gate outcomes and wall-clock/cost telemetry, folded onto
    /// the report after the run by [`super::run_live_checks::attach_post_run_sections`].
    pub(super) sections: super::run_live_checks::PostRunSections,
}

/// One matrix cell dispatched live during a record run.
///
/// Holds the (agent, model) identity plus the reporter row names the
/// cell produced (one per `runs:` iteration). The post-run write-back
/// marks the cell complete in the journal only when every row passed.
pub(super) struct RecordedCell {
    /// Agent name, the first half of the journal cell key.
    pub(super) agent: String,
    /// Model id, the second half of the journal cell key.
    pub(super) model_id: String,
    /// Reporter row names this cell contributed to the run report.
    pub(super) rows: Vec<String>,
}

/// Suite-wide agent spend budget from the YAML `budget:` caps. Built once
/// per run so every agent cell draws from the same caps.
pub(super) fn suite_budget(
    suite: &mcptest_core::suite::ResolvedSuite,
) -> std::sync::Arc<mcptest_agent::Budget> {
    std::sync::Arc::new(mcptest_agent::Budget::new(
        suite.budget.per_test_usd_cents,
        suite.budget.per_suite_usd_cents,
    ))
}

/// Build the tool plans plus every agent-matrix cell.
///
/// Extracted from the live runner so that function stays under the
/// function-length lint. Walks the tool tests into plans, then fans each agent
/// test out across its model matrix, collecting the specs, the plan map, and
/// the post-run accumulators (pass^k floors, recorded cells, matrix cells, the
/// last real provider).
pub(super) async fn build_all_plans(ctx: BuildPlansCtx<'_>) -> Result<PlanBundle> {
    use super::run_live_agents::{dispatch_agent_cell, AgentCell, CellAcc};
    use mcptest_core::executor::PlannedTest;
    use mcptest_core::runner::TestSpec;
    use std::collections::HashMap;
    use std::sync::Arc;

    let mut specs: Vec<TestSpec> = Vec::new();
    let mut plans: HashMap<String, Arc<PlannedTest>> = HashMap::new();
    build_tool_plans(
        ctx.suite,
        ctx.selected_set,
        ctx.proxy,
        &mut specs,
        &mut plans,
    )?;

    let mut last_real_provider = None;
    let mut last_real_model = None;
    let mut floor_outcomes = Vec::new();
    let mut recorded_cells = Vec::new();
    let mut matrix_cells = Vec::new();
    let mut stability_outcomes = Vec::new();
    let mut f1_outcomes = Vec::new();
    let mut eval_outcomes = Vec::new();
    let mut recovery_outcomes = Vec::new();
    let mut agent_metrics = Vec::new();
    for agent in &ctx.suite.agents {
        // Fan out the same test across every model the YAML names so the
        // reporter shows pass / fail per model when a new release ships.
        for model_ref in &agent.models {
            let cell = AgentCell {
                suite: ctx.suite,
                selected_set: ctx.selected_set,
                config_path: ctx.config_path,
                record: ctx.record,
                quiet: ctx.quiet,
                proxy: ctx.proxy,
                journal: ctx.journal,
                pool: ctx.pool,
                budget: ctx.budget,
                agent,
                model_ref,
                allow_stub_provider: ctx.allow_stub_provider,
            };
            let mut acc = CellAcc {
                specs: &mut specs,
                plans: &mut plans,
                last_real_provider: &mut last_real_provider,
                last_real_model: &mut last_real_model,
                floor_outcomes: &mut floor_outcomes,
                recorded_cells: &mut recorded_cells,
                matrix_cells: &mut matrix_cells,
                stability_outcomes: &mut stability_outcomes,
                f1_outcomes: &mut f1_outcomes,
                eval_outcomes: &mut eval_outcomes,
                recovery_outcomes: &mut recovery_outcomes,
                agent_metrics: &mut agent_metrics,
                verdict_cache: &mut *ctx.verdict_cache,
            };
            dispatch_agent_cell(cell, &mut acc).await?;
        }
    }

    Ok(PlanBundle {
        specs,
        plans,
        last_real_provider,
        last_real_model,
        floor_outcomes,
        recorded_cells,
        matrix_cells,
        sections: super::run_live_checks::PostRunSections {
            stability_outcomes,
            f1_outcomes,
            eval_outcomes,
            recovery_outcomes,
            agent_metrics,
        },
    })
}

/// Build the tool/resource/prompt plans for every selected test (
/// split). Each plan carries the test's scoring inputs (assert-sets,
/// threshold, derived metrics) so the executor computes the combined
/// score; tests that declare none get the legacy all-pass default.
fn build_tool_plans(
    suite: &mcptest_core::suite::ResolvedSuite,
    selected_set: &std::collections::BTreeSet<String>,
    proxy: &mcptest_core::network::ProxyConfig,
    specs: &mut Vec<mcptest_core::runner::TestSpec>,
    plans: &mut std::collections::HashMap<
        String,
        std::sync::Arc<mcptest_core::executor::PlannedTest>,
    >,
) -> Result<()> {
    use mcptest_core::executor::{PlannedTest, Scoring, TargetTransport, ToolPlan};
    use mcptest_core::runner::TestSpec;
    use std::sync::Arc;
    for t in &suite.tests {
        if !selected_set.contains(&t.name) {
            continue;
        }
        specs.push(TestSpec::new(t.name.clone()));
        plans.insert(
            t.name.clone(),
            Arc::new(PlannedTest::Tool(ToolPlan {
                server: t.server.clone(),
                action: t.action.clone(),
                expectations: t.expectations.clone(),
                // WOR-1280: thread the long-form `expect:` budgets through so
                // the executor gates on them, plus the target's transport
                // class so header checks fail actionably on headerless
                // targets. The server reference was validated at load time,
                // so the stdio default only covers a test the validator
                // somehow skipped.
                budgets: t.budgets.clone(),
                transport: suite
                    .servers
                    .get(&t.server)
                    .map(TargetTransport::from_spec)
                    .unwrap_or_default(),
                scoring: Scoring {
                    assert_sets: t.assert_sets.clone(),
                    threshold: t.threshold,
                    derived_metrics: t.derived_metrics.clone(),
                    // Tool tests never opt into the score-without-threshold path.
                    emit_score: false,
                },
                // Thread the test's request/response transform through to the
                // executor. `None` keeps the no-transform path.
                transform: t.transform.clone(),
                // Resolve `inject_error: <name>` against the suite's fixtures
                // table at plan-build time. The cross-reference validator ran
                // at suite-load time, so a missing name here means the fixture
                // was dropped after parsing; leave it `None` and dispatch as if
                // none was declared.
                inject_error: t
                    .inject_error
                    .as_deref()
                    .and_then(|name| suite.fixtures.resolve(name).cloned().map(Box::new)),
                // Thread the test's metamorphic relations through to the
                // executor. `None` keeps the no-metamorphic path.
                metamorphic: t.metamorphic.clone(),
                // Thread the test's fuzz block through. `None` keeps the
                // no-fuzz path.
                fuzz: t.fuzz.clone(),
                // Thread the test's negative-path block through.
                negative_path: t.negative_path.clone(),
                // Resolve the elicitation answer source (test override beats the
                // suite default). REST construction can fail on a bad url /
                // proxy, which fails plan building rather than the first round.
                responder: resolve_responder(t, suite, proxy)?,
            })),
        );
    }
    Ok(())
}

/// Resolve a test's effective elicitation answer source at plan-build
/// time (WOR-1383). Precedence: the test's `input_responses` (static),
/// then its `input_responder`, then the suite-level `input_responder`.
/// A REST responder builds a proxy-aware client here so a bad endpoint
/// fails the run setup, not the first elicitation.
fn resolve_responder(
    test: &mcptest_core::suite::TypedTest,
    suite: &mcptest_core::suite::ResolvedSuite,
    proxy: &mcptest_core::network::ProxyConfig,
) -> Result<Option<mcptest_core::executor::elicitation::Responder>> {
    use mcptest_core::executor::elicitation::{InputResponseFixture, Responder, RestResponder};
    use mcptest_core::suite::InputResponderSpec;

    if let Some(spec) = &test.input_responses {
        return Ok(Some(Responder::Static(InputResponseFixture::from_map(
            spec.values.clone(),
        ))));
    }
    let spec = test
        .input_responder
        .as_ref()
        .or(suite.input_responder.as_ref());
    match spec {
        Some(InputResponderSpec::Rest(rest)) => {
            let responder = RestResponder::from_spec(rest, proxy)
                .map_err(|e| anyhow::anyhow!("test `{}` input_responder: {e}", test.name))?;
            Ok(Some(Responder::Rest(responder)))
        }
        None => Ok(None),
    }
}
