use cfait::context::TestContext;
use cfait::model::{Task, TaskStatus};
use cfait::store::{FilterOptions, TaskStore};
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
fn make_store() -> TaskStore {
let ctx = Arc::new(TestContext::new());
TaskStore::new(ctx)
}
#[test]
fn test_filter_by_tag() {
let mut store = make_store();
let mut t1 = Task::new("Work Task #work", &HashMap::new(), None);
t1.uid = "1".to_string();
t1.calendar_href = "cal1".to_string();
let mut t2 = Task::new("Home Task #home", &HashMap::new(), None);
t2.uid = "2".to_string();
t2.calendar_href = "cal1".to_string();
store.add_task(t1);
store.add_task(t2);
let mut cats = HashSet::new();
cats.insert("work".to_string());
let empty_set = HashSet::new();
let filter_res = store.filter(FilterOptions {
active_cal_href: None,
hidden_calendars: &empty_set,
selected_categories: &cats,
selected_locations: &empty_set,
match_all_categories: false,
search_term: "",
hide_completed_global: false,
hide_fully_completed_tags: false,
cutoff_date: None,
min_duration: None,
max_duration: None,
include_unset_duration: true,
urgent_days: 1,
urgent_prio: 1,
default_priority: 5,
start_grace_period_days: 1,
expanded_done_groups: &empty_set,
max_done_roots: usize::MAX,
max_done_subtasks: usize::MAX,
tag_aliases: &HashMap::new(),
});
let results = filter_res.items;
assert_eq!(results.len(), 1);
if let cfait::store::TaskListItem::Task(task) = &results[0] {
assert_eq!(task.summary, "Work Task");
} else {
panic!("Expected Task variant");
}
}
#[test]
fn test_filter_hierarchical_tags() {
let mut store = make_store();
let mut t1 = Task::new("Backend #dev:backend", &HashMap::new(), None);
t1.uid = "1".to_string();
t1.calendar_href = "cal1".to_string();
store.add_task(t1);
let mut cats = HashSet::new();
cats.insert("dev".to_string());
let empty_set = HashSet::new();
let filter_res = store.filter(FilterOptions {
active_cal_href: None,
hidden_calendars: &empty_set,
selected_categories: &cats,
selected_locations: &empty_set,
match_all_categories: false,
search_term: "",
hide_completed_global: false,
hide_fully_completed_tags: false,
cutoff_date: None,
min_duration: None,
max_duration: None,
include_unset_duration: true,
urgent_days: 1,
urgent_prio: 1,
default_priority: 5,
start_grace_period_days: 1,
expanded_done_groups: &empty_set,
max_done_roots: usize::MAX,
max_done_subtasks: usize::MAX,
tag_aliases: &HashMap::new(),
});
let results = filter_res.items;
assert_eq!(results.len(), 1);
if let cfait::store::TaskListItem::Task(task) = &results[0] {
assert_eq!(task.summary, "Backend");
} else {
panic!("Expected Task variant");
}
}
#[test]
fn test_hide_hidden_calendars() {
let mut store = make_store();
let mut t1 = Task::new("Cal 1 Task", &HashMap::new(), None);
t1.uid = "1".to_string();
t1.calendar_href = "cal1".to_string();
let mut t2 = Task::new("Cal 2 Task", &HashMap::new(), None);
t2.uid = "2".to_string();
t2.calendar_href = "cal2".to_string();
store.add_task(t1);
store.add_task(t2);
let mut hidden = HashSet::new();
hidden.insert("cal2".to_string());
let empty_set = HashSet::new();
let filter_res = store.filter(FilterOptions {
active_cal_href: None,
hidden_calendars: &hidden,
selected_categories: &empty_set,
selected_locations: &empty_set,
match_all_categories: false,
search_term: "",
hide_completed_global: false,
hide_fully_completed_tags: false,
cutoff_date: None,
min_duration: None,
max_duration: None,
include_unset_duration: true,
urgent_days: 1,
urgent_prio: 1,
default_priority: 5,
start_grace_period_days: 1,
expanded_done_groups: &empty_set,
max_done_roots: usize::MAX,
max_done_subtasks: usize::MAX,
tag_aliases: &HashMap::new(),
});
let results = filter_res.items;
assert_eq!(results.len(), 1);
if let cfait::store::TaskListItem::Task(task) = &results[0] {
assert_eq!(task.summary, "Cal 1 Task");
} else {
panic!("Expected Task variant");
}
}
#[test]
fn test_set_status_cancelled_advances_recurring_task() {
let mut store = make_store();
let mut t = Task::new("Daily Task", &HashMap::new(), None);
t.uid = "recurring-1".to_string();
t.calendar_href = "cal1".to_string();
let original_due = chrono::Utc::now() - chrono::Duration::days(1);
t.due = Some(cfait::model::DateType::Specific(original_due));
t.rrule = Some("FREQ=DAILY".to_string());
t.status = TaskStatus::NeedsAction;
store.add_task(t);
let updated = store.set_status("recurring-1", TaskStatus::Cancelled);
assert!(updated.is_some());
let (history, secondary, _children) = updated.unwrap();
let recycled_task = secondary.expect("Recurring task should recycle into a secondary task");
assert_eq!(recycled_task.status, TaskStatus::NeedsAction);
assert_eq!(recycled_task.exdates.len(), 1);
assert_eq!(
recycled_task.exdates[0],
cfait::model::DateType::Specific(original_due)
);
assert_eq!(history.status, TaskStatus::Cancelled);
assert!(history.rrule.is_none());
}
#[test]
fn test_set_status_cancelled_non_recurring_task() {
let mut store = make_store();
let mut t = Task::new("One-time Task", &HashMap::new(), None);
t.uid = "one-time-1".to_string();
t.calendar_href = "cal1".to_string();
t.status = TaskStatus::NeedsAction;
store.add_task(t);
let updated = store.set_status("one-time-1", TaskStatus::Cancelled);
assert!(updated.is_some());
let (task, _secondary, _children) = updated.unwrap();
assert_eq!(task.status, TaskStatus::Cancelled);
assert!(task.rrule.is_none());
}
#[test]
fn test_toggle_status_cancelled_back_to_needs_action() {
let mut store = make_store();
let mut t = Task::new("Task", &HashMap::new(), None);
t.uid = "toggle-1".to_string();
t.calendar_href = "cal1".to_string();
t.status = TaskStatus::NeedsAction;
store.add_task(t);
let updated = store.set_status("toggle-1", TaskStatus::Cancelled);
assert!(updated.is_some());
let (primary, _sec, _children) = updated.unwrap();
assert_eq!(primary.status, TaskStatus::Cancelled);
let updated = store.set_status("toggle-1", TaskStatus::Cancelled);
assert!(updated.is_some());
let (primary, _sec, _children) = updated.unwrap();
assert_eq!(primary.status, TaskStatus::NeedsAction);
}
#[test]
fn test_taskstore_clear_and_remove() {
let ctx = Arc::new(TestContext::new());
let mut store = TaskStore::new(ctx);
let mut task = Task::new("Test", &HashMap::new(), None);
task.uid = "123".to_string();
task.calendar_href = "cal1".to_string();
store.add_task(task);
assert!(store.has_any_tasks());
store.remove("cal1");
assert!(!store.has_any_tasks());
let mut task2 = Task::new("Test2", &HashMap::new(), None);
task2.uid = "456".to_string();
task2.calendar_href = "cal1".to_string();
store.add_task(task2);
assert!(store.has_any_tasks());
store.clear();
assert!(!store.has_any_tasks());
}
#[test]
fn test_get_descendant_uids() {
let ctx = Arc::new(TestContext::new());
let mut store = TaskStore::new(ctx);
let mut parent = Task::new("Parent", &HashMap::new(), None);
parent.uid = "p1".to_string();
store.add_task(parent);
let mut child = Task::new("Child", &HashMap::new(), None);
child.uid = "c1".to_string();
child.parent_uid = Some("p1".to_string());
store.add_task(child);
let mut grandchild = Task::new("Grandchild", &HashMap::new(), None);
grandchild.uid = "g1".to_string();
grandchild.parent_uid = Some("c1".to_string());
store.add_task(grandchild);
let descendants = store.get_descendant_uids("p1");
assert_eq!(descendants.len(), 2);
assert!(descendants.contains(&"c1".to_string()));
assert!(descendants.contains(&"g1".to_string()));
}
#[test]
fn test_task_time_tracking_intents() {
let ctx = Arc::new(TestContext::new());
let mut store = TaskStore::new(ctx);
let config = cfait::config::Config::default();
let mut task = Task::new("Test", &HashMap::new(), None);
task.uid = "123".to_string();
store.add_task(task);
let intent_start = cfait::model::AppIntent::StartTask { uid: "123".to_string() };
store.apply_task_intent(&intent_start, &config);
let t = store.get_task_ref("123").unwrap();
assert_eq!(t.status, TaskStatus::InProcess);
assert!(t.last_started_at.is_some());
let intent_pause = cfait::model::AppIntent::PauseTask { uid: "123".to_string() };
store.apply_task_intent(&intent_pause, &config);
let t2 = store.get_task_ref("123").unwrap();
assert_eq!(t2.status, TaskStatus::NeedsAction);
assert!(t2.last_started_at.is_none());
store.apply_task_intent(&intent_start, &config);
let intent_stop = cfait::model::AppIntent::StopTask { uid: "123".to_string() };
store.apply_task_intent(&intent_stop, &config);
let t3 = store.get_task_ref("123").unwrap();
assert_eq!(t3.status, TaskStatus::NeedsAction);
assert!(t3.last_started_at.is_none());
}
#[test]
fn test_apply_task_intent_comprehensive() {
let ctx = Arc::new(TestContext::new());
let mut store = TaskStore::new(ctx);
let config = cfait::config::Config {
trash_retention_days: 0,
..Default::default()
};
let mut t1 = Task::new("T1", &HashMap::new(), None);
t1.uid = "1".to_string();
t1.calendar_href = "cal1".to_string();
store.add_task(t1);
let mut t2 = Task::new("T2", &HashMap::new(), None);
t2.uid = "2".to_string();
t2.calendar_href = "cal1".to_string();
store.add_task(t2);
store.apply_task_intent(&cfait::model::AppIntent::MakeChild { uid: "2".to_string(), parent_uid: "1".to_string() }, &config);
assert_eq!(store.get_task_ref("2").unwrap().parent_uid, Some("1".to_string()));
store.apply_task_intent(&cfait::model::AppIntent::RemoveParent { uid: "2".to_string() }, &config);
assert_eq!(store.get_task_ref("2").unwrap().parent_uid, None);
store.apply_task_intent(&cfait::model::AppIntent::AddDependency { uid: "1".to_string(), blocker_uid: "2".to_string() }, &config);
assert!(store.get_task_ref("1").unwrap().dependencies.contains(&"2".to_string()));
store.apply_task_intent(&cfait::model::AppIntent::RemoveDependency { uid: "1".to_string(), blocker_uid: "2".to_string() }, &config);
assert!(!store.get_task_ref("1").unwrap().dependencies.contains(&"2".to_string()));
store.apply_task_intent(&cfait::model::AppIntent::AddRelatedTo { uid: "1".to_string(), related_uid: "2".to_string() }, &config);
assert!(store.get_task_ref("1").unwrap().related_to.contains(&"2".to_string()));
store.apply_task_intent(&cfait::model::AppIntent::RemoveRelatedTo { uid: "1".to_string(), related_uid: "2".to_string() }, &config);
assert!(!store.get_task_ref("1").unwrap().related_to.contains(&"2".to_string()));
store.apply_task_intent(&cfait::model::AppIntent::ChangePriority { uid: "1".to_string(), delta: 1 }, &config);
store.apply_task_intent(&cfait::model::AppIntent::ToggleTreeCollapse { uid: "1".to_string() }, &config);
assert!(store.get_task_ref("1").unwrap().collapsed);
store.apply_task_intent(&cfait::model::AppIntent::MoveTask { uid: "1".to_string(), target_href: "cal2".to_string() }, &config);
assert_eq!(store.get_task_ref("1").unwrap().calendar_href, "cal2");
store.apply_task_intent(&cfait::model::AppIntent::CancelTask { uid: "2".to_string() }, &config);
assert_eq!(store.get_task_ref("2").unwrap().status, TaskStatus::Cancelled);
store.apply_task_intent(&cfait::model::AppIntent::DuplicateTaskTree { uid: "1".to_string() }, &config);
store.apply_task_intent(&cfait::model::AppIntent::DeleteTask { uid: "1".to_string() }, &config);
assert!(store.get_task_ref("1").is_none());
let mut t3 = Task::new("T3", &HashMap::new(), None);
t3.uid = "3".to_string();
store.add_task(t3);
store.apply_task_intent(&cfait::model::AppIntent::DeleteTaskTree { uid: "3".to_string() }, &config);
assert!(store.get_task_ref("3").is_none());
}
#[test]
fn test_extract_markdown_tasks_full() {
let input = "Root description.\n\n- [ ] Subtask 1\n Details for subtask 1\n* [x] Subtask 2\n1. [ ] Numbered 1\n2. [ ] Numbered 2\n";
let (root_desc, tasks) = cfait::model::extract_markdown_tasks(input);
assert_eq!(root_desc, "Root description.");
assert_eq!(tasks.len(), 4);
assert_eq!(tasks[0].raw_text, "Subtask 1");
assert!(!tasks[0].is_completed);
assert_eq!(tasks[0].description, "Details for subtask 1");
assert_eq!(tasks[1].raw_text, "Subtask 2");
assert!(tasks[1].is_completed);
assert_eq!(tasks[1].description, "");
assert_eq!(tasks[2].raw_text, "Numbered 1");
assert_eq!(tasks[3].raw_text, "Numbered 2");
assert_eq!(tasks[3].dependencies.len(), 1);
}
#[test]
fn test_task_display_logic() {
let mut task = Task::new("Test", &HashMap::new(), None);
assert_eq!(task.checkbox_symbol(), "[ ]");
assert!(!task.is_paused());
assert_eq!(task.format_duration_short(), "");
task.status = TaskStatus::InProcess;
assert_eq!(task.checkbox_symbol(), "[â–¶]");
task.status = TaskStatus::Completed;
assert_eq!(task.checkbox_symbol(), "[✔]");
task.status = TaskStatus::Cancelled;
assert_eq!(task.checkbox_symbol(), "[✘]");
task.status = TaskStatus::NeedsAction;
task.time_spent_seconds = 3600;
assert!(task.is_paused());
assert_eq!(task.checkbox_symbol(), "[‖]");
assert!(task.format_duration_short().contains("1h"));
}
#[test]
fn test_color_utils_hex_parsing() {
let floats = cfait::color_utils::parse_hex_to_floats("#FF0000").unwrap();
assert_eq!(floats, (1.0, 0.0, 0.0));
let u8s = cfait::color_utils::parse_hex_to_u8("00FF00").unwrap();
assert_eq!(u8s, (0, 255, 0));
assert!(cfait::color_utils::parse_hex_to_floats("invalid").is_none());
let color = cfait::color_utils::generate_color("test_tag");
assert!(color.0 >= 0.0 && color.0 <= 1.0);
assert!(color.1 >= 0.0 && color.1 <= 1.0);
assert!(color.2 >= 0.0 && color.2 <= 1.0);
let _dark = cfait::color_utils::is_dark(color.0, color.1, color.2);
}
#[test]
fn test_session_state_intents() {
let mut session = cfait::model::SessionState::default();
session.apply_session_intent(&cfait::model::AppIntent::SetSearchTerm { term: "test".to_string() });
assert_eq!(session.search_term, "test");
session.apply_session_intent(&cfait::model::AppIntent::ToggleTagFilter { tag: "work".to_string() });
assert!(session.selected_categories.contains(&"work".to_string()));
session.apply_session_intent(&cfait::model::AppIntent::ToggleTagFilter { tag: "work".to_string() });
assert!(!session.selected_categories.contains(&"work".to_string()));
session.apply_session_intent(&cfait::model::AppIntent::ToggleLocationFilter { location: "home".to_string() });
assert!(session.selected_locations.contains(&"home".to_string()));
session.apply_session_intent(&cfait::model::AppIntent::ToggleMatchAllCategories);
assert!(session.match_all_categories);
session.apply_session_intent(&cfait::model::AppIntent::ClearFilters);
assert!(session.search_term.is_empty());
assert!(session.selected_categories.is_empty());
assert!(session.selected_locations.is_empty());
session.apply_session_intent(&cfait::model::AppIntent::ToggleDoneGroup { key: "group".to_string() });
assert!(session.expanded_done_groups.contains(&"group".to_string()));
}
#[test]
fn test_alarm_index_empty_and_default() {
let ctx = Arc::new(TestContext::new());
let idx = cfait::alarm_index::AlarmIndex::load(ctx.as_ref());
assert!(idx.is_empty());
assert_eq!(idx.len(), 0);
let next = idx.get_next_alarm_timestamp();
assert!(next.is_none());
idx.save(ctx.as_ref()).unwrap();
let idx2 = cfait::alarm_index::AlarmIndex::load(ctx.as_ref());
assert!(idx2.is_empty());
}
#[test]
fn test_config_errors_and_save() {
let ctx = Arc::new(TestContext::new());
let err = cfait::config::Config::load(ctx.as_ref()).unwrap_err();
assert!(cfait::config::Config::is_missing_config_error(&err));
let config = cfait::config::Config {
url: "http://example.com".to_string(),
..Default::default()
};
config.save_with_credentials(ctx.as_ref()).unwrap();
let loaded = cfait::config::Config::load(ctx.as_ref()).unwrap();
assert_eq!(loaded.url, "http://example.com");
let loaded_cred = cfait::config::Config::load_with_credentials(ctx.as_ref()).unwrap();
assert_eq!(loaded_cred.url, "http://example.com");
assert_eq!(cfait::config::LogLevel::Debug.to_level_filter(), log::LevelFilter::Debug);
assert!(!cfait::config::TaskAction::OpenUrl.label().is_empty());
}
#[test]
fn test_system_logging_and_keyring() {
let ctx = Arc::new(TestContext::new());
cfait::system::init_logging(ctx.as_ref(), false, Some(cfait::config::LogLevel::Debug.to_level_filter()));
cfait::system::set_log_level(log::LevelFilter::Trace);
cfait::system::init_keyring();
}
#[tokio::test]
async fn test_companion_events_batching() {
let ctx = Arc::new(TestContext::new());
let mut server = mockito::Server::new_async().await;
let url = server.url();
let client = cfait::client::RustyClient::new(ctx.clone(), &url, "u", "p", true, None).unwrap();
let mut task1 = Task::new("T1", &HashMap::new(), None);
task1.uid = "t1".to_string();
task1.calendar_href = format!("{}/cal/", url);
task1.href = format!("{}/cal/t1.ics", url);
let _mock_report = server.mock("REPORT", "/cal/")
.with_status(207)
.with_body(r#"<d:multistatus xmlns:d="DAV:"></d:multistatus>"#)
.create_async().await;
let count = client.delete_all_companion_events(&task1.calendar_href).await.unwrap();
assert_eq!(count, 0);
let count = client.sync_multiple_companion_events(&[task1], true, false).await.unwrap();
assert_eq!(count, 3); }