//! End to end tests that run the xtodo binary in a temporary directory.

use std::fs;
use std::path::PathBuf;
use std::process::{Command, Output};
use std::sync::atomic::{AtomicUsize, Ordering};

struct TestDir {
    path: PathBuf,
}

impl TestDir {
    fn new(name: &str) -> TestDir {
        static COUNTER: AtomicUsize = AtomicUsize::new(0);
        let unique = COUNTER.fetch_add(1, Ordering::SeqCst);
        let path = std::env::temp_dir().join(format!(
            "xtodo-test-{}-{}-{}",
            std::process::id(),
            name,
            unique
        ));
        let _ = fs::remove_dir_all(&path);
        fs::create_dir_all(&path).expect("create temp dir");
        TestDir { path }
    }

    fn write(&self, name: &str, content: &str) -> PathBuf {
        let path = self.path.join(name);
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent).expect("create fixture directory");
        }
        fs::write(&path, content).expect("write fixture");
        path
    }

    fn read(&self, name: &str) -> String {
        fs::read_to_string(self.path.join(name)).expect("read fixture")
    }

    fn run(&self, args: &[&str]) -> Output {
        Command::new(env!("CARGO_BIN_EXE_xtodo"))
            .args(args)
            .current_dir(&self.path)
            .env("XTODO_TODAY", "2024-03-10")
            .env_remove("XTODO_UTC_OFFSET")
            .output()
            .expect("run xtodo")
    }

    /// Run with answers piped to stdin, the way a user types them.
    fn run_with_input(&self, args: &[&str], input: &str) -> Output {
        use std::io::Write;
        use std::process::Stdio;
        let mut child = Command::new(env!("CARGO_BIN_EXE_xtodo"))
            .args(args)
            .current_dir(&self.path)
            .env("XTODO_TODAY", "2024-03-10")
            .env_remove("XTODO_UTC_OFFSET")
            .stdin(Stdio::piped())
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .spawn()
            .expect("spawn xtodo");
        child
            .stdin
            .as_mut()
            .expect("stdin")
            .write_all(input.as_bytes())
            .expect("write answers");
        child.wait_with_output().expect("wait for xtodo")
    }
}

impl Drop for TestDir {
    fn drop(&mut self) {
        let _ = fs::remove_dir_all(&self.path);
    }
}

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

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

const SAMPLE: &str = "\
Work
[ ] !! Fix the parser -> 2024-03-01 #bug
[ ] Write tests
    with more detail
[@] Review the pull request #review
[x] Old finished task

Personal
[ ] Buy milk #shopping
[?] Refactor the renderer?
[ ] Ship it -> 2024-12-24 #release
";

#[test]
fn show_lists_tasks_with_ids_and_sections() {
    let dir = TestDir::new("show");
    dir.write("todo.xit", SAMPLE);
    let output = dir.run(&["show", "--no-due"]);
    let text = stdout(&output);
    assert!(text.contains("▌ Work"), "{}", text);
    assert!(text.contains("#001  [ ]  !!  Fix the parser -> 2024-03-01 #bug"));
    assert!(text.contains("with more detail"));
    // checked tasks move to the end
    let checked = text.find("Old finished task").unwrap();
    let ongoing = text.find("Review the pull request").unwrap();
    assert!(checked > ongoing);
}

#[test]
fn show_filters_and_sorting() {
    let dir = TestDir::new("filters");
    dir.write("todo.xit", SAMPLE);

    let text = stdout(&dir.run(&["show", "--active", "--no-due"]));
    assert!(!text.contains("Old finished task"));

    let text = stdout(&dir.run(&["show", "-t", "bug", "--no-due"]));
    assert!(text.contains("Fix the parser"));
    assert!(!text.contains("Buy milk"));

    let text = stdout(&dir.run(&["show", "--overdue", "--no-due"]));
    assert!(text.contains("Fix the parser"));
    assert!(!text.contains("Ship it"));

    let text = stdout(&dir.run(&["show", "--due-by", "2024-06-01", "--no-due"]));
    assert!(text.contains("Fix the parser"));
    assert!(!text.contains("Ship it"));

    let text = stdout(&dir.run(&["show", "--sort", "priority", "--order", "desc", "--no-due"]));
    let high = text.find("Fix the parser").unwrap();
    let plain = text.find("Write tests").unwrap();
    assert!(high < plain, "{}", text);

    let text = stdout(&dir.run(&["show", "--sort", "due_date", "--no-due"]));
    let early = text.find("Fix the parser").unwrap();
    let late = text.find("Ship it").unwrap();
    let undated = text.find("Write tests").unwrap();
    assert!(early < late && late < undated, "{}", text);

    let text = stdout(&dir.run(&["show", "--count"]));
    assert_eq!(text.trim(), "7 tasks found.");
}

