//! Resume-journal and snapshot path helpers for the live runner.
//!
//! These small helpers (journal and snapshot path derivation, the suite
//! fingerprint, and the record-mode resume-journal load) live in their
//! own module so `run_live.rs` stays under the module-size ceiling. They
//! are called only from `run_live`'s async path and stay `pub(super)`.

/// Path to the record-mode resume journal.
///
/// Lives next to the cassettes (`<suite_dir>/cassettes/.run-journal.json`)
/// so it travels with the recordings it complements. Derived from
/// [`cassette_path`]'s parent so the two stay in lockstep if the cassette
/// layout ever moves.
pub(super) fn run_journal_path(config_path: &std::path::Path) -> std::path::PathBuf {
    let parent = config_path
        .parent()
        .unwrap_or_else(|| std::path::Path::new("."));
    parent.join("cassettes").join(".run-journal.json")
}

/// Directory the `snapshot` matcher records and replays under.
///
/// Lives next to the suite as `<suite_dir>/snapshots/`, mirroring the
/// cassette layout so recordings travel with the suite that produced them.
/// A `Matcher::Snapshot(name)` resolves to `<this dir>/<name>.json`.
pub(super) fn snapshot_dir(config_path: &std::path::Path) -> std::path::PathBuf {
    config_path
        .parent()
        .unwrap_or_else(|| std::path::Path::new("."))
        .join("snapshots")
}

/// Build the snapshot record/replay context for a run.
///
/// Snapshots live under [`snapshot_dir`]; `--update-snapshots` flips the mode
/// to re-record (the CI-refusal gate runs upstream in `run_command`).
pub(super) fn snapshot_context(
    config_path: &std::path::Path,
    args: &crate::cli::args::RunArgs,
) -> mcptest_core::executor::SnapshotContext {
    mcptest_core::executor::SnapshotContext {
        dir: snapshot_dir(config_path),
        mode: crate::cli::args::update_snapshots_mode_for(args),
    }
}

/// Stable fingerprint of the resolved suite.
///
/// Hashes the canonical JSON of the post-override suite YAML so any
/// change to agents, models, prompts, or expectations changes the
/// fingerprint and invalidates a resumed run's prior progress. Falls back
/// to the YAML debug rendering if the value cannot serialize to JSON
/// (every suite that parsed does), which still yields a stable string.
pub(super) fn compute_suite_fingerprint(yaml: &serde_yaml::Value) -> String {
    let canonical = serde_json::to_string(yaml).unwrap_or_else(|_| format!("{yaml:?}"));
    mcptest_core::runner::resume::fingerprint(&canonical)
}

/// Load (or freshly stamp) the record-mode resume journal.
///
/// Returns `None` outside record mode (the non-record path replays
/// cassettes, so there is no partial-record state to track). In record
/// mode without `--resume`, returns a fresh journal stamped with the
/// current fingerprint so a future resume has progress to read. With
/// `--resume`, reuses an on-disk journal whose fingerprint matches the
/// current suite, and otherwise warns and starts fresh so a changed suite
/// never reuses stale cells.
pub(super) fn load_resume_journal(
    record: bool,
    resume: bool,
    path: &std::path::Path,
    fingerprint: &str,
    quiet: bool,
) -> Option<mcptest_core::runner::resume::RunJournal> {
    use mcptest_core::runner::resume::RunJournal;
    if !record {
        return None;
    }
    if !resume {
        return Some(RunJournal::for_suite(fingerprint));
    }
    // `load` never errors out of band; a missing or corrupt journal is
    // `Ok(None)`, which we treat the same as a fresh start.
    match RunJournal::load(path).ok().flatten() {
        Some(journal) if journal.suite_fingerprint == fingerprint => Some(journal),
        Some(_) => {
            if !quiet {
                eprintln!(
                    "mcptest run --resume: the suite changed since the last record run, so prior \
                     cells are not reusable; starting fresh."
                );
            }
            Some(RunJournal::for_suite(fingerprint))
        }
        None => Some(RunJournal::for_suite(fingerprint)),
    }
}
