wtui 0.1.1

A terminal UI and CLI for managing Git worktrees across repositories
use std::fs;
use std::path::Path;
use std::process::{Command, Output};

#[test]
fn catalog_commands_register_deduplicate_edit_list_and_remove() {
    let directory = tempfile::tempdir().unwrap();
    let main = directory.path().join("main repository");
    let linked = directory.path().join("linked repository");
    let config = directory.path().join("config/wt.json");
    git(directory.path(), &["init", main.to_str().unwrap()]);
    git(&main, &["config", "user.email", "test@example.com"]);
    git(&main, &["config", "user.name", "Test User"]);
    git(&main, &["commit", "--allow-empty", "-m", "initial"]);
    git(
        &main,
        &["worktree", "add", "-b", "linked", linked.to_str().unwrap()],
    );

    let add = wt(
        &config,
        &[
            "repo",
            "add",
            linked.to_str().unwrap(),
            "--label",
            "project",
            "--worktree-root",
            "trees",
            "--github-remote",
            "upstream",
        ],
    );
    assert_success(&add);
    let stored: serde_json::Value = serde_json::from_slice(&fs::read(&config).unwrap()).unwrap();
    assert_eq!(stored["version"], 1);
    assert_eq!(
        stored["repositories"][0]["path"],
        fs::canonicalize(&main).unwrap().to_str().unwrap()
    );
    assert_eq!(stored["repositories"][0]["label"], "project");
    assert!(Path::new(stored["repositories"][0]["worktree_root"].as_str().unwrap()).is_absolute());

    let duplicate = wt(&config, &["repo", "add", main.to_str().unwrap()]);
    assert!(!duplicate.status.success());
    assert!(stderr(&duplicate).contains("already registered"));

    let edit = wt(
        &config,
        &[
            "repo",
            "edit",
            "project",
            "--label",
            "renamed",
            "--clear-worktree-root",
            "--clear-github-remote",
        ],
    );
    assert_success(&edit);
    let list = wt(&config, &["repo", "list"]);
    assert_success(&list);
    assert!(stdout(&list).contains("renamed"));
    assert!(stdout(&list).contains("normal"));

    let remove = wt(&config, &["repo", "remove", "renamed"]);
    assert_success(&remove);
    assert!(
        main.exists(),
        "unregistering must not delete the repository"
    );
    let stored: serde_json::Value = serde_json::from_slice(&fs::read(&config).unwrap()).unwrap();
    assert_eq!(stored["repositories"].as_array().unwrap().len(), 0);
}

#[test]
fn list_retains_and_marks_a_missing_repository_stale() {
    let directory = tempfile::tempdir().unwrap();
    let config = directory.path().join("wt.json");
    fs::write(
        &config,
        format!(
            "{{\"version\":1,\"repositories\":[{{\"path\":{path:?},\"label\":\"missing\"}}]}}",
            path = directory.path().join("gone").to_str().unwrap()
        ),
    )
    .unwrap();
    let list = wt(&config, &["repo", "list"]);
    assert_success(&list);
    assert!(stdout(&list).contains("stale:"));
    assert!(fs::read_to_string(config).unwrap().contains("missing"));
}

#[test]
fn edit_relinks_a_stale_repository() {
    let directory = tempfile::tempdir().unwrap();
    let repository = directory.path().join("new location");
    let config = directory.path().join("wt.json");
    fs::write(
        &config,
        format!(
            "{{\"version\":1,\"repositories\":[{{\"path\":{path:?},\"label\":\"moved\"}}]}}",
            path = directory.path().join("old location").to_str().unwrap()
        ),
    )
    .unwrap();
    git(directory.path(), &["init", repository.to_str().unwrap()]);

    let edit = wt(
        &config,
        &[
            "repo",
            "edit",
            "moved",
            "--path",
            repository.to_str().unwrap(),
            "--clear-label",
        ],
    );
    assert_success(&edit);
    let list = wt(&config, &["repo", "list"]);
    assert_success(&list);
    assert!(stdout(&list).contains("new location"));
    assert!(!stdout(&list).contains("stale:"));
}

