intent_engine/
tasks.rs

1use crate::db::models::{
2    CurrentTaskInfo, DoneTaskResponse, Event, EventsSummary, NextStepSuggestion, ParentTaskInfo,
3    PickNextResponse, PreviousTaskInfo, SpawnSubtaskResponse, SubtaskInfo, SwitchTaskResponse,
4    Task, 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 events summary for a task
79    async fn get_events_summary(&self, task_id: i64) -> Result<EventsSummary> {
80        let total_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM events WHERE task_id = ?")
81            .bind(task_id)
82            .fetch_one(self.pool)
83            .await?;
84
85        let recent_events = sqlx::query_as::<_, Event>(
86            r#"
87            SELECT id, task_id, timestamp, log_type, discussion_data
88            FROM events
89            WHERE task_id = ?
90            ORDER BY timestamp DESC
91            LIMIT 10
92            "#,
93        )
94        .bind(task_id)
95        .fetch_all(self.pool)
96        .await?;
97
98        Ok(EventsSummary {
99            total_count,
100            recent_events,
101        })
102    }
103
104    /// Update a task
105    #[allow(clippy::too_many_arguments)]
106    pub async fn update_task(
107        &self,
108        id: i64,
109        name: Option<&str>,
110        spec: Option<&str>,
111        parent_id: Option<Option<i64>>,
112        status: Option<&str>,
113        complexity: Option<i32>,
114        priority: Option<i32>,
115    ) -> Result<Task> {
116        // Check task exists
117        let task = self.get_task(id).await?;
118
119        // Validate status if provided
120        if let Some(s) = status {
121            if !["todo", "doing", "done"].contains(&s) {
122                return Err(IntentError::InvalidInput(format!("Invalid status: {}", s)));
123            }
124        }
125
126        // Check for circular dependency if parent_id is being changed
127        if let Some(Some(pid)) = parent_id {
128            if pid == id {
129                return Err(IntentError::CircularDependency);
130            }
131            self.check_task_exists(pid).await?;
132            self.check_circular_dependency(id, pid).await?;
133        }
134
135        // Build dynamic update query
136        let mut query = String::from("UPDATE tasks SET ");
137        let mut updates = Vec::new();
138
139        if let Some(n) = name {
140            updates.push(format!("name = '{}'", n.replace('\'', "''")));
141        }
142
143        if let Some(s) = spec {
144            updates.push(format!("spec = '{}'", s.replace('\'', "''")));
145        }
146
147        if let Some(pid) = parent_id {
148            match pid {
149                Some(p) => updates.push(format!("parent_id = {}", p)),
150                None => updates.push("parent_id = NULL".to_string()),
151            }
152        }
153
154        if let Some(c) = complexity {
155            updates.push(format!("complexity = {}", c));
156        }
157
158        if let Some(p) = priority {
159            updates.push(format!("priority = {}", p));
160        }
161
162        if let Some(s) = status {
163            updates.push(format!("status = '{}'", s));
164
165            // Update timestamp fields based on status
166            let now = Utc::now();
167            match s {
168                "todo" if task.first_todo_at.is_none() => {
169                    updates.push(format!("first_todo_at = '{}'", now.to_rfc3339()));
170                },
171                "doing" if task.first_doing_at.is_none() => {
172                    updates.push(format!("first_doing_at = '{}'", now.to_rfc3339()));
173                },
174                "done" if task.first_done_at.is_none() => {
175                    updates.push(format!("first_done_at = '{}'", now.to_rfc3339()));
176                },
177                _ => {},
178            }
179        }
180
181        if updates.is_empty() {
182            return Ok(task);
183        }
184
185        query.push_str(&updates.join(", "));
186        query.push_str(&format!(" WHERE id = {}", id));
187
188        sqlx::query(&query).execute(self.pool).await?;
189
190        self.get_task(id).await
191    }
192
193    /// Delete a task
194    pub async fn delete_task(&self, id: i64) -> Result<()> {
195        self.check_task_exists(id).await?;
196
197        sqlx::query("DELETE FROM tasks WHERE id = ?")
198            .bind(id)
199            .execute(self.pool)
200            .await?;
201
202        Ok(())
203    }
204
205    /// Find tasks with optional filters
206    pub async fn find_tasks(
207        &self,
208        status: Option<&str>,
209        parent_id: Option<Option<i64>>,
210    ) -> Result<Vec<Task>> {
211        let mut query = String::from(
212            "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"
213        );
214        let mut conditions = Vec::new();
215
216        if let Some(s) = status {
217            query.push_str(" AND status = ?");
218            conditions.push(s.to_string());
219        }
220
221        if let Some(pid) = parent_id {
222            if let Some(p) = pid {
223                query.push_str(" AND parent_id = ?");
224                conditions.push(p.to_string());
225            } else {
226                query.push_str(" AND parent_id IS NULL");
227            }
228        }
229
230        query.push_str(" ORDER BY id");
231
232        let mut q = sqlx::query_as::<_, Task>(&query);
233        for cond in conditions {
234            q = q.bind(cond);
235        }
236
237        let tasks = q.fetch_all(self.pool).await?;
238        Ok(tasks)
239    }
240
241    /// Search tasks using full-text search (FTS5)
242    /// Returns tasks with match snippets showing highlighted keywords
243    pub async fn search_tasks(&self, query: &str) -> Result<Vec<TaskSearchResult>> {
244        // Escape special FTS5 characters in the query
245        let escaped_query = self.escape_fts_query(query);
246
247        // Use FTS5 to search and get snippets
248        // snippet(table, column, start_mark, end_mark, ellipsis, max_tokens)
249        // We search in both name (column 0) and spec (column 1)
250        let results = sqlx::query(
251            r#"
252            SELECT
253                t.id,
254                t.parent_id,
255                t.name,
256                t.spec,
257                t.status,
258                t.complexity,
259                t.priority,
260                t.first_todo_at,
261                t.first_doing_at,
262                t.first_done_at,
263                COALESCE(
264                    snippet(tasks_fts, 1, '**', '**', '...', 15),
265                    snippet(tasks_fts, 0, '**', '**', '...', 15)
266                ) as match_snippet
267            FROM tasks_fts
268            INNER JOIN tasks t ON tasks_fts.rowid = t.id
269            WHERE tasks_fts MATCH ?
270            ORDER BY rank
271            "#,
272        )
273        .bind(&escaped_query)
274        .fetch_all(self.pool)
275        .await?;
276
277        let mut search_results = Vec::new();
278        for row in results {
279            let task = Task {
280                id: row.get("id"),
281                parent_id: row.get("parent_id"),
282                name: row.get("name"),
283                spec: row.get("spec"),
284                status: row.get("status"),
285                complexity: row.get("complexity"),
286                priority: row.get("priority"),
287                first_todo_at: row.get("first_todo_at"),
288                first_doing_at: row.get("first_doing_at"),
289                first_done_at: row.get("first_done_at"),
290            };
291            let match_snippet: String = row.get("match_snippet");
292
293            search_results.push(TaskSearchResult {
294                task,
295                match_snippet,
296            });
297        }
298
299        Ok(search_results)
300    }
301
302    /// Escape FTS5 special characters in query
303    fn escape_fts_query(&self, query: &str) -> String {
304        // FTS5 queries are passed through as-is to support advanced syntax
305        // Users can use operators like AND, OR, NOT, *, "phrase search", etc.
306        // We only need to handle basic escaping for quotes
307        query.replace('"', "\"\"")
308    }
309
310    /// Start a task (atomic: update status + set current)
311    pub async fn start_task(&self, id: i64, with_events: bool) -> Result<TaskWithEvents> {
312        let mut tx = self.pool.begin().await?;
313
314        let now = Utc::now();
315
316        // Update task status to doing
317        sqlx::query(
318            r#"
319            UPDATE tasks
320            SET status = 'doing', first_doing_at = COALESCE(first_doing_at, ?)
321            WHERE id = ?
322            "#,
323        )
324        .bind(now)
325        .bind(id)
326        .execute(&mut *tx)
327        .await?;
328
329        // Set as current task
330        sqlx::query(
331            r#"
332            INSERT OR REPLACE INTO workspace_state (key, value)
333            VALUES ('current_task_id', ?)
334            "#,
335        )
336        .bind(id.to_string())
337        .execute(&mut *tx)
338        .await?;
339
340        tx.commit().await?;
341
342        if with_events {
343            self.get_task_with_events(id).await
344        } else {
345            let task = self.get_task(id).await?;
346            Ok(TaskWithEvents {
347                task,
348                events_summary: None,
349            })
350        }
351    }
352
353    /// Complete the current focused task (atomic: check children + update status + clear current)
354    /// This command only operates on the current_task_id.
355    /// Prerequisites: A task must be set as current
356    pub async fn done_task(&self) -> Result<DoneTaskResponse> {
357        let mut tx = self.pool.begin().await?;
358
359        // Get the current task ID
360        let current_task_id: Option<String> =
361            sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
362                .fetch_optional(&mut *tx)
363                .await?;
364
365        let id = current_task_id.and_then(|s| s.parse::<i64>().ok()).ok_or(
366            IntentError::InvalidInput(
367                "No current task is set. Use 'current --set <ID>' to set a task first.".to_string(),
368            ),
369        )?;
370
371        // Get the task info before completing it
372        let task_info: (String, Option<i64>) =
373            sqlx::query_as("SELECT name, parent_id FROM tasks WHERE id = ?")
374                .bind(id)
375                .fetch_one(&mut *tx)
376                .await?;
377        let (task_name, parent_id) = task_info;
378
379        // Check if all children are done
380        let uncompleted_children: i64 = sqlx::query_scalar(
381            "SELECT COUNT(*) FROM tasks WHERE parent_id = ? AND status != 'done'",
382        )
383        .bind(id)
384        .fetch_one(&mut *tx)
385        .await?;
386
387        if uncompleted_children > 0 {
388            return Err(IntentError::UncompletedChildren);
389        }
390
391        let now = Utc::now();
392
393        // Update task status to done
394        sqlx::query(
395            r#"
396            UPDATE tasks
397            SET status = 'done', first_done_at = COALESCE(first_done_at, ?)
398            WHERE id = ?
399            "#,
400        )
401        .bind(now)
402        .bind(id)
403        .execute(&mut *tx)
404        .await?;
405
406        // Clear the current task
407        sqlx::query("DELETE FROM workspace_state WHERE key = 'current_task_id'")
408            .execute(&mut *tx)
409            .await?;
410
411        // Determine next step suggestion based on context
412        let next_step_suggestion = if let Some(parent_task_id) = parent_id {
413            // Task has a parent - check sibling status
414            let remaining_siblings: i64 = sqlx::query_scalar(
415                "SELECT COUNT(*) FROM tasks WHERE parent_id = ? AND status != 'done' AND id != ?",
416            )
417            .bind(parent_task_id)
418            .bind(id)
419            .fetch_one(&mut *tx)
420            .await?;
421
422            if remaining_siblings == 0 {
423                // All siblings are done - parent is ready
424                let parent_name: String = sqlx::query_scalar("SELECT name FROM tasks WHERE id = ?")
425                    .bind(parent_task_id)
426                    .fetch_one(&mut *tx)
427                    .await?;
428
429                NextStepSuggestion::ParentIsReady {
430                    message: format!(
431                        "All sub-tasks of parent #{} '{}' are now complete. The parent task is ready for your attention.",
432                        parent_task_id, parent_name
433                    ),
434                    parent_task_id,
435                    parent_task_name: parent_name,
436                }
437            } else {
438                // Siblings remain
439                let parent_name: String = sqlx::query_scalar("SELECT name FROM tasks WHERE id = ?")
440                    .bind(parent_task_id)
441                    .fetch_one(&mut *tx)
442                    .await?;
443
444                NextStepSuggestion::SiblingTasksRemain {
445                    message: format!(
446                        "Task #{} completed. Parent task #{} '{}' has other sub-tasks remaining.",
447                        id, parent_task_id, parent_name
448                    ),
449                    parent_task_id,
450                    parent_task_name: parent_name,
451                    remaining_siblings_count: remaining_siblings,
452                }
453            }
454        } else {
455            // No parent - check if this was a top-level task with children or standalone
456            let child_count: i64 =
457                sqlx::query_scalar("SELECT COUNT(*) FROM tasks WHERE parent_id = ?")
458                    .bind(id)
459                    .fetch_one(&mut *tx)
460                    .await?;
461
462            if child_count > 0 {
463                // Top-level task with children completed
464                NextStepSuggestion::TopLevelTaskCompleted {
465                    message: format!(
466                        "Top-level task #{} '{}' has been completed. Well done!",
467                        id, task_name
468                    ),
469                    completed_task_id: id,
470                    completed_task_name: task_name.clone(),
471                }
472            } else {
473                // Check if workspace is clear
474                let remaining_tasks: i64 = sqlx::query_scalar(
475                    "SELECT COUNT(*) FROM tasks WHERE status != 'done' AND id != ?",
476                )
477                .bind(id)
478                .fetch_one(&mut *tx)
479                .await?;
480
481                if remaining_tasks == 0 {
482                    NextStepSuggestion::WorkspaceIsClear {
483                        message: format!(
484                            "Project complete! Task #{} was the last remaining task. There are no more 'todo' or 'doing' tasks.",
485                            id
486                        ),
487                        completed_task_id: id,
488                    }
489                } else {
490                    NextStepSuggestion::NoParentContext {
491                        message: format!("Task #{} '{}' has been completed.", id, task_name),
492                        completed_task_id: id,
493                        completed_task_name: task_name.clone(),
494                    }
495                }
496            }
497        };
498
499        tx.commit().await?;
500
501        let completed_task = self.get_task(id).await?;
502
503        Ok(DoneTaskResponse {
504            completed_task,
505            workspace_status: WorkspaceStatus {
506                current_task_id: None,
507            },
508            next_step_suggestion,
509        })
510    }
511
512    /// Check if a task exists
513    async fn check_task_exists(&self, id: i64) -> Result<()> {
514        let exists: bool = sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM tasks WHERE id = ?)")
515            .bind(id)
516            .fetch_one(self.pool)
517            .await?;
518
519        if !exists {
520            return Err(IntentError::TaskNotFound(id));
521        }
522
523        Ok(())
524    }
525
526    /// Check for circular dependencies
527    async fn check_circular_dependency(&self, task_id: i64, new_parent_id: i64) -> Result<()> {
528        let mut current_id = new_parent_id;
529
530        loop {
531            if current_id == task_id {
532                return Err(IntentError::CircularDependency);
533            }
534
535            let parent: Option<i64> =
536                sqlx::query_scalar("SELECT parent_id FROM tasks WHERE id = ?")
537                    .bind(current_id)
538                    .fetch_optional(self.pool)
539                    .await?;
540
541            match parent {
542                Some(pid) => current_id = pid,
543                None => break,
544            }
545        }
546
547        Ok(())
548    }
549
550    /// Switch to a specific task (atomic: update status to doing + set as current)
551    /// If the task is not in 'doing' status, it will be transitioned to 'doing'
552    /// Returns response with previous task info (if any) and current task info
553    pub async fn switch_to_task(&self, id: i64) -> Result<SwitchTaskResponse> {
554        // Verify task exists
555        self.check_task_exists(id).await?;
556
557        let mut tx = self.pool.begin().await?;
558        let now = Utc::now();
559
560        // Get current task info before switching (if any)
561        let current_task_id: Option<String> =
562            sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
563                .fetch_optional(&mut *tx)
564                .await?;
565
566        let previous_task = if let Some(prev_id_str) = current_task_id {
567            if let Ok(prev_id) = prev_id_str.parse::<i64>() {
568                // Set previous task back to 'todo' if it was 'doing'
569                sqlx::query(
570                    r#"
571                    UPDATE tasks
572                    SET status = 'todo'
573                    WHERE id = ? AND status = 'doing'
574                    "#,
575                )
576                .bind(prev_id)
577                .execute(&mut *tx)
578                .await?;
579
580                Some(PreviousTaskInfo {
581                    id: prev_id,
582                    status: "todo".to_string(),
583                })
584            } else {
585                None
586            }
587        } else {
588            None
589        };
590
591        // Update new task to doing status if not already
592        sqlx::query(
593            r#"
594            UPDATE tasks
595            SET status = 'doing',
596                first_doing_at = COALESCE(first_doing_at, ?)
597            WHERE id = ? AND status != 'doing'
598            "#,
599        )
600        .bind(now)
601        .bind(id)
602        .execute(&mut *tx)
603        .await?;
604
605        // Get new task name for response
606        let (task_name, task_status): (String, String) =
607            sqlx::query_as("SELECT name, status FROM tasks WHERE id = ?")
608                .bind(id)
609                .fetch_one(&mut *tx)
610                .await?;
611
612        // Set as current task
613        sqlx::query(
614            r#"
615            INSERT OR REPLACE INTO workspace_state (key, value)
616            VALUES ('current_task_id', ?)
617            "#,
618        )
619        .bind(id.to_string())
620        .execute(&mut *tx)
621        .await?;
622
623        tx.commit().await?;
624
625        Ok(SwitchTaskResponse {
626            previous_task,
627            current_task: CurrentTaskInfo {
628                id,
629                name: task_name,
630                status: task_status,
631            },
632        })
633    }
634
635    /// Create a subtask under the current task and switch to it (atomic operation)
636    /// Returns error if there is no current task
637    /// Returns response with subtask info and parent task info
638    pub async fn spawn_subtask(
639        &self,
640        name: &str,
641        spec: Option<&str>,
642    ) -> Result<SpawnSubtaskResponse> {
643        // Get current task
644        let current_task_id: Option<String> =
645            sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
646                .fetch_optional(self.pool)
647                .await?;
648
649        let parent_id = current_task_id.and_then(|s| s.parse::<i64>().ok()).ok_or(
650            IntentError::InvalidInput("No current task to create subtask under".to_string()),
651        )?;
652
653        // Get parent task info
654        let parent_name: String = sqlx::query_scalar("SELECT name FROM tasks WHERE id = ?")
655            .bind(parent_id)
656            .fetch_one(self.pool)
657            .await?;
658
659        // Create the subtask
660        let subtask = self.add_task(name, spec, Some(parent_id)).await?;
661
662        // Switch to the new subtask (sets status to doing and updates current_task_id)
663        self.switch_to_task(subtask.id).await?;
664
665        Ok(SpawnSubtaskResponse {
666            subtask: SubtaskInfo {
667                id: subtask.id,
668                name: subtask.name,
669                parent_id,
670                status: "doing".to_string(),
671            },
672            parent_task: ParentTaskInfo {
673                id: parent_id,
674                name: parent_name,
675            },
676        })
677    }
678
679    /// Intelligently pick tasks from 'todo' and transition them to 'doing'
680    /// Returns tasks that were successfully transitioned
681    ///
682    /// # Arguments
683    /// * `max_count` - Maximum number of tasks to pick
684    /// * `capacity_limit` - Maximum total number of tasks allowed in 'doing' status
685    ///
686    /// # Logic
687    /// 1. Check current 'doing' task count
688    /// 2. Calculate available capacity
689    /// 3. Select tasks from 'todo' (prioritized by: priority DESC, complexity ASC)
690    /// 4. Transition selected tasks to 'doing'
691    pub async fn pick_next_tasks(
692        &self,
693        max_count: usize,
694        capacity_limit: usize,
695    ) -> Result<Vec<Task>> {
696        let mut tx = self.pool.begin().await?;
697
698        // Get current doing count
699        let doing_count: i64 =
700            sqlx::query_scalar("SELECT COUNT(*) FROM tasks WHERE status = 'doing'")
701                .fetch_one(&mut *tx)
702                .await?;
703
704        // Calculate available capacity
705        let available = capacity_limit.saturating_sub(doing_count as usize);
706        if available == 0 {
707            return Ok(vec![]);
708        }
709
710        let limit = std::cmp::min(max_count, available);
711
712        // Select tasks from todo, prioritizing by priority DESC, complexity ASC
713        let todo_tasks = sqlx::query_as::<_, Task>(
714            r#"
715            SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at
716            FROM tasks
717            WHERE status = 'todo'
718            ORDER BY
719                COALESCE(priority, 0) DESC,
720                COALESCE(complexity, 5) ASC,
721                id ASC
722            LIMIT ?
723            "#,
724        )
725        .bind(limit as i64)
726        .fetch_all(&mut *tx)
727        .await?;
728
729        if todo_tasks.is_empty() {
730            return Ok(vec![]);
731        }
732
733        let now = Utc::now();
734
735        // Transition selected tasks to 'doing'
736        for task in &todo_tasks {
737            sqlx::query(
738                r#"
739                UPDATE tasks
740                SET status = 'doing',
741                    first_doing_at = COALESCE(first_doing_at, ?)
742                WHERE id = ?
743                "#,
744            )
745            .bind(now)
746            .bind(task.id)
747            .execute(&mut *tx)
748            .await?;
749        }
750
751        tx.commit().await?;
752
753        // Fetch and return updated tasks in the same order
754        let task_ids: Vec<i64> = todo_tasks.iter().map(|t| t.id).collect();
755        let placeholders = vec!["?"; task_ids.len()].join(",");
756        let query = format!(
757            "SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at
758             FROM tasks WHERE id IN ({})
759             ORDER BY
760                 COALESCE(priority, 0) DESC,
761                 COALESCE(complexity, 5) ASC,
762                 id ASC",
763            placeholders
764        );
765
766        let mut q = sqlx::query_as::<_, Task>(&query);
767        for id in task_ids {
768            q = q.bind(id);
769        }
770
771        let updated_tasks = q.fetch_all(self.pool).await?;
772        Ok(updated_tasks)
773    }
774
775    /// Intelligently recommend the next task to work on based on context-aware priority model.
776    ///
777    /// Priority logic:
778    /// 1. First priority: Subtasks of the current focused task (depth-first)
779    /// 2. Second priority: Top-level tasks (breadth-first)
780    /// 3. No recommendation: Return appropriate empty state
781    ///
782    /// This command does NOT modify task status.
783    pub async fn pick_next(&self) -> Result<PickNextResponse> {
784        // Step 1: Check if there's a current focused task
785        let current_task_id: Option<String> =
786            sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
787                .fetch_optional(self.pool)
788                .await?;
789
790        if let Some(current_id_str) = current_task_id {
791            if let Ok(current_id) = current_id_str.parse::<i64>() {
792                // First priority: Get todo subtasks of current focused task
793                let subtasks = sqlx::query_as::<_, Task>(
794                    r#"
795                    SELECT id, parent_id, name, spec, status, complexity, priority,
796                           first_todo_at, first_doing_at, first_done_at
797                    FROM tasks
798                    WHERE parent_id = ? AND status = 'todo'
799                    ORDER BY COALESCE(priority, 999999) ASC, id ASC
800                    LIMIT 1
801                    "#,
802                )
803                .bind(current_id)
804                .fetch_optional(self.pool)
805                .await?;
806
807                if let Some(task) = subtasks {
808                    return Ok(PickNextResponse::focused_subtask(task));
809                }
810            }
811        }
812
813        // Step 2: Second priority - get top-level todo tasks
814        let top_level_task = sqlx::query_as::<_, Task>(
815            r#"
816            SELECT id, parent_id, name, spec, status, complexity, priority,
817                   first_todo_at, first_doing_at, first_done_at
818            FROM tasks
819            WHERE parent_id IS NULL AND status = 'todo'
820            ORDER BY COALESCE(priority, 999999) ASC, id ASC
821            LIMIT 1
822            "#,
823        )
824        .fetch_optional(self.pool)
825        .await?;
826
827        if let Some(task) = top_level_task {
828            return Ok(PickNextResponse::top_level_task(task));
829        }
830
831        // Step 3: No recommendation - determine why
832        // Check if there are any tasks at all
833        let total_tasks: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM tasks")
834            .fetch_one(self.pool)
835            .await?;
836
837        if total_tasks == 0 {
838            return Ok(PickNextResponse::no_tasks_in_project());
839        }
840
841        // Check if all tasks are completed
842        let todo_or_doing_count: i64 =
843            sqlx::query_scalar("SELECT COUNT(*) FROM tasks WHERE status IN ('todo', 'doing')")
844                .fetch_one(self.pool)
845                .await?;
846
847        if todo_or_doing_count == 0 {
848            return Ok(PickNextResponse::all_tasks_completed());
849        }
850
851        // Otherwise, there are tasks but none available based on current context
852        Ok(PickNextResponse::no_available_todos())
853    }
854}
855
856#[cfg(test)]
857mod tests {
858    use super::*;
859    use crate::events::EventManager;
860    use crate::test_utils::test_helpers::TestContext;
861
862    #[tokio::test]
863    async fn test_add_task() {
864        let ctx = TestContext::new().await;
865        let manager = TaskManager::new(ctx.pool());
866
867        let task = manager.add_task("Test task", None, None).await.unwrap();
868
869        assert_eq!(task.name, "Test task");
870        assert_eq!(task.status, "todo");
871        assert!(task.first_todo_at.is_some());
872        assert!(task.first_doing_at.is_none());
873        assert!(task.first_done_at.is_none());
874    }
875
876    #[tokio::test]
877    async fn test_add_task_with_spec() {
878        let ctx = TestContext::new().await;
879        let manager = TaskManager::new(ctx.pool());
880
881        let spec = "This is a task specification";
882        let task = manager
883            .add_task("Test task", Some(spec), None)
884            .await
885            .unwrap();
886
887        assert_eq!(task.name, "Test task");
888        assert_eq!(task.spec.as_deref(), Some(spec));
889    }
890
891    #[tokio::test]
892    async fn test_add_task_with_parent() {
893        let ctx = TestContext::new().await;
894        let manager = TaskManager::new(ctx.pool());
895
896        let parent = manager.add_task("Parent task", None, None).await.unwrap();
897        let child = manager
898            .add_task("Child task", None, Some(parent.id))
899            .await
900            .unwrap();
901
902        assert_eq!(child.parent_id, Some(parent.id));
903    }
904
905    #[tokio::test]
906    async fn test_get_task() {
907        let ctx = TestContext::new().await;
908        let manager = TaskManager::new(ctx.pool());
909
910        let created = manager.add_task("Test task", None, None).await.unwrap();
911        let retrieved = manager.get_task(created.id).await.unwrap();
912
913        assert_eq!(created.id, retrieved.id);
914        assert_eq!(created.name, retrieved.name);
915    }
916
917    #[tokio::test]
918    async fn test_get_task_not_found() {
919        let ctx = TestContext::new().await;
920        let manager = TaskManager::new(ctx.pool());
921
922        let result = manager.get_task(999).await;
923        assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
924    }
925
926    #[tokio::test]
927    async fn test_update_task_name() {
928        let ctx = TestContext::new().await;
929        let manager = TaskManager::new(ctx.pool());
930
931        let task = manager.add_task("Original name", None, None).await.unwrap();
932        let updated = manager
933            .update_task(task.id, Some("New name"), None, None, None, None, None)
934            .await
935            .unwrap();
936
937        assert_eq!(updated.name, "New name");
938    }
939
940    #[tokio::test]
941    async fn test_update_task_status() {
942        let ctx = TestContext::new().await;
943        let manager = TaskManager::new(ctx.pool());
944
945        let task = manager.add_task("Test task", None, None).await.unwrap();
946        let updated = manager
947            .update_task(task.id, None, None, None, Some("doing"), None, None)
948            .await
949            .unwrap();
950
951        assert_eq!(updated.status, "doing");
952        assert!(updated.first_doing_at.is_some());
953    }
954
955    #[tokio::test]
956    async fn test_delete_task() {
957        let ctx = TestContext::new().await;
958        let manager = TaskManager::new(ctx.pool());
959
960        let task = manager.add_task("Test task", None, None).await.unwrap();
961        manager.delete_task(task.id).await.unwrap();
962
963        let result = manager.get_task(task.id).await;
964        assert!(result.is_err());
965    }
966
967    #[tokio::test]
968    async fn test_find_tasks_by_status() {
969        let ctx = TestContext::new().await;
970        let manager = TaskManager::new(ctx.pool());
971
972        manager.add_task("Todo task", None, None).await.unwrap();
973        let doing_task = manager.add_task("Doing task", None, None).await.unwrap();
974        manager
975            .update_task(doing_task.id, None, None, None, Some("doing"), None, None)
976            .await
977            .unwrap();
978
979        let todo_tasks = manager.find_tasks(Some("todo"), None).await.unwrap();
980        let doing_tasks = manager.find_tasks(Some("doing"), None).await.unwrap();
981
982        assert_eq!(todo_tasks.len(), 1);
983        assert_eq!(doing_tasks.len(), 1);
984        assert_eq!(doing_tasks[0].status, "doing");
985    }
986
987    #[tokio::test]
988    async fn test_find_tasks_by_parent() {
989        let ctx = TestContext::new().await;
990        let manager = TaskManager::new(ctx.pool());
991
992        let parent = manager.add_task("Parent", None, None).await.unwrap();
993        manager
994            .add_task("Child 1", None, Some(parent.id))
995            .await
996            .unwrap();
997        manager
998            .add_task("Child 2", None, Some(parent.id))
999            .await
1000            .unwrap();
1001
1002        let children = manager
1003            .find_tasks(None, Some(Some(parent.id)))
1004            .await
1005            .unwrap();
1006
1007        assert_eq!(children.len(), 2);
1008    }
1009
1010    #[tokio::test]
1011    async fn test_start_task() {
1012        let ctx = TestContext::new().await;
1013        let manager = TaskManager::new(ctx.pool());
1014
1015        let task = manager.add_task("Test task", None, None).await.unwrap();
1016        let started = manager.start_task(task.id, false).await.unwrap();
1017
1018        assert_eq!(started.task.status, "doing");
1019        assert!(started.task.first_doing_at.is_some());
1020
1021        // Verify it's set as current task
1022        let current: Option<String> =
1023            sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
1024                .fetch_optional(ctx.pool())
1025                .await
1026                .unwrap();
1027
1028        assert_eq!(current, Some(task.id.to_string()));
1029    }
1030
1031    #[tokio::test]
1032    async fn test_start_task_with_events() {
1033        let ctx = TestContext::new().await;
1034        let manager = TaskManager::new(ctx.pool());
1035
1036        let task = manager.add_task("Test task", None, None).await.unwrap();
1037
1038        // Add an event
1039        sqlx::query("INSERT INTO events (task_id, log_type, discussion_data) VALUES (?, ?, ?)")
1040            .bind(task.id)
1041            .bind("test")
1042            .bind("test event")
1043            .execute(ctx.pool())
1044            .await
1045            .unwrap();
1046
1047        let started = manager.start_task(task.id, true).await.unwrap();
1048
1049        assert!(started.events_summary.is_some());
1050        let summary = started.events_summary.unwrap();
1051        assert_eq!(summary.total_count, 1);
1052    }
1053
1054    #[tokio::test]
1055    async fn test_done_task() {
1056        let ctx = TestContext::new().await;
1057        let manager = TaskManager::new(ctx.pool());
1058
1059        let task = manager.add_task("Test task", None, None).await.unwrap();
1060        manager.start_task(task.id, false).await.unwrap();
1061        let response = manager.done_task().await.unwrap();
1062
1063        assert_eq!(response.completed_task.status, "done");
1064        assert!(response.completed_task.first_done_at.is_some());
1065        assert_eq!(response.workspace_status.current_task_id, None);
1066
1067        // Should be WORKSPACE_IS_CLEAR since it's the only task
1068        match response.next_step_suggestion {
1069            NextStepSuggestion::WorkspaceIsClear { .. } => {},
1070            _ => panic!("Expected WorkspaceIsClear suggestion"),
1071        }
1072
1073        // Verify current task is cleared
1074        let current: Option<String> =
1075            sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
1076                .fetch_optional(ctx.pool())
1077                .await
1078                .unwrap();
1079
1080        assert!(current.is_none());
1081    }
1082
1083    #[tokio::test]
1084    async fn test_done_task_with_uncompleted_children() {
1085        let ctx = TestContext::new().await;
1086        let manager = TaskManager::new(ctx.pool());
1087
1088        let parent = manager.add_task("Parent", None, None).await.unwrap();
1089        manager
1090            .add_task("Child", None, Some(parent.id))
1091            .await
1092            .unwrap();
1093
1094        // Set parent as current task
1095        manager.start_task(parent.id, false).await.unwrap();
1096
1097        let result = manager.done_task().await;
1098        assert!(matches!(result, Err(IntentError::UncompletedChildren)));
1099    }
1100
1101    #[tokio::test]
1102    async fn test_done_task_with_completed_children() {
1103        let ctx = TestContext::new().await;
1104        let manager = TaskManager::new(ctx.pool());
1105
1106        let parent = manager.add_task("Parent", None, None).await.unwrap();
1107        let child = manager
1108            .add_task("Child", None, Some(parent.id))
1109            .await
1110            .unwrap();
1111
1112        // Complete child first
1113        manager.start_task(child.id, false).await.unwrap();
1114        let child_response = manager.done_task().await.unwrap();
1115
1116        // Child completion should suggest parent is ready
1117        match child_response.next_step_suggestion {
1118            NextStepSuggestion::ParentIsReady { parent_task_id, .. } => {
1119                assert_eq!(parent_task_id, parent.id);
1120            },
1121            _ => panic!("Expected ParentIsReady suggestion"),
1122        }
1123
1124        // Now parent can be completed
1125        manager.start_task(parent.id, false).await.unwrap();
1126        let parent_response = manager.done_task().await.unwrap();
1127        assert_eq!(parent_response.completed_task.status, "done");
1128
1129        // Parent completion should indicate top-level task completed (since it had children)
1130        match parent_response.next_step_suggestion {
1131            NextStepSuggestion::TopLevelTaskCompleted { .. } => {},
1132            _ => panic!("Expected TopLevelTaskCompleted suggestion"),
1133        }
1134    }
1135
1136    #[tokio::test]
1137    async fn test_circular_dependency() {
1138        let ctx = TestContext::new().await;
1139        let manager = TaskManager::new(ctx.pool());
1140
1141        let task1 = manager.add_task("Task 1", None, None).await.unwrap();
1142        let task2 = manager
1143            .add_task("Task 2", None, Some(task1.id))
1144            .await
1145            .unwrap();
1146
1147        // Try to make task1 a child of task2 (circular)
1148        let result = manager
1149            .update_task(task1.id, None, None, Some(Some(task2.id)), None, None, None)
1150            .await;
1151
1152        assert!(matches!(result, Err(IntentError::CircularDependency)));
1153    }
1154
1155    #[tokio::test]
1156    async fn test_invalid_parent_id() {
1157        let ctx = TestContext::new().await;
1158        let manager = TaskManager::new(ctx.pool());
1159
1160        let result = manager.add_task("Test", None, Some(999)).await;
1161        assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
1162    }
1163
1164    #[tokio::test]
1165    async fn test_update_task_complexity_and_priority() {
1166        let ctx = TestContext::new().await;
1167        let manager = TaskManager::new(ctx.pool());
1168
1169        let task = manager.add_task("Test task", None, None).await.unwrap();
1170        let updated = manager
1171            .update_task(task.id, None, None, None, None, Some(8), Some(10))
1172            .await
1173            .unwrap();
1174
1175        assert_eq!(updated.complexity, Some(8));
1176        assert_eq!(updated.priority, Some(10));
1177    }
1178
1179    #[tokio::test]
1180    async fn test_switch_to_task() {
1181        let ctx = TestContext::new().await;
1182        let manager = TaskManager::new(ctx.pool());
1183
1184        // Create a task
1185        let task = manager.add_task("Test task", None, None).await.unwrap();
1186        assert_eq!(task.status, "todo");
1187
1188        // Switch to it
1189        let response = manager.switch_to_task(task.id).await.unwrap();
1190        assert_eq!(response.current_task.id, task.id);
1191        assert_eq!(response.current_task.status, "doing");
1192        assert!(response.previous_task.is_none());
1193
1194        // Verify it's set as current task
1195        let current: Option<String> =
1196            sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
1197                .fetch_optional(ctx.pool())
1198                .await
1199                .unwrap();
1200
1201        assert_eq!(current, Some(task.id.to_string()));
1202    }
1203
1204    #[tokio::test]
1205    async fn test_switch_to_task_already_doing() {
1206        let ctx = TestContext::new().await;
1207        let manager = TaskManager::new(ctx.pool());
1208
1209        // Create and start a task
1210        let task = manager.add_task("Test task", None, None).await.unwrap();
1211        manager.start_task(task.id, false).await.unwrap();
1212
1213        // Switch to it again (should be idempotent)
1214        let response = manager.switch_to_task(task.id).await.unwrap();
1215        assert_eq!(response.current_task.id, task.id);
1216        assert_eq!(response.current_task.status, "doing");
1217    }
1218
1219    #[tokio::test]
1220    async fn test_spawn_subtask() {
1221        let ctx = TestContext::new().await;
1222        let manager = TaskManager::new(ctx.pool());
1223
1224        // Create and start a parent task
1225        let parent = manager.add_task("Parent task", None, None).await.unwrap();
1226        manager.start_task(parent.id, false).await.unwrap();
1227
1228        // Spawn a subtask
1229        let response = manager
1230            .spawn_subtask("Child task", Some("Details"))
1231            .await
1232            .unwrap();
1233
1234        assert_eq!(response.subtask.parent_id, parent.id);
1235        assert_eq!(response.subtask.name, "Child task");
1236        assert_eq!(response.subtask.status, "doing");
1237        assert_eq!(response.parent_task.id, parent.id);
1238        assert_eq!(response.parent_task.name, "Parent task");
1239
1240        // Verify subtask is now the current task
1241        let current: Option<String> =
1242            sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
1243                .fetch_optional(ctx.pool())
1244                .await
1245                .unwrap();
1246
1247        assert_eq!(current, Some(response.subtask.id.to_string()));
1248
1249        // Verify subtask is in doing status
1250        let retrieved = manager.get_task(response.subtask.id).await.unwrap();
1251        assert_eq!(retrieved.status, "doing");
1252    }
1253
1254    #[tokio::test]
1255    async fn test_spawn_subtask_no_current_task() {
1256        let ctx = TestContext::new().await;
1257        let manager = TaskManager::new(ctx.pool());
1258
1259        // Try to spawn subtask without a current task
1260        let result = manager.spawn_subtask("Child", None).await;
1261        assert!(result.is_err());
1262    }
1263
1264    #[tokio::test]
1265    async fn test_pick_next_tasks_basic() {
1266        let ctx = TestContext::new().await;
1267        let manager = TaskManager::new(ctx.pool());
1268
1269        // Create 10 todo tasks
1270        for i in 1..=10 {
1271            manager
1272                .add_task(&format!("Task {}", i), None, None)
1273                .await
1274                .unwrap();
1275        }
1276
1277        // Pick 5 tasks with capacity limit of 5
1278        let picked = manager.pick_next_tasks(5, 5).await.unwrap();
1279
1280        assert_eq!(picked.len(), 5);
1281        for task in &picked {
1282            assert_eq!(task.status, "doing");
1283            assert!(task.first_doing_at.is_some());
1284        }
1285
1286        // Verify total doing count
1287        let doing_count: i64 =
1288            sqlx::query_scalar("SELECT COUNT(*) FROM tasks WHERE status = 'doing'")
1289                .fetch_one(ctx.pool())
1290                .await
1291                .unwrap();
1292
1293        assert_eq!(doing_count, 5);
1294    }
1295
1296    #[tokio::test]
1297    async fn test_pick_next_tasks_with_existing_doing() {
1298        let ctx = TestContext::new().await;
1299        let manager = TaskManager::new(ctx.pool());
1300
1301        // Create 10 todo tasks
1302        for i in 1..=10 {
1303            manager
1304                .add_task(&format!("Task {}", i), None, None)
1305                .await
1306                .unwrap();
1307        }
1308
1309        // Start 2 tasks
1310        let tasks = manager.find_tasks(Some("todo"), None).await.unwrap();
1311        manager.start_task(tasks[0].id, false).await.unwrap();
1312        manager.start_task(tasks[1].id, false).await.unwrap();
1313
1314        // Pick more tasks with capacity limit of 5
1315        let picked = manager.pick_next_tasks(10, 5).await.unwrap();
1316
1317        // Should only pick 3 more (5 - 2 = 3)
1318        assert_eq!(picked.len(), 3);
1319
1320        // Verify total doing count
1321        let doing_count: i64 =
1322            sqlx::query_scalar("SELECT COUNT(*) FROM tasks WHERE status = 'doing'")
1323                .fetch_one(ctx.pool())
1324                .await
1325                .unwrap();
1326
1327        assert_eq!(doing_count, 5);
1328    }
1329
1330    #[tokio::test]
1331    async fn test_pick_next_tasks_at_capacity() {
1332        let ctx = TestContext::new().await;
1333        let manager = TaskManager::new(ctx.pool());
1334
1335        // Create 10 tasks
1336        for i in 1..=10 {
1337            manager
1338                .add_task(&format!("Task {}", i), None, None)
1339                .await
1340                .unwrap();
1341        }
1342
1343        // Fill capacity
1344        let first_batch = manager.pick_next_tasks(5, 5).await.unwrap();
1345        assert_eq!(first_batch.len(), 5);
1346
1347        // Try to pick more (should return empty)
1348        let second_batch = manager.pick_next_tasks(5, 5).await.unwrap();
1349        assert_eq!(second_batch.len(), 0);
1350    }
1351
1352    #[tokio::test]
1353    async fn test_pick_next_tasks_priority_ordering() {
1354        let ctx = TestContext::new().await;
1355        let manager = TaskManager::new(ctx.pool());
1356
1357        // Create tasks with different priorities
1358        let low = manager.add_task("Low priority", None, None).await.unwrap();
1359        manager
1360            .update_task(low.id, None, None, None, None, None, Some(1))
1361            .await
1362            .unwrap();
1363
1364        let high = manager.add_task("High priority", None, None).await.unwrap();
1365        manager
1366            .update_task(high.id, None, None, None, None, None, Some(10))
1367            .await
1368            .unwrap();
1369
1370        let medium = manager
1371            .add_task("Medium priority", None, None)
1372            .await
1373            .unwrap();
1374        manager
1375            .update_task(medium.id, None, None, None, None, None, Some(5))
1376            .await
1377            .unwrap();
1378
1379        // Pick tasks
1380        let picked = manager.pick_next_tasks(3, 5).await.unwrap();
1381
1382        // Should be ordered by priority DESC
1383        assert_eq!(picked.len(), 3);
1384        assert_eq!(picked[0].priority, Some(10)); // high
1385        assert_eq!(picked[1].priority, Some(5)); // medium
1386        assert_eq!(picked[2].priority, Some(1)); // low
1387    }
1388
1389    #[tokio::test]
1390    async fn test_pick_next_tasks_complexity_ordering() {
1391        let ctx = TestContext::new().await;
1392        let manager = TaskManager::new(ctx.pool());
1393
1394        // Create tasks with different complexities (same priority)
1395        let complex = manager.add_task("Complex", None, None).await.unwrap();
1396        manager
1397            .update_task(complex.id, None, None, None, None, Some(9), Some(5))
1398            .await
1399            .unwrap();
1400
1401        let simple = manager.add_task("Simple", None, None).await.unwrap();
1402        manager
1403            .update_task(simple.id, None, None, None, None, Some(1), Some(5))
1404            .await
1405            .unwrap();
1406
1407        let medium = manager.add_task("Medium", None, None).await.unwrap();
1408        manager
1409            .update_task(medium.id, None, None, None, None, Some(5), Some(5))
1410            .await
1411            .unwrap();
1412
1413        // Pick tasks
1414        let picked = manager.pick_next_tasks(3, 5).await.unwrap();
1415
1416        // Should be ordered by complexity ASC (simple first)
1417        assert_eq!(picked.len(), 3);
1418        assert_eq!(picked[0].complexity, Some(1)); // simple
1419        assert_eq!(picked[1].complexity, Some(5)); // medium
1420        assert_eq!(picked[2].complexity, Some(9)); // complex
1421    }
1422
1423    #[tokio::test]
1424    async fn test_done_task_sibling_tasks_remain() {
1425        let ctx = TestContext::new().await;
1426        let manager = TaskManager::new(ctx.pool());
1427
1428        // Create parent with multiple children
1429        let parent = manager.add_task("Parent Task", None, None).await.unwrap();
1430        let child1 = manager
1431            .add_task("Child 1", None, Some(parent.id))
1432            .await
1433            .unwrap();
1434        let child2 = manager
1435            .add_task("Child 2", None, Some(parent.id))
1436            .await
1437            .unwrap();
1438        let _child3 = manager
1439            .add_task("Child 3", None, Some(parent.id))
1440            .await
1441            .unwrap();
1442
1443        // Complete first child
1444        manager.start_task(child1.id, false).await.unwrap();
1445        let response = manager.done_task().await.unwrap();
1446
1447        // Should indicate siblings remain
1448        match response.next_step_suggestion {
1449            NextStepSuggestion::SiblingTasksRemain {
1450                parent_task_id,
1451                remaining_siblings_count,
1452                ..
1453            } => {
1454                assert_eq!(parent_task_id, parent.id);
1455                assert_eq!(remaining_siblings_count, 2); // child2 and child3
1456            },
1457            _ => panic!("Expected SiblingTasksRemain suggestion"),
1458        }
1459
1460        // Complete second child
1461        manager.start_task(child2.id, false).await.unwrap();
1462        let response2 = manager.done_task().await.unwrap();
1463
1464        // Should still indicate siblings remain
1465        match response2.next_step_suggestion {
1466            NextStepSuggestion::SiblingTasksRemain {
1467                remaining_siblings_count,
1468                ..
1469            } => {
1470                assert_eq!(remaining_siblings_count, 1); // only child3
1471            },
1472            _ => panic!("Expected SiblingTasksRemain suggestion"),
1473        }
1474    }
1475
1476    #[tokio::test]
1477    async fn test_done_task_top_level_with_children() {
1478        let ctx = TestContext::new().await;
1479        let manager = TaskManager::new(ctx.pool());
1480
1481        // Create top-level task with children
1482        let parent = manager.add_task("Epic Task", None, None).await.unwrap();
1483        let child = manager
1484            .add_task("Sub Task", None, Some(parent.id))
1485            .await
1486            .unwrap();
1487
1488        // Complete child first
1489        manager.start_task(child.id, false).await.unwrap();
1490        manager.done_task().await.unwrap();
1491
1492        // Complete parent
1493        manager.start_task(parent.id, false).await.unwrap();
1494        let response = manager.done_task().await.unwrap();
1495
1496        // Should be TOP_LEVEL_TASK_COMPLETED
1497        match response.next_step_suggestion {
1498            NextStepSuggestion::TopLevelTaskCompleted {
1499                completed_task_id,
1500                completed_task_name,
1501                ..
1502            } => {
1503                assert_eq!(completed_task_id, parent.id);
1504                assert_eq!(completed_task_name, "Epic Task");
1505            },
1506            _ => panic!("Expected TopLevelTaskCompleted suggestion"),
1507        }
1508    }
1509
1510    #[tokio::test]
1511    async fn test_done_task_no_parent_context() {
1512        let ctx = TestContext::new().await;
1513        let manager = TaskManager::new(ctx.pool());
1514
1515        // Create multiple standalone tasks
1516        let task1 = manager
1517            .add_task("Standalone Task 1", None, None)
1518            .await
1519            .unwrap();
1520        let _task2 = manager
1521            .add_task("Standalone Task 2", None, None)
1522            .await
1523            .unwrap();
1524
1525        // Complete first task
1526        manager.start_task(task1.id, false).await.unwrap();
1527        let response = manager.done_task().await.unwrap();
1528
1529        // Should be NO_PARENT_CONTEXT since task2 is still pending
1530        match response.next_step_suggestion {
1531            NextStepSuggestion::NoParentContext {
1532                completed_task_id,
1533                completed_task_name,
1534                ..
1535            } => {
1536                assert_eq!(completed_task_id, task1.id);
1537                assert_eq!(completed_task_name, "Standalone Task 1");
1538            },
1539            _ => panic!("Expected NoParentContext suggestion"),
1540        }
1541    }
1542
1543    #[tokio::test]
1544    async fn test_search_tasks_by_name() {
1545        let ctx = TestContext::new().await;
1546        let manager = TaskManager::new(ctx.pool());
1547
1548        // Create tasks with different names
1549        manager
1550            .add_task("Authentication bug fix", Some("Fix login issue"), None)
1551            .await
1552            .unwrap();
1553        manager
1554            .add_task("Database migration", Some("Migrate to PostgreSQL"), None)
1555            .await
1556            .unwrap();
1557        manager
1558            .add_task("Authentication feature", Some("Add OAuth2 support"), None)
1559            .await
1560            .unwrap();
1561
1562        // Search for "authentication"
1563        let results = manager.search_tasks("authentication").await.unwrap();
1564
1565        assert_eq!(results.len(), 2);
1566        assert!(results[0]
1567            .task
1568            .name
1569            .to_lowercase()
1570            .contains("authentication"));
1571        assert!(results[1]
1572            .task
1573            .name
1574            .to_lowercase()
1575            .contains("authentication"));
1576
1577        // Check that match_snippet is present
1578        assert!(!results[0].match_snippet.is_empty());
1579    }
1580
1581    #[tokio::test]
1582    async fn test_search_tasks_by_spec() {
1583        let ctx = TestContext::new().await;
1584        let manager = TaskManager::new(ctx.pool());
1585
1586        // Create tasks
1587        manager
1588            .add_task("Task 1", Some("Implement JWT authentication"), None)
1589            .await
1590            .unwrap();
1591        manager
1592            .add_task("Task 2", Some("Add user registration"), None)
1593            .await
1594            .unwrap();
1595        manager
1596            .add_task("Task 3", Some("JWT token refresh"), None)
1597            .await
1598            .unwrap();
1599
1600        // Search for "JWT"
1601        let results = manager.search_tasks("JWT").await.unwrap();
1602
1603        assert_eq!(results.len(), 2);
1604        for result in &results {
1605            assert!(result
1606                .task
1607                .spec
1608                .as_ref()
1609                .unwrap()
1610                .to_uppercase()
1611                .contains("JWT"));
1612        }
1613    }
1614
1615    #[tokio::test]
1616    async fn test_search_tasks_with_advanced_query() {
1617        let ctx = TestContext::new().await;
1618        let manager = TaskManager::new(ctx.pool());
1619
1620        // Create tasks
1621        manager
1622            .add_task("Bug fix", Some("Fix critical authentication bug"), None)
1623            .await
1624            .unwrap();
1625        manager
1626            .add_task("Feature", Some("Add authentication feature"), None)
1627            .await
1628            .unwrap();
1629        manager
1630            .add_task("Bug report", Some("Report critical database bug"), None)
1631            .await
1632            .unwrap();
1633
1634        // Search with AND operator
1635        let results = manager
1636            .search_tasks("authentication AND bug")
1637            .await
1638            .unwrap();
1639
1640        assert_eq!(results.len(), 1);
1641        assert!(results[0]
1642            .task
1643            .spec
1644            .as_ref()
1645            .unwrap()
1646            .contains("authentication"));
1647        assert!(results[0].task.spec.as_ref().unwrap().contains("bug"));
1648    }
1649
1650    #[tokio::test]
1651    async fn test_search_tasks_no_results() {
1652        let ctx = TestContext::new().await;
1653        let manager = TaskManager::new(ctx.pool());
1654
1655        // Create tasks
1656        manager
1657            .add_task("Task 1", Some("Some description"), None)
1658            .await
1659            .unwrap();
1660
1661        // Search for non-existent term
1662        let results = manager.search_tasks("nonexistent").await.unwrap();
1663
1664        assert_eq!(results.len(), 0);
1665    }
1666
1667    #[tokio::test]
1668    async fn test_search_tasks_snippet_highlighting() {
1669        let ctx = TestContext::new().await;
1670        let manager = TaskManager::new(ctx.pool());
1671
1672        // Create task with keyword in spec
1673        manager
1674            .add_task(
1675                "Test task",
1676                Some("This is a description with the keyword authentication in the middle"),
1677                None,
1678            )
1679            .await
1680            .unwrap();
1681
1682        // Search for "authentication"
1683        let results = manager.search_tasks("authentication").await.unwrap();
1684
1685        assert_eq!(results.len(), 1);
1686        // Check that snippet contains highlighted keyword (marked with **)
1687        assert!(results[0].match_snippet.contains("**authentication**"));
1688    }
1689
1690    #[tokio::test]
1691    async fn test_pick_next_focused_subtask() {
1692        let ctx = TestContext::new().await;
1693        let manager = TaskManager::new(ctx.pool());
1694
1695        // Create parent task and set as current
1696        let parent = manager.add_task("Parent task", None, None).await.unwrap();
1697        manager.start_task(parent.id, false).await.unwrap();
1698
1699        // Create subtasks with different priorities
1700        let subtask1 = manager
1701            .add_task("Subtask 1", None, Some(parent.id))
1702            .await
1703            .unwrap();
1704        let subtask2 = manager
1705            .add_task("Subtask 2", None, Some(parent.id))
1706            .await
1707            .unwrap();
1708
1709        // Set priorities: subtask1 = 2, subtask2 = 1 (lower number = higher priority)
1710        manager
1711            .update_task(subtask1.id, None, None, None, None, None, Some(2))
1712            .await
1713            .unwrap();
1714        manager
1715            .update_task(subtask2.id, None, None, None, None, None, Some(1))
1716            .await
1717            .unwrap();
1718
1719        // Pick next should recommend subtask2 (priority 1)
1720        let response = manager.pick_next().await.unwrap();
1721
1722        assert_eq!(response.suggestion_type, "FOCUSED_SUB_TASK");
1723        assert!(response.task.is_some());
1724        assert_eq!(response.task.as_ref().unwrap().id, subtask2.id);
1725        assert_eq!(response.task.as_ref().unwrap().name, "Subtask 2");
1726    }
1727
1728    #[tokio::test]
1729    async fn test_pick_next_top_level_task() {
1730        let ctx = TestContext::new().await;
1731        let manager = TaskManager::new(ctx.pool());
1732
1733        // Create top-level tasks with different priorities
1734        let task1 = manager.add_task("Task 1", None, None).await.unwrap();
1735        let task2 = manager.add_task("Task 2", None, None).await.unwrap();
1736
1737        // Set priorities: task1 = 5, task2 = 3 (lower number = higher priority)
1738        manager
1739            .update_task(task1.id, None, None, None, None, None, Some(5))
1740            .await
1741            .unwrap();
1742        manager
1743            .update_task(task2.id, None, None, None, None, None, Some(3))
1744            .await
1745            .unwrap();
1746
1747        // Pick next should recommend task2 (priority 3)
1748        let response = manager.pick_next().await.unwrap();
1749
1750        assert_eq!(response.suggestion_type, "TOP_LEVEL_TASK");
1751        assert!(response.task.is_some());
1752        assert_eq!(response.task.as_ref().unwrap().id, task2.id);
1753        assert_eq!(response.task.as_ref().unwrap().name, "Task 2");
1754    }
1755
1756    #[tokio::test]
1757    async fn test_pick_next_no_tasks() {
1758        let ctx = TestContext::new().await;
1759        let manager = TaskManager::new(ctx.pool());
1760
1761        // No tasks created
1762        let response = manager.pick_next().await.unwrap();
1763
1764        assert_eq!(response.suggestion_type, "NONE");
1765        assert_eq!(response.reason_code.as_deref(), Some("NO_TASKS_IN_PROJECT"));
1766        assert!(response.message.is_some());
1767    }
1768
1769    #[tokio::test]
1770    async fn test_pick_next_all_completed() {
1771        let ctx = TestContext::new().await;
1772        let manager = TaskManager::new(ctx.pool());
1773
1774        // Create task and mark as done
1775        let task = manager.add_task("Task 1", None, None).await.unwrap();
1776        manager.start_task(task.id, false).await.unwrap();
1777        manager.done_task().await.unwrap();
1778
1779        // Pick next should indicate all tasks completed
1780        let response = manager.pick_next().await.unwrap();
1781
1782        assert_eq!(response.suggestion_type, "NONE");
1783        assert_eq!(response.reason_code.as_deref(), Some("ALL_TASKS_COMPLETED"));
1784        assert!(response.message.is_some());
1785    }
1786
1787    #[tokio::test]
1788    async fn test_pick_next_no_available_todos() {
1789        let ctx = TestContext::new().await;
1790        let manager = TaskManager::new(ctx.pool());
1791
1792        // Create a parent task that's in "doing" status
1793        let parent = manager.add_task("Parent task", None, None).await.unwrap();
1794        manager.start_task(parent.id, false).await.unwrap();
1795
1796        // Create a subtask also in "doing" status (no "todo" subtasks)
1797        let subtask = manager
1798            .add_task("Subtask", None, Some(parent.id))
1799            .await
1800            .unwrap();
1801        // Switch to subtask (this will set parent back to todo, so we need to manually set subtask to doing)
1802        sqlx::query("UPDATE tasks SET status = 'doing' WHERE id = ?")
1803            .bind(subtask.id)
1804            .execute(ctx.pool())
1805            .await
1806            .unwrap();
1807
1808        // Set subtask as current
1809        sqlx::query(
1810            "INSERT OR REPLACE INTO workspace_state (key, value) VALUES ('current_task_id', ?)",
1811        )
1812        .bind(subtask.id.to_string())
1813        .execute(ctx.pool())
1814        .await
1815        .unwrap();
1816
1817        // Set parent to doing (not todo)
1818        sqlx::query("UPDATE tasks SET status = 'doing' WHERE id = ?")
1819            .bind(parent.id)
1820            .execute(ctx.pool())
1821            .await
1822            .unwrap();
1823
1824        // Pick next should indicate no available todos
1825        let response = manager.pick_next().await.unwrap();
1826
1827        assert_eq!(response.suggestion_type, "NONE");
1828        assert_eq!(response.reason_code.as_deref(), Some("NO_AVAILABLE_TODOS"));
1829        assert!(response.message.is_some());
1830    }
1831
1832    #[tokio::test]
1833    async fn test_pick_next_priority_ordering() {
1834        let ctx = TestContext::new().await;
1835        let manager = TaskManager::new(ctx.pool());
1836
1837        // Create parent and set as current
1838        let parent = manager.add_task("Parent", None, None).await.unwrap();
1839        manager.start_task(parent.id, false).await.unwrap();
1840
1841        // Create multiple subtasks with various priorities
1842        let sub1 = manager
1843            .add_task("Priority 10", None, Some(parent.id))
1844            .await
1845            .unwrap();
1846        manager
1847            .update_task(sub1.id, None, None, None, None, None, Some(10))
1848            .await
1849            .unwrap();
1850
1851        let sub2 = manager
1852            .add_task("Priority 1", None, Some(parent.id))
1853            .await
1854            .unwrap();
1855        manager
1856            .update_task(sub2.id, None, None, None, None, None, Some(1))
1857            .await
1858            .unwrap();
1859
1860        let sub3 = manager
1861            .add_task("Priority 5", None, Some(parent.id))
1862            .await
1863            .unwrap();
1864        manager
1865            .update_task(sub3.id, None, None, None, None, None, Some(5))
1866            .await
1867            .unwrap();
1868
1869        // Pick next should recommend the task with priority 1 (lowest number)
1870        let response = manager.pick_next().await.unwrap();
1871
1872        assert_eq!(response.suggestion_type, "FOCUSED_SUB_TASK");
1873        assert_eq!(response.task.as_ref().unwrap().id, sub2.id);
1874        assert_eq!(response.task.as_ref().unwrap().name, "Priority 1");
1875    }
1876
1877    #[tokio::test]
1878    async fn test_pick_next_falls_back_to_top_level_when_no_subtasks() {
1879        let ctx = TestContext::new().await;
1880        let manager = TaskManager::new(ctx.pool());
1881
1882        // Create parent without subtasks and set as current
1883        let parent = manager.add_task("Parent", None, None).await.unwrap();
1884        manager.start_task(parent.id, false).await.unwrap();
1885
1886        // Create another top-level task
1887        let top_level = manager
1888            .add_task("Top level task", None, None)
1889            .await
1890            .unwrap();
1891
1892        // Pick next should fall back to top-level task since parent has no todo subtasks
1893        let response = manager.pick_next().await.unwrap();
1894
1895        assert_eq!(response.suggestion_type, "TOP_LEVEL_TASK");
1896        assert_eq!(response.task.as_ref().unwrap().id, top_level.id);
1897    }
1898
1899    // ===== Missing coverage tests =====
1900
1901    #[tokio::test]
1902    async fn test_get_task_with_events() {
1903        let ctx = TestContext::new().await;
1904        let task_mgr = TaskManager::new(ctx.pool());
1905        let event_mgr = EventManager::new(ctx.pool());
1906
1907        let task = task_mgr.add_task("Test", None, None).await.unwrap();
1908
1909        // Add some events
1910        event_mgr
1911            .add_event(task.id, "progress", "Event 1")
1912            .await
1913            .unwrap();
1914        event_mgr
1915            .add_event(task.id, "decision", "Event 2")
1916            .await
1917            .unwrap();
1918
1919        let result = task_mgr.get_task_with_events(task.id).await.unwrap();
1920
1921        assert_eq!(result.task.id, task.id);
1922        assert!(result.events_summary.is_some());
1923
1924        let summary = result.events_summary.unwrap();
1925        assert_eq!(summary.total_count, 2);
1926        assert_eq!(summary.recent_events.len(), 2);
1927        assert_eq!(summary.recent_events[0].log_type, "decision"); // Most recent first
1928        assert_eq!(summary.recent_events[1].log_type, "progress");
1929    }
1930
1931    #[tokio::test]
1932    async fn test_get_task_with_events_nonexistent() {
1933        let ctx = TestContext::new().await;
1934        let task_mgr = TaskManager::new(ctx.pool());
1935
1936        let result = task_mgr.get_task_with_events(999).await;
1937        assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
1938    }
1939
1940    #[tokio::test]
1941    async fn test_get_task_with_many_events() {
1942        let ctx = TestContext::new().await;
1943        let task_mgr = TaskManager::new(ctx.pool());
1944        let event_mgr = EventManager::new(ctx.pool());
1945
1946        let task = task_mgr.add_task("Test", None, None).await.unwrap();
1947
1948        // Add 20 events
1949        for i in 0..20 {
1950            event_mgr
1951                .add_event(task.id, "test", &format!("Event {}", i))
1952                .await
1953                .unwrap();
1954        }
1955
1956        let result = task_mgr.get_task_with_events(task.id).await.unwrap();
1957        let summary = result.events_summary.unwrap();
1958
1959        assert_eq!(summary.total_count, 20);
1960        assert_eq!(summary.recent_events.len(), 10); // Limited to 10
1961    }
1962
1963    #[tokio::test]
1964    async fn test_get_task_with_no_events() {
1965        let ctx = TestContext::new().await;
1966        let task_mgr = TaskManager::new(ctx.pool());
1967
1968        let task = task_mgr.add_task("Test", None, None).await.unwrap();
1969
1970        let result = task_mgr.get_task_with_events(task.id).await.unwrap();
1971        let summary = result.events_summary.unwrap();
1972
1973        assert_eq!(summary.total_count, 0);
1974        assert_eq!(summary.recent_events.len(), 0);
1975    }
1976
1977    #[tokio::test]
1978    async fn test_pick_next_tasks_zero_capacity() {
1979        let ctx = TestContext::new().await;
1980        let task_mgr = TaskManager::new(ctx.pool());
1981
1982        task_mgr.add_task("Task 1", None, None).await.unwrap();
1983
1984        // capacity_limit = 0 means no capacity available
1985        let results = task_mgr.pick_next_tasks(10, 0).await.unwrap();
1986        assert_eq!(results.len(), 0);
1987    }
1988
1989    #[tokio::test]
1990    async fn test_pick_next_tasks_capacity_exceeds_available() {
1991        let ctx = TestContext::new().await;
1992        let task_mgr = TaskManager::new(ctx.pool());
1993
1994        task_mgr.add_task("Task 1", None, None).await.unwrap();
1995        task_mgr.add_task("Task 2", None, None).await.unwrap();
1996
1997        // Request 10 tasks but only 2 available, capacity = 100
1998        let results = task_mgr.pick_next_tasks(10, 100).await.unwrap();
1999        assert_eq!(results.len(), 2); // Only returns available tasks
2000    }
2001}