whi 0.8.0

Stupid simple PATH management
Documentation
mod support;

use std::path::Path;

use support::{env_lock, set_env_var};
use whi::path::PathSearcher;
use whi::path::diff::{DiffEntry, compute_diff};
use whi::path::file::{apply_path_sections, expand_shell_vars, format_path_file, parse_path_file};
use whi::path::fuzzy::FuzzyMatcher;
use whi::path::guard::PathGuard;
use whi::path::resolve::expand_tilde;

#[test]
fn expand_tilde_and_fuzzy_matcher() {
    let _lock = env_lock();
    let _home = set_env_var("HOME", "/home/testuser");

    assert_eq!(expand_tilde("~/bin"), "/home/testuser/bin");
    assert_eq!(expand_tilde("~"), "/home/testuser");

    let matcher = FuzzyMatcher::new("cargo bin");
    assert!(matcher.matches(Path::new("/Users/alxknt/.cargo/bin")));
    assert!(!matcher.matches(Path::new("/usr/local/bin")));
}

#[test]
fn path_file_roundtrip_and_shell_vars() {
    let _lock = env_lock();
    let _home = set_env_var("HOME", "/home/testuser");

    let original = "/usr/bin:/bin:/usr/local/bin:/opt/bin";
    let parsed = parse_path_file(&format_path_file(original)).unwrap();
    let reconstructed = apply_path_sections("", &parsed.path).unwrap();
    assert_eq!(reconstructed, original);

    assert_eq!(
        expand_shell_vars("~/bin:$HOME/tools:${HOME}/more"),
        "/home/testuser/bin:/home/testuser/tools:/home/testuser/more"
    );
}

#[test]
fn path_file_validation_error() {
    let result = parse_path_file("!path.replace\n/usr/bin\n\n!path.prepend\n/opt/bin\n");
    assert!(result.unwrap_err().contains("Cannot combine"));
}

#[test]
fn path_searcher_mutations() {
    let searcher = PathSearcher::new("/a:/b:/c:/d:/e");

    assert_eq!(searcher.move_entry(5, 2).unwrap(), "/a:/e:/b:/c:/d");
    assert_eq!(searcher.swap_entries(2, 4).unwrap(), "/a:/d:/c:/b:/e");
    assert_eq!(searcher.delete_entries(&[2, 4]).unwrap(), "/a:/c:/e");

    let (cleaned, removed) = searcher.clean_duplicates();
    assert_eq!(cleaned, "/a:/b:/c:/d:/e");
    assert!(removed.is_empty());
}

#[test]
fn path_searcher_security_filters() {
    let searcher = PathSearcher::new("/good:/bad\0path:/also_good");
    let dirs = searcher.dirs();

    assert_eq!(dirs.len(), 2);
    assert_eq!(dirs[0].to_str().unwrap(), "/good");
    assert_eq!(dirs[1].to_str().unwrap(), "/also_good");
}

#[test]
fn path_diff_mixed_changes() {
    let diff = compute_diff("/d:/a:/c", "/a:/b:/c", false);

    assert!(
        diff.entries
            .iter()
            .any(|e| matches!(e, DiffEntry::Removed(p) if p == "/b"))
    );
    assert!(
        diff.entries
            .iter()
            .any(|e| matches!(e, DiffEntry::Added(p) if p == "/d"))
    );
    assert!(
        diff.entries
            .iter()
            .any(|e| matches!(e, DiffEntry::Moved(p) if p == "/a"))
    );
}

#[test]
fn path_guard_preserves_entries() {
    let guard = PathGuard::new(&["nonexistent_binary_xyz123"]);
    let result = guard.ensure_protected_paths("/usr/bin", "/usr/local/bin".to_string());

    assert_eq!(result, "/usr/local/bin");
}