//! `run_options:` folding for the live runner (WOR-1283, WOR-1305,
//! WOR-1323).
//!
//! Pulled out of `run_live.rs` so that module stays under the
//! production-module-size guard; behavior is unchanged. Owns reading the
//! typed block from the resolved suite YAML, the parallelism merge, the
//! restart-policy resolution, the inconclusive exit-code fold, and the
//! runner-option construction.

use crate::cli::args::run::RunArgs;

/// The suite's `run_options:` block resolved against the CLI flags, in the
/// shape [`run_live`] consumes (WOR-1323). One struct rather than a tuple
/// so each knob the resolution produces is named at the call site.
pub(super) struct ResolvedRunOptions {
    /// Runner options with the folded parallelism applied.
    pub options: mcptest_core::runner::RunnerOptions,
    /// Whether `inconclusive.treat_as: fail` arms the inconclusive exit
    /// gate (WOR-1305).
    pub inconclusive_fails_run: bool,
    /// The restart policy mapped onto the core runner's enum, ready for
    /// the respawn wrapper (WOR-1323).
    pub restart_policy: mcptest_core::runner::restart::RestartPolicy,
    /// The opt-in failure-source policy (WOR-1508): downgrade classified
    /// transient/external failures out of the exit code. Off by default.
    pub failure_policy: mcptest_core::eval::FailurePolicy,
}

/// Resolve the suite's `run_options:` block against the CLI flags in one
/// pass. Folds `parallel` into the runner's worker count (an explicit CLI
/// flag wins over YAML, YAML over the Auto default; `parallel: false` or
/// `--parallel 1` serializes the run), maps `restart_policy` onto the core
/// runner's enum, and reads whether `inconclusive.treat_as: fail` arms the
/// inconclusive exit gate. A `per_file` or `per_test` policy forces serial
/// execution (WOR-1323): a respawn between boundaries only isolates state
/// when no concurrent test is mid-flight on the same server, so the policy
/// wins over any wider parallel setting, with a stderr note when it
/// actually narrowed one. Split out of [`run_live`] for the
/// function-length lint.
pub(super) fn apply_run_options(
    yaml: &serde_yaml::Value,
    parallel_flag: Option<usize>,
    options: mcptest_core::runner::RunnerOptions,
) -> anyhow::Result<ResolvedRunOptions> {
    use mcptest_core::runner::restart::RestartPolicy;
    use mcptest_core::runner::Parallelism;

    let run_opts = suite_run_options(yaml)?;
    let restart_policy = core_restart_policy(run_opts.restart_policy);
    let inconclusive_fails_run = matches!(
        run_opts.inconclusive.treat_as,
        mcptest_config::run_options::InconclusiveTreatAs::Fail
    );
    let mut parallel = Parallelism::effective(parallel_flag, suite_parallelism(run_opts.parallel));
    if restart_policy != RestartPolicy::PerRun {
        if parallel.resolve() > 1 {
            eprintln!(
                "run_options.restart_policy: {} forces serial execution (parallel setting ignored)",
                restart_policy.label()
            );
        }
        parallel = Parallelism::Fixed(1);
    }
    Ok(ResolvedRunOptions {
        options: mcptest_core::runner::RunnerOptions {
            parallel,
            ..options
        },
        inconclusive_fails_run,
        restart_policy,
        failure_policy: mcptest_core::eval::FailurePolicy {
            downgrade_transient: run_opts.failure_policy.downgrade_transient,
            downgrade_external: run_opts.failure_policy.downgrade_external,
        },
    })
}

/// Bridge the config crate's parsed `restart_policy` onto the core
/// runner's enum (WOR-1323). Mirrors [`suite_parallelism`]: the core
/// runner deliberately does not depend on the config crate, so the CLI
/// converts at the boundary.
pub(super) fn core_restart_policy(
    policy: mcptest_config::run_options::RestartPolicy,
) -> mcptest_core::runner::restart::RestartPolicy {
    use mcptest_config::run_options::RestartPolicy as Config;
    use mcptest_core::runner::restart::RestartPolicy as Core;
    match policy {
        Config::PerRun => Core::PerRun,
        Config::PerFile => Core::PerFile,
        Config::PerTest => Core::PerTest,
    }
}

/// Read the typed `run_options:` block from the resolved suite YAML. An
/// absent block yields the defaults, which match today's behavior, so
/// callers never special-case "no block". Parsed once in [`run_live`] so
/// the parallel, restart-policy, and inconclusive knobs all read the same
/// parse.
pub(super) fn suite_run_options(
    yaml: &serde_yaml::Value,
) -> anyhow::Result<mcptest_config::run_options::RunOptions> {
    let Some(block) = yaml.get("run_options") else {
        return Ok(mcptest_config::run_options::RunOptions::default());
    };
    serde_yaml::from_value(block.clone())
        .map_err(|e| anyhow::anyhow!("run_options parse failed: {e}"))
}

/// Map the suite's `run_options.parallel` onto the runner's
/// [`Parallelism`](mcptest_core::runner::Parallelism). The typed parse
/// lives in `mcptest-config`; this bridges the two crates' enums because
/// the core runner deliberately does not depend on the config crate.
/// Auto stays Auto, `false` serializes, an integer pins workers.
pub(super) fn suite_parallelism(
    parallel: mcptest_config::run_options::Parallel,
) -> mcptest_core::runner::Parallelism {
    use mcptest_config::run_options::Parallel;
    use mcptest_core::runner::Parallelism;
    match parallel {
        Parallel::Auto => Parallelism::Auto,
        Parallel::Off => Parallelism::Fixed(1),
        Parallel::Workers(n) => Parallelism::Fixed(n as usize),
    }
}

/// Fold the run's verdict counts and gate outcomes into the process exit
/// code. Split out of [`report_and_gate`] so the decision is unit-testable
/// without building a full report.
///
/// WOR-1305: an inconclusive verdict only gates when the suite opted in
/// via `run_options.inconclusive.treat_as: fail`; the default keeps it
/// neutral so a genuinely undecided probe does not break CI by surprise.
pub(super) fn fold_exit_code(
    failed: usize,
    gates_failed: bool,
    inconclusive: usize,
    inconclusive_fails_run: bool,
) -> i32 {
    if failed > 0 || gates_failed || (inconclusive_fails_run && inconclusive > 0) {
        1
    } else {
        0
    }
}

/// Project [`RunArgs`] onto [`mcptest_core::runner::RunnerOptions`].
/// Per-flag overrides on individual specs land in a follow-up ticket;
/// today we rely on the suite defaults.
pub(crate) fn build_runner_options(args: &RunArgs) -> mcptest_core::runner::RunnerOptions {
    // `--maxfail N` wins over `--bail` when both are set so a
    // `--bail --maxfail 3` invocation honors the larger explicit cap.
    let max_failures = match (args.maxfail, args.bail) {
        (Some(0), _) => None,
        (Some(n), _) => Some(n),
        (None, true) => Some(1),
        (None, false) => None,
    };
    mcptest_core::runner::RunnerOptions {
        max_failures,
        // WOR-1386: honor --sample-rate; absent means run every test (1.0).
        sample_rate: args.sample_rate.unwrap_or(1.0),
        ..mcptest_core::runner::RunnerOptions::default()
    }
}
