my_task 1.3.0

A simple CLI task manager powered by SQLite
use assert_cmd::Command;
use predicates::prelude::*;
use tempfile::TempDir;

fn cmd(db_path: &std::path::Path) -> Command {
    let mut c = Command::cargo_bin("my-task").unwrap();
    c.env("MY_TASK_DATA_FILE", db_path);
    c
}

#[test]
fn test_add_basic() {
    let tmp = TempDir::new().unwrap();
    let db_path = tmp.path().join("tasks.db");

    cmd(&db_path)
        .args(["add", "Test task"])
        .assert()
        .success()
        .stdout(predicate::str::contains("Added: #1 Test task"));

    let conn = rusqlite::Connection::open(&db_path).unwrap();
    let title: String = conn
        .query_row("SELECT title FROM tasks WHERE id = 1", [], |row| row.get(0))
        .unwrap();
    assert_eq!(title, "Test task");
}

#[test]
fn test_add_with_options() {
    let tmp = TempDir::new().unwrap();
    let db_path = tmp.path().join("tasks.db");

    cmd(&db_path)
        .args([
            "add",
            "With opts",
            "--project",
            "myproj",
            "--due",
            "2026-04-15",
        ])
        .assert()
        .success()
        .stdout(predicate::str::contains("Added: #1 With opts"));

    let conn = rusqlite::Connection::open(&db_path).unwrap();
    let (project, due): (Option<String>, Option<String>) = conn
        .query_row("SELECT project, due FROM tasks WHERE id = 1", [], |row| {
            Ok((row.get(0)?, row.get(1)?))
        })
        .unwrap();
    assert_eq!(project, Some("myproj".to_string()));
    assert_eq!(due, Some("2026-04-15".to_string()));
}

#[test]
fn test_add_auto_id() {
    let tmp = TempDir::new().unwrap();
    let db_path = tmp.path().join("tasks.db");

    cmd(&db_path)
        .args(["add", "First"])
        .assert()
        .success()
        .stdout(predicate::str::contains("Added: #1"));

    cmd(&db_path)
        .args(["add", "Second"])
        .assert()
        .success()
        .stdout(predicate::str::contains("Added: #2"));
}

#[test]
fn test_add_fuzzy_due_today() {
    let tmp = TempDir::new().unwrap();
    let db_path = tmp.path().join("tasks.db");

    cmd(&db_path)
        .args(["add", "Fuzzy task", "--due", "今日"])
        .assert()
        .success()
        .stdout(predicate::str::contains("Added: #1"));

    let conn = rusqlite::Connection::open(&db_path).unwrap();
    let due: Option<String> = conn
        .query_row("SELECT due FROM tasks WHERE id = 1", [], |row| row.get(0))
        .unwrap();
    let today = chrono::Local::now().format("%Y-%m-%d").to_string();
    assert_eq!(due, Some(today));
}

#[test]
fn test_add_fuzzy_due_tomorrow() {
    let tmp = TempDir::new().unwrap();
    let db_path = tmp.path().join("tasks.db");

    cmd(&db_path)
        .args(["add", "Tomorrow task", "--due", "明日"])
        .assert()
        .success();

    let conn = rusqlite::Connection::open(&db_path).unwrap();
    let due: Option<String> = conn
        .query_row("SELECT due FROM tasks WHERE id = 1", [], |row| row.get(0))
        .unwrap();
    let tomorrow = (chrono::Local::now().date_naive() + chrono::Duration::days(1)).to_string();
    assert_eq!(due, Some(tomorrow));
}

#[test]
fn test_add_invalid_due() {
    let tmp = TempDir::new().unwrap();
    let db_path = tmp.path().join("tasks.db");

    cmd(&db_path)
        .args(["add", "Bad due", "--due", "aaaa"])
        .assert()
        .failure()
        .stderr(predicate::str::contains("invalid due date"));
}

#[test]
fn test_add_with_remind() {
    let tmp = TempDir::new().unwrap();
    let db_path = tmp.path().join("tasks.db");

    cmd(&db_path)
        .args(["add", "Remind task", "--remind", "2026-04-10"])
        .assert()
        .success()
        .stdout(predicate::str::contains("Added: #1 Remind task"));

    let conn = rusqlite::Connection::open(&db_path).unwrap();
    let remind: String = conn
        .query_row(
            "SELECT remind_at FROM task_reminds WHERE task_id = 1",
            [],
            |row| row.get(0),
        )
        .unwrap();
    assert_eq!(remind, "2026-04-10");
}

#[test]
fn test_add_with_remind_fuzzy() {
    let tmp = TempDir::new().unwrap();
    let db_path = tmp.path().join("tasks.db");

    cmd(&db_path)
        .args(["add", "Remind tomorrow", "--remind", "明日"])
        .assert()
        .success()
        .stdout(predicate::str::contains("Added: #1"));

    let conn = rusqlite::Connection::open(&db_path).unwrap();
    let remind: String = conn
        .query_row(
            "SELECT remind_at FROM task_reminds WHERE task_id = 1",
            [],
            |row| row.get(0),
        )
        .unwrap();
    let tomorrow = (chrono::Local::now().date_naive() + chrono::Duration::days(1)).to_string();
    assert_eq!(remind, tomorrow);
}

#[test]
fn test_add_with_remind_invalid() {
    let tmp = TempDir::new().unwrap();
    let db_path = tmp.path().join("tasks.db");

    cmd(&db_path)
        .args(["add", "Bad remind", "--remind", "invalid"])
        .assert()
        .failure()
        .stderr(predicate::str::contains("invalid remind date"));
}

#[test]
fn test_add_with_remind_short_flag() {
    let tmp = TempDir::new().unwrap();
    let db_path = tmp.path().join("tasks.db");

    cmd(&db_path)
        .args(["add", "Short flag", "-r", "2026-05-01"])
        .assert()
        .success()
        .stdout(predicate::str::contains("Added: #1"));

    let conn = rusqlite::Connection::open(&db_path).unwrap();
    let remind: String = conn
        .query_row(
            "SELECT remind_at FROM task_reminds WHERE task_id = 1",
            [],
            |row| row.get(0),
        )
        .unwrap();
    assert_eq!(remind, "2026-05-01");
}

#[test]
fn test_add_empty_title() {
    let tmp = TempDir::new().unwrap();
    let db_path = tmp.path().join("tasks.db");

    cmd(&db_path)
        .args(["add", ""])
        .assert()
        .failure()
        .stderr(predicate::str::contains("Error: title cannot be empty"));
}

#[test]
fn test_add_with_important() {
    let tmp = TempDir::new().unwrap();
    let db_path = tmp.path().join("tasks.db");

    cmd(&db_path)
        .args(["add", "Important task", "--important"])
        .assert()
        .success()
        .stdout(predicate::str::contains("Added: #1 Important task"));

    let conn = rusqlite::Connection::open(&db_path).unwrap();
    let important: i32 = conn
        .query_row("SELECT important FROM tasks WHERE id = 1", [], |row| {
            row.get(0)
        })
        .unwrap();
    assert_eq!(important, 1);
}

#[test]
fn test_add_without_important() {
    let tmp = TempDir::new().unwrap();
    let db_path = tmp.path().join("tasks.db");

    cmd(&db_path)
        .args(["add", "Normal task"])
        .assert()
        .success()
        .stdout(predicate::str::contains("Added: #1 Normal task"));

    let conn = rusqlite::Connection::open(&db_path).unwrap();
    let important: i32 = conn
        .query_row("SELECT important FROM tasks WHERE id = 1", [], |row| {
            row.get(0)
        })
        .unwrap();
    assert_eq!(important, 0);
}