torudo 0.18.0

A terminal-based todo.txt viewer and manager with TUI interface
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;

const fn bin() -> &'static str {
    env!("CARGO_BIN_EXE_torudo")
}

fn fresh_dir(name: &str) -> PathBuf {
    let dir = std::env::temp_dir().join(name);
    fs::remove_dir_all(&dir).ok();
    fs::create_dir_all(&dir).unwrap();
    dir
}

fn run(dir: &Path, args: &[&str]) -> std::process::Output {
    let mut full = vec!["--todotxt-dir", dir.to_str().unwrap()];
    full.extend_from_slice(args);
    Command::new(bin())
        .args(&full)
        .output()
        .expect("failed to run torudo")
}

fn stdout_of(output: &std::process::Output) -> String {
    assert!(
        output.status.success(),
        "non-zero exit: stderr={}",
        String::from_utf8_lossy(&output.stderr)
    );
    String::from_utf8_lossy(&output.stdout).to_string()
}

#[test]
fn show_returns_the_item_with_its_detail_md() {
    let dir = fresh_dir("torudo_it_show_with_md");
    fs::write(dir.join("todo.txt"), "Fix the spec id:aaa\n").unwrap();
    fs::create_dir_all(dir.join("todos")).unwrap();
    fs::write(dir.join("todos/aaa.md"), "- [x] done\n- [ ] todo\n").unwrap();

    let out = stdout_of(&run(&dir, &["show", "aaa"]));
    let json: serde_json::Value = serde_json::from_str(out.trim()).unwrap();

    assert_eq!(json["id"], "aaa");
    assert_eq!(json["title"], "Fix the spec");
    assert_eq!(json["md"], "- [x] done\n- [ ] todo\n");
}

#[test]
fn show_reports_the_mode_it_found_the_item_in() {
    let dir = fresh_dir("torudo_it_show_mode");
    fs::write(dir.join("waiting.txt"), "Reply from bob id:bbb\n").unwrap();

    let out = stdout_of(&run(&dir, &["show", "bbb"]));
    let json: serde_json::Value = serde_json::from_str(out.trim()).unwrap();

    assert_eq!(json["mode"], "waiting");
}

#[test]
fn show_omits_the_md_key_when_there_is_no_detail_file() {
    let dir = fresh_dir("torudo_it_show_no_md");
    fs::write(dir.join("todo.txt"), "Fix the spec id:aaa\n").unwrap();

    let out = stdout_of(&run(&dir, &["show", "aaa"]));
    let json: serde_json::Value = serde_json::from_str(out.trim()).unwrap();

    assert!(
        json.get("md").is_none(),
        "a missing detail file drops the key instead of printing null: {json}"
    );
}

#[test]
fn show_finds_a_completed_item_in_the_archive() {
    let dir = fresh_dir("torudo_it_show_done");
    fs::write(dir.join("done.txt"), "x 2026-08-19 Old thing id:zzz\n").unwrap();

    let out = stdout_of(&run(&dir, &["show", "zzz"]));
    let json: serde_json::Value = serde_json::from_str(out.trim()).unwrap();

    assert_eq!(json["id"], "zzz");
    assert_eq!(json["mode"], "done");
}

#[test]
fn show_prefers_a_live_mode_over_the_archive() {
    let dir = fresh_dir("torudo_it_show_live_wins");
    fs::write(dir.join("todo.txt"), "Fix the spec id:aaa\n").unwrap();
    fs::write(dir.join("done.txt"), "x 2026-08-19 Fix the spec id:aaa\n").unwrap();

    let out = stdout_of(&run(&dir, &["show", "aaa"]));
    let json: serde_json::Value = serde_json::from_str(out.trim()).unwrap();

    assert_eq!(json["mode"], "todo", "the actionable copy wins: {json}");
}

#[test]
fn show_fails_on_an_unknown_id() {
    let dir = fresh_dir("torudo_it_show_unknown");
    fs::write(dir.join("todo.txt"), "Fix the spec id:aaa\n").unwrap();

    let output = run(&dir, &["show", "nope"]);

    assert!(!output.status.success(), "an unknown id is an error");
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(stderr.contains("nope"), "stderr names the id: {stderr}");
}