#[test]
fn show_json_is_machine_readable() {
    let dir = TestDir::new("json");
    dir.write("todo.xit", SAMPLE);
    let text = stdout(&dir.run(&["--json", "show"]));
    assert!(text.contains("\"status\": \"open\""));
    assert!(text.contains("\"date\": \"2024-03-01\""));
    assert!(text.contains("\"days_left\": -9"));
    assert!(text.contains("\"name\": \"bug\""));
}

#[test]
fn add_preserves_the_rest_of_the_file() {
    let dir = TestDir::new("add");
    dir.write(
        "todo.xit",
        "# a comment that must survive\n\nWork\n[ ] existing\n",
    );
    let output = dir.run(&["add", "new task -> 2024-05-01 #new", "-p", "2"]);
    assert!(output.status.success(), "{}", stderr(&output));
    let text = dir.read("todo.xit");
    assert!(text.starts_with("# a comment that must survive\n\nWork\n[ ] existing\n"));
    assert!(text.contains("[ ] !! new task -> 2024-05-01 #new\n"));
    assert!(stdout(&output).contains("added task #002"));
}

#[test]
fn add_resolves_date_expressions_and_tags() {
    let dir = TestDir::new("add-dates");
    dir.write("todo.xit", "");
    let output = dir.run(&["add", "pay rent", "--due", "tomorrow", "-t", "home"]);
    assert!(output.status.success(), "{}", stderr(&output));
    assert!(dir
        .read("todo.xit")
        .contains("[ ] pay rent #home -> 2024-03-11\n"));
}

#[test]
fn add_resolves_inline_date_expressions() {
    let dir = TestDir::new("add-inline");
    dir.write("todo.xit", "");
    assert!(dir
        .run(&["add", "write docs -> tomorrow #docs"])
        .status
        .success());
    assert_eq!(dir.read("todo.xit"), "[ ] write docs #docs -> 2024-03-11\n");
    // a specification date pattern is kept exactly as written
    assert!(dir.run(&["add", "spec style -> 2024-12"]).status.success());
    assert_eq!(
        dir.read("todo.xit"),
        "[ ] write docs #docs -> 2024-03-11\n[ ] spec style -> 2024-12\n"
    );
}

#[test]
fn mark_changes_only_the_checkbox() {
    let dir = TestDir::new("mark");
    dir.write("todo.xit", "[ ] keep  the   spacing\n[ ] second\n");
    let output = dir.run(&["mark", "1", "--done"]);
    assert!(output.status.success());
    assert_eq!(
        dir.read("todo.xit"),
        "[x] keep  the   spacing\n[ ] second\n"
    );
    let output = dir.run(&["mark", "2", "--ongoing"]);
    assert!(output.status.success());
    assert!(dir.read("todo.xit").contains("[@] second"));
}

#[test]
fn rm_marks_obsolete_and_force_deletes() {
    let dir = TestDir::new("rm");
    dir.write("todo.xit", "Work\n[ ] one\n[ ] two\n");
    let output = dir.run(&["rm", "1"]);
    assert!(output.status.success());
    assert_eq!(dir.read("todo.xit"), "Work\n[~] one\n[ ] two\n");
    assert!(stdout(&output).contains("--force"));

    let output = dir.run(&["rm", "1", "--force"]);
    assert!(output.status.success(), "{}", stderr(&output));
    assert_eq!(dir.read("todo.xit"), "Work\n[ ] two\n");
}

#[test]
fn tags_and_priority_can_be_changed() {
    let dir = TestDir::new("tags");
    dir.write("todo.xit", "[ ] task #old\n");
    assert!(dir.run(&["untag", "1", "-t", "old"]).status.success());
    assert_eq!(dir.read("todo.xit"), "[ ] task\n");
    assert!(dir
        .run(&["tag", "1", "-t", "urgent", "-t", "when=next week"])
        .status
        .success());
    assert_eq!(
        dir.read("todo.xit"),
        "[ ] task #urgent #when=\"next week\"\n"
    );
    assert!(dir.run(&["prio", "1", "-p", "3"]).status.success());
    assert_eq!(
        dir.read("todo.xit"),
        "[ ] !!! task #urgent #when=\"next week\"\n"
    );
    assert!(dir.run(&["prio", "1", "-p", "0"]).status.success());
    assert_eq!(
        dir.read("todo.xit"),
        "[ ] task #urgent #when=\"next week\"\n"
    );
}

