intent_engine/
tasks.rs

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