git-workty 0.3.3

Git worktrees as daily-driver workspaces
Documentation
use std::process::Command;
use tempfile::TempDir;

fn git(dir: &std::path::Path, args: &[&str]) -> String {
    let output = Command::new("git")
        .current_dir(dir)
        .args(args)
        .output()
        .expect("Failed to execute git");
    String::from_utf8_lossy(&output.stdout).to_string()
}

fn git_init_repo(dir: &std::path::Path) {
    git(dir, &["init", "-b", "main"]);
    git(dir, &["config", "user.email", "test@test.com"]);
    git(dir, &["config", "user.name", "Test User"]);

    std::fs::write(dir.join("README.md"), "# Test Repo\n").unwrap();
    git(dir, &["add", "."]);
    git(dir, &["commit", "-m", "Initial commit"]);
}

fn workty(dir: &std::path::Path, args: &[&str]) -> std::process::Output {
    let binary = env!("CARGO_BIN_EXE_git-workty");
    Command::new(binary)
        .current_dir(dir)
        .args(args)
        .output()
        .expect("Failed to execute git-workty")
}

fn workty_success(dir: &std::path::Path, args: &[&str]) -> String {
    let output = workty(dir, args);
    assert!(
        output.status.success(),
        "Command failed: {:?}\nstdout: {}\nstderr: {}",
        args,
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );
    String::from_utf8_lossy(&output.stdout).to_string()
}

#[test]
fn test_list_shows_main_worktree() {
    let temp = TempDir::new().unwrap();
    let repo_dir = temp.path();

    git_init_repo(repo_dir);

    let output = workty_success(repo_dir, &["list", "--no-color"]);

    assert!(
        output.contains("master") || output.contains("main"),
        "Output should contain main/master branch: {}",
        output
    );
}

#[test]
fn test_new_creates_worktree() {
    let temp = TempDir::new().unwrap();
    let repo_dir = temp.path();

    git_init_repo(repo_dir);

    let output = workty_success(repo_dir, &["new", "feat/test", "--print-path"]);
    let worktree_path = output.trim();

    assert!(
        std::path::Path::new(worktree_path).exists(),
        "Worktree path should exist: {}",
        worktree_path
    );

    let list_output = workty_success(repo_dir, &["list", "--no-color"]);
    assert!(
        list_output.contains("feat/test"),
        "List should show new worktree: {}",
        list_output
    );
}

#[test]
fn test_go_returns_path() {
    let temp = TempDir::new().unwrap();
    let repo_dir = temp.path();

    git_init_repo(repo_dir);

    let new_output = workty_success(repo_dir, &["new", "test-branch", "--print-path"]);
    let expected_path = new_output.trim();

    let go_output = workty_success(repo_dir, &["go", "test-branch"]);
    let actual_path = go_output.trim();

    assert_eq!(
        expected_path, actual_path,
        "go should return the same path as new"
    );
}

#[test]
fn test_list_json_output() {
    let temp = TempDir::new().unwrap();
    let repo_dir = temp.path();

    git_init_repo(repo_dir);

    let output = workty_success(repo_dir, &["list", "--json"]);

    let parsed: serde_json::Value =
        serde_json::from_str(&output).expect("Output should be valid JSON");

    assert!(parsed.get("repo").is_some(), "JSON should have repo field");
    assert!(
        parsed.get("worktrees").is_some(),
        "JSON should have worktrees field"
    );
}

#[test]
fn test_dirty_detection() {
    let temp = TempDir::new().unwrap();
    let repo_dir = temp.path();

    git_init_repo(repo_dir);

    workty_success(repo_dir, &["new", "dirty-test", "--print-path"]);

    let list_clean = workty_success(repo_dir, &["list", "--json"]);
    let parsed_clean: serde_json::Value = serde_json::from_str(&list_clean).unwrap();

    let worktrees = parsed_clean["worktrees"].as_array().unwrap();
    let dirty_wt = worktrees
        .iter()
        .find(|wt| wt["branch_short"].as_str() == Some("dirty-test"))
        .expect("Should find dirty-test worktree");

    assert_eq!(
        dirty_wt["dirty_count"].as_u64(),
        Some(0),
        "Should be clean initially"
    );

    let go_output = workty_success(repo_dir, &["go", "dirty-test"]);
    let wt_path = std::path::Path::new(go_output.trim());
    std::fs::write(wt_path.join("new-file.txt"), "dirty content").unwrap();

    let list_dirty = workty_success(repo_dir, &["list", "--json"]);
    let parsed_dirty: serde_json::Value = serde_json::from_str(&list_dirty).unwrap();

    let worktrees_dirty = parsed_dirty["worktrees"].as_array().unwrap();
    let dirty_wt_after = worktrees_dirty
        .iter()
        .find(|wt| wt["branch_short"].as_str() == Some("dirty-test"))
        .expect("Should find dirty-test worktree");

    assert!(
        dirty_wt_after["dirty_count"].as_u64().unwrap() > 0,
        "Should detect dirty state"
    );
}