#[test]
fn reschedule_replaces_the_due_date() {
    let dir = TestDir::new("reschedule");
    dir.write("todo.xit", "[ ] task -> 2024-01-01 and more text\n");
    assert!(dir
        .run(&["reschedule", "1", "-n", "tomorrow"])
        .status
        .success());
    assert_eq!(
        dir.read("todo.xit"),
        "[ ] task -> 2024-03-11 and more text\n"
    );
    assert!(dir.run(&["reschedule", "1", "-n", "none"]).status.success());
    assert_eq!(dir.read("todo.xit"), "[ ] task and more text\n");
}

#[test]
fn edit_replaces_the_description() {
    let dir = TestDir::new("edit");
    dir.write("todo.xit", "Work\n[ ] !! old text\n    old continuation\n");
    let output = dir.run(&["edit", "1", "brand new text"]);
    assert!(output.status.success(), "{}", stderr(&output));
    assert_eq!(dir.read("todo.xit"), "Work\n[ ] !! brand new text\n");
}

#[test]
fn move_between_files() {
    let dir = TestDir::new("move");
    dir.write("todo.xit", "Work\n[ ] one\n[ ] two\n");
    dir.write("archive.xit", "");
    let output = dir.run(&["move", "2", "--target-file", "archive.xit", "-g", "Later"]);
    assert!(output.status.success(), "{}", stderr(&output));
    assert_eq!(dir.read("todo.xit"), "Work\n[ ] one\n");
    assert_eq!(dir.read("archive.xit"), "Later\n[ ] two\n");
}

#[test]
fn recur_creates_follow_up_tasks() {
    let dir = TestDir::new("recur");
    dir.write("todo.xit", "[ ] weekly review -> 2024-03-01\n");
    let output = dir.run(&["recur", "1", "--interval", "1w", "-c", "2"]);
    assert!(output.status.success(), "{}", stderr(&output));
    assert_eq!(
        dir.read("todo.xit"),
        "[ ] weekly review -> 2024-03-01\n[ ] weekly review -> 2024-03-08\n[ ] weekly review -> 2024-03-15\n"
    );
}

#[test]
fn validate_reports_problems_and_exits_non_zero() {
    let dir = TestDir::new("validate");
    dir.write(
        "todo.xit",
        "Title\n[a] bad checkbox\n[ ]nospace\n[ ] bad date -> 2022-13-45\n[ ] fine\n",
    );
    let output = dir.run(&["validate"]);
    let text = stdout(&output);
    assert!(text.contains("todo.xit:2: error"));
    assert!(text.contains("todo.xit:3: error"));
    assert!(text.contains("not a valid date"));
    assert_eq!(output.status.code(), Some(1));

    let dir = TestDir::new("validate-ok");
    dir.write("todo.xit", "Work\n[ ] fine -> 2024-01-01 #tag\n");
    let output = dir.run(&["validate"]);
    assert_eq!(output.status.code(), Some(0), "{}", stdout(&output));
}

#[test]
fn format_normalises_and_check_reports() {
    let dir = TestDir::new("format");
    dir.write("todo.xit", "[x] messy  \n    continuation  \n");
    let output = dir.run(&["format", "--check"]);
    assert_eq!(output.status.code(), Some(1));
    assert!(dir.run(&["format"]).status.success());
    assert_eq!(dir.read("todo.xit"), "[x] messy\n    continuation\n");
    assert_eq!(dir.run(&["format", "--check"]).status.code(), Some(0));
}

#[test]
fn multiple_files_are_numbered_globally() {
    let dir = TestDir::new("multi");
    dir.write("a.xit", "[ ] first\n");
    dir.write("b.xit", "[ ] second\n");
    let text = stdout(&dir.run(&["show", "--no-due"]));
    assert!(text.contains("#001  [ ]  first"), "{}", text);
    assert!(text.contains("#002  [ ]  second"), "{}", text);
    // marking by global number touches the right file
    assert!(dir.run(&["mark", "2", "--done"]).status.success());
    assert_eq!(dir.read("a.xit"), "[ ] first\n");
    assert_eq!(dir.read("b.xit"), "[x] second\n");
}

