//! The interruptible drive loop for `mcptest run`.
//!
//! Captures the restart context, brackets the runner with the suite-level
//! hooks, and races the drive against Ctrl-C so a deliberate SIGINT emits a
//! well-formed partial report instead of looking like a test regression. Split
//! out of `run_live` so that module stays under the production-module-size
//! guard; behavior is unchanged.

use anyhow::Result;

use crate::cli::args::RunArgs;
use crate::cli::handlers::run_live_paths::snapshot_context;
use crate::cli::Reporter;

/// Inputs for [`drive_with_restart`]. Bundled (like `BuildPlansCtx`) so the
/// helper has one parameter instead of eleven and `run_live_async` stays under
/// the function-length lint.
pub(super) struct DriveCtx<'a> {
    pub(super) pool: &'a std::sync::Arc<mcptest_core::connector::ServerPool>,
    pub(super) suite: &'a mcptest_core::suite::ResolvedSuite,
    pub(super) plans:
        std::collections::HashMap<String, std::sync::Arc<mcptest_core::executor::PlannedTest>>,
    pub(super) specs: &'a [mcptest_core::runner::TestSpec],
    pub(super) options: mcptest_core::runner::RunnerOptions,
    pub(super) last_real_provider:
        Option<std::sync::Arc<dyn mcptest_core::eval::provider::LlmProvider>>,
    pub(super) last_real_model: Option<String>,
    pub(super) config_path: &'a std::path::Path,
    pub(super) args: &'a RunArgs,
    pub(super) proxy: &'a mcptest_core::network::ProxyConfig,
    pub(super) restart_policy: mcptest_core::runner::restart::RestartPolicy,
    pub(super) coverage: Option<super::run_live_coverage::CoverageHandle>,
}

/// Capture the WOR-1323 respawn context and drive the hook-bracketed
/// runner. The two steps live together because the restart context must
/// be built from the plans before they move into the runner. The suite
/// name (config file stem) seeds each context hook's payload. Split out
/// of `run_live_async` for the function-length lint.
pub(super) async fn drive_with_restart(ctx: DriveCtx<'_>) -> Result<mcptest_core::report::Report> {
    let restart_ctx = super::run_live_restart::build_restart_context(
        ctx.restart_policy,
        ctx.suite,
        &ctx.plans,
        ctx.proxy,
        &ctx.args.server_passthrough,
    );
    super::run_live_drive::run_with_hooks(
        ctx.pool,
        ctx.plans,
        ctx.specs,
        ctx.options,
        ctx.last_real_provider,
        ctx.last_real_model,
        &ctx.suite.hooks,
        suite_name_of(ctx.config_path),
        snapshot_context(ctx.config_path, ctx.args),
        ctx.args.allow_stub_provider,
        restart_ctx,
        ctx.coverage,
    )
    .await
}

/// Outcome of racing the runner drive against Ctrl-C (WOR-1434).
pub(super) enum DriveResult {
    /// The runner finished on its own; carry the report through to the gate.
    Completed(mcptest_core::report::Report),
    /// A SIGINT arrived before the runner finished; the drive future was
    /// dropped and the caller should emit a partial result.
    Interrupted,
}

/// Race the runner drive against `tokio::signal::ctrl_c` (WOR-1434).
///
/// On Ctrl-C the `select!` drops `drive`, which cancels the executor's
/// in-flight tasks and tears the server pool down through `Drop`, then returns
/// [`DriveResult::Interrupted`] so the caller can write the partial result and
/// exit with [`EXIT_INTERRUPTED`]. A `ctrl_c()` registration error (rare, and
/// only on platforms without a working handler) is treated as "no interrupt":
/// the drive simply runs to completion, which is the safe degradation.
pub(super) async fn drive_or_interrupt(
    drive: impl std::future::Future<Output = Result<mcptest_core::report::Report>>,
) -> Result<DriveResult> {
    tokio::pin!(drive);
    tokio::select! {
        result = &mut drive => Ok(DriveResult::Completed(result?)),
        signal = tokio::signal::ctrl_c() => match signal {
            Ok(()) => Ok(DriveResult::Interrupted),
            Err(_) => Ok(DriveResult::Completed(drive.await?)),
        },
    }
}

/// Already-resolved inputs [`emit_interrupted_run`] needs to write the partial
/// result. Bundled so the helper has one parameter and stays under the
/// function-length lint. Borrowed where the emit only reads.
pub(super) struct InterruptCtx<'a> {
    pub(super) specs: &'a [mcptest_core::runner::TestSpec],
    pub(super) config_path: &'a std::path::Path,
    pub(super) quiet: bool,
    pub(super) output: Option<&'a std::path::Path>,
    pub(super) reporter: Option<Reporter>,
    pub(super) annotations: crate::cli::global::AnnotationsMode,
    pub(super) color: crate::cli::global::ColorWhen,
    pub(super) agent_budget: Option<usize>,
    /// WOR-1454: the run's redactor, so the partial result a Ctrl-C produces
    /// is scrubbed against every resolved secret just like a completed run.
    pub(super) redactor: &'a mcptest_core::redaction::Redactor,
}

/// Emit the partial result after a Ctrl-C and return [`EXIT_INTERRUPTED`]
/// (WOR-1434).
///
/// The runner was dropped mid-flight, so there is no per-test report; we
/// synthesize one row per known spec, each marked `Inconclusive` with an
/// "interrupted by SIGINT" note, so the report stays well-formed (the summary
/// counts match the rows) and a reader can see which tests were in scope. The
/// report is stamped, emitted through the normal reporter path, and persisted
/// like any other run before we return.
pub(super) fn emit_interrupted_run(ctx: InterruptCtx<'_>) -> Result<i32> {
    let mut report = super::run_live_print::interrupted_report(ctx.specs);
    super::run_live_print::finalize_run_identity(&mut report, None);
    if !ctx.quiet {
        eprintln!(
            "mcptest run: interrupted by SIGINT after {} test(s) were selected; \
             writing the partial result and exiting {}.",
            ctx.specs.len(),
            crate::cli::exit_codes::EXIT_INTERRUPTED
        );
    }
    super::run_live_print::emit_report(
        &report,
        ctx.output,
        ctx.reporter,
        ctx.annotations,
        ctx.color,
        ctx.quiet,
        ctx.agent_budget,
        Some(ctx.config_path),
        None,
        ctx.redactor,
    )?;
    super::run_live_print::persist_run_artifacts(&report, ctx.redactor);
    Ok(crate::cli::exit_codes::EXIT_INTERRUPTED)
}

/// Suite name for hook payloads: the config file stem, or empty when the
/// path has none. Split out so the drive stays under the function-length lint.
fn suite_name_of(config_path: &std::path::Path) -> &str {
    config_path
        .file_stem()
        .and_then(|s| s.to_str())
        .unwrap_or_default()
}
