//! Default config-path resolution shared by every subcommand that falls
//! back to a config file in the working directory.
//!
//! Why this exists (WOR-1350): `mcptest init` scaffolds `mcptest.yml`,
//! but the run-shaped subcommands historically defaulted to
//! `./mcptest.yaml` only, so a fresh `init` followed by a bare `run`
//! dead-ended on "no config". Centralizing the candidate list here keeps
//! every subcommand resolving the same two names in the same order, and
//! makes the both-files-present case a loud error instead of a silent
//! pick that lets the other file rot unnoticed.

use std::path::{Path, PathBuf};

/// The file names a bare invocation looks for, in resolution order.
/// `mcptest.yaml` stays first for continuity with the historical
/// default; `mcptest.yml` is what `mcptest init` scaffolds.
pub(crate) const DEFAULT_CONFIG_CANDIDATES: [&str; 2] = ["mcptest.yaml", "mcptest.yml"];

/// Outcome of resolving the implicit config path in a directory.
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum DefaultConfig {
    /// Exactly one candidate exists; use it.
    Found(PathBuf),
    /// Neither candidate exists. Carries `<dir>/mcptest.yaml` so callers
    /// that proceed without a config still hold a concrete path, and
    /// their downstream "missing file" handling stays unchanged.
    Missing(PathBuf),
}

impl DefaultConfig {
    /// Collapse to the path, for callers whose downstream error path
    /// already copes with a missing file (a read error, a "nothing to
    /// analyze" no-op) and only needs something to name.
    pub(crate) fn into_path(self) -> PathBuf {
        match self {
            DefaultConfig::Found(path) | DefaultConfig::Missing(path) => path,
        }
    }
}

/// Both candidate files exist, so a bare invocation is ambiguous.
///
/// Picking one silently would let edits to the other file go unnoticed,
/// which is worse than stopping. Callers print [`AmbiguousConfig::message`]
/// and exit with [`crate::cli::exit_codes::EXIT_INVALID_INVOCATION`].
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct AmbiguousConfig {
    /// Path of the existing `mcptest.yaml`.
    pub(crate) yaml: PathBuf,
    /// Path of the existing `mcptest.yml`.
    pub(crate) yml: PathBuf,
}

impl AmbiguousConfig {
    /// One-line error naming both files and the way out, so the operator
    /// never has to guess which one a silent pick would have used.
    pub(crate) fn message(&self) -> String {
        format!(
            "both {} and {} exist; pass --config (or a path argument) to choose one, or remove one of the files",
            self.yaml.display(),
            self.yml.display()
        )
    }
}

/// Resolve the implicit config path in `dir`: `mcptest.yaml` first, then
/// `mcptest.yml`, with a hard error when both exist.
pub(crate) fn resolve_default_config(dir: &Path) -> Result<DefaultConfig, AmbiguousConfig> {
    let yaml = dir.join(DEFAULT_CONFIG_CANDIDATES[0]);
    let yml = dir.join(DEFAULT_CONFIG_CANDIDATES[1]);
    match (yaml.is_file(), yml.is_file()) {
        (true, true) => Err(AmbiguousConfig { yaml, yml }),
        (true, false) => Ok(DefaultConfig::Found(yaml)),
        (false, true) => Ok(DefaultConfig::Found(yml)),
        (false, false) => Ok(DefaultConfig::Missing(yaml)),
    }
}

/// Render the "no config" hint naming both accepted file names, so a
/// user with neither file learns every spelling the default accepts.
pub(crate) fn no_config_message(dir: &Path) -> String {
    format!(
        "no config at {} or {}",
        dir.join(DEFAULT_CONFIG_CANDIDATES[0]).display(),
        dir.join(DEFAULT_CONFIG_CANDIDATES[1]).display()
    )
}

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

    #[test]
    fn resolves_yaml_when_only_yaml_exists() {
        let dir = tempfile::tempdir().unwrap();
        std::fs::write(dir.path().join("mcptest.yaml"), "tests: []\n").unwrap();
        let resolved = resolve_default_config(dir.path()).unwrap();
        assert_eq!(
            resolved,
            DefaultConfig::Found(dir.path().join("mcptest.yaml"))
        );
    }

    #[test]
    fn falls_back_to_yml_when_only_yml_exists() {
        let dir = tempfile::tempdir().unwrap();
        std::fs::write(dir.path().join("mcptest.yml"), "tests: []\n").unwrap();
        let resolved = resolve_default_config(dir.path()).unwrap();
        assert_eq!(
            resolved,
            DefaultConfig::Found(dir.path().join("mcptest.yml"))
        );
    }

    #[test]
    fn errors_when_both_candidates_exist() {
        let dir = tempfile::tempdir().unwrap();
        std::fs::write(dir.path().join("mcptest.yaml"), "tests: []\n").unwrap();
        std::fs::write(dir.path().join("mcptest.yml"), "tests: []\n").unwrap();
        let err = resolve_default_config(dir.path()).unwrap_err();
        let message = err.message();
        assert!(message.contains("mcptest.yaml"), "{message}");
        assert!(message.contains("mcptest.yml"), "{message}");
        assert!(message.contains("--config"), "{message}");
    }

    #[test]
    fn reports_missing_with_the_primary_candidate_path() {
        let dir = tempfile::tempdir().unwrap();
        let resolved = resolve_default_config(dir.path()).unwrap();
        assert_eq!(
            resolved,
            DefaultConfig::Missing(dir.path().join("mcptest.yaml"))
        );
    }

    #[test]
    fn missing_message_names_both_candidates() {
        let dir = tempfile::tempdir().unwrap();
        let message = no_config_message(dir.path());
        assert!(message.contains("mcptest.yaml"), "{message}");
        assert!(message.contains("mcptest.yml"), "{message}");
    }
}