#[test]
fn unknown_lines_survive_every_mutation() {
    let dir = TestDir::new("preserve");
    dir.write(
        "todo.xit",
        "# heading comment\n\nWork\n[ ] one\n[ ] two\n\nsome trailing prose\n",
    );
    assert!(dir.run(&["mark", "1", "--done"]).status.success());
    assert!(dir.run(&["tag", "2", "-t", "x"]).status.success());
    let text = dir.read("todo.xit");
    assert_eq!(
        text,
        "# heading comment\n\nWork\n[x] one\n[ ] two #x\n\nsome trailing prose\n"
    );
}

#[test]
fn dry_run_does_not_write() {
    let dir = TestDir::new("dry-run");
    dir.write("todo.xit", "[ ] one\n");
    let output = dir.run(&["--dry-run", "mark", "1", "--done"]);
    assert!(output.status.success());
    assert!(stdout(&output).contains("dry run"));
    assert_eq!(dir.read("todo.xit"), "[ ] one\n");
}

#[test]
fn new_creates_a_file() {
    let dir = TestDir::new("new");
    let output = dir.run(&["new", "tasks.xit", "--template"]);
    assert!(output.status.success(), "{}", stderr(&output));
    assert!(dir.read("tasks.xit").starts_with("TODO\n"));
    assert!(!dir.run(&["new", "tasks.xit"]).status.success());
}

#[test]
fn unknown_command_and_bad_numbers_fail_cleanly() {
    let dir = TestDir::new("errors");
    dir.write("todo.xit", "[ ] one\n");
    let output = dir.run(&["frobnicate"]);
    assert!(!output.status.success());
    assert!(stderr(&output).contains("unknown command"));

    let output = dir.run(&["mark", "99", "--done"]);
    assert!(!output.status.success());
    assert!(stderr(&output).contains("99"));

    let output = dir.run(&["mark", "1"]);
    assert!(!output.status.success());
    assert!(stderr(&output).contains("status"));
}

#[test]
fn default_location_prefers_the_todos_directory() {
    let dir = TestDir::new("todos-default");
    dir.write("todo.xit", "[ ] root task\n");
    dir.write("todos/todo.xit", "[ ] todos task\n");
    let text = stdout(&dir.run(&["show", "--no-due"]));
    assert!(text.contains("todos task"), "{}", text);
    assert!(!text.contains("root task"), "{}", text);

    // an explicit file always wins
    let text = stdout(&dir.run(&["-f", "todo.xit", "show", "--no-due"]));
    assert!(text.contains("root task"), "{}", text);
    assert!(!text.contains("todos task"), "{}", text);
}

#[test]
fn default_location_falls_back_to_the_working_directory() {
    let dir = TestDir::new("todos-fallback");
    dir.write("todo.xit", "[ ] root task\n");
    let text = stdout(&dir.run(&["show", "--no-due"]));
    assert!(text.contains("root task"), "{}", text);

    // an empty todos/ directory does not hide the working directory
    dir.write("todos/.keep", "");
    let text = stdout(&dir.run(&["show", "--no-due"]));
    assert!(text.contains("root task"), "{}", text);
}

#[test]
fn add_and_new_create_the_default_file_under_todos() {
    let dir = TestDir::new("todos-add");
    let output = dir.run(&["add", "fresh task"]);
    assert!(output.status.success(), "{}", stderr(&output));
    assert_eq!(dir.read("todos/todo.xit"), "[ ] fresh task\n");
    let text = stdout(&dir.run(&["show", "--no-due"]));
    assert!(text.contains("fresh task"), "{}", text);

    let other = TestDir::new("todos-new");
    assert!(other.run(&["new"]).status.success());
    assert!(other.path.join("todos/todo.xit").exists());
}

#[test]
fn directory_option_directory_does_not_swallow_a_date() {
    let dir = TestDir::new("todos-guard");
    let output = dir.run(&["-d", "tomorrow", "add", "write the report"]);
    assert!(!output.status.success());
    assert!(stderr(&output).contains("--due"), "{}", stderr(&output));
    assert!(!dir.path.join("tomorrow").exists());

    let output = dir.run(&["add", "write the report", "--due", "tomorrow"]);
    assert!(output.status.success(), "{}", stderr(&output));
    assert!(
        dir.read("todos/todo.xit").contains("-> 2024-03-11"),
        "{}",
        dir.read("todos/todo.xit")
    );
}

