pmpx 0.2.1

Detects a project and routes package manager commands to the matching tool.
//! Tests for the global file, the layered project files, and the path/environment handling.

use super::paths::{ENV_CONFIG_DIR, ENV_DATA_DIR};
use super::*;

fn write(dir: &Path, name: &str, body: &str) -> PathBuf {
    let p = dir.join(name);
    std::fs::write(&p, body).unwrap();
    p
}

#[test]
fn missing_global_config_is_all_defaults() {
    let tmp = tempfile::tempdir().unwrap();
    let cfg = GlobalConfig::load_from(&tmp.path().join("nope.toml")).unwrap();

    assert_eq!(cfg.plugin.family_priority[0], "node");
    assert_eq!(cfg.plugin.priority[0], "pnpm");
    assert!(cfg.discovery.walk_up);
    assert_eq!(cfg.discovery.max_depth, 8);
    assert!(cfg.discovery.stop_at_git);
    assert!(cfg.plugin_store.effective_prefer_prebuilt());
}

#[test]
fn unknown_keys_in_the_global_config_are_kept() {
    let tmp = tempfile::tempdir().unwrap();
    let path = write(
        tmp.path(),
        "config.toml",
        r#"
[plugin]
family_priority = ["rust", "node"]

# added by the user, pmpx does not know it
[my_own_thing]
keep = "me"
"#,
    );

    let cfg = GlobalConfig::load_from(&path).unwrap();
    assert_eq!(cfg.plugin.family_priority, vec!["rust", "node"]);
    assert_eq!(cfg.plugin.priority[0], "pnpm");
    // Unrecognized sections must survive — `pmpx config set` relies on this to carry them back
    // into the file verbatim
    assert!(cfg.extra.contains_key("my_own_thing"));
}

#[test]
fn a_broken_global_config_is_an_error_not_a_silent_default() {
    let tmp = tempfile::tempdir().unwrap();
    let path = write(tmp.path(), "config.toml", "this is not toml = = =");

    let err = GlobalConfig::load_from(&path).unwrap_err();
    assert!(
        err.to_string()
            .contains("failed to parse the global config"),
        "{err}"
    );
}

#[test]
fn project_config_missing_is_none_not_default() {
    let tmp = tempfile::tempdir().unwrap();
    assert!(ProjectConfig::load_from(&tmp.path().join("nope.toml"))
        .unwrap()
        .is_none());
}

#[test]
fn near_config_wins_over_far_one() {
    let tmp = tempfile::tempdir().unwrap();
    let far = write(
        tmp.path(),
        "far.toml",
        "[plugin]\nnode = \"npm\"\nrust = \"cargo\"\n",
    );
    let near = write(tmp.path(), "near.toml", "[plugin]\nnode = \"pnpm\"\n");

    let merged = MergedProjectConfig::from_paths_near_to_far(&[near.clone(), far.clone()]).unwrap();

    assert_eq!(
        merged.pinned_plugin("node"),
        Some("pnpm"),
        "the nearer one wins"
    );
    assert_eq!(
        merged.pinned_plugin("rust"),
        Some("cargo"),
        "the farther one is not clobbered"
    );
    assert_eq!(merged.pinned_plugin("python"), None);
}

#[test]
fn overlaying_unknown_keys_does_not_clobber_known_ones() {
    let mut base = ProjectConfig::default();
    base.plugin.insert("node".into(), "npm".into());

    let mut other = ProjectConfig::default();
    other.plugin.insert("node".into(), "pnpm".into());
    base.overlay(other);

    assert_eq!(base.plugin.get("node").map(String::as_str), Some("pnpm"));
}

#[test]
fn scripts_merge_across_layers() {
    let tmp = tempfile::tempdir().unwrap();
    let far = write(
        tmp.path(),
        "far.toml",
        "[scripts]\nfmt = \"run format\"\nlint = \"run lint\"\n",
    );
    let near = write(tmp.path(), "near.toml", "[scripts]\nfmt = \"run f\"\n");

    let merged = MergedProjectConfig::from_paths_near_to_far(&[near, far]).unwrap();
    assert_eq!(merged.scripts.get("fmt").map(String::as_str), Some("run f"));
    assert_eq!(
        merged.scripts.get("lint").map(String::as_str),
        Some("run lint")
    );
}