#[test]
fn bare_repository_is_registered_and_classified() {
    let directory = tempfile::tempdir().unwrap();
    let bare = directory.path().join("project.git");
    let config = directory.path().join("wt.json");
    git(
        directory.path(),
        &["init", "--bare", bare.to_str().unwrap()],
    );
    assert_success(&wt(&config, &["repo", "add", bare.to_str().unwrap()]));
    let list = wt(&config, &["repo", "list"]);
    assert_success(&list);
    assert!(stdout(&list).contains("bare"));
}

#[test]
fn config_repository_root_preserves_expression_and_shows_resolution() {
    let directory = tempfile::tempdir().unwrap();
    let config = directory.path().join("config/wt.json");
    let repository_base = directory.path().join("repository base");
    let expression = "${WT_TEST_REPOSITORY_BASE}/authored";
    let set = Command::new(env!("CARGO_BIN_EXE_wt"))
        .env("WT_CONFIG_PATH", &config)
        .env("WT_TEST_REPOSITORY_BASE", &repository_base)
        .args(["config", "set", "repository-root", expression])
        .output()
        .unwrap();
    assert_success(&set);
    assert!(repository_base.join("authored").is_dir());
    assert!(stdout(&set).contains(&format!("configured={expression}")));
    assert!(stdout(&set).contains(&format!(
        "resolved={}",
        fs::canonicalize(repository_base.join("authored"))
            .unwrap()
            .display()
    )));

    let stored: serde_json::Value = serde_json::from_slice(&fs::read(&config).unwrap()).unwrap();
    assert_eq!(stored["repository_root"], expression);

    let show = Command::new(env!("CARGO_BIN_EXE_wt"))
        .env("WT_CONFIG_PATH", &config)
        .env("WT_TEST_REPOSITORY_BASE", &repository_base)
        .args(["config", "show"])
        .output()
        .unwrap();
    assert_success(&show);
    assert!(stdout(&show).contains(&format!("configured={expression}")));
    assert!(stdout(&show).contains("github-host\tgithub.com"));
}

#[test]
fn config_rejects_undefined_variables_without_changing_the_catalog() {
    let directory = tempfile::tempdir().unwrap();
    let config = directory.path().join("wt.json");
    let output = Command::new(env!("CARGO_BIN_EXE_wt"))
        .env("WT_CONFIG_PATH", &config)
        .env_remove("WT_TEST_UNDEFINED_ROOT")
        .args([
            "config",
            "set",
            "repository-root",
            "$WT_TEST_UNDEFINED_ROOT/repos",
        ])
        .output()
        .unwrap();
    assert!(!output.status.success());
    assert!(stderr(&output).contains("is not defined"));
    assert!(!config.exists());
}

#[test]
fn config_commands_are_completed() {
    let directory = tempfile::tempdir().unwrap();
    let config = directory.path().join("wt.json");
    let top = wt(&config, &["__complete", "c"]);
    assert_success(&top);
    assert!(stdout(&top).lines().any(|line| line == "config"));

    let setting = wt(&config, &["__complete", "config", "set", "r"]);
    assert_success(&setting);
    assert!(
        stdout(&setting)
            .lines()
            .any(|line| line == "repository-root")
    );
}

fn wt(config: &Path, arguments: &[&str]) -> Output {
    Command::new(env!("CARGO_BIN_EXE_wt"))
        .env("WT_CONFIG_PATH", config)
        .args(arguments)
        .output()
        .unwrap()
}

fn git(directory: &Path, arguments: &[&str]) {
    let output = Command::new("git")
        .arg("-C")
        .arg(directory)
        .args(arguments)
        .output()
        .unwrap();
    assert_success(&output);
}

fn assert_success(output: &Output) {
    assert!(
        output.status.success(),
        "command failed\nstdout: {}\nstderr: {}",
        stdout(output),
        stderr(output)
    );
}

fn stdout(output: &Output) -> String {
    String::from_utf8_lossy(&output.stdout).into_owned()
}

fn stderr(output: &Output) -> String {
    String::from_utf8_lossy(&output.stderr).into_owned()
}