#[test]
fn test_rm_refuses_dirty_without_force() {
    let temp = TempDir::new().unwrap();
    let repo_dir = temp.path();

    git_init_repo(repo_dir);

    let new_output = workty_success(repo_dir, &["new", "to-remove", "--print-path"]);
    let wt_path = std::path::Path::new(new_output.trim());

    std::fs::write(wt_path.join("dirty.txt"), "uncommitted").unwrap();

    let rm_output = workty(repo_dir, &["rm", "to-remove", "--yes"]);

    assert!(
        !rm_output.status.success(),
        "rm should fail for dirty worktree without --force"
    );

    let stderr = String::from_utf8_lossy(&rm_output.stderr);
    assert!(
        stderr.contains("uncommitted") || stderr.contains("--force"),
        "Error should mention uncommitted changes or --force: {}",
        stderr
    );
}

#[test]
fn test_rm_with_force() {
    let temp = TempDir::new().unwrap();
    let repo_dir = temp.path();

    git_init_repo(repo_dir);

    let new_output = workty_success(repo_dir, &["new", "force-remove", "--print-path"]);
    let wt_path = std::path::Path::new(new_output.trim());

    std::fs::write(wt_path.join("dirty.txt"), "uncommitted").unwrap();

    workty_success(repo_dir, &["rm", "force-remove", "--force", "--yes"]);

    let list_output = workty_success(repo_dir, &["list", "--no-color"]);
    assert!(
        !list_output.contains("force-remove"),
        "Worktree should be removed"
    );
}

#[test]
fn test_clean_dry_run() {
    let temp = TempDir::new().unwrap();
    let repo_dir = temp.path();

    git_init_repo(repo_dir);

    workty_success(repo_dir, &["new", "to-clean", "--print-path"]);

    // Clean requires at least one filter (--merged, --gone, or --stale)
    // The worktree isn't merged so it won't be listed, but we verify the command runs
    let clean_output = workty(repo_dir, &["clean", "--merged", "--dry-run"]);

    assert!(
        clean_output.status.success(),
        "Clean command should succeed"
    );

    let list_output = workty_success(repo_dir, &["list", "--no-color"]);
    assert!(
        list_output.contains("to-clean"),
        "Worktree should still exist after dry run"
    );
}

#[test]
fn test_doctor_runs() {
    let temp = TempDir::new().unwrap();
    let repo_dir = temp.path();

    git_init_repo(repo_dir);

    let output = workty(repo_dir, &["doctor"]);

    assert!(output.status.success(), "doctor should succeed");

    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        stderr.contains("Git installed") || stderr.contains(""),
        "Doctor should check Git: {}",
        stderr
    );
}

#[test]
fn test_init_generates_shell_script() {
    let temp = TempDir::new().unwrap();

    let output = workty_success(temp.path(), &["init", "zsh"]);

    assert!(output.contains("wcd"), "Init should define wcd function");
    assert!(output.contains("wnew"), "Init should define wnew function");
    assert!(output.contains("wgo"), "Init should define wgo function");
}

#[test]
fn test_completions_generates_output() {
    let temp = TempDir::new().unwrap();

    let output = workty_success(temp.path(), &["completions", "zsh"]);

    assert!(
        output.contains("git-workty") || output.contains("compdef"),
        "Should generate completion script"
    );
}

#[test]
fn test_help_contains_examples() {
    let temp = TempDir::new().unwrap();

    let output = workty_success(temp.path(), &["--help"]);

    assert!(
        output.contains("EXAMPLES"),
        "Help should contain examples: {}",
        output
    );
    assert!(
        output.contains("git workty new"),
        "Help should show new command example"
    );
}

#[test]
fn test_new_subcommand_help() {
    let temp = TempDir::new().unwrap();

    let output = workty_success(temp.path(), &["new", "--help"]);

    assert!(
        output.contains("--from"),
        "new help should show --from flag"
    );
    assert!(
        output.contains("--print-path"),
        "new help should show --print-path flag"
    );
}