#[test]
fn sources_are_recorded_near_to_far() {
    let tmp = tempfile::tempdir().unwrap();
    let far = write(tmp.path(), "far.toml", "[plugin]\nrust = \"cargo\"\n");
    let near = write(tmp.path(), "near.toml", "[plugin]\nnode = \"pnpm\"\n");

    let merged = MergedProjectConfig::from_paths_near_to_far(&[near.clone(), far.clone()]).unwrap();

    assert_eq!(merged.sources, vec![near, far], "near to far");
}

#[test]
fn a_config_file_that_vanished_mid_walk_is_skipped() {
    let tmp = tempfile::tempdir().unwrap();
    let real = write(tmp.path(), "real.toml", "[plugin]\nrust = \"cargo\"\n");
    let ghost = tmp.path().join("ghost.toml");

    let merged = MergedProjectConfig::from_paths_near_to_far(&[ghost, real]).unwrap();
    assert_eq!(merged.pinned_plugin("rust"), Some("cargo"));
    assert_eq!(merged.sources.len(), 1);
}

#[test]
fn expand_tilde_expands_to_home() {
    let home = directories::UserDirs::new()
        .unwrap()
        .home_dir()
        .to_path_buf();

    assert_eq!(expand_tilde("~/x/y"), home.join("x").join("y"));
    assert_eq!(expand_tilde("~"), home);
    // `~user` is not handled, and ordinary paths must not be mangled
    assert_eq!(expand_tilde("/abs/path"), PathBuf::from("/abs/path"));
    assert_eq!(
        expand_tilde("relative/path"),
        PathBuf::from("relative/path")
    );
}

#[test]
fn pinned_families_lists_every_pin() {
    let tmp = tempfile::tempdir().unwrap();
    let a = write(
        tmp.path(),
        "a.toml",
        "[plugin]\nnode = \"pnpm\"\nrust = \"cargo\"\n",
    );

    let merged = MergedProjectConfig::from_paths_near_to_far(&[a]).unwrap();
    let mut fams = merged.pinned_families();
    fams.sort_unstable();
    assert_eq!(fams, vec!["node", "rust"]);
}

// ---- Environment variable overrides -----------------------------------

/// Both override variables must actually take effect — otherwise e2e tests could only touch the
/// user's real config directory.
///
/// It mutates process-level environment variables: set inside one test only, original values
/// saved, restored before asserting, and only the `PMPX_`-prefixed variables read by this module
/// are touched.
#[test]
fn env_overrides_take_effect() {
    let tmp = tempfile::tempdir().unwrap();
    let cfg_dir = tmp.path().join("cfg");
    let data_dir = tmp.path().join("data");

    let old_cfg = std::env::var_os(ENV_CONFIG_DIR);
    let old_data = std::env::var_os(ENV_DATA_DIR);

    // SAFETY: see the note above about scope.
    unsafe {
        std::env::set_var(ENV_CONFIG_DIR, &cfg_dir);
        std::env::set_var(ENV_DATA_DIR, &data_dir);
    }

    let got_cfg = global_config_path().unwrap();
    let got_data = default_data_dir().unwrap();

    // Restore before asserting — a failing assertion must not leave the environment dirty
    unsafe {
        match old_cfg {
            Some(v) => std::env::set_var(ENV_CONFIG_DIR, v),
            None => std::env::remove_var(ENV_CONFIG_DIR),
        }
        match old_data {
            Some(v) => std::env::set_var(ENV_DATA_DIR, v),
            None => std::env::remove_var(ENV_DATA_DIR),
        }
    }

    assert_eq!(got_cfg, cfg_dir.join("config.toml"));
    assert_eq!(got_data, data_dir);
}

/// Without overrides the platform-correct locations are used.
#[test]
fn without_overrides_the_platform_paths_are_used() {
    if std::env::var_os(ENV_CONFIG_DIR).is_none() {
        let p = global_config_path().unwrap();
        assert!(p.ends_with("config.toml"), "{p:?}");
        assert!(
            p.to_string_lossy().contains("pmpx"),
            "the path should contain pmpx: {p:?}"
        );
    }
    if std::env::var_os(ENV_DATA_DIR).is_none() {
        let p = default_data_dir().unwrap();
        assert_eq!(p.file_name().unwrap().to_string_lossy(), ".pmpx");
    }
}