intent_engine/
tasks.rs

1use crate::db::models::{
2    DoneTaskResponse, Event, EventsSummary, NextStepSuggestion, PaginatedTasks, ParentTaskInfo,
3    PickNextResponse, SpawnSubtaskResponse, SubtaskInfo, Task, TaskSortBy, TaskWithEvents,
4    WorkspaceStats, WorkspaceStatus,
5};
6use crate::error::{IntentError, Result};
7use chrono::Utc;
8use sqlx::SqlitePool;
9use std::sync::Arc;
10
11pub use crate::db::models::TaskContext;
12pub struct TaskManager<'a> {
13    pool: &'a SqlitePool,
14    notifier: crate::notifications::NotificationSender,
15    project_path: Option<String>,
16}
17
18impl<'a> TaskManager<'a> {
19    pub fn new(pool: &'a SqlitePool) -> Self {
20        Self {
21            pool,
22            notifier: crate::notifications::NotificationSender::new(None, None),
23            project_path: None,
24        }
25    }
26
27    /// Create a TaskManager with MCP notification support
28    pub fn with_mcp_notifier(
29        pool: &'a SqlitePool,
30        project_path: String,
31        mcp_notifier: tokio::sync::mpsc::UnboundedSender<String>,
32    ) -> Self {
33        Self {
34            pool,
35            notifier: crate::notifications::NotificationSender::new(None, Some(mcp_notifier)),
36            project_path: Some(project_path),
37        }
38    }
39
40    /// Create a TaskManager with WebSocket notification support
41    pub fn with_websocket(
42        pool: &'a SqlitePool,
43        ws_state: Arc<crate::dashboard::websocket::WebSocketState>,
44        project_path: String,
45    ) -> Self {
46        Self {
47            pool,
48            notifier: crate::notifications::NotificationSender::new(Some(ws_state), None),
49            project_path: Some(project_path),
50        }
51    }
52
53    /// Internal helper: Notify UI about task creation
54    async fn notify_task_created(&self, task: &Task) {
55        use crate::dashboard::websocket::DatabaseOperationPayload;
56
57        let Some(project_path) = &self.project_path else {
58            return;
59        };
60
61        let task_json = match serde_json::to_value(task) {
62            Ok(json) => json,
63            Err(e) => {
64                tracing::warn!("Failed to serialize task for notification: {}", e);
65                return;
66            },
67        };
68
69        let payload =
70            DatabaseOperationPayload::task_created(task.id, task_json, project_path.clone());
71        self.notifier.send(payload).await;
72    }
73
74    /// Internal helper: Notify UI about task update
75    async fn notify_task_updated(&self, task: &Task) {
76        use crate::dashboard::websocket::DatabaseOperationPayload;
77
78        let Some(project_path) = &self.project_path else {
79            return;
80        };
81
82        let task_json = match serde_json::to_value(task) {
83            Ok(json) => json,
84            Err(e) => {
85                tracing::warn!("Failed to serialize task for notification: {}", e);
86                return;
87            },
88        };
89
90        let payload =
91            DatabaseOperationPayload::task_updated(task.id, task_json, project_path.clone());
92        self.notifier.send(payload).await;
93    }
94
95    /// Internal helper: Notify UI about task deletion
96    async fn notify_task_deleted(&self, task_id: i64) {
97        use crate::dashboard::websocket::DatabaseOperationPayload;
98
99        let Some(project_path) = &self.project_path else {
100            return;
101        };
102
103        let payload = DatabaseOperationPayload::task_deleted(task_id, project_path.clone());
104        self.notifier.send(payload).await;
105    }
106
107    /// Add a new task
108    pub async fn add_task(
109        &self,
110        name: &str,
111        spec: Option<&str>,
112        parent_id: Option<i64>,
113    ) -> Result<Task> {
114        // Check for circular dependency if parent_id is provided
115        if let Some(pid) = parent_id {
116            self.check_task_exists(pid).await?;
117        }
118
119        let now = Utc::now();
120
121        let result = sqlx::query(
122            r#"
123            INSERT INTO tasks (name, spec, parent_id, status, first_todo_at)
124            VALUES (?, ?, ?, 'todo', ?)
125            "#,
126        )
127        .bind(name)
128        .bind(spec)
129        .bind(parent_id)
130        .bind(now)
131        .execute(self.pool)
132        .await?;
133
134        let id = result.last_insert_rowid();
135        let task = self.get_task(id).await?;
136
137        // Notify WebSocket clients about the new task
138        self.notify_task_created(&task).await;
139
140        Ok(task)
141    }
142
143    /// Get a task by ID
144    pub async fn get_task(&self, id: i64) -> Result<Task> {
145        let task = sqlx::query_as::<_, Task>(
146            r#"
147            SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form
148            FROM tasks
149            WHERE id = ?
150            "#,
151        )
152        .bind(id)
153        .fetch_optional(self.pool)
154        .await?
155        .ok_or(IntentError::TaskNotFound(id))?;
156
157        Ok(task)
158    }
159
160    /// Get a task with events summary
161    pub async fn get_task_with_events(&self, id: i64) -> Result<TaskWithEvents> {
162        let task = self.get_task(id).await?;
163        let events_summary = self.get_events_summary(id).await?;
164
165        Ok(TaskWithEvents {
166            task,
167            events_summary: Some(events_summary),
168        })
169    }
170
171    /// Get full ancestry chain for a task
172    ///
173    /// Returns a vector of tasks from the given task up to the root:
174    /// [task itself, parent, grandparent, ..., root]
175    ///
176    /// Example:
177    /// - Task 42 (parent_id: 55) → [Task 42, Task 55, ...]
178    /// - Task 100 (parent_id: null) → [Task 100]
179    pub async fn get_task_ancestry(&self, task_id: i64) -> Result<Vec<Task>> {
180        let mut chain = Vec::new();
181        let mut current_id = Some(task_id);
182
183        while let Some(id) = current_id {
184            let task = self.get_task(id).await?;
185            current_id = task.parent_id;
186            chain.push(task);
187        }
188
189        Ok(chain)
190    }
191
192    /// Get task context - the complete family tree of a task
193    ///
194    /// Returns:
195    /// - task: The requested task
196    /// - ancestors: Parent chain up to root (ordered from immediate parent to root)
197    /// - siblings: Other tasks at the same level (same parent_id)
198    /// - children: Direct subtasks of this task
199    pub async fn get_task_context(&self, id: i64) -> Result<TaskContext> {
200        // Get the main task
201        let task = self.get_task(id).await?;
202
203        // Get ancestors (walk up parent chain)
204        let mut ancestors = Vec::new();
205        let mut current_parent_id = task.parent_id;
206
207        while let Some(parent_id) = current_parent_id {
208            let parent = self.get_task(parent_id).await?;
209            current_parent_id = parent.parent_id;
210            ancestors.push(parent);
211        }
212
213        // Get siblings (tasks with same parent_id)
214        let siblings = if let Some(parent_id) = task.parent_id {
215            sqlx::query_as::<_, Task>(
216                r#"
217                SELECT id, parent_id, name, spec, status, complexity, priority,
218                       first_todo_at, first_doing_at, first_done_at, active_form
219                FROM tasks
220                WHERE parent_id = ? AND id != ?
221                ORDER BY priority ASC NULLS LAST, id ASC
222                "#,
223            )
224            .bind(parent_id)
225            .bind(id)
226            .fetch_all(self.pool)
227            .await?
228        } else {
229            // For root tasks, get other root tasks as siblings
230            sqlx::query_as::<_, Task>(
231                r#"
232                SELECT id, parent_id, name, spec, status, complexity, priority,
233                       first_todo_at, first_doing_at, first_done_at, active_form
234                FROM tasks
235                WHERE parent_id IS NULL AND id != ?
236                ORDER BY priority ASC NULLS LAST, id ASC
237                "#,
238            )
239            .bind(id)
240            .fetch_all(self.pool)
241            .await?
242        };
243
244        // Get children (direct subtasks)
245        let children = sqlx::query_as::<_, Task>(
246            r#"
247            SELECT id, parent_id, name, spec, status, complexity, priority,
248                   first_todo_at, first_doing_at, first_done_at, active_form
249            FROM tasks
250            WHERE parent_id = ?
251            ORDER BY priority ASC NULLS LAST, id ASC
252            "#,
253        )
254        .bind(id)
255        .fetch_all(self.pool)
256        .await?;
257
258        // Get blocking tasks (tasks that this task depends on)
259        let blocking_tasks = sqlx::query_as::<_, Task>(
260            r#"
261            SELECT t.id, t.parent_id, t.name, t.spec, t.status, t.complexity, t.priority,
262                   t.first_todo_at, t.first_doing_at, t.first_done_at, t.active_form
263            FROM tasks t
264            JOIN dependencies d ON t.id = d.blocking_task_id
265            WHERE d.blocked_task_id = ?
266            ORDER BY t.priority ASC NULLS LAST, t.id ASC
267            "#,
268        )
269        .bind(id)
270        .fetch_all(self.pool)
271        .await?;
272
273        // Get blocked_by tasks (tasks that depend on this task)
274        let blocked_by_tasks = sqlx::query_as::<_, Task>(
275            r#"
276            SELECT t.id, t.parent_id, t.name, t.spec, t.status, t.complexity, t.priority,
277                   t.first_todo_at, t.first_doing_at, t.first_done_at, t.active_form
278            FROM tasks t
279            JOIN dependencies d ON t.id = d.blocked_task_id
280            WHERE d.blocking_task_id = ?
281            ORDER BY t.priority ASC NULLS LAST, t.id ASC
282            "#,
283        )
284        .bind(id)
285        .fetch_all(self.pool)
286        .await?;
287
288        Ok(TaskContext {
289            task,
290            ancestors,
291            siblings,
292            children,
293            dependencies: crate::db::models::TaskDependencies {
294                blocking_tasks,
295                blocked_by_tasks,
296            },
297        })
298    }
299
300    /// Get events summary for a task
301    async fn get_events_summary(&self, task_id: i64) -> Result<EventsSummary> {
302        let total_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM events WHERE task_id = ?")
303            .bind(task_id)
304            .fetch_one(self.pool)
305            .await?;
306
307        let recent_events = sqlx::query_as::<_, Event>(
308            r#"
309            SELECT id, task_id, timestamp, log_type, discussion_data
310            FROM events
311            WHERE task_id = ?
312            ORDER BY timestamp DESC
313            LIMIT 10
314            "#,
315        )
316        .bind(task_id)
317        .fetch_all(self.pool)
318        .await?;
319
320        Ok(EventsSummary {
321            total_count,
322            recent_events,
323        })
324    }
325
326    /// Update a task
327    #[allow(clippy::too_many_arguments)]
328    pub async fn update_task(
329        &self,
330        id: i64,
331        name: Option<&str>,
332        spec: Option<&str>,
333        parent_id: Option<Option<i64>>,
334        status: Option<&str>,
335        complexity: Option<i32>,
336        priority: Option<i32>,
337    ) -> Result<Task> {
338        // Check task exists
339        let task = self.get_task(id).await?;
340
341        // Validate status if provided
342        if let Some(s) = status {
343            if !["todo", "doing", "done"].contains(&s) {
344                return Err(IntentError::InvalidInput(format!("Invalid status: {}", s)));
345            }
346        }
347
348        // Check for circular dependency if parent_id is being changed
349        if let Some(Some(pid)) = parent_id {
350            if pid == id {
351                return Err(IntentError::CircularDependency {
352                    blocking_task_id: pid,
353                    blocked_task_id: id,
354                });
355            }
356            self.check_task_exists(pid).await?;
357            self.check_circular_dependency(id, pid).await?;
358        }
359
360        // Build dynamic update query using QueryBuilder for SQL injection safety
361        let mut builder: sqlx::QueryBuilder<sqlx::Sqlite> =
362            sqlx::QueryBuilder::new("UPDATE tasks SET ");
363        let mut has_updates = false;
364
365        if let Some(n) = name {
366            if has_updates {
367                builder.push(", ");
368            }
369            builder.push("name = ").push_bind(n);
370            has_updates = true;
371        }
372
373        if let Some(s) = spec {
374            if has_updates {
375                builder.push(", ");
376            }
377            builder.push("spec = ").push_bind(s);
378            has_updates = true;
379        }
380
381        if let Some(pid) = parent_id {
382            if has_updates {
383                builder.push(", ");
384            }
385            match pid {
386                Some(p) => {
387                    builder.push("parent_id = ").push_bind(p);
388                },
389                None => {
390                    builder.push("parent_id = NULL");
391                },
392            }
393            has_updates = true;
394        }
395
396        if let Some(c) = complexity {
397            if has_updates {
398                builder.push(", ");
399            }
400            builder.push("complexity = ").push_bind(c);
401            has_updates = true;
402        }
403
404        if let Some(p) = priority {
405            if has_updates {
406                builder.push(", ");
407            }
408            builder.push("priority = ").push_bind(p);
409            has_updates = true;
410        }
411
412        if let Some(s) = status {
413            if has_updates {
414                builder.push(", ");
415            }
416            builder.push("status = ").push_bind(s);
417            has_updates = true;
418
419            // Update timestamp fields based on status
420            let now = Utc::now();
421            let timestamp = now.to_rfc3339();
422            match s {
423                "todo" if task.first_todo_at.is_none() => {
424                    builder.push(", first_todo_at = ").push_bind(timestamp);
425                },
426                "doing" if task.first_doing_at.is_none() => {
427                    builder.push(", first_doing_at = ").push_bind(timestamp);
428                },
429                "done" if task.first_done_at.is_none() => {
430                    builder.push(", first_done_at = ").push_bind(timestamp);
431                },
432                _ => {},
433            }
434        }
435
436        if !has_updates {
437            return Ok(task);
438        }
439
440        builder.push(" WHERE id = ").push_bind(id);
441
442        builder.build().execute(self.pool).await?;
443
444        let task = self.get_task(id).await?;
445
446        // Notify WebSocket clients about the task update
447        self.notify_task_updated(&task).await;
448
449        Ok(task)
450    }
451
452    /// Delete a task
453    pub async fn delete_task(&self, id: i64) -> Result<()> {
454        self.check_task_exists(id).await?;
455
456        sqlx::query("DELETE FROM tasks WHERE id = ?")
457            .bind(id)
458            .execute(self.pool)
459            .await?;
460
461        // Notify WebSocket clients about the task deletion
462        self.notify_task_deleted(id).await;
463
464        Ok(())
465    }
466
467    /// Find tasks with optional filters, sorting, and pagination
468    pub async fn find_tasks(
469        &self,
470        status: Option<&str>,
471        parent_id: Option<Option<i64>>,
472        sort_by: Option<TaskSortBy>,
473        limit: Option<i64>,
474        offset: Option<i64>,
475    ) -> Result<PaginatedTasks> {
476        // Apply defaults
477        let sort_by = sort_by.unwrap_or_default(); // Default: FocusAware
478        let limit = limit.unwrap_or(100);
479        let offset = offset.unwrap_or(0);
480
481        // Build WHERE clause
482        let mut where_clause = String::from("WHERE 1=1");
483        let mut conditions = Vec::new();
484
485        if let Some(s) = status {
486            where_clause.push_str(" AND status = ?");
487            conditions.push(s.to_string());
488        }
489
490        if let Some(pid) = parent_id {
491            if let Some(p) = pid {
492                where_clause.push_str(" AND parent_id = ?");
493                conditions.push(p.to_string());
494            } else {
495                where_clause.push_str(" AND parent_id IS NULL");
496            }
497        }
498
499        // Build ORDER BY clause based on sort mode
500        let order_clause = match sort_by {
501            TaskSortBy::Id => {
502                // Legacy: simple ORDER BY id ASC
503                "ORDER BY id ASC".to_string()
504            },
505            TaskSortBy::Priority => {
506                // ORDER BY priority ASC, complexity ASC, id ASC
507                "ORDER BY COALESCE(priority, 999) ASC, COALESCE(complexity, 5) ASC, id ASC"
508                    .to_string()
509            },
510            TaskSortBy::Time => {
511                // ORDER BY timestamp based on status
512                r#"ORDER BY
513                    CASE status
514                        WHEN 'doing' THEN first_doing_at
515                        WHEN 'todo' THEN first_todo_at
516                        WHEN 'done' THEN first_done_at
517                    END ASC NULLS LAST,
518                    id ASC"#
519                    .to_string()
520            },
521            TaskSortBy::FocusAware => {
522                // Focus-aware: current focused task → doing tasks → todo tasks
523                r#"ORDER BY
524                    CASE
525                        WHEN t.id = (SELECT value FROM workspace_state WHERE key = 'current_task_id') THEN 0
526                        WHEN t.status = 'doing' THEN 1
527                        WHEN t.status = 'todo' THEN 2
528                        ELSE 3
529                    END ASC,
530                    COALESCE(t.priority, 999) ASC,
531                    t.id ASC"#
532                    .to_string()
533            },
534        };
535
536        // Get total count
537        let count_query = format!("SELECT COUNT(*) FROM tasks {}", where_clause);
538        let mut count_q = sqlx::query_scalar::<_, i64>(&count_query);
539        for cond in &conditions {
540            count_q = count_q.bind(cond);
541        }
542        let total_count = count_q.fetch_one(self.pool).await?;
543
544        // Build main query with pagination
545        let main_query = format!(
546            "SELECT id, parent_id, name, NULL as spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form FROM tasks t {} {} LIMIT ? OFFSET ?",
547            where_clause, order_clause
548        );
549
550        let mut q = sqlx::query_as::<_, Task>(&main_query);
551        for cond in conditions {
552            q = q.bind(cond);
553        }
554        q = q.bind(limit);
555        q = q.bind(offset);
556
557        let tasks = q.fetch_all(self.pool).await?;
558
559        // Calculate has_more
560        let has_more = offset + (tasks.len() as i64) < total_count;
561
562        Ok(PaginatedTasks {
563            tasks,
564            total_count,
565            has_more,
566            limit,
567            offset,
568        })
569    }
570
571    /// Get workspace statistics using SQL aggregation (no data loading)
572    ///
573    /// This is much more efficient than loading all tasks just to count them.
574    /// Used by session restore when there's no focused task.
575    pub async fn get_stats(&self) -> Result<WorkspaceStats> {
576        let row = sqlx::query_as::<_, (i64, i64, i64, i64)>(
577            r#"SELECT
578                COUNT(*) as total,
579                COALESCE(SUM(CASE WHEN status = 'todo' THEN 1 ELSE 0 END), 0),
580                COALESCE(SUM(CASE WHEN status = 'doing' THEN 1 ELSE 0 END), 0),
581                COALESCE(SUM(CASE WHEN status = 'done' THEN 1 ELSE 0 END), 0)
582            FROM tasks"#,
583        )
584        .fetch_one(self.pool)
585        .await?;
586
587        Ok(WorkspaceStats {
588            total_tasks: row.0,
589            todo: row.1,
590            doing: row.2,
591            done: row.3,
592        })
593    }
594
595    /// Start a task (atomic: update status + set current)
596    pub async fn start_task(&self, id: i64, with_events: bool) -> Result<TaskWithEvents> {
597        // Check if task is blocked by incomplete dependencies
598        use crate::dependencies::get_incomplete_blocking_tasks;
599        if let Some(blocking_tasks) = get_incomplete_blocking_tasks(self.pool, id).await? {
600            return Err(IntentError::TaskBlocked {
601                task_id: id,
602                blocking_task_ids: blocking_tasks,
603            });
604        }
605
606        let mut tx = self.pool.begin().await?;
607
608        let now = Utc::now();
609
610        // Update task status to doing
611        sqlx::query(
612            r#"
613            UPDATE tasks
614            SET status = 'doing', first_doing_at = COALESCE(first_doing_at, ?)
615            WHERE id = ?
616            "#,
617        )
618        .bind(now)
619        .bind(id)
620        .execute(&mut *tx)
621        .await?;
622
623        // Set as current task
624        sqlx::query(
625            r#"
626            INSERT OR REPLACE INTO workspace_state (key, value)
627            VALUES ('current_task_id', ?)
628            "#,
629        )
630        .bind(id.to_string())
631        .execute(&mut *tx)
632        .await?;
633
634        tx.commit().await?;
635
636        if with_events {
637            let result = self.get_task_with_events(id).await?;
638            self.notify_task_updated(&result.task).await;
639            Ok(result)
640        } else {
641            let task = self.get_task(id).await?;
642            self.notify_task_updated(&task).await;
643            Ok(TaskWithEvents {
644                task,
645                events_summary: None,
646            })
647        }
648    }
649
650    /// Complete the current focused task (atomic: check children + update status + clear current)
651    /// This command only operates on the current_task_id.
652    /// Prerequisites: A task must be set as current
653    pub async fn done_task(&self) -> Result<DoneTaskResponse> {
654        let mut tx = self.pool.begin().await?;
655
656        // Get the current task ID
657        let current_task_id: Option<String> =
658            sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
659                .fetch_optional(&mut *tx)
660                .await?;
661
662        let id = current_task_id.and_then(|s| s.parse::<i64>().ok()).ok_or(
663            IntentError::InvalidInput(
664                "No current task is set. Use 'current --set <ID>' to set a task first.".to_string(),
665            ),
666        )?;
667
668        // Get the task info before completing it
669        let task_info: (String, Option<i64>) =
670            sqlx::query_as(crate::sql_constants::SELECT_TASK_NAME_PARENT)
671                .bind(id)
672                .fetch_one(&mut *tx)
673                .await?;
674        let (task_name, parent_id) = task_info;
675
676        // Check if all children are done
677        let uncompleted_children: i64 = sqlx::query_scalar(
678            "SELECT COUNT(*) FROM tasks WHERE parent_id = ? AND status != 'done'",
679        )
680        .bind(id)
681        .fetch_one(&mut *tx)
682        .await?;
683
684        if uncompleted_children > 0 {
685            return Err(IntentError::UncompletedChildren);
686        }
687
688        let now = Utc::now();
689
690        // Update task status to done
691        sqlx::query(
692            r#"
693            UPDATE tasks
694            SET status = 'done', first_done_at = COALESCE(first_done_at, ?)
695            WHERE id = ?
696            "#,
697        )
698        .bind(now)
699        .bind(id)
700        .execute(&mut *tx)
701        .await?;
702
703        // Clear the current task
704        sqlx::query("DELETE FROM workspace_state WHERE key = 'current_task_id'")
705            .execute(&mut *tx)
706            .await?;
707
708        // Determine next step suggestion based on context
709        let next_step_suggestion = if let Some(parent_task_id) = parent_id {
710            // Task has a parent - check sibling status
711            let remaining_siblings: i64 = sqlx::query_scalar(
712                "SELECT COUNT(*) FROM tasks WHERE parent_id = ? AND status != 'done' AND id != ?",
713            )
714            .bind(parent_task_id)
715            .bind(id)
716            .fetch_one(&mut *tx)
717            .await?;
718
719            if remaining_siblings == 0 {
720                // All siblings are done - parent is ready
721                let parent_name: String =
722                    sqlx::query_scalar(crate::sql_constants::SELECT_TASK_NAME)
723                        .bind(parent_task_id)
724                        .fetch_one(&mut *tx)
725                        .await?;
726
727                NextStepSuggestion::ParentIsReady {
728                    message: format!(
729                        "All sub-tasks of parent #{} '{}' are now complete. The parent task is ready for your attention.",
730                        parent_task_id, parent_name
731                    ),
732                    parent_task_id,
733                    parent_task_name: parent_name,
734                }
735            } else {
736                // Siblings remain
737                let parent_name: String =
738                    sqlx::query_scalar(crate::sql_constants::SELECT_TASK_NAME)
739                        .bind(parent_task_id)
740                        .fetch_one(&mut *tx)
741                        .await?;
742
743                NextStepSuggestion::SiblingTasksRemain {
744                    message: format!(
745                        "Task #{} completed. Parent task #{} '{}' has other sub-tasks remaining.",
746                        id, parent_task_id, parent_name
747                    ),
748                    parent_task_id,
749                    parent_task_name: parent_name,
750                    remaining_siblings_count: remaining_siblings,
751                }
752            }
753        } else {
754            // No parent - check if this was a top-level task with children or standalone
755            let child_count: i64 = sqlx::query_scalar(crate::sql_constants::COUNT_CHILDREN_TOTAL)
756                .bind(id)
757                .fetch_one(&mut *tx)
758                .await?;
759
760            if child_count > 0 {
761                // Top-level task with children completed
762                NextStepSuggestion::TopLevelTaskCompleted {
763                    message: format!(
764                        "Top-level task #{} '{}' has been completed. Well done!",
765                        id, task_name
766                    ),
767                    completed_task_id: id,
768                    completed_task_name: task_name.clone(),
769                }
770            } else {
771                // Check if workspace is clear
772                let remaining_tasks: i64 = sqlx::query_scalar(
773                    "SELECT COUNT(*) FROM tasks WHERE status != 'done' AND id != ?",
774                )
775                .bind(id)
776                .fetch_one(&mut *tx)
777                .await?;
778
779                if remaining_tasks == 0 {
780                    NextStepSuggestion::WorkspaceIsClear {
781                        message: format!(
782                            "Project complete! Task #{} was the last remaining task. There are no more 'todo' or 'doing' tasks.",
783                            id
784                        ),
785                        completed_task_id: id,
786                    }
787                } else {
788                    NextStepSuggestion::NoParentContext {
789                        message: format!("Task #{} '{}' has been completed.", id, task_name),
790                        completed_task_id: id,
791                        completed_task_name: task_name.clone(),
792                    }
793                }
794            }
795        };
796
797        tx.commit().await?;
798
799        // Fetch the completed task to notify UI
800        let completed_task = self.get_task(id).await?;
801        self.notify_task_updated(&completed_task).await;
802
803        Ok(DoneTaskResponse {
804            completed_task,
805            workspace_status: WorkspaceStatus {
806                current_task_id: None,
807            },
808            next_step_suggestion,
809        })
810    }
811
812    /// Check if a task exists
813    async fn check_task_exists(&self, id: i64) -> Result<()> {
814        let exists: bool = sqlx::query_scalar(crate::sql_constants::CHECK_TASK_EXISTS)
815            .bind(id)
816            .fetch_one(self.pool)
817            .await?;
818
819        if !exists {
820            return Err(IntentError::TaskNotFound(id));
821        }
822
823        Ok(())
824    }
825
826    /// Check for circular dependencies
827    async fn check_circular_dependency(&self, task_id: i64, new_parent_id: i64) -> Result<()> {
828        let mut current_id = new_parent_id;
829
830        loop {
831            if current_id == task_id {
832                return Err(IntentError::CircularDependency {
833                    blocking_task_id: new_parent_id,
834                    blocked_task_id: task_id,
835                });
836            }
837
838            let parent: Option<i64> =
839                sqlx::query_scalar(crate::sql_constants::SELECT_TASK_PARENT_ID)
840                    .bind(current_id)
841                    .fetch_optional(self.pool)
842                    .await?;
843
844            match parent {
845                Some(pid) => current_id = pid,
846                None => break,
847            }
848        }
849
850        Ok(())
851    }
852    /// Create a subtask under the current task and switch to it (atomic operation)
853    /// Returns error if there is no current task
854    /// Returns response with subtask info and parent task info
855    pub async fn spawn_subtask(
856        &self,
857        name: &str,
858        spec: Option<&str>,
859    ) -> Result<SpawnSubtaskResponse> {
860        // Get current task
861        let current_task_id: Option<String> =
862            sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
863                .fetch_optional(self.pool)
864                .await?;
865
866        let parent_id = current_task_id.and_then(|s| s.parse::<i64>().ok()).ok_or(
867            IntentError::InvalidInput("No current task to create subtask under".to_string()),
868        )?;
869
870        // Get parent task info
871        let parent_name: String = sqlx::query_scalar(crate::sql_constants::SELECT_TASK_NAME)
872            .bind(parent_id)
873            .fetch_one(self.pool)
874            .await?;
875
876        // Create the subtask
877        let subtask = self.add_task(name, spec, Some(parent_id)).await?;
878
879        // Start the new subtask (sets status to doing and updates current_task_id)
880        // This keeps the parent task in 'doing' status (multi-doing design)
881        self.start_task(subtask.id, false).await?;
882
883        Ok(SpawnSubtaskResponse {
884            subtask: SubtaskInfo {
885                id: subtask.id,
886                name: subtask.name,
887                parent_id,
888                status: "doing".to_string(),
889            },
890            parent_task: ParentTaskInfo {
891                id: parent_id,
892                name: parent_name,
893            },
894        })
895    }
896
897    /// Intelligently pick tasks from 'todo' and transition them to 'doing'
898    /// Returns tasks that were successfully transitioned
899    ///
900    /// # Arguments
901    /// * `max_count` - Maximum number of tasks to pick
902    /// * `capacity_limit` - Maximum total number of tasks allowed in 'doing' status
903    ///
904    /// # Logic
905    /// 1. Check current 'doing' task count
906    /// 2. Calculate available capacity
907    /// 3. Select tasks from 'todo' (prioritized by: priority DESC, complexity ASC)
908    /// 4. Transition selected tasks to 'doing'
909    pub async fn pick_next_tasks(
910        &self,
911        max_count: usize,
912        capacity_limit: usize,
913    ) -> Result<Vec<Task>> {
914        let mut tx = self.pool.begin().await?;
915
916        // Get current doing count
917        let doing_count: i64 = sqlx::query_scalar(crate::sql_constants::COUNT_TASKS_DOING)
918            .fetch_one(&mut *tx)
919            .await?;
920
921        // Calculate available capacity
922        let available = capacity_limit.saturating_sub(doing_count as usize);
923        if available == 0 {
924            return Ok(vec![]);
925        }
926
927        let limit = std::cmp::min(max_count, available);
928
929        // Select tasks from todo, prioritizing by priority DESC, complexity ASC
930        let todo_tasks = sqlx::query_as::<_, Task>(
931            r#"
932                        SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form
933                        FROM tasks
934                        WHERE status = 'todo'
935                        ORDER BY
936                            COALESCE(priority, 0) ASC,
937                            COALESCE(complexity, 5) ASC,
938                            id ASC
939                        LIMIT ?
940                        "#,
941        )
942        .bind(limit as i64)
943        .fetch_all(&mut *tx)
944        .await?;
945
946        if todo_tasks.is_empty() {
947            return Ok(vec![]);
948        }
949
950        let now = Utc::now();
951
952        // Transition selected tasks to 'doing'
953        for task in &todo_tasks {
954            sqlx::query(
955                r#"
956                UPDATE tasks
957                SET status = 'doing',
958                    first_doing_at = COALESCE(first_doing_at, ?)
959                WHERE id = ?
960                "#,
961            )
962            .bind(now)
963            .bind(task.id)
964            .execute(&mut *tx)
965            .await?;
966        }
967
968        tx.commit().await?;
969
970        // Fetch and return updated tasks in the same order
971        let task_ids: Vec<i64> = todo_tasks.iter().map(|t| t.id).collect();
972        let placeholders = vec!["?"; task_ids.len()].join(",");
973        let query = format!(
974            "SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form
975                         FROM tasks WHERE id IN ({})
976                         ORDER BY
977                             COALESCE(priority, 0) ASC,
978                             COALESCE(complexity, 5) ASC,
979                             id ASC",
980            placeholders
981        );
982
983        let mut q = sqlx::query_as::<_, Task>(&query);
984        for id in task_ids {
985            q = q.bind(id);
986        }
987
988        let updated_tasks = q.fetch_all(self.pool).await?;
989        Ok(updated_tasks)
990    }
991
992    /// Intelligently recommend the next task to work on based on context-aware priority model.
993    ///
994    /// Priority logic:
995    /// 1. First priority: Subtasks of the current focused task (depth-first)
996    /// 2. Second priority: Top-level tasks (breadth-first)
997    /// 3. No recommendation: Return appropriate empty state
998    ///
999    /// This command does NOT modify task status.
1000    pub async fn pick_next(&self) -> Result<PickNextResponse> {
1001        // Step 1: Check if there's a current focused task
1002        let current_task_id: Option<String> =
1003            sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
1004                .fetch_optional(self.pool)
1005                .await?;
1006
1007        if let Some(current_id_str) = current_task_id.as_ref() {
1008            if let Ok(current_id) = current_id_str.parse::<i64>() {
1009                // Step 1a: First priority - Get **doing** subtasks of current focused task
1010                // Exclude tasks blocked by incomplete dependencies
1011                let doing_subtasks = sqlx::query_as::<_, Task>(
1012                    r#"
1013                            SELECT id, parent_id, name, spec, status, complexity, priority,
1014                                   first_todo_at, first_doing_at, first_done_at, active_form
1015                            FROM tasks
1016                            WHERE parent_id = ? AND status = 'doing'
1017                              AND NOT EXISTS (
1018                                SELECT 1 FROM dependencies d
1019                                JOIN tasks bt ON d.blocking_task_id = bt.id
1020                                WHERE d.blocked_task_id = tasks.id
1021                                  AND bt.status != 'done'
1022                              )
1023                            ORDER BY COALESCE(priority, 999999) ASC, id ASC
1024                            LIMIT 1
1025                            "#,
1026                )
1027                .bind(current_id)
1028                .fetch_optional(self.pool)
1029                .await?;
1030
1031                if let Some(task) = doing_subtasks {
1032                    return Ok(PickNextResponse::focused_subtask(task));
1033                }
1034
1035                // Step 1b: Second priority - Get **todo** subtasks if no doing subtasks
1036                let todo_subtasks = sqlx::query_as::<_, Task>(
1037                    r#"
1038                            SELECT id, parent_id, name, spec, status, complexity, priority,
1039                                   first_todo_at, first_doing_at, first_done_at, active_form
1040                            FROM tasks
1041                            WHERE parent_id = ? AND status = 'todo'
1042                              AND NOT EXISTS (
1043                                SELECT 1 FROM dependencies d
1044                                JOIN tasks bt ON d.blocking_task_id = bt.id
1045                                WHERE d.blocked_task_id = tasks.id
1046                                  AND bt.status != 'done'
1047                              )
1048                            ORDER BY COALESCE(priority, 999999) ASC, id ASC
1049                            LIMIT 1
1050                            "#,
1051                )
1052                .bind(current_id)
1053                .fetch_optional(self.pool)
1054                .await?;
1055
1056                if let Some(task) = todo_subtasks {
1057                    return Ok(PickNextResponse::focused_subtask(task));
1058                }
1059            }
1060        }
1061
1062        // Step 2a: Third priority - Get top-level **doing** tasks (excluding current task)
1063        // Exclude tasks blocked by incomplete dependencies
1064        let doing_top_level = if let Some(current_id_str) = current_task_id.as_ref() {
1065            if let Ok(current_id) = current_id_str.parse::<i64>() {
1066                sqlx::query_as::<_, Task>(
1067                    r#"
1068                    SELECT id, parent_id, name, spec, status, complexity, priority,
1069                           first_todo_at, first_doing_at, first_done_at, active_form
1070                    FROM tasks
1071                    WHERE parent_id IS NULL AND status = 'doing' AND id != ?
1072                      AND NOT EXISTS (
1073                        SELECT 1 FROM dependencies d
1074                        JOIN tasks bt ON d.blocking_task_id = bt.id
1075                        WHERE d.blocked_task_id = tasks.id
1076                          AND bt.status != 'done'
1077                      )
1078                    ORDER BY COALESCE(priority, 999999) ASC, id ASC
1079                    LIMIT 1
1080                    "#,
1081                )
1082                .bind(current_id)
1083                .fetch_optional(self.pool)
1084                .await?
1085            } else {
1086                None
1087            }
1088        } else {
1089            sqlx::query_as::<_, Task>(
1090                r#"
1091                SELECT id, parent_id, name, spec, status, complexity, priority,
1092                       first_todo_at, first_doing_at, first_done_at, active_form
1093                FROM tasks
1094                WHERE parent_id IS NULL AND status = 'doing'
1095                  AND NOT EXISTS (
1096                    SELECT 1 FROM dependencies d
1097                    JOIN tasks bt ON d.blocking_task_id = bt.id
1098                    WHERE d.blocked_task_id = tasks.id
1099                      AND bt.status != 'done'
1100                  )
1101                ORDER BY COALESCE(priority, 999999) ASC, id ASC
1102                LIMIT 1
1103                "#,
1104            )
1105            .fetch_optional(self.pool)
1106            .await?
1107        };
1108
1109        if let Some(task) = doing_top_level {
1110            return Ok(PickNextResponse::top_level_task(task));
1111        }
1112
1113        // Step 2b: Fourth priority - Get top-level **todo** tasks
1114        // Exclude tasks blocked by incomplete dependencies
1115        let todo_top_level = sqlx::query_as::<_, Task>(
1116            r#"
1117            SELECT id, parent_id, name, spec, status, complexity, priority,
1118                   first_todo_at, first_doing_at, first_done_at, active_form
1119            FROM tasks
1120            WHERE parent_id IS NULL AND status = 'todo'
1121              AND NOT EXISTS (
1122                SELECT 1 FROM dependencies d
1123                JOIN tasks bt ON d.blocking_task_id = bt.id
1124                WHERE d.blocked_task_id = tasks.id
1125                  AND bt.status != 'done'
1126              )
1127            ORDER BY COALESCE(priority, 999999) ASC, id ASC
1128            LIMIT 1
1129            "#,
1130        )
1131        .fetch_optional(self.pool)
1132        .await?;
1133
1134        if let Some(task) = todo_top_level {
1135            return Ok(PickNextResponse::top_level_task(task));
1136        }
1137
1138        // Step 3: No recommendation - determine why
1139        // Check if there are any tasks at all
1140        let total_tasks: i64 = sqlx::query_scalar(crate::sql_constants::COUNT_TASKS_TOTAL)
1141            .fetch_one(self.pool)
1142            .await?;
1143
1144        if total_tasks == 0 {
1145            return Ok(PickNextResponse::no_tasks_in_project());
1146        }
1147
1148        // Check if all tasks are completed
1149        let todo_or_doing_count: i64 =
1150            sqlx::query_scalar("SELECT COUNT(*) FROM tasks WHERE status IN ('todo', 'doing')")
1151                .fetch_one(self.pool)
1152                .await?;
1153
1154        if todo_or_doing_count == 0 {
1155            return Ok(PickNextResponse::all_tasks_completed());
1156        }
1157
1158        // Otherwise, there are tasks but none available based on current context
1159        Ok(PickNextResponse::no_available_todos())
1160    }
1161}
1162
1163#[cfg(test)]
1164mod tests {
1165    use super::*;
1166    use crate::events::EventManager;
1167    use crate::test_utils::test_helpers::TestContext;
1168    use crate::workspace::WorkspaceManager;
1169
1170    #[tokio::test]
1171    async fn test_get_stats_empty() {
1172        let ctx = TestContext::new().await;
1173        let manager = TaskManager::new(ctx.pool());
1174
1175        let stats = manager.get_stats().await.unwrap();
1176
1177        assert_eq!(stats.total_tasks, 0);
1178        assert_eq!(stats.todo, 0);
1179        assert_eq!(stats.doing, 0);
1180        assert_eq!(stats.done, 0);
1181    }
1182
1183    #[tokio::test]
1184    async fn test_get_stats_with_tasks() {
1185        let ctx = TestContext::new().await;
1186        let manager = TaskManager::new(ctx.pool());
1187
1188        // Create tasks with different statuses
1189        let task1 = manager.add_task("Task 1", None, None).await.unwrap();
1190        let task2 = manager.add_task("Task 2", None, None).await.unwrap();
1191        let _task3 = manager.add_task("Task 3", None, None).await.unwrap();
1192
1193        // Update statuses
1194        manager
1195            .update_task(task1.id, None, None, None, Some("doing"), None, None)
1196            .await
1197            .unwrap();
1198        manager
1199            .update_task(task2.id, None, None, None, Some("done"), None, None)
1200            .await
1201            .unwrap();
1202        // task3 stays as todo
1203
1204        let stats = manager.get_stats().await.unwrap();
1205
1206        assert_eq!(stats.total_tasks, 3);
1207        assert_eq!(stats.todo, 1);
1208        assert_eq!(stats.doing, 1);
1209        assert_eq!(stats.done, 1);
1210    }
1211
1212    #[tokio::test]
1213    async fn test_add_task() {
1214        let ctx = TestContext::new().await;
1215        let manager = TaskManager::new(ctx.pool());
1216
1217        let task = manager.add_task("Test task", None, None).await.unwrap();
1218
1219        assert_eq!(task.name, "Test task");
1220        assert_eq!(task.status, "todo");
1221        assert!(task.first_todo_at.is_some());
1222        assert!(task.first_doing_at.is_none());
1223        assert!(task.first_done_at.is_none());
1224    }
1225
1226    #[tokio::test]
1227    async fn test_add_task_with_spec() {
1228        let ctx = TestContext::new().await;
1229        let manager = TaskManager::new(ctx.pool());
1230
1231        let spec = "This is a task specification";
1232        let task = manager
1233            .add_task("Test task", Some(spec), None)
1234            .await
1235            .unwrap();
1236
1237        assert_eq!(task.name, "Test task");
1238        assert_eq!(task.spec.as_deref(), Some(spec));
1239    }
1240
1241    #[tokio::test]
1242    async fn test_add_task_with_parent() {
1243        let ctx = TestContext::new().await;
1244        let manager = TaskManager::new(ctx.pool());
1245
1246        let parent = manager.add_task("Parent task", None, None).await.unwrap();
1247        let child = manager
1248            .add_task("Child task", None, Some(parent.id))
1249            .await
1250            .unwrap();
1251
1252        assert_eq!(child.parent_id, Some(parent.id));
1253    }
1254
1255    #[tokio::test]
1256    async fn test_get_task() {
1257        let ctx = TestContext::new().await;
1258        let manager = TaskManager::new(ctx.pool());
1259
1260        let created = manager.add_task("Test task", None, None).await.unwrap();
1261        let retrieved = manager.get_task(created.id).await.unwrap();
1262
1263        assert_eq!(created.id, retrieved.id);
1264        assert_eq!(created.name, retrieved.name);
1265    }
1266
1267    #[tokio::test]
1268    async fn test_get_task_not_found() {
1269        let ctx = TestContext::new().await;
1270        let manager = TaskManager::new(ctx.pool());
1271
1272        let result = manager.get_task(999).await;
1273        assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
1274    }
1275
1276    #[tokio::test]
1277    async fn test_update_task_name() {
1278        let ctx = TestContext::new().await;
1279        let manager = TaskManager::new(ctx.pool());
1280
1281        let task = manager.add_task("Original name", None, None).await.unwrap();
1282        let updated = manager
1283            .update_task(task.id, Some("New name"), None, None, None, None, None)
1284            .await
1285            .unwrap();
1286
1287        assert_eq!(updated.name, "New name");
1288    }
1289
1290    #[tokio::test]
1291    async fn test_update_task_status() {
1292        let ctx = TestContext::new().await;
1293        let manager = TaskManager::new(ctx.pool());
1294
1295        let task = manager.add_task("Test task", None, None).await.unwrap();
1296        let updated = manager
1297            .update_task(task.id, None, None, None, Some("doing"), None, None)
1298            .await
1299            .unwrap();
1300
1301        assert_eq!(updated.status, "doing");
1302        assert!(updated.first_doing_at.is_some());
1303    }
1304
1305    #[tokio::test]
1306    async fn test_delete_task() {
1307        let ctx = TestContext::new().await;
1308        let manager = TaskManager::new(ctx.pool());
1309
1310        let task = manager.add_task("Test task", None, None).await.unwrap();
1311        manager.delete_task(task.id).await.unwrap();
1312
1313        let result = manager.get_task(task.id).await;
1314        assert!(result.is_err());
1315    }
1316
1317    #[tokio::test]
1318    async fn test_find_tasks_by_status() {
1319        let ctx = TestContext::new().await;
1320        let manager = TaskManager::new(ctx.pool());
1321
1322        manager.add_task("Todo task", None, None).await.unwrap();
1323        let doing_task = manager.add_task("Doing task", None, None).await.unwrap();
1324        manager
1325            .update_task(doing_task.id, None, None, None, Some("doing"), None, None)
1326            .await
1327            .unwrap();
1328
1329        let todo_result = manager
1330            .find_tasks(Some("todo"), None, None, None, None)
1331            .await
1332            .unwrap();
1333        let doing_result = manager
1334            .find_tasks(Some("doing"), None, None, None, None)
1335            .await
1336            .unwrap();
1337
1338        assert_eq!(todo_result.tasks.len(), 1);
1339        assert_eq!(doing_result.tasks.len(), 1);
1340        assert_eq!(doing_result.tasks[0].status, "doing");
1341    }
1342
1343    #[tokio::test]
1344    async fn test_find_tasks_by_parent() {
1345        let ctx = TestContext::new().await;
1346        let manager = TaskManager::new(ctx.pool());
1347
1348        let parent = manager.add_task("Parent", None, None).await.unwrap();
1349        manager
1350            .add_task("Child 1", None, Some(parent.id))
1351            .await
1352            .unwrap();
1353        manager
1354            .add_task("Child 2", None, Some(parent.id))
1355            .await
1356            .unwrap();
1357
1358        let result = manager
1359            .find_tasks(None, Some(Some(parent.id)), None, None, None)
1360            .await
1361            .unwrap();
1362
1363        assert_eq!(result.tasks.len(), 2);
1364    }
1365
1366    #[tokio::test]
1367    async fn test_start_task() {
1368        let ctx = TestContext::new().await;
1369        let manager = TaskManager::new(ctx.pool());
1370
1371        let task = manager.add_task("Test task", None, None).await.unwrap();
1372        let started = manager.start_task(task.id, false).await.unwrap();
1373
1374        assert_eq!(started.task.status, "doing");
1375        assert!(started.task.first_doing_at.is_some());
1376
1377        // Verify it's set as current task
1378        let current: Option<String> =
1379            sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
1380                .fetch_optional(ctx.pool())
1381                .await
1382                .unwrap();
1383
1384        assert_eq!(current, Some(task.id.to_string()));
1385    }
1386
1387    #[tokio::test]
1388    async fn test_start_task_with_events() {
1389        let ctx = TestContext::new().await;
1390        let manager = TaskManager::new(ctx.pool());
1391
1392        let task = manager.add_task("Test task", None, None).await.unwrap();
1393
1394        // Add an event
1395        sqlx::query("INSERT INTO events (task_id, log_type, discussion_data) VALUES (?, ?, ?)")
1396            .bind(task.id)
1397            .bind("test")
1398            .bind("test event")
1399            .execute(ctx.pool())
1400            .await
1401            .unwrap();
1402
1403        let started = manager.start_task(task.id, true).await.unwrap();
1404
1405        assert!(started.events_summary.is_some());
1406        let summary = started.events_summary.unwrap();
1407        assert_eq!(summary.total_count, 1);
1408    }
1409
1410    #[tokio::test]
1411    async fn test_done_task() {
1412        let ctx = TestContext::new().await;
1413        let manager = TaskManager::new(ctx.pool());
1414
1415        let task = manager.add_task("Test task", None, None).await.unwrap();
1416        manager.start_task(task.id, false).await.unwrap();
1417        let response = manager.done_task().await.unwrap();
1418
1419        assert_eq!(response.completed_task.status, "done");
1420        assert!(response.completed_task.first_done_at.is_some());
1421        assert_eq!(response.workspace_status.current_task_id, None);
1422
1423        // Should be WORKSPACE_IS_CLEAR since it's the only task
1424        match response.next_step_suggestion {
1425            NextStepSuggestion::WorkspaceIsClear { .. } => {},
1426            _ => panic!("Expected WorkspaceIsClear suggestion"),
1427        }
1428
1429        // Verify current task is cleared
1430        let current: Option<String> =
1431            sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
1432                .fetch_optional(ctx.pool())
1433                .await
1434                .unwrap();
1435
1436        assert!(current.is_none());
1437    }
1438
1439    #[tokio::test]
1440    async fn test_done_task_with_uncompleted_children() {
1441        let ctx = TestContext::new().await;
1442        let manager = TaskManager::new(ctx.pool());
1443
1444        let parent = manager.add_task("Parent", None, None).await.unwrap();
1445        manager
1446            .add_task("Child", None, Some(parent.id))
1447            .await
1448            .unwrap();
1449
1450        // Set parent as current task
1451        manager.start_task(parent.id, false).await.unwrap();
1452
1453        let result = manager.done_task().await;
1454        assert!(matches!(result, Err(IntentError::UncompletedChildren)));
1455    }
1456
1457    #[tokio::test]
1458    async fn test_done_task_with_completed_children() {
1459        let ctx = TestContext::new().await;
1460        let manager = TaskManager::new(ctx.pool());
1461
1462        let parent = manager.add_task("Parent", None, None).await.unwrap();
1463        let child = manager
1464            .add_task("Child", None, Some(parent.id))
1465            .await
1466            .unwrap();
1467
1468        // Complete child first
1469        manager.start_task(child.id, false).await.unwrap();
1470        let child_response = manager.done_task().await.unwrap();
1471
1472        // Child completion should suggest parent is ready
1473        match child_response.next_step_suggestion {
1474            NextStepSuggestion::ParentIsReady { parent_task_id, .. } => {
1475                assert_eq!(parent_task_id, parent.id);
1476            },
1477            _ => panic!("Expected ParentIsReady suggestion"),
1478        }
1479
1480        // Now parent can be completed
1481        manager.start_task(parent.id, false).await.unwrap();
1482        let parent_response = manager.done_task().await.unwrap();
1483        assert_eq!(parent_response.completed_task.status, "done");
1484
1485        // Parent completion should indicate top-level task completed (since it had children)
1486        match parent_response.next_step_suggestion {
1487            NextStepSuggestion::TopLevelTaskCompleted { .. } => {},
1488            _ => panic!("Expected TopLevelTaskCompleted suggestion"),
1489        }
1490    }
1491
1492    #[tokio::test]
1493    async fn test_circular_dependency() {
1494        let ctx = TestContext::new().await;
1495        let manager = TaskManager::new(ctx.pool());
1496
1497        let task1 = manager.add_task("Task 1", None, None).await.unwrap();
1498        let task2 = manager
1499            .add_task("Task 2", None, Some(task1.id))
1500            .await
1501            .unwrap();
1502
1503        // Try to make task1 a child of task2 (circular)
1504        let result = manager
1505            .update_task(task1.id, None, None, Some(Some(task2.id)), None, None, None)
1506            .await;
1507
1508        assert!(matches!(
1509            result,
1510            Err(IntentError::CircularDependency { .. })
1511        ));
1512    }
1513
1514    #[tokio::test]
1515    async fn test_invalid_parent_id() {
1516        let ctx = TestContext::new().await;
1517        let manager = TaskManager::new(ctx.pool());
1518
1519        let result = manager.add_task("Test", None, Some(999)).await;
1520        assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
1521    }
1522
1523    #[tokio::test]
1524    async fn test_update_task_complexity_and_priority() {
1525        let ctx = TestContext::new().await;
1526        let manager = TaskManager::new(ctx.pool());
1527
1528        let task = manager.add_task("Test task", None, None).await.unwrap();
1529        let updated = manager
1530            .update_task(task.id, None, None, None, None, Some(8), Some(10))
1531            .await
1532            .unwrap();
1533
1534        assert_eq!(updated.complexity, Some(8));
1535        assert_eq!(updated.priority, Some(10));
1536    }
1537
1538    #[tokio::test]
1539    async fn test_spawn_subtask() {
1540        let ctx = TestContext::new().await;
1541        let manager = TaskManager::new(ctx.pool());
1542
1543        // Create and start a parent task
1544        let parent = manager.add_task("Parent task", None, None).await.unwrap();
1545        manager.start_task(parent.id, false).await.unwrap();
1546
1547        // Spawn a subtask
1548        let response = manager
1549            .spawn_subtask("Child task", Some("Details"))
1550            .await
1551            .unwrap();
1552
1553        assert_eq!(response.subtask.parent_id, parent.id);
1554        assert_eq!(response.subtask.name, "Child task");
1555        assert_eq!(response.subtask.status, "doing");
1556        assert_eq!(response.parent_task.id, parent.id);
1557        assert_eq!(response.parent_task.name, "Parent task");
1558
1559        // Verify subtask is now the current task
1560        let current: Option<String> =
1561            sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
1562                .fetch_optional(ctx.pool())
1563                .await
1564                .unwrap();
1565
1566        assert_eq!(current, Some(response.subtask.id.to_string()));
1567
1568        // Verify subtask is in doing status
1569        let retrieved = manager.get_task(response.subtask.id).await.unwrap();
1570        assert_eq!(retrieved.status, "doing");
1571    }
1572
1573    #[tokio::test]
1574    async fn test_spawn_subtask_no_current_task() {
1575        let ctx = TestContext::new().await;
1576        let manager = TaskManager::new(ctx.pool());
1577
1578        // Try to spawn subtask without a current task
1579        let result = manager.spawn_subtask("Child", None).await;
1580        assert!(result.is_err());
1581    }
1582
1583    #[tokio::test]
1584    async fn test_pick_next_tasks_basic() {
1585        let ctx = TestContext::new().await;
1586        let manager = TaskManager::new(ctx.pool());
1587
1588        // Create 10 todo tasks
1589        for i in 1..=10 {
1590            manager
1591                .add_task(&format!("Task {}", i), None, None)
1592                .await
1593                .unwrap();
1594        }
1595
1596        // Pick 5 tasks with capacity limit of 5
1597        let picked = manager.pick_next_tasks(5, 5).await.unwrap();
1598
1599        assert_eq!(picked.len(), 5);
1600        for task in &picked {
1601            assert_eq!(task.status, "doing");
1602            assert!(task.first_doing_at.is_some());
1603        }
1604
1605        // Verify total doing count
1606        let doing_count: i64 = sqlx::query_scalar(crate::sql_constants::COUNT_TASKS_DOING)
1607            .fetch_one(ctx.pool())
1608            .await
1609            .unwrap();
1610
1611        assert_eq!(doing_count, 5);
1612    }
1613
1614    #[tokio::test]
1615    async fn test_pick_next_tasks_with_existing_doing() {
1616        let ctx = TestContext::new().await;
1617        let manager = TaskManager::new(ctx.pool());
1618
1619        // Create 10 todo tasks
1620        for i in 1..=10 {
1621            manager
1622                .add_task(&format!("Task {}", i), None, None)
1623                .await
1624                .unwrap();
1625        }
1626
1627        // Start 2 tasks
1628        let result = manager
1629            .find_tasks(Some("todo"), None, None, None, None)
1630            .await
1631            .unwrap();
1632        manager.start_task(result.tasks[0].id, false).await.unwrap();
1633        manager.start_task(result.tasks[1].id, false).await.unwrap();
1634
1635        // Pick more tasks with capacity limit of 5
1636        let picked = manager.pick_next_tasks(10, 5).await.unwrap();
1637
1638        // Should only pick 3 more (5 - 2 = 3)
1639        assert_eq!(picked.len(), 3);
1640
1641        // Verify total doing count
1642        let doing_count: i64 = sqlx::query_scalar(crate::sql_constants::COUNT_TASKS_DOING)
1643            .fetch_one(ctx.pool())
1644            .await
1645            .unwrap();
1646
1647        assert_eq!(doing_count, 5);
1648    }
1649
1650    #[tokio::test]
1651    async fn test_pick_next_tasks_at_capacity() {
1652        let ctx = TestContext::new().await;
1653        let manager = TaskManager::new(ctx.pool());
1654
1655        // Create 10 tasks
1656        for i in 1..=10 {
1657            manager
1658                .add_task(&format!("Task {}", i), None, None)
1659                .await
1660                .unwrap();
1661        }
1662
1663        // Fill capacity
1664        let first_batch = manager.pick_next_tasks(5, 5).await.unwrap();
1665        assert_eq!(first_batch.len(), 5);
1666
1667        // Try to pick more (should return empty)
1668        let second_batch = manager.pick_next_tasks(5, 5).await.unwrap();
1669        assert_eq!(second_batch.len(), 0);
1670    }
1671
1672    #[tokio::test]
1673    async fn test_pick_next_tasks_priority_ordering() {
1674        let ctx = TestContext::new().await;
1675        let manager = TaskManager::new(ctx.pool());
1676
1677        // Create tasks with different priorities
1678        let low = manager.add_task("Low priority", None, None).await.unwrap();
1679        manager
1680            .update_task(low.id, None, None, None, None, None, Some(1))
1681            .await
1682            .unwrap();
1683
1684        let high = manager.add_task("High priority", None, None).await.unwrap();
1685        manager
1686            .update_task(high.id, None, None, None, None, None, Some(10))
1687            .await
1688            .unwrap();
1689
1690        let medium = manager
1691            .add_task("Medium priority", None, None)
1692            .await
1693            .unwrap();
1694        manager
1695            .update_task(medium.id, None, None, None, None, None, Some(5))
1696            .await
1697            .unwrap();
1698
1699        // Pick tasks
1700        let picked = manager.pick_next_tasks(3, 5).await.unwrap();
1701
1702        // Should be ordered by priority ASC (lower number = higher priority)
1703        assert_eq!(picked.len(), 3);
1704        assert_eq!(picked[0].priority, Some(1)); // lowest number = highest priority
1705        assert_eq!(picked[1].priority, Some(5)); // medium
1706        assert_eq!(picked[2].priority, Some(10)); // highest number = lowest priority
1707    }
1708
1709    #[tokio::test]
1710    async fn test_pick_next_tasks_complexity_ordering() {
1711        let ctx = TestContext::new().await;
1712        let manager = TaskManager::new(ctx.pool());
1713
1714        // Create tasks with different complexities (same priority)
1715        let complex = manager.add_task("Complex", None, None).await.unwrap();
1716        manager
1717            .update_task(complex.id, None, None, None, None, Some(9), Some(5))
1718            .await
1719            .unwrap();
1720
1721        let simple = manager.add_task("Simple", None, None).await.unwrap();
1722        manager
1723            .update_task(simple.id, None, None, None, None, Some(1), Some(5))
1724            .await
1725            .unwrap();
1726
1727        let medium = manager.add_task("Medium", None, None).await.unwrap();
1728        manager
1729            .update_task(medium.id, None, None, None, None, Some(5), Some(5))
1730            .await
1731            .unwrap();
1732
1733        // Pick tasks
1734        let picked = manager.pick_next_tasks(3, 5).await.unwrap();
1735
1736        // Should be ordered by complexity ASC (simple first)
1737        assert_eq!(picked.len(), 3);
1738        assert_eq!(picked[0].complexity, Some(1)); // simple
1739        assert_eq!(picked[1].complexity, Some(5)); // medium
1740        assert_eq!(picked[2].complexity, Some(9)); // complex
1741    }
1742
1743    #[tokio::test]
1744    async fn test_done_task_sibling_tasks_remain() {
1745        let ctx = TestContext::new().await;
1746        let manager = TaskManager::new(ctx.pool());
1747
1748        // Create parent with multiple children
1749        let parent = manager.add_task("Parent Task", None, None).await.unwrap();
1750        let child1 = manager
1751            .add_task("Child 1", None, Some(parent.id))
1752            .await
1753            .unwrap();
1754        let child2 = manager
1755            .add_task("Child 2", None, Some(parent.id))
1756            .await
1757            .unwrap();
1758        let _child3 = manager
1759            .add_task("Child 3", None, Some(parent.id))
1760            .await
1761            .unwrap();
1762
1763        // Complete first child
1764        manager.start_task(child1.id, false).await.unwrap();
1765        let response = manager.done_task().await.unwrap();
1766
1767        // Should indicate siblings remain
1768        match response.next_step_suggestion {
1769            NextStepSuggestion::SiblingTasksRemain {
1770                parent_task_id,
1771                remaining_siblings_count,
1772                ..
1773            } => {
1774                assert_eq!(parent_task_id, parent.id);
1775                assert_eq!(remaining_siblings_count, 2); // child2 and child3
1776            },
1777            _ => panic!("Expected SiblingTasksRemain suggestion"),
1778        }
1779
1780        // Complete second child
1781        manager.start_task(child2.id, false).await.unwrap();
1782        let response2 = manager.done_task().await.unwrap();
1783
1784        // Should still indicate siblings remain
1785        match response2.next_step_suggestion {
1786            NextStepSuggestion::SiblingTasksRemain {
1787                remaining_siblings_count,
1788                ..
1789            } => {
1790                assert_eq!(remaining_siblings_count, 1); // only child3
1791            },
1792            _ => panic!("Expected SiblingTasksRemain suggestion"),
1793        }
1794    }
1795
1796    #[tokio::test]
1797    async fn test_done_task_top_level_with_children() {
1798        let ctx = TestContext::new().await;
1799        let manager = TaskManager::new(ctx.pool());
1800
1801        // Create top-level task with children
1802        let parent = manager.add_task("Epic Task", None, None).await.unwrap();
1803        let child = manager
1804            .add_task("Sub Task", None, Some(parent.id))
1805            .await
1806            .unwrap();
1807
1808        // Complete child first
1809        manager.start_task(child.id, false).await.unwrap();
1810        manager.done_task().await.unwrap();
1811
1812        // Complete parent
1813        manager.start_task(parent.id, false).await.unwrap();
1814        let response = manager.done_task().await.unwrap();
1815
1816        // Should be TOP_LEVEL_TASK_COMPLETED
1817        match response.next_step_suggestion {
1818            NextStepSuggestion::TopLevelTaskCompleted {
1819                completed_task_id,
1820                completed_task_name,
1821                ..
1822            } => {
1823                assert_eq!(completed_task_id, parent.id);
1824                assert_eq!(completed_task_name, "Epic Task");
1825            },
1826            _ => panic!("Expected TopLevelTaskCompleted suggestion"),
1827        }
1828    }
1829
1830    #[tokio::test]
1831    async fn test_done_task_no_parent_context() {
1832        let ctx = TestContext::new().await;
1833        let manager = TaskManager::new(ctx.pool());
1834
1835        // Create multiple standalone tasks
1836        let task1 = manager
1837            .add_task("Standalone Task 1", None, None)
1838            .await
1839            .unwrap();
1840        let _task2 = manager
1841            .add_task("Standalone Task 2", None, None)
1842            .await
1843            .unwrap();
1844
1845        // Complete first task
1846        manager.start_task(task1.id, false).await.unwrap();
1847        let response = manager.done_task().await.unwrap();
1848
1849        // Should be NO_PARENT_CONTEXT since task2 is still pending
1850        match response.next_step_suggestion {
1851            NextStepSuggestion::NoParentContext {
1852                completed_task_id,
1853                completed_task_name,
1854                ..
1855            } => {
1856                assert_eq!(completed_task_id, task1.id);
1857                assert_eq!(completed_task_name, "Standalone Task 1");
1858            },
1859            _ => panic!("Expected NoParentContext suggestion"),
1860        }
1861    }
1862
1863    #[tokio::test]
1864    async fn test_pick_next_focused_subtask() {
1865        let ctx = TestContext::new().await;
1866        let manager = TaskManager::new(ctx.pool());
1867
1868        // Create parent task and set as current
1869        let parent = manager.add_task("Parent task", None, None).await.unwrap();
1870        manager.start_task(parent.id, false).await.unwrap();
1871
1872        // Create subtasks with different priorities
1873        let subtask1 = manager
1874            .add_task("Subtask 1", None, Some(parent.id))
1875            .await
1876            .unwrap();
1877        let subtask2 = manager
1878            .add_task("Subtask 2", None, Some(parent.id))
1879            .await
1880            .unwrap();
1881
1882        // Set priorities: subtask1 = 2, subtask2 = 1 (lower number = higher priority)
1883        manager
1884            .update_task(subtask1.id, None, None, None, None, None, Some(2))
1885            .await
1886            .unwrap();
1887        manager
1888            .update_task(subtask2.id, None, None, None, None, None, Some(1))
1889            .await
1890            .unwrap();
1891
1892        // Pick next should recommend subtask2 (priority 1)
1893        let response = manager.pick_next().await.unwrap();
1894
1895        assert_eq!(response.suggestion_type, "FOCUSED_SUB_TASK");
1896        assert!(response.task.is_some());
1897        assert_eq!(response.task.as_ref().unwrap().id, subtask2.id);
1898        assert_eq!(response.task.as_ref().unwrap().name, "Subtask 2");
1899    }
1900
1901    #[tokio::test]
1902    async fn test_pick_next_top_level_task() {
1903        let ctx = TestContext::new().await;
1904        let manager = TaskManager::new(ctx.pool());
1905
1906        // Create top-level tasks with different priorities
1907        let task1 = manager.add_task("Task 1", None, None).await.unwrap();
1908        let task2 = manager.add_task("Task 2", None, None).await.unwrap();
1909
1910        // Set priorities: task1 = 5, task2 = 3 (lower number = higher priority)
1911        manager
1912            .update_task(task1.id, None, None, None, None, None, Some(5))
1913            .await
1914            .unwrap();
1915        manager
1916            .update_task(task2.id, None, None, None, None, None, Some(3))
1917            .await
1918            .unwrap();
1919
1920        // Pick next should recommend task2 (priority 3)
1921        let response = manager.pick_next().await.unwrap();
1922
1923        assert_eq!(response.suggestion_type, "TOP_LEVEL_TASK");
1924        assert!(response.task.is_some());
1925        assert_eq!(response.task.as_ref().unwrap().id, task2.id);
1926        assert_eq!(response.task.as_ref().unwrap().name, "Task 2");
1927    }
1928
1929    #[tokio::test]
1930    async fn test_pick_next_no_tasks() {
1931        let ctx = TestContext::new().await;
1932        let manager = TaskManager::new(ctx.pool());
1933
1934        // No tasks created
1935        let response = manager.pick_next().await.unwrap();
1936
1937        assert_eq!(response.suggestion_type, "NONE");
1938        assert_eq!(response.reason_code.as_deref(), Some("NO_TASKS_IN_PROJECT"));
1939        assert!(response.message.is_some());
1940    }
1941
1942    #[tokio::test]
1943    async fn test_pick_next_all_completed() {
1944        let ctx = TestContext::new().await;
1945        let manager = TaskManager::new(ctx.pool());
1946
1947        // Create task and mark as done
1948        let task = manager.add_task("Task 1", None, None).await.unwrap();
1949        manager.start_task(task.id, false).await.unwrap();
1950        manager.done_task().await.unwrap();
1951
1952        // Pick next should indicate all tasks completed
1953        let response = manager.pick_next().await.unwrap();
1954
1955        assert_eq!(response.suggestion_type, "NONE");
1956        assert_eq!(response.reason_code.as_deref(), Some("ALL_TASKS_COMPLETED"));
1957        assert!(response.message.is_some());
1958    }
1959
1960    #[tokio::test]
1961    async fn test_pick_next_no_available_todos() {
1962        let ctx = TestContext::new().await;
1963        let manager = TaskManager::new(ctx.pool());
1964
1965        // Create a parent task that's in "doing" status
1966        let parent = manager.add_task("Parent task", None, None).await.unwrap();
1967        manager.start_task(parent.id, false).await.unwrap();
1968
1969        // Create a subtask also in "doing" status (no "todo" subtasks)
1970        let subtask = manager
1971            .add_task("Subtask", None, Some(parent.id))
1972            .await
1973            .unwrap();
1974        // Switch to subtask (this will set parent back to todo, so we need to manually set subtask to doing)
1975        sqlx::query("UPDATE tasks SET status = 'doing' WHERE id = ?")
1976            .bind(subtask.id)
1977            .execute(ctx.pool())
1978            .await
1979            .unwrap();
1980
1981        // Set subtask as current
1982        sqlx::query(
1983            "INSERT OR REPLACE INTO workspace_state (key, value) VALUES ('current_task_id', ?)",
1984        )
1985        .bind(subtask.id.to_string())
1986        .execute(ctx.pool())
1987        .await
1988        .unwrap();
1989
1990        // Set parent to doing (not todo)
1991        sqlx::query("UPDATE tasks SET status = 'doing' WHERE id = ?")
1992            .bind(parent.id)
1993            .execute(ctx.pool())
1994            .await
1995            .unwrap();
1996
1997        // With multi-doing semantics, pick next should recommend the doing parent
1998        // (it's a valid top-level doing task that's not current)
1999        let response = manager.pick_next().await.unwrap();
2000
2001        assert_eq!(response.suggestion_type, "TOP_LEVEL_TASK");
2002        assert_eq!(response.task.as_ref().unwrap().id, parent.id);
2003        assert_eq!(response.task.as_ref().unwrap().status, "doing");
2004    }
2005
2006    #[tokio::test]
2007    async fn test_pick_next_priority_ordering() {
2008        let ctx = TestContext::new().await;
2009        let manager = TaskManager::new(ctx.pool());
2010
2011        // Create parent and set as current
2012        let parent = manager.add_task("Parent", None, None).await.unwrap();
2013        manager.start_task(parent.id, false).await.unwrap();
2014
2015        // Create multiple subtasks with various priorities
2016        let sub1 = manager
2017            .add_task("Priority 10", None, Some(parent.id))
2018            .await
2019            .unwrap();
2020        manager
2021            .update_task(sub1.id, None, None, None, None, None, Some(10))
2022            .await
2023            .unwrap();
2024
2025        let sub2 = manager
2026            .add_task("Priority 1", None, Some(parent.id))
2027            .await
2028            .unwrap();
2029        manager
2030            .update_task(sub2.id, None, None, None, None, None, Some(1))
2031            .await
2032            .unwrap();
2033
2034        let sub3 = manager
2035            .add_task("Priority 5", None, Some(parent.id))
2036            .await
2037            .unwrap();
2038        manager
2039            .update_task(sub3.id, None, None, None, None, None, Some(5))
2040            .await
2041            .unwrap();
2042
2043        // Pick next should recommend the task with priority 1 (lowest number)
2044        let response = manager.pick_next().await.unwrap();
2045
2046        assert_eq!(response.suggestion_type, "FOCUSED_SUB_TASK");
2047        assert_eq!(response.task.as_ref().unwrap().id, sub2.id);
2048        assert_eq!(response.task.as_ref().unwrap().name, "Priority 1");
2049    }
2050
2051    #[tokio::test]
2052    async fn test_pick_next_falls_back_to_top_level_when_no_subtasks() {
2053        let ctx = TestContext::new().await;
2054        let manager = TaskManager::new(ctx.pool());
2055
2056        // Create parent without subtasks and set as current
2057        let parent = manager.add_task("Parent", None, None).await.unwrap();
2058        manager.start_task(parent.id, false).await.unwrap();
2059
2060        // Create another top-level task
2061        let top_level = manager
2062            .add_task("Top level task", None, None)
2063            .await
2064            .unwrap();
2065
2066        // Pick next should fall back to top-level task since parent has no todo subtasks
2067        let response = manager.pick_next().await.unwrap();
2068
2069        assert_eq!(response.suggestion_type, "TOP_LEVEL_TASK");
2070        assert_eq!(response.task.as_ref().unwrap().id, top_level.id);
2071    }
2072
2073    // ===== Missing coverage tests =====
2074
2075    #[tokio::test]
2076    async fn test_get_task_with_events() {
2077        let ctx = TestContext::new().await;
2078        let task_mgr = TaskManager::new(ctx.pool());
2079        let event_mgr = EventManager::new(ctx.pool());
2080
2081        let task = task_mgr.add_task("Test", None, None).await.unwrap();
2082
2083        // Add some events
2084        event_mgr
2085            .add_event(task.id, "progress", "Event 1")
2086            .await
2087            .unwrap();
2088        event_mgr
2089            .add_event(task.id, "decision", "Event 2")
2090            .await
2091            .unwrap();
2092
2093        let result = task_mgr.get_task_with_events(task.id).await.unwrap();
2094
2095        assert_eq!(result.task.id, task.id);
2096        assert!(result.events_summary.is_some());
2097
2098        let summary = result.events_summary.unwrap();
2099        assert_eq!(summary.total_count, 2);
2100        assert_eq!(summary.recent_events.len(), 2);
2101        assert_eq!(summary.recent_events[0].log_type, "decision"); // Most recent first
2102        assert_eq!(summary.recent_events[1].log_type, "progress");
2103    }
2104
2105    #[tokio::test]
2106    async fn test_get_task_with_events_nonexistent() {
2107        let ctx = TestContext::new().await;
2108        let task_mgr = TaskManager::new(ctx.pool());
2109
2110        let result = task_mgr.get_task_with_events(999).await;
2111        assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
2112    }
2113
2114    #[tokio::test]
2115    async fn test_get_task_with_many_events() {
2116        let ctx = TestContext::new().await;
2117        let task_mgr = TaskManager::new(ctx.pool());
2118        let event_mgr = EventManager::new(ctx.pool());
2119
2120        let task = task_mgr.add_task("Test", None, None).await.unwrap();
2121
2122        // Add 20 events
2123        for i in 0..20 {
2124            event_mgr
2125                .add_event(task.id, "test", &format!("Event {}", i))
2126                .await
2127                .unwrap();
2128        }
2129
2130        let result = task_mgr.get_task_with_events(task.id).await.unwrap();
2131        let summary = result.events_summary.unwrap();
2132
2133        assert_eq!(summary.total_count, 20);
2134        assert_eq!(summary.recent_events.len(), 10); // Limited to 10
2135    }
2136
2137    #[tokio::test]
2138    async fn test_get_task_with_no_events() {
2139        let ctx = TestContext::new().await;
2140        let task_mgr = TaskManager::new(ctx.pool());
2141
2142        let task = task_mgr.add_task("Test", None, None).await.unwrap();
2143
2144        let result = task_mgr.get_task_with_events(task.id).await.unwrap();
2145        let summary = result.events_summary.unwrap();
2146
2147        assert_eq!(summary.total_count, 0);
2148        assert_eq!(summary.recent_events.len(), 0);
2149    }
2150
2151    #[tokio::test]
2152    async fn test_pick_next_tasks_zero_capacity() {
2153        let ctx = TestContext::new().await;
2154        let task_mgr = TaskManager::new(ctx.pool());
2155
2156        task_mgr.add_task("Task 1", None, None).await.unwrap();
2157
2158        // capacity_limit = 0 means no capacity available
2159        let results = task_mgr.pick_next_tasks(10, 0).await.unwrap();
2160        assert_eq!(results.len(), 0);
2161    }
2162
2163    #[tokio::test]
2164    async fn test_pick_next_tasks_capacity_exceeds_available() {
2165        let ctx = TestContext::new().await;
2166        let task_mgr = TaskManager::new(ctx.pool());
2167
2168        task_mgr.add_task("Task 1", None, None).await.unwrap();
2169        task_mgr.add_task("Task 2", None, None).await.unwrap();
2170
2171        // Request 10 tasks but only 2 available, capacity = 100
2172        let results = task_mgr.pick_next_tasks(10, 100).await.unwrap();
2173        assert_eq!(results.len(), 2); // Only returns available tasks
2174    }
2175
2176    // ========== task_context tests ==========
2177
2178    #[tokio::test]
2179    async fn test_get_task_context_root_task_no_relations() {
2180        let ctx = TestContext::new().await;
2181        let task_mgr = TaskManager::new(ctx.pool());
2182
2183        // Create a single root task with no relations
2184        let task = task_mgr.add_task("Root task", None, None).await.unwrap();
2185
2186        let context = task_mgr.get_task_context(task.id).await.unwrap();
2187
2188        // Verify task itself
2189        assert_eq!(context.task.id, task.id);
2190        assert_eq!(context.task.name, "Root task");
2191
2192        // No ancestors (root task)
2193        assert_eq!(context.ancestors.len(), 0);
2194
2195        // No siblings
2196        assert_eq!(context.siblings.len(), 0);
2197
2198        // No children
2199        assert_eq!(context.children.len(), 0);
2200    }
2201
2202    #[tokio::test]
2203    async fn test_get_task_context_with_siblings() {
2204        let ctx = TestContext::new().await;
2205        let task_mgr = TaskManager::new(ctx.pool());
2206
2207        // Create multiple root tasks (siblings)
2208        let task1 = task_mgr.add_task("Task 1", None, None).await.unwrap();
2209        let task2 = task_mgr.add_task("Task 2", None, None).await.unwrap();
2210        let task3 = task_mgr.add_task("Task 3", None, None).await.unwrap();
2211
2212        let context = task_mgr.get_task_context(task2.id).await.unwrap();
2213
2214        // Verify task itself
2215        assert_eq!(context.task.id, task2.id);
2216
2217        // No ancestors (root task)
2218        assert_eq!(context.ancestors.len(), 0);
2219
2220        // Should have 2 siblings
2221        assert_eq!(context.siblings.len(), 2);
2222        let sibling_ids: Vec<i64> = context.siblings.iter().map(|t| t.id).collect();
2223        assert!(sibling_ids.contains(&task1.id));
2224        assert!(sibling_ids.contains(&task3.id));
2225        assert!(!sibling_ids.contains(&task2.id)); // Should not include itself
2226
2227        // No children
2228        assert_eq!(context.children.len(), 0);
2229    }
2230
2231    #[tokio::test]
2232    async fn test_get_task_context_with_parent() {
2233        let ctx = TestContext::new().await;
2234        let task_mgr = TaskManager::new(ctx.pool());
2235
2236        // Create parent-child relationship
2237        let parent = task_mgr.add_task("Parent task", None, None).await.unwrap();
2238        let child = task_mgr
2239            .add_task("Child task", None, Some(parent.id))
2240            .await
2241            .unwrap();
2242
2243        let context = task_mgr.get_task_context(child.id).await.unwrap();
2244
2245        // Verify task itself
2246        assert_eq!(context.task.id, child.id);
2247        assert_eq!(context.task.parent_id, Some(parent.id));
2248
2249        // Should have 1 ancestor (the parent)
2250        assert_eq!(context.ancestors.len(), 1);
2251        assert_eq!(context.ancestors[0].id, parent.id);
2252        assert_eq!(context.ancestors[0].name, "Parent task");
2253
2254        // No siblings
2255        assert_eq!(context.siblings.len(), 0);
2256
2257        // No children
2258        assert_eq!(context.children.len(), 0);
2259    }
2260
2261    #[tokio::test]
2262    async fn test_get_task_context_with_children() {
2263        let ctx = TestContext::new().await;
2264        let task_mgr = TaskManager::new(ctx.pool());
2265
2266        // Create parent with multiple children
2267        let parent = task_mgr.add_task("Parent task", None, None).await.unwrap();
2268        let child1 = task_mgr
2269            .add_task("Child 1", None, Some(parent.id))
2270            .await
2271            .unwrap();
2272        let child2 = task_mgr
2273            .add_task("Child 2", None, Some(parent.id))
2274            .await
2275            .unwrap();
2276        let child3 = task_mgr
2277            .add_task("Child 3", None, Some(parent.id))
2278            .await
2279            .unwrap();
2280
2281        let context = task_mgr.get_task_context(parent.id).await.unwrap();
2282
2283        // Verify task itself
2284        assert_eq!(context.task.id, parent.id);
2285
2286        // No ancestors (root task)
2287        assert_eq!(context.ancestors.len(), 0);
2288
2289        // No siblings
2290        assert_eq!(context.siblings.len(), 0);
2291
2292        // Should have 3 children
2293        assert_eq!(context.children.len(), 3);
2294        let child_ids: Vec<i64> = context.children.iter().map(|t| t.id).collect();
2295        assert!(child_ids.contains(&child1.id));
2296        assert!(child_ids.contains(&child2.id));
2297        assert!(child_ids.contains(&child3.id));
2298    }
2299
2300    #[tokio::test]
2301    async fn test_get_task_context_multi_level_hierarchy() {
2302        let ctx = TestContext::new().await;
2303        let task_mgr = TaskManager::new(ctx.pool());
2304
2305        // Create 3-level hierarchy: grandparent -> parent -> child
2306        let grandparent = task_mgr.add_task("Grandparent", None, None).await.unwrap();
2307        let parent = task_mgr
2308            .add_task("Parent", None, Some(grandparent.id))
2309            .await
2310            .unwrap();
2311        let child = task_mgr
2312            .add_task("Child", None, Some(parent.id))
2313            .await
2314            .unwrap();
2315
2316        let context = task_mgr.get_task_context(child.id).await.unwrap();
2317
2318        // Verify task itself
2319        assert_eq!(context.task.id, child.id);
2320
2321        // Should have 2 ancestors (parent and grandparent, ordered from immediate to root)
2322        assert_eq!(context.ancestors.len(), 2);
2323        assert_eq!(context.ancestors[0].id, parent.id);
2324        assert_eq!(context.ancestors[0].name, "Parent");
2325        assert_eq!(context.ancestors[1].id, grandparent.id);
2326        assert_eq!(context.ancestors[1].name, "Grandparent");
2327
2328        // No siblings
2329        assert_eq!(context.siblings.len(), 0);
2330
2331        // No children
2332        assert_eq!(context.children.len(), 0);
2333    }
2334
2335    #[tokio::test]
2336    async fn test_get_task_context_complex_family_tree() {
2337        let ctx = TestContext::new().await;
2338        let task_mgr = TaskManager::new(ctx.pool());
2339
2340        // Create complex structure:
2341        // Root
2342        //  ├─ Child1
2343        //  │   ├─ Grandchild1
2344        //  │   └─ Grandchild2 (target)
2345        //  └─ Child2
2346
2347        let root = task_mgr.add_task("Root", None, None).await.unwrap();
2348        let child1 = task_mgr
2349            .add_task("Child1", None, Some(root.id))
2350            .await
2351            .unwrap();
2352        let child2 = task_mgr
2353            .add_task("Child2", None, Some(root.id))
2354            .await
2355            .unwrap();
2356        let grandchild1 = task_mgr
2357            .add_task("Grandchild1", None, Some(child1.id))
2358            .await
2359            .unwrap();
2360        let grandchild2 = task_mgr
2361            .add_task("Grandchild2", None, Some(child1.id))
2362            .await
2363            .unwrap();
2364
2365        // Get context for grandchild2
2366        let context = task_mgr.get_task_context(grandchild2.id).await.unwrap();
2367
2368        // Verify task itself
2369        assert_eq!(context.task.id, grandchild2.id);
2370
2371        // Should have 2 ancestors: child1 and root
2372        assert_eq!(context.ancestors.len(), 2);
2373        assert_eq!(context.ancestors[0].id, child1.id);
2374        assert_eq!(context.ancestors[1].id, root.id);
2375
2376        // Should have 1 sibling: grandchild1
2377        assert_eq!(context.siblings.len(), 1);
2378        assert_eq!(context.siblings[0].id, grandchild1.id);
2379
2380        // No children
2381        assert_eq!(context.children.len(), 0);
2382
2383        // Now get context for child1 to verify it sees both grandchildren
2384        let context_child1 = task_mgr.get_task_context(child1.id).await.unwrap();
2385        assert_eq!(context_child1.ancestors.len(), 1);
2386        assert_eq!(context_child1.ancestors[0].id, root.id);
2387        assert_eq!(context_child1.siblings.len(), 1);
2388        assert_eq!(context_child1.siblings[0].id, child2.id);
2389        assert_eq!(context_child1.children.len(), 2);
2390    }
2391
2392    #[tokio::test]
2393    async fn test_get_task_context_respects_priority_ordering() {
2394        let ctx = TestContext::new().await;
2395        let task_mgr = TaskManager::new(ctx.pool());
2396
2397        // Create parent with children having different priorities
2398        let parent = task_mgr.add_task("Parent", None, None).await.unwrap();
2399
2400        // Add children with priorities (lower number = higher priority)
2401        let child_low = task_mgr
2402            .add_task("Low priority", None, Some(parent.id))
2403            .await
2404            .unwrap();
2405        let _ = task_mgr
2406            .update_task(child_low.id, None, None, None, None, None, Some(10))
2407            .await
2408            .unwrap();
2409
2410        let child_high = task_mgr
2411            .add_task("High priority", None, Some(parent.id))
2412            .await
2413            .unwrap();
2414        let _ = task_mgr
2415            .update_task(child_high.id, None, None, None, None, None, Some(1))
2416            .await
2417            .unwrap();
2418
2419        let child_medium = task_mgr
2420            .add_task("Medium priority", None, Some(parent.id))
2421            .await
2422            .unwrap();
2423        let _ = task_mgr
2424            .update_task(child_medium.id, None, None, None, None, None, Some(5))
2425            .await
2426            .unwrap();
2427
2428        let context = task_mgr.get_task_context(parent.id).await.unwrap();
2429
2430        // Children should be ordered by priority (1, 5, 10)
2431        assert_eq!(context.children.len(), 3);
2432        assert_eq!(context.children[0].priority, Some(1));
2433        assert_eq!(context.children[1].priority, Some(5));
2434        assert_eq!(context.children[2].priority, Some(10));
2435    }
2436
2437    #[tokio::test]
2438    async fn test_get_task_context_nonexistent_task() {
2439        let ctx = TestContext::new().await;
2440        let task_mgr = TaskManager::new(ctx.pool());
2441
2442        let result = task_mgr.get_task_context(99999).await;
2443        assert!(result.is_err());
2444        assert!(matches!(result, Err(IntentError::TaskNotFound(99999))));
2445    }
2446
2447    #[tokio::test]
2448    async fn test_get_task_context_handles_null_priority() {
2449        let ctx = TestContext::new().await;
2450        let task_mgr = TaskManager::new(ctx.pool());
2451
2452        // Create siblings with mixed null and set priorities
2453        let task1 = task_mgr.add_task("Task 1", None, None).await.unwrap();
2454        let _ = task_mgr
2455            .update_task(task1.id, None, None, None, None, None, Some(1))
2456            .await
2457            .unwrap();
2458
2459        let task2 = task_mgr.add_task("Task 2", None, None).await.unwrap();
2460        // task2 has NULL priority
2461
2462        let task3 = task_mgr.add_task("Task 3", None, None).await.unwrap();
2463        let _ = task_mgr
2464            .update_task(task3.id, None, None, None, None, None, Some(5))
2465            .await
2466            .unwrap();
2467
2468        let context = task_mgr.get_task_context(task2.id).await.unwrap();
2469
2470        // Should have 2 siblings, ordered by priority (non-null first, then null)
2471        assert_eq!(context.siblings.len(), 2);
2472        // Task with priority 1 should come first
2473        assert_eq!(context.siblings[0].id, task1.id);
2474        assert_eq!(context.siblings[0].priority, Some(1));
2475        // Task with priority 5 should come second
2476        assert_eq!(context.siblings[1].id, task3.id);
2477        assert_eq!(context.siblings[1].priority, Some(5));
2478    }
2479
2480    #[tokio::test]
2481    async fn test_pick_next_tasks_priority_order() {
2482        let ctx = TestContext::new().await;
2483        let task_mgr = TaskManager::new(ctx.pool());
2484
2485        // Create 4 tasks with different priorities
2486        let critical = task_mgr
2487            .add_task("Critical Task", None, None)
2488            .await
2489            .unwrap();
2490        task_mgr
2491            .update_task(critical.id, None, None, None, None, None, Some(1))
2492            .await
2493            .unwrap();
2494
2495        let low = task_mgr.add_task("Low Task", None, None).await.unwrap();
2496        task_mgr
2497            .update_task(low.id, None, None, None, None, None, Some(4))
2498            .await
2499            .unwrap();
2500
2501        let high = task_mgr.add_task("High Task", None, None).await.unwrap();
2502        task_mgr
2503            .update_task(high.id, None, None, None, None, None, Some(2))
2504            .await
2505            .unwrap();
2506
2507        let medium = task_mgr.add_task("Medium Task", None, None).await.unwrap();
2508        task_mgr
2509            .update_task(medium.id, None, None, None, None, None, Some(3))
2510            .await
2511            .unwrap();
2512
2513        // Pick next tasks should return them in priority order: critical > high > medium > low
2514        let tasks = task_mgr.pick_next_tasks(10, 10).await.unwrap();
2515
2516        assert_eq!(tasks.len(), 4);
2517        assert_eq!(tasks[0].id, critical.id); // Priority 1
2518        assert_eq!(tasks[1].id, high.id); // Priority 2
2519        assert_eq!(tasks[2].id, medium.id); // Priority 3
2520        assert_eq!(tasks[3].id, low.id); // Priority 4
2521    }
2522
2523    #[tokio::test]
2524    async fn test_pick_next_prefers_doing_over_todo() {
2525        let ctx = TestContext::new().await;
2526        let task_mgr = TaskManager::new(ctx.pool());
2527        let workspace_mgr = WorkspaceManager::new(ctx.pool());
2528
2529        // Create a parent task and set it as current
2530        let parent = task_mgr.add_task("Parent", None, None).await.unwrap();
2531        let parent_started = task_mgr.start_task(parent.id, false).await.unwrap();
2532        workspace_mgr
2533            .set_current_task(parent_started.task.id)
2534            .await
2535            .unwrap();
2536
2537        // Create two subtasks with same priority: one doing, one todo
2538        let doing_subtask = task_mgr
2539            .add_task("Doing Subtask", None, Some(parent.id))
2540            .await
2541            .unwrap();
2542        task_mgr.start_task(doing_subtask.id, false).await.unwrap();
2543        // Switch back to parent so doing_subtask is "pending" (doing but not current)
2544        workspace_mgr.set_current_task(parent.id).await.unwrap();
2545
2546        let _todo_subtask = task_mgr
2547            .add_task("Todo Subtask", None, Some(parent.id))
2548            .await
2549            .unwrap();
2550
2551        // Both have same priority (default), but doing should be picked first
2552        let result = task_mgr.pick_next().await.unwrap();
2553
2554        if let Some(task) = result.task {
2555            assert_eq!(
2556                task.id, doing_subtask.id,
2557                "Should recommend doing subtask over todo subtask"
2558            );
2559            assert_eq!(task.status, "doing");
2560        } else {
2561            panic!("Expected a task recommendation");
2562        }
2563    }
2564
2565    #[tokio::test]
2566    async fn test_multiple_doing_tasks_allowed() {
2567        let ctx = TestContext::new().await;
2568        let task_mgr = TaskManager::new(ctx.pool());
2569        let workspace_mgr = WorkspaceManager::new(ctx.pool());
2570
2571        // Create and start task A
2572        let task_a = task_mgr.add_task("Task A", None, None).await.unwrap();
2573        let task_a_started = task_mgr.start_task(task_a.id, false).await.unwrap();
2574        assert_eq!(task_a_started.task.status, "doing");
2575
2576        // Verify task A is current
2577        let current = workspace_mgr.get_current_task().await.unwrap();
2578        assert_eq!(current.current_task_id, Some(task_a.id));
2579
2580        // Create and start task B
2581        let task_b = task_mgr.add_task("Task B", None, None).await.unwrap();
2582        let task_b_started = task_mgr.start_task(task_b.id, false).await.unwrap();
2583        assert_eq!(task_b_started.task.status, "doing");
2584
2585        // Verify task B is now current
2586        let current = workspace_mgr.get_current_task().await.unwrap();
2587        assert_eq!(current.current_task_id, Some(task_b.id));
2588
2589        // Verify task A is still doing (not reverted to todo)
2590        let task_a_after = task_mgr.get_task(task_a.id).await.unwrap();
2591        assert_eq!(
2592            task_a_after.status, "doing",
2593            "Task A should remain doing even though it is not current"
2594        );
2595
2596        // Verify both tasks are in doing status
2597        let doing_tasks: Vec<Task> = sqlx::query_as(
2598            r#"SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form
2599             FROM tasks WHERE status = 'doing' ORDER BY id"#
2600        )
2601        .fetch_all(ctx.pool())
2602        .await
2603        .unwrap();
2604
2605        assert_eq!(doing_tasks.len(), 2, "Should have 2 doing tasks");
2606        assert_eq!(doing_tasks[0].id, task_a.id);
2607        assert_eq!(doing_tasks[1].id, task_b.id);
2608    }
2609    #[tokio::test]
2610    async fn test_find_tasks_pagination() {
2611        let ctx = TestContext::new().await;
2612        let task_mgr = TaskManager::new(ctx.pool());
2613
2614        // Create 15 tasks
2615        for i in 0..15 {
2616            task_mgr
2617                .add_task(&format!("Task {}", i), None, None)
2618                .await
2619                .unwrap();
2620        }
2621
2622        // Page 1: Limit 10, Offset 0
2623        let page1 = task_mgr
2624            .find_tasks(None, None, None, Some(10), Some(0))
2625            .await
2626            .unwrap();
2627        assert_eq!(page1.tasks.len(), 10);
2628        assert_eq!(page1.total_count, 15);
2629        assert!(page1.has_more);
2630        assert_eq!(page1.offset, 0);
2631
2632        // Page 2: Limit 10, Offset 10
2633        let page2 = task_mgr
2634            .find_tasks(None, None, None, Some(10), Some(10))
2635            .await
2636            .unwrap();
2637        assert_eq!(page2.tasks.len(), 5);
2638        assert_eq!(page2.total_count, 15);
2639        assert!(!page2.has_more);
2640        assert_eq!(page2.offset, 10);
2641    }
2642}
2643
2644// Re-export TaskContext for cli_handlers