#[test]
fn markdown_files_are_scanned() {
    let dir = TestDir::new("markdown");
    dir.write("notes.md", "# Notes\n\n[ ] md task #md\n");
    let text = stdout(&dir.run(&["show", "--no-due"]));
    assert!(text.contains("md task #md"), "{}", text);
    assert!(text.contains("Notes"));
}

#[test]
fn setting_a_value_that_is_already_set_says_nothing_changed() {
    let dir = TestDir::new("noop");
    // 2024-03-10 + 3 days = 2024-03-13
    dir.write("todo.xit", "[ ] task -> 2024-03-13\n");
    let output = dir.run(&["reschedule", "1", "-n", "3d"]);
    assert!(output.status.success(), "{}", stderr(&output));
    let text = stdout(&output);
    assert!(text.contains("nothing changed for #001"), "{}", text);
    assert!(text.contains("already due on 2024-03-13"), "{}", text);
    assert_eq!(dir.read("todo.xit"), "[ ] task -> 2024-03-13\n");

    let output = dir.run(&["mark", "1", "--open"]);
    assert!(stdout(&output).contains("nothing changed for #001"));

    let output = dir.run(&["prio", "1", "-p", "0"]);
    assert!(stdout(&output).contains("nothing changed for #001"));
}

#[test]
fn mutations_echo_the_resulting_line() {
    let dir = TestDir::new("echo");
    dir.write("todo.xit", "Work\n[ ] task\n");
    let output = dir.run(&["reschedule", "1", "-n", "2024-12-24"]);
    let text = stdout(&output);
    assert!(
        text.contains("ok: #001 due date set to 2024-12-24"),
        "{}",
        text
    );
    assert!(text.contains("[ ] task -> 2024-12-24"), "{}", text);

    let output = dir.run(&["mark", "1", "--done"]);
    let text = stdout(&output);
    assert!(text.contains("ok: #001 marked as checked"), "{}", text);
    assert!(text.contains("[x] task -> 2024-12-24"), "{}", text);

    // JSON distinguishes what changed from what was already up to date
    let output = dir.run(&["--json", "mark", "1", "--done"]);
    let text = stdout(&output);
    assert!(text.contains("\"changed\": []"), "{}", text);
    assert!(text.contains("\"unchanged\""), "{}", text);
}

#[test]
fn finished_tasks_are_moved_to_the_end_with_a_hint() {
    let dir = TestDir::new("finished");
    dir.write("todo.xit", "Work\n[ ] first\n[x] second -> 2024-01-01\n");
    let output = dir.run(&["show"]);
    let text = stdout(&output);
    let first = text.find("first").unwrap();
    let second = text.find("second").unwrap();
    assert!(first < second, "{}", text);
    assert!(
        text.contains("1 finished task(s) are listed at the end (--active hides them)"),
        "{}",
        text
    );
    // no urgency reminder for a task that is already done
    assert!(!text.contains("overdue"), "{}", text);
    // --active hides them entirely
    let text = stdout(&dir.run(&["show", "--active"]));
    assert!(!text.contains("second"), "{}", text);
}

#[test]
fn interactive_mark_asks_for_status_and_tasks() {
    let dir = TestDir::new("interactive-mark");
    dir.write("todo.xit", "[ ] one\n[ ] two\n[ ] three\n");
    // answer 1: status menu -> 2 (checked); answer 2: tasks -> 1 and 3
    let output = dir.run_with_input(&["--no-color", "mark", "-i"], "2\n1,3\n");
    assert!(output.status.success(), "{}", stderr(&output));
    assert!(
        stderr(&output).contains("New status?"),
        "{}",
        stderr(&output)
    );
    assert_eq!(dir.read("todo.xit"), "[x] one\n[ ] two\n[x] three\n");
}

#[test]
fn interactive_rm_can_delete_permanently() {
    let dir = TestDir::new("interactive-rm");
    dir.write("todo.xit", "Work\n[ ] one\n[ ] two\n");
    // answer 1: task selection -> 2; answer 2: method -> 2 (delete)
    let output = dir.run_with_input(&["--no-color", "rm", "-i"], "2\n2\n");
    assert!(output.status.success(), "{}", stderr(&output));
    assert_eq!(dir.read("todo.xit"), "Work\n[ ] one\n");
}

#[test]
fn interactive_reschedule_asks_for_date_and_tasks() {
    let dir = TestDir::new("interactive-reschedule");
    dir.write("todo.xit", "[ ] one\n[ ] two\n");
    let output = dir.run_with_input(&["--no-color", "reschedule", "-i"], "next week\n1\n");
    assert!(output.status.success(), "{}", stderr(&output));
    assert_eq!(dir.read("todo.xit"), "[ ] one -> 2024-03-17\n[ ] two\n");
}

#[test]
fn interactive_show_asks_for_sections_and_tasks() {
    let dir = TestDir::new("interactive-show");
    dir.write(
        "todo.xit",
        "Work\n[ ] work item\n\nPersonal\n[ ] private item\n",
    );
    let output = dir.run_with_input(&["--no-color", "show", "-i", "--no-due"], "all\n2\n");
    assert!(output.status.success(), "{}", stderr(&output));
    let text = stdout(&output);
    assert!(text.contains("private item"), "{}", text);
    assert!(!text.contains("work item"), "{}", text);
}

#[test]
fn interactive_mode_rejects_a_bad_menu_choice() {
    let dir = TestDir::new("interactive-bad");
    dir.write("todo.xit", "[ ] one\n");
    let output = dir.run_with_input(&["--no-color", "mark", "-i"], "9\n");
    assert!(!output.status.success());
    assert!(
        stderr(&output).contains("out of range"),
        "{}",
        stderr(&output)
    );
}

#[test]
fn interactive_mode_needs_input() {
    let dir = TestDir::new("interactive-eof");
    dir.write("todo.xit", "[ ] one\n");
    let output = dir.run_with_input(&["--no-color", "mark", "-i"], "");
    assert!(!output.status.success());
    assert!(
        stderr(&output).contains("no input left"),
        "{}",
        stderr(&output)
    );
}

#[test]
fn pomodoro_links_and_completes_tasks() {
    let dir = TestDir::new("pomodoro");
    dir.write("todo.xit", "[ ] focus task\n[ ] other task\n");
    // one 1.2 second work session, no break, linked to task 1, completed
    let output = dir.run_with_input(
        &[
            "--no-color",
            "pomodoro",
            "0.02",
            "0",
            "-c",
            "1",
            "-t",
            "1",
            "--complete",
            "--no-sound",
        ],
        "",
    );
    assert!(output.status.success(), "{}", stderr(&output));
    assert_eq!(dir.read("todo.xit"), "[x] focus task\n[ ] other task\n");
    let text = stdout(&output);
    assert!(text.contains("work session 1 of 1"), "{}", text);
    assert!(text.contains("1 work session(s)"), "{}", text);
}

#[test]
fn pomodoro_quits_on_q_and_validates_arguments() {
    let dir = TestDir::new("pomodoro-quit");
    dir.write("todo.xit", "[ ] one\n");
    let output = dir.run_with_input(&["--no-color", "pomodoro", "5", "1", "--no-sound"], "q");
    assert!(output.status.success(), "{}", stderr(&output));
    assert!(stdout(&output).contains("stopped"), "{}", stdout(&output));

    let output = dir.run(&["pomodoro", "5", "1", "-c", "0"]);
    assert!(!output.status.success());
    assert!(stderr(&output).contains("cycles"));

    let output = dir.run(&["pomodoro", "0", "1"]);
    assert!(!output.status.success());
    assert!(stderr(&output).contains("greater than zero"));
}

#[test]
fn recur_flag_is_interval_not_interactive() {
    let dir = TestDir::new("recur-flag");
    dir.write("todo.xit", "[ ] weekly -> 2024-03-01\n");
    // -I is the short form of --interval; -i means interactive
    let output = dir.run(&["recur", "1", "-I", "1w", "-c", "1"]);
    assert!(output.status.success(), "{}", stderr(&output));
    assert_eq!(
        dir.read("todo.xit"),
        "[ ] weekly -> 2024-03-01\n[ ] weekly -> 2024-03-08\n"
    );
}

#[test]
fn reschedule_does_not_change_the_status() {
    let dir = TestDir::new("status-kept");
    dir.write("todo.xit", "[x] done task -> 2024-01-01\n");
    assert!(dir
        .run(&["reschedule", "1", "-n", "2024-12-24"])
        .status
        .success());
    assert_eq!(dir.read("todo.xit"), "[x] done task -> 2024-12-24\n");
    // reopening is an explicit, separate action
    assert!(dir.run(&["mark", "1", "--open"]).status.success());
    assert_eq!(dir.read("todo.xit"), "[ ] done task -> 2024-12-24\n");
}