//! Decide when a `restart_policy:` configured suite needs to bounce
//! a stdio server between test boundaries.
//!
//! The runner queries this library at three boundaries during a
//! `mcptest run` invocation:
//!
//! 1. Before the first test in the run.
//! 2. Between two tests inside the same file.
//! 3. Between two tests across a file boundary.
//!
//! For each one the runner asks
//! [`restart_action(policy, boundary)`] which kind of restart to
//! perform. The decision is pure: no I/O, no server pool access. The
//! runner is responsible for actually calling
//! [`ServerPool::shutdown_one`][crate::connector::ServerPool::shutdown_one]
//! followed by `connect_server` + `insert` when the decision says
//! restart.
//!
//! `RestartPolicy` is the typed value the `run_options:` parser hands
//! the runner; the variants are mirrored from
//! `mcptest_config::run_options::RestartPolicy` so the runner does
//! not depend on the config crate.

/// Restart policy values the suite can declare. Mirrors the
/// `mcptest-config` parser shape; the runner converts at the boundary.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum RestartPolicy {
    /// Spawn the server once per `mcptest run` invocation (the default,
    /// used when no policy is set).
    #[default]
    PerRun,
    /// Restart the server before each test file.
    PerFile,
    /// Restart the server before each test.
    PerTest,
}

impl RestartPolicy {
    /// The YAML spelling of this policy (`per_run`, `per_file`,
    /// `per_test`). Exists so user-facing messages quote the exact value
    /// the author wrote instead of the Rust variant name (WOR-1323).
    pub fn label(self) -> &'static str {
        match self {
            RestartPolicy::PerRun => "per_run",
            RestartPolicy::PerFile => "per_file",
            RestartPolicy::PerTest => "per_test",
        }
    }
}

/// Where in the run the runner is asking about. The runner advances
/// through these as it iterates tests.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Boundary {
    /// Before the first test in the run.
    RunStart,
    /// Between two tests in the same file.
    BetweenTestsInFile,
    /// Between two tests across a file boundary.
    BetweenFiles,
}

/// What the runner should do at this boundary.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RestartAction {
    /// No restart needed; reuse the existing server.
    Keep,
    /// Tear the named server down and reconnect before the next test.
    Restart,
}

/// Decide the restart action for a boundary.
///
/// `PerRun` never restarts mid-run. `PerFile` restarts between files
/// and at run start (so the first file in the run begins against a
/// fresh process). `PerTest` restarts before every test, including the
/// first.
pub fn restart_action(policy: RestartPolicy, boundary: Boundary) -> RestartAction {
    match (policy, boundary) {
        (RestartPolicy::PerRun, _) => RestartAction::Keep,
        (RestartPolicy::PerFile, Boundary::RunStart | Boundary::BetweenFiles) => {
            RestartAction::Restart
        }
        (RestartPolicy::PerFile, Boundary::BetweenTestsInFile) => RestartAction::Keep,
        (RestartPolicy::PerTest, _) => RestartAction::Restart,
    }
}

/// Classify the boundary between the previously executed test and the next
/// one from their source files (WOR-1323).
///
/// The runner iterates tests serially under a non-default restart policy,
/// so the only state it needs is the previous test's `file`. The outer
/// `Option` is "was there a previous test at all" (`None` means the run is
/// about to dispatch its first test); the inner `Option<String>` mirrors
/// [`TestSpec::file`](super::TestSpec), which a single-document run leaves
/// `None`, so two file-less tests deliberately classify as the same file.
pub fn classify_boundary(
    prev_file: Option<&Option<String>>,
    next_file: &Option<String>,
) -> Boundary {
    match prev_file {
        None => Boundary::RunStart,
        Some(prev) if prev == next_file => Boundary::BetweenTestsInFile,
        Some(_) => Boundary::BetweenFiles,
    }
}

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

    #[test]
    fn per_run_never_restarts_mid_run() {
        for b in [
            Boundary::RunStart,
            Boundary::BetweenTestsInFile,
            Boundary::BetweenFiles,
        ] {
            assert_eq!(
                restart_action(RestartPolicy::PerRun, b),
                RestartAction::Keep
            );
        }
    }

    #[test]
    fn per_file_restarts_at_run_start_and_between_files() {
        assert_eq!(
            restart_action(RestartPolicy::PerFile, Boundary::RunStart),
            RestartAction::Restart
        );
        assert_eq!(
            restart_action(RestartPolicy::PerFile, Boundary::BetweenFiles),
            RestartAction::Restart
        );
    }

    #[test]
    fn per_file_does_not_restart_between_tests_inside_one_file() {
        // Tests inside a file share state on purpose. Restarting between
        // them would be the per-test policy, not per-file.
        assert_eq!(
            restart_action(RestartPolicy::PerFile, Boundary::BetweenTestsInFile),
            RestartAction::Keep
        );
    }

    #[test]
    fn per_test_restarts_at_every_boundary_including_run_start() {
        for b in [
            Boundary::RunStart,
            Boundary::BetweenTestsInFile,
            Boundary::BetweenFiles,
        ] {
            assert_eq!(
                restart_action(RestartPolicy::PerTest, b),
                RestartAction::Restart
            );
        }
    }

    #[test]
    fn default_policy_is_per_run() {
        let p: RestartPolicy = Default::default();
        assert_eq!(p, RestartPolicy::PerRun);
    }

    #[test]
    fn first_test_classifies_as_run_start() {
        assert_eq!(
            classify_boundary(None, &Some("a.yaml".to_string())),
            Boundary::RunStart
        );
        assert_eq!(classify_boundary(None, &None), Boundary::RunStart);
    }

    #[test]
    fn same_file_classifies_between_tests_in_file() {
        let file = Some("a.yaml".to_string());
        assert_eq!(
            classify_boundary(Some(&file), &file.clone()),
            Boundary::BetweenTestsInFile
        );
        // A single-document run leaves `TestSpec::file` unset on every
        // test, so file-less tests share one "file" by construction.
        assert_eq!(
            classify_boundary(Some(&None), &None),
            Boundary::BetweenTestsInFile
        );
    }

    #[test]
    fn different_file_classifies_between_files() {
        let prev = Some("a.yaml".to_string());
        assert_eq!(
            classify_boundary(Some(&prev), &Some("b.yaml".to_string())),
            Boundary::BetweenFiles
        );
        assert_eq!(
            classify_boundary(Some(&prev), &None),
            Boundary::BetweenFiles
        );
    }

    #[test]
    fn labels_match_the_yaml_spelling() {
        assert_eq!(RestartPolicy::PerRun.label(), "per_run");
        assert_eq!(RestartPolicy::PerFile.label(), "per_file");
        assert_eq!(RestartPolicy::PerTest.label(), "per_test");
    }
}
