//! Integration tests for day planning commands. //! //! Tests schedule/unschedule operations through the repository layer, //! since day_planning commands are thin wrappers that require Tauri `State<'_>`. use chrono::{Duration, Utc}; use goingson_core::{NewEvent, NewTask, Priority, TaskId}; use crate::test_utils::{create_test_project, setup_test_state}; // ============ Schedule Task ============ #[tokio::test] async fn test_schedule_task_success() { let (state, user_id) = setup_test_state().await; let project_id = create_test_project(&state, user_id).await; let new_task = NewTask::builder("Plan sprint") .project_id(project_id) .priority(Priority::High) .build(); let task = state.tasks.create(user_id, new_task).await.unwrap(); // Schedule the task (mirrors what schedule_task command does) let start_time = Utc::now() + Duration::hours(2); let duration = 45; // minutes let scheduled = state .tasks .update_schedule(task.id, user_id, Some(start_time), Some(duration)) .await .unwrap(); assert!(scheduled.is_some()); let scheduled = scheduled.unwrap(); // SQLite stores timestamps at second precision, so compare within 1 second let stored_start = scheduled.scheduled_start.unwrap(); assert!((stored_start - start_time).num_seconds().abs() <= 1); assert_eq!(scheduled.scheduled_duration, Some(duration)); // Create the linked event (mirrors schedule_task command behavior) let end_time = start_time + Duration::minutes(duration as i64); let event = NewEvent::builder(&task.description, start_time) .end_time(end_time) .project_id(project_id) .linked_task_id(task.id) .build(); let created_event = state.events.create(user_id, event).await.unwrap(); assert_eq!(created_event.title, "Plan sprint"); assert_eq!(created_event.linked_task_id, Some(task.id)); assert_eq!(created_event.project_id, Some(project_id)); // Verify event is linked to task let linked = state .events .get_by_linked_task(user_id, task.id) .await .unwrap(); assert!(linked.is_some()); assert_eq!(linked.unwrap().title, "Plan sprint"); } #[tokio::test] async fn test_schedule_task_not_found() { let (state, user_id) = setup_test_state().await; let nonexistent_id = TaskId::new(); let start_time = Utc::now() + Duration::hours(1); // update_schedule returns None for nonexistent task let result = state .tasks .update_schedule(nonexistent_id, user_id, Some(start_time), Some(30)) .await .unwrap(); assert!(result.is_none()); } // ============ Unschedule Task ============ #[tokio::test] async fn test_unschedule_task_success() { let (state, user_id) = setup_test_state().await; let new_task = NewTask::builder("Scheduled then removed") .priority(Priority::Medium) .build(); let task = state.tasks.create(user_id, new_task).await.unwrap(); // Schedule it let start_time = Utc::now() + Duration::hours(3); state .tasks .update_schedule(task.id, user_id, Some(start_time), Some(60)) .await .unwrap(); // Create linked event let event = NewEvent::builder("Scheduled then removed", start_time) .linked_task_id(task.id) .build(); state.events.create(user_id, event).await.unwrap(); // Unschedule (mirrors unschedule_task command) state .events .delete_by_linked_task(user_id, task.id) .await .unwrap(); let cleared = state .tasks .update_schedule(task.id, user_id, None, None) .await .unwrap(); assert!(cleared.is_some()); let cleared = cleared.unwrap(); assert!(cleared.scheduled_start.is_none()); assert!(cleared.scheduled_duration.is_none()); // Verify linked event is gone let linked = state .events .get_by_linked_task(user_id, task.id) .await .unwrap(); assert!(linked.is_none()); } #[tokio::test] async fn test_unschedule_task_not_found() { let (state, user_id) = setup_test_state().await; let nonexistent_id = TaskId::new(); // delete_by_linked_task returns false for nonexistent task link let deleted = state .events .delete_by_linked_task(user_id, nonexistent_id) .await .unwrap(); assert!(!deleted); // update_schedule returns None for nonexistent task let result = state .tasks .update_schedule(nonexistent_id, user_id, None, None) .await .unwrap(); assert!(result.is_none()); } // ============ Day Plan View ============ #[tokio::test] async fn test_get_day_plan_empty() { let (state, user_id) = setup_test_state().await; // Query for a date with no events or tasks let date = (Utc::now() + Duration::days(30)).date_naive(); let events = state.events.list_for_date(user_id, date).await.unwrap(); assert!(events.is_empty()); let unscheduled = state .tasks .list_unscheduled_due_on_date(user_id, date) .await .unwrap(); assert!(unscheduled.is_empty()); } #[tokio::test] async fn test_get_day_plan_with_events() { let (state, user_id) = setup_test_state().await; // Create an event for tomorrow let tomorrow = (Utc::now() + Duration::days(1)).date_naive(); let start = tomorrow .and_hms_opt(10, 0, 0) .unwrap(); let start_utc = chrono::DateTime::::from_naive_utc_and_offset(start, Utc); let event = NewEvent::builder("Morning standup", start_utc) .end_time(start_utc + Duration::hours(1)) .build(); state.events.create(user_id, event).await.unwrap(); let events = state.events.list_for_date(user_id, tomorrow).await.unwrap(); assert_eq!(events.len(), 1); assert_eq!(events[0].title, "Morning standup"); } #[tokio::test] async fn test_get_day_plan_with_due_task() { let (state, user_id) = setup_test_state().await; // Create a task due tomorrow with no schedule let tomorrow = Utc::now() + Duration::days(1); let task = NewTask::builder("Due tomorrow") .priority(Priority::High) .due(tomorrow) .build(); state.tasks.create(user_id, task).await.unwrap(); let date = tomorrow.date_naive(); let unscheduled = state .tasks .list_unscheduled_due_on_date(user_id, date) .await .unwrap(); assert_eq!(unscheduled.len(), 1); assert_eq!(unscheduled[0].description, "Due tomorrow"); }