intent_engine/
tasks.rs

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