intent_engine/
plan.rs

1//! Plan Interface - Declarative Task Management
2//!
3//! Provides a declarative API for creating and updating task structures,
4//! inspired by TodoWrite pattern. Simplifies complex operations into
5//! single atomic calls.
6
7use serde::{Deserialize, Serialize};
8use sqlx::Row;
9use std::collections::HashMap;
10
11/// Request for creating/updating task structure declaratively
12#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
13pub struct PlanRequest {
14    /// Task tree to create or update
15    pub tasks: Vec<TaskTree>,
16}
17
18/// Hierarchical task definition with nested children
19#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
20pub struct TaskTree {
21    /// Task name (used as identifier for lookups)
22    pub name: String,
23
24    /// Optional task specification/description
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub spec: Option<String>,
27
28    /// Optional priority level
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub priority: Option<PriorityValue>,
31
32    /// Nested child tasks (direct hierarchy expression)
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub children: Option<Vec<TaskTree>>,
35
36    /// Task dependencies by name (name-based references)
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub depends_on: Option<Vec<String>>,
39
40    /// Optional explicit task ID (for forced updates)
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub task_id: Option<i64>,
43}
44
45/// Priority value as string enum for JSON API
46#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
47#[serde(rename_all = "lowercase")]
48pub enum PriorityValue {
49    Critical,
50    High,
51    Medium,
52    Low,
53}
54
55impl PriorityValue {
56    /// Convert to integer representation for database storage
57    pub fn to_int(&self) -> i32 {
58        match self {
59            PriorityValue::Critical => 1,
60            PriorityValue::High => 2,
61            PriorityValue::Medium => 3,
62            PriorityValue::Low => 4,
63        }
64    }
65
66    /// Create from integer representation
67    pub fn from_int(value: i32) -> Option<Self> {
68        match value {
69            1 => Some(PriorityValue::Critical),
70            2 => Some(PriorityValue::High),
71            3 => Some(PriorityValue::Medium),
72            4 => Some(PriorityValue::Low),
73            _ => None,
74        }
75    }
76
77    /// Convert to string representation
78    pub fn as_str(&self) -> &'static str {
79        match self {
80            PriorityValue::Critical => "critical",
81            PriorityValue::High => "high",
82            PriorityValue::Medium => "medium",
83            PriorityValue::Low => "low",
84        }
85    }
86}
87
88/// Result of plan execution
89#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
90pub struct PlanResult {
91    /// Whether the operation succeeded
92    pub success: bool,
93
94    /// Mapping of task names to their IDs (for reference)
95    pub task_id_map: HashMap<String, i64>,
96
97    /// Number of tasks created
98    pub created_count: usize,
99
100    /// Number of tasks updated
101    pub updated_count: usize,
102
103    /// Number of dependencies created
104    pub dependency_count: usize,
105
106    /// Optional error message if success = false
107    #[serde(skip_serializing_if = "Option::is_none")]
108    pub error: Option<String>,
109}
110
111impl PlanResult {
112    /// Create a successful result
113    pub fn success(
114        task_id_map: HashMap<String, i64>,
115        created_count: usize,
116        updated_count: usize,
117        dependency_count: usize,
118    ) -> Self {
119        Self {
120            success: true,
121            task_id_map,
122            created_count,
123            updated_count,
124            dependency_count,
125            error: None,
126        }
127    }
128
129    /// Create an error result
130    pub fn error(message: impl Into<String>) -> Self {
131        Self {
132            success: false,
133            task_id_map: HashMap::new(),
134            created_count: 0,
135            updated_count: 0,
136            dependency_count: 0,
137            error: Some(message.into()),
138        }
139    }
140}
141
142// ============================================================================
143// Name Extraction and Classification Logic
144// ============================================================================
145
146/// Extract all task names from a task tree (recursive)
147pub fn extract_all_names(tasks: &[TaskTree]) -> Vec<String> {
148    let mut names = Vec::new();
149
150    for task in tasks {
151        names.push(task.name.clone());
152
153        if let Some(children) = &task.children {
154            names.extend(extract_all_names(children));
155        }
156    }
157
158    names
159}
160
161/// Flatten task tree into a linear list with parent information
162#[derive(Debug, Clone, PartialEq)]
163pub struct FlatTask {
164    pub name: String,
165    pub spec: Option<String>,
166    pub priority: Option<PriorityValue>,
167    pub parent_name: Option<String>,
168    pub depends_on: Vec<String>,
169    pub task_id: Option<i64>,
170}
171
172pub fn flatten_task_tree(tasks: &[TaskTree]) -> Vec<FlatTask> {
173    flatten_task_tree_recursive(tasks, None)
174}
175
176fn flatten_task_tree_recursive(tasks: &[TaskTree], parent_name: Option<String>) -> Vec<FlatTask> {
177    let mut flat = Vec::new();
178
179    for task in tasks {
180        let flat_task = FlatTask {
181            name: task.name.clone(),
182            spec: task.spec.clone(),
183            priority: task.priority.clone(),
184            parent_name: parent_name.clone(),
185            depends_on: task.depends_on.clone().unwrap_or_default(),
186            task_id: task.task_id,
187        };
188
189        flat.push(flat_task);
190
191        // Recursively flatten children
192        if let Some(children) = &task.children {
193            flat.extend(flatten_task_tree_recursive(
194                children,
195                Some(task.name.clone()),
196            ));
197        }
198    }
199
200    flat
201}
202
203/// Operation classification result
204#[derive(Debug, Clone, PartialEq)]
205pub enum Operation {
206    Create(FlatTask),
207    Update { task_id: i64, task: FlatTask },
208}
209
210/// Classify tasks into create/update operations based on existing task IDs
211///
212/// # Arguments
213/// * `flat_tasks` - Flattened task list
214/// * `existing_names` - Map of existing task names to their IDs
215///
216/// # Returns
217/// Classified operations (create or update)
218pub fn classify_operations(
219    flat_tasks: &[FlatTask],
220    existing_names: &HashMap<String, i64>,
221) -> Vec<Operation> {
222    let mut operations = Vec::new();
223
224    for task in flat_tasks {
225        // Priority: explicit task_id > name lookup > create
226        let operation = if let Some(task_id) = task.task_id {
227            // Explicit task_id → forced update
228            Operation::Update {
229                task_id,
230                task: task.clone(),
231            }
232        } else if let Some(&task_id) = existing_names.get(&task.name) {
233            // Name found in DB → update
234            Operation::Update {
235                task_id,
236                task: task.clone(),
237            }
238        } else {
239            // Not found → create
240            Operation::Create(task.clone())
241        };
242
243        operations.push(operation);
244    }
245
246    operations
247}
248
249/// Find duplicate names in a task list
250pub fn find_duplicate_names(tasks: &[TaskTree]) -> Vec<String> {
251    let mut seen = HashMap::new();
252    let mut duplicates = Vec::new();
253
254    for name in extract_all_names(tasks) {
255        let count = seen.entry(name.clone()).or_insert(0);
256        *count += 1;
257        if *count == 2 {
258            // Only add once when we first detect the duplicate
259            duplicates.push(name);
260        }
261    }
262
263    duplicates
264}
265
266// ============================================================================
267// Database Operations (Plan Executor)
268// ============================================================================
269
270use crate::error::{IntentError, Result};
271use chrono::Utc;
272use sqlx::SqlitePool;
273
274/// Plan executor for creating/updating task structures
275pub struct PlanExecutor<'a> {
276    pool: &'a SqlitePool,
277}
278
279impl<'a> PlanExecutor<'a> {
280    /// Create a new plan executor
281    pub fn new(pool: &'a SqlitePool) -> Self {
282        Self { pool }
283    }
284
285    /// Execute a plan request (Phase 2: create + update mode)
286    pub async fn execute(&self, request: &PlanRequest) -> Result<PlanResult> {
287        // 1. Check for duplicate names in the request
288        let duplicates = find_duplicate_names(&request.tasks);
289        if !duplicates.is_empty() {
290            return Ok(PlanResult::error(format!(
291                "Duplicate task names in request: {:?}",
292                duplicates
293            )));
294        }
295
296        // 2. Extract all task names
297        let all_names = extract_all_names(&request.tasks);
298
299        // 3. Find existing tasks by name
300        let existing = self.find_tasks_by_names(&all_names).await?;
301
302        // 4. Flatten the task tree
303        let flat_tasks = flatten_task_tree(&request.tasks);
304
305        // 5. Validate dependencies exist in the plan
306        if let Err(e) = self.validate_dependencies(&flat_tasks) {
307            return Ok(PlanResult::error(e.to_string()));
308        }
309
310        // 6. Detect circular dependencies
311        if let Err(e) = self.detect_circular_dependencies(&flat_tasks) {
312            return Ok(PlanResult::error(e.to_string()));
313        }
314
315        // 7. Execute in transaction
316        let mut tx = self.pool.begin().await?;
317
318        // 8. Create or update tasks based on existence
319        let mut task_id_map = HashMap::new();
320        let mut created_count = 0;
321        let mut updated_count = 0;
322
323        for task in &flat_tasks {
324            if let Some(&existing_id) = existing.get(&task.name) {
325                // Task exists -> UPDATE
326                self.update_task_in_tx(&mut tx, existing_id, task).await?;
327                task_id_map.insert(task.name.clone(), existing_id);
328                updated_count += 1;
329            } else {
330                // Task doesn't exist -> CREATE
331                let id = self.create_task_in_tx(&mut tx, task).await?;
332                task_id_map.insert(task.name.clone(), id);
333                created_count += 1;
334            }
335        }
336
337        // 9. Build parent-child relationships
338        self.build_parent_child_relations(&mut tx, &flat_tasks, &task_id_map)
339            .await?;
340
341        // 10. Build dependencies
342        let dep_count = self
343            .build_dependencies(&mut tx, &flat_tasks, &task_id_map)
344            .await?;
345
346        // 11. Commit transaction
347        tx.commit().await?;
348
349        // 12. Return success result
350        Ok(PlanResult::success(
351            task_id_map,
352            created_count,
353            updated_count,
354            dep_count,
355        ))
356    }
357
358    /// Find tasks by names
359    async fn find_tasks_by_names(&self, names: &[String]) -> Result<HashMap<String, i64>> {
360        if names.is_empty() {
361            return Ok(HashMap::new());
362        }
363
364        let mut map = HashMap::new();
365
366        // Query all names at once using IN clause
367        // Build placeholders: ?, ?, ?...
368        let placeholders = names.iter().map(|_| "?").collect::<Vec<_>>().join(",");
369        let query = format!(
370            "SELECT id, name FROM tasks WHERE name IN ({})",
371            placeholders
372        );
373
374        let mut query_builder = sqlx::query(&query);
375        for name in names {
376            query_builder = query_builder.bind(name);
377        }
378
379        let rows = query_builder.fetch_all(self.pool).await?;
380
381        for row in rows {
382            let id: i64 = row.get("id");
383            let name: String = row.get("name");
384            map.insert(name, id);
385        }
386
387        Ok(map)
388    }
389
390    /// Create a single task in a transaction
391    async fn create_task_in_tx(
392        &self,
393        tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
394        task: &FlatTask,
395    ) -> Result<i64> {
396        let now = Utc::now();
397        let priority = task.priority.as_ref().map(|p| p.to_int()).unwrap_or(3); // Default: medium
398
399        let result = sqlx::query(
400            r#"
401            INSERT INTO tasks (name, spec, priority, status, first_todo_at)
402            VALUES (?, ?, ?, 'todo', ?)
403            "#,
404        )
405        .bind(&task.name)
406        .bind(&task.spec)
407        .bind(priority)
408        .bind(now)
409        .execute(&mut **tx)
410        .await?;
411
412        Ok(result.last_insert_rowid())
413    }
414
415    /// Update a single task in a transaction (Phase 2)
416    /// Only updates non-None fields to support partial updates
417    async fn update_task_in_tx(
418        &self,
419        tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
420        task_id: i64,
421        task: &FlatTask,
422    ) -> Result<()> {
423        // Update spec if provided
424        if let Some(spec) = &task.spec {
425            sqlx::query("UPDATE tasks SET spec = ? WHERE id = ?")
426                .bind(spec)
427                .bind(task_id)
428                .execute(&mut **tx)
429                .await?;
430        }
431
432        // Update priority if provided
433        if let Some(priority) = &task.priority {
434            sqlx::query("UPDATE tasks SET priority = ? WHERE id = ?")
435                .bind(priority.to_int())
436                .bind(task_id)
437                .execute(&mut **tx)
438                .await?;
439        }
440
441        // Note: We don't update name, status, or timestamps
442        // - name: Used for identity, changing it would break references
443        // - status: Should be managed through task lifecycle commands
444        // - timestamps: Should preserve original creation time
445
446        Ok(())
447    }
448
449    /// Build parent-child relationships
450    async fn build_parent_child_relations(
451        &self,
452        tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
453        flat_tasks: &[FlatTask],
454        task_id_map: &HashMap<String, i64>,
455    ) -> Result<()> {
456        for task in flat_tasks {
457            if let Some(parent_name) = &task.parent_name {
458                let task_id = task_id_map.get(&task.name).ok_or_else(|| {
459                    IntentError::InvalidInput(format!("Task not found: {}", task.name))
460                })?;
461
462                let parent_id = task_id_map.get(parent_name).ok_or_else(|| {
463                    IntentError::InvalidInput(format!("Parent task not found: {}", parent_name))
464                })?;
465
466                sqlx::query("UPDATE tasks SET parent_id = ? WHERE id = ?")
467                    .bind(parent_id)
468                    .bind(task_id)
469                    .execute(&mut **tx)
470                    .await?;
471            }
472        }
473
474        Ok(())
475    }
476
477    /// Build dependency relationships
478    async fn build_dependencies(
479        &self,
480        tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
481        flat_tasks: &[FlatTask],
482        task_id_map: &HashMap<String, i64>,
483    ) -> Result<usize> {
484        let mut count = 0;
485
486        for task in flat_tasks {
487            if !task.depends_on.is_empty() {
488                let blocked_id = task_id_map.get(&task.name).ok_or_else(|| {
489                    IntentError::InvalidInput(format!("Task not found: {}", task.name))
490                })?;
491
492                for dep_name in &task.depends_on {
493                    let blocking_id = task_id_map.get(dep_name).ok_or_else(|| {
494                        IntentError::InvalidInput(format!(
495                            "Dependency '{}' not found for task '{}'",
496                            dep_name, task.name
497                        ))
498                    })?;
499
500                    sqlx::query(
501                        "INSERT INTO dependencies (blocking_task_id, blocked_task_id) VALUES (?, ?)",
502                    )
503                    .bind(blocking_id)
504                    .bind(blocked_id)
505                    .execute(&mut **tx)
506                    .await?;
507
508                    count += 1;
509                }
510            }
511        }
512
513        Ok(count)
514    }
515
516    /// Validate that all dependencies exist in the plan
517    fn validate_dependencies(&self, flat_tasks: &[FlatTask]) -> Result<()> {
518        let task_names: std::collections::HashSet<_> =
519            flat_tasks.iter().map(|t| t.name.as_str()).collect();
520
521        for task in flat_tasks {
522            for dep_name in &task.depends_on {
523                if !task_names.contains(dep_name.as_str()) {
524                    return Err(IntentError::InvalidInput(format!(
525                        "Task '{}' depends on '{}', but '{}' is not in the plan",
526                        task.name, dep_name, dep_name
527                    )));
528                }
529            }
530        }
531
532        Ok(())
533    }
534
535    /// Detect circular dependencies using Tarjan's algorithm for strongly connected components
536    fn detect_circular_dependencies(&self, flat_tasks: &[FlatTask]) -> Result<()> {
537        if flat_tasks.is_empty() {
538            return Ok(());
539        }
540
541        // Build name-to-index mapping
542        let name_to_idx: HashMap<&str, usize> = flat_tasks
543            .iter()
544            .enumerate()
545            .map(|(i, t)| (t.name.as_str(), i))
546            .collect();
547
548        // Build dependency graph (adjacency list)
549        let mut graph: Vec<Vec<usize>> = vec![Vec::new(); flat_tasks.len()];
550        for (idx, task) in flat_tasks.iter().enumerate() {
551            for dep_name in &task.depends_on {
552                if let Some(&dep_idx) = name_to_idx.get(dep_name.as_str()) {
553                    graph[idx].push(dep_idx);
554                }
555            }
556        }
557
558        // Check for self-loops first
559        for task in flat_tasks {
560            if task.depends_on.contains(&task.name) {
561                return Err(IntentError::InvalidInput(format!(
562                    "Circular dependency detected: task '{}' depends on itself",
563                    task.name
564                )));
565            }
566        }
567
568        // Run Tarjan's SCC algorithm
569        let sccs = self.tarjan_scc(&graph);
570
571        // Check for cycles (any SCC with size > 1)
572        for scc in sccs {
573            if scc.len() > 1 {
574                // Found a cycle - build error message
575                let cycle_names: Vec<&str> = scc
576                    .iter()
577                    .map(|&idx| flat_tasks[idx].name.as_str())
578                    .collect();
579
580                return Err(IntentError::InvalidInput(format!(
581                    "Circular dependency detected: {}",
582                    cycle_names.join(" → ")
583                )));
584            }
585        }
586
587        Ok(())
588    }
589
590    /// Tarjan's algorithm for finding strongly connected components
591    /// Returns a list of SCCs, where each SCC is a list of node indices
592    fn tarjan_scc(&self, graph: &[Vec<usize>]) -> Vec<Vec<usize>> {
593        let n = graph.len();
594        let mut index = 0;
595        let mut stack = Vec::new();
596        let mut indices = vec![None; n];
597        let mut lowlinks = vec![0; n];
598        let mut on_stack = vec![false; n];
599        let mut sccs = Vec::new();
600
601        #[allow(clippy::too_many_arguments)]
602        fn strongconnect(
603            v: usize,
604            graph: &[Vec<usize>],
605            index: &mut usize,
606            stack: &mut Vec<usize>,
607            indices: &mut [Option<usize>],
608            lowlinks: &mut [usize],
609            on_stack: &mut [bool],
610            sccs: &mut Vec<Vec<usize>>,
611        ) {
612            // Set the depth index for v to the smallest unused index
613            indices[v] = Some(*index);
614            lowlinks[v] = *index;
615            *index += 1;
616            stack.push(v);
617            on_stack[v] = true;
618
619            // Consider successors of v
620            for &w in &graph[v] {
621                if indices[w].is_none() {
622                    // Successor w has not yet been visited; recurse on it
623                    strongconnect(w, graph, index, stack, indices, lowlinks, on_stack, sccs);
624                    lowlinks[v] = lowlinks[v].min(lowlinks[w]);
625                } else if on_stack[w] {
626                    // Successor w is in stack and hence in the current SCC
627                    lowlinks[v] = lowlinks[v].min(indices[w].unwrap());
628                }
629            }
630
631            // If v is a root node, pop the stack and generate an SCC
632            if lowlinks[v] == indices[v].unwrap() {
633                let mut scc = Vec::new();
634                loop {
635                    let w = stack.pop().unwrap();
636                    on_stack[w] = false;
637                    scc.push(w);
638                    if w == v {
639                        break;
640                    }
641                }
642                sccs.push(scc);
643            }
644        }
645
646        // Find SCCs for all nodes
647        for v in 0..n {
648            if indices[v].is_none() {
649                strongconnect(
650                    v,
651                    graph,
652                    &mut index,
653                    &mut stack,
654                    &mut indices,
655                    &mut lowlinks,
656                    &mut on_stack,
657                    &mut sccs,
658                );
659            }
660        }
661
662        sccs
663    }
664}
665
666#[cfg(test)]
667mod tests {
668    use super::*;
669
670    #[test]
671    fn test_priority_value_to_int() {
672        assert_eq!(PriorityValue::Critical.to_int(), 1);
673        assert_eq!(PriorityValue::High.to_int(), 2);
674        assert_eq!(PriorityValue::Medium.to_int(), 3);
675        assert_eq!(PriorityValue::Low.to_int(), 4);
676    }
677
678    #[test]
679    fn test_priority_value_from_int() {
680        assert_eq!(PriorityValue::from_int(1), Some(PriorityValue::Critical));
681        assert_eq!(PriorityValue::from_int(2), Some(PriorityValue::High));
682        assert_eq!(PriorityValue::from_int(3), Some(PriorityValue::Medium));
683        assert_eq!(PriorityValue::from_int(4), Some(PriorityValue::Low));
684        assert_eq!(PriorityValue::from_int(999), None);
685    }
686
687    #[test]
688    fn test_priority_value_as_str() {
689        assert_eq!(PriorityValue::Critical.as_str(), "critical");
690        assert_eq!(PriorityValue::High.as_str(), "high");
691        assert_eq!(PriorityValue::Medium.as_str(), "medium");
692        assert_eq!(PriorityValue::Low.as_str(), "low");
693    }
694
695    #[test]
696    fn test_plan_request_deserialization_minimal() {
697        let json = r#"{"tasks": [{"name": "Test Task"}]}"#;
698        let request: PlanRequest = serde_json::from_str(json).unwrap();
699
700        assert_eq!(request.tasks.len(), 1);
701        assert_eq!(request.tasks[0].name, "Test Task");
702        assert_eq!(request.tasks[0].spec, None);
703        assert_eq!(request.tasks[0].priority, None);
704        assert_eq!(request.tasks[0].children, None);
705        assert_eq!(request.tasks[0].depends_on, None);
706        assert_eq!(request.tasks[0].task_id, None);
707    }
708
709    #[test]
710    fn test_plan_request_deserialization_full() {
711        let json = r#"{
712            "tasks": [{
713                "name": "Parent Task",
714                "spec": "Parent spec",
715                "priority": "high",
716                "children": [{
717                    "name": "Child Task",
718                    "spec": "Child spec"
719                }],
720                "depends_on": ["Other Task"],
721                "task_id": 42
722            }]
723        }"#;
724
725        let request: PlanRequest = serde_json::from_str(json).unwrap();
726
727        assert_eq!(request.tasks.len(), 1);
728        let parent = &request.tasks[0];
729        assert_eq!(parent.name, "Parent Task");
730        assert_eq!(parent.spec, Some("Parent spec".to_string()));
731        assert_eq!(parent.priority, Some(PriorityValue::High));
732        assert_eq!(parent.task_id, Some(42));
733
734        let children = parent.children.as_ref().unwrap();
735        assert_eq!(children.len(), 1);
736        assert_eq!(children[0].name, "Child Task");
737
738        let depends = parent.depends_on.as_ref().unwrap();
739        assert_eq!(depends.len(), 1);
740        assert_eq!(depends[0], "Other Task");
741    }
742
743    #[test]
744    fn test_plan_request_serialization() {
745        let request = PlanRequest {
746            tasks: vec![TaskTree {
747                name: "Test Task".to_string(),
748                spec: Some("Test spec".to_string()),
749                priority: Some(PriorityValue::Medium),
750                children: None,
751                depends_on: None,
752                task_id: None,
753            }],
754        };
755
756        let json = serde_json::to_string(&request).unwrap();
757        assert!(json.contains("\"name\":\"Test Task\""));
758        assert!(json.contains("\"spec\":\"Test spec\""));
759        assert!(json.contains("\"priority\":\"medium\""));
760    }
761
762    #[test]
763    fn test_plan_result_success() {
764        let mut map = HashMap::new();
765        map.insert("Task 1".to_string(), 1);
766        map.insert("Task 2".to_string(), 2);
767
768        let result = PlanResult::success(map.clone(), 2, 0, 1);
769
770        assert!(result.success);
771        assert_eq!(result.task_id_map, map);
772        assert_eq!(result.created_count, 2);
773        assert_eq!(result.updated_count, 0);
774        assert_eq!(result.dependency_count, 1);
775        assert_eq!(result.error, None);
776    }
777
778    #[test]
779    fn test_plan_result_error() {
780        let result = PlanResult::error("Test error");
781
782        assert!(!result.success);
783        assert_eq!(result.task_id_map.len(), 0);
784        assert_eq!(result.created_count, 0);
785        assert_eq!(result.updated_count, 0);
786        assert_eq!(result.dependency_count, 0);
787        assert_eq!(result.error, Some("Test error".to_string()));
788    }
789
790    #[test]
791    fn test_task_tree_nested() {
792        let tree = TaskTree {
793            name: "Parent".to_string(),
794            spec: None,
795            priority: None,
796            children: Some(vec![
797                TaskTree {
798                    name: "Child 1".to_string(),
799                    spec: None,
800                    priority: None,
801                    children: None,
802                    depends_on: None,
803                    task_id: None,
804                },
805                TaskTree {
806                    name: "Child 2".to_string(),
807                    spec: None,
808                    priority: Some(PriorityValue::High),
809                    children: None,
810                    depends_on: None,
811                    task_id: None,
812                },
813            ]),
814            depends_on: None,
815            task_id: None,
816        };
817
818        let json = serde_json::to_string_pretty(&tree).unwrap();
819        let deserialized: TaskTree = serde_json::from_str(&json).unwrap();
820
821        assert_eq!(tree, deserialized);
822        assert_eq!(deserialized.children.as_ref().unwrap().len(), 2);
823    }
824
825    #[test]
826    fn test_priority_value_case_insensitive_deserialization() {
827        // Test lowercase
828        let json = r#"{"name": "Test", "priority": "high"}"#;
829        let task: TaskTree = serde_json::from_str(json).unwrap();
830        assert_eq!(task.priority, Some(PriorityValue::High));
831
832        // Serde expects exact case match for rename_all = "lowercase"
833        // So "High" would fail, which is correct behavior
834    }
835
836    #[test]
837    fn test_extract_all_names_simple() {
838        let tasks = vec![
839            TaskTree {
840                name: "Task 1".to_string(),
841                spec: None,
842                priority: None,
843                children: None,
844                depends_on: None,
845                task_id: None,
846            },
847            TaskTree {
848                name: "Task 2".to_string(),
849                spec: None,
850                priority: None,
851                children: None,
852                depends_on: None,
853                task_id: None,
854            },
855        ];
856
857        let names = extract_all_names(&tasks);
858        assert_eq!(names, vec!["Task 1", "Task 2"]);
859    }
860
861    #[test]
862    fn test_extract_all_names_nested() {
863        let tasks = vec![TaskTree {
864            name: "Parent".to_string(),
865            spec: None,
866            priority: None,
867            children: Some(vec![
868                TaskTree {
869                    name: "Child 1".to_string(),
870                    spec: None,
871                    priority: None,
872                    children: None,
873                    depends_on: None,
874                    task_id: None,
875                },
876                TaskTree {
877                    name: "Child 2".to_string(),
878                    spec: None,
879                    priority: None,
880                    children: Some(vec![TaskTree {
881                        name: "Grandchild".to_string(),
882                        spec: None,
883                        priority: None,
884                        children: None,
885                        depends_on: None,
886                        task_id: None,
887                    }]),
888                    depends_on: None,
889                    task_id: None,
890                },
891            ]),
892            depends_on: None,
893            task_id: None,
894        }];
895
896        let names = extract_all_names(&tasks);
897        assert_eq!(names, vec!["Parent", "Child 1", "Child 2", "Grandchild"]);
898    }
899
900    #[test]
901    fn test_flatten_task_tree_simple() {
902        let tasks = vec![TaskTree {
903            name: "Task 1".to_string(),
904            spec: Some("Spec 1".to_string()),
905            priority: Some(PriorityValue::High),
906            children: None,
907            depends_on: Some(vec!["Task 0".to_string()]),
908            task_id: None,
909        }];
910
911        let flat = flatten_task_tree(&tasks);
912        assert_eq!(flat.len(), 1);
913        assert_eq!(flat[0].name, "Task 1");
914        assert_eq!(flat[0].spec, Some("Spec 1".to_string()));
915        assert_eq!(flat[0].priority, Some(PriorityValue::High));
916        assert_eq!(flat[0].parent_name, None);
917        assert_eq!(flat[0].depends_on, vec!["Task 0"]);
918    }
919
920    #[test]
921    fn test_flatten_task_tree_nested() {
922        let tasks = vec![TaskTree {
923            name: "Parent".to_string(),
924            spec: None,
925            priority: None,
926            children: Some(vec![
927                TaskTree {
928                    name: "Child 1".to_string(),
929                    spec: None,
930                    priority: None,
931                    children: None,
932                    depends_on: None,
933                    task_id: None,
934                },
935                TaskTree {
936                    name: "Child 2".to_string(),
937                    spec: None,
938                    priority: None,
939                    children: None,
940                    depends_on: None,
941                    task_id: None,
942                },
943            ]),
944            depends_on: None,
945            task_id: None,
946        }];
947
948        let flat = flatten_task_tree(&tasks);
949        assert_eq!(flat.len(), 3);
950
951        // Parent should have no parent_name
952        assert_eq!(flat[0].name, "Parent");
953        assert_eq!(flat[0].parent_name, None);
954
955        // Children should have Parent as parent_name
956        assert_eq!(flat[1].name, "Child 1");
957        assert_eq!(flat[1].parent_name, Some("Parent".to_string()));
958
959        assert_eq!(flat[2].name, "Child 2");
960        assert_eq!(flat[2].parent_name, Some("Parent".to_string()));
961    }
962
963    #[test]
964    fn test_classify_operations_all_create() {
965        let flat_tasks = vec![
966            FlatTask {
967                name: "Task 1".to_string(),
968                spec: None,
969                priority: None,
970                parent_name: None,
971                depends_on: vec![],
972                task_id: None,
973            },
974            FlatTask {
975                name: "Task 2".to_string(),
976                spec: None,
977                priority: None,
978                parent_name: None,
979                depends_on: vec![],
980                task_id: None,
981            },
982        ];
983
984        let existing = HashMap::new();
985        let operations = classify_operations(&flat_tasks, &existing);
986
987        assert_eq!(operations.len(), 2);
988        assert!(matches!(operations[0], Operation::Create(_)));
989        assert!(matches!(operations[1], Operation::Create(_)));
990    }
991
992    #[test]
993    fn test_classify_operations_all_update() {
994        let flat_tasks = vec![
995            FlatTask {
996                name: "Task 1".to_string(),
997                spec: None,
998                priority: None,
999                parent_name: None,
1000                depends_on: vec![],
1001                task_id: None,
1002            },
1003            FlatTask {
1004                name: "Task 2".to_string(),
1005                spec: None,
1006                priority: None,
1007                parent_name: None,
1008                depends_on: vec![],
1009                task_id: None,
1010            },
1011        ];
1012
1013        let mut existing = HashMap::new();
1014        existing.insert("Task 1".to_string(), 1);
1015        existing.insert("Task 2".to_string(), 2);
1016
1017        let operations = classify_operations(&flat_tasks, &existing);
1018
1019        assert_eq!(operations.len(), 2);
1020        assert!(matches!(
1021            operations[0],
1022            Operation::Update { task_id: 1, .. }
1023        ));
1024        assert!(matches!(
1025            operations[1],
1026            Operation::Update { task_id: 2, .. }
1027        ));
1028    }
1029
1030    #[test]
1031    fn test_classify_operations_mixed() {
1032        let flat_tasks = vec![
1033            FlatTask {
1034                name: "Existing Task".to_string(),
1035                spec: None,
1036                priority: None,
1037                parent_name: None,
1038                depends_on: vec![],
1039                task_id: None,
1040            },
1041            FlatTask {
1042                name: "New Task".to_string(),
1043                spec: None,
1044                priority: None,
1045                parent_name: None,
1046                depends_on: vec![],
1047                task_id: None,
1048            },
1049        ];
1050
1051        let mut existing = HashMap::new();
1052        existing.insert("Existing Task".to_string(), 42);
1053
1054        let operations = classify_operations(&flat_tasks, &existing);
1055
1056        assert_eq!(operations.len(), 2);
1057        assert!(matches!(
1058            operations[0],
1059            Operation::Update { task_id: 42, .. }
1060        ));
1061        assert!(matches!(operations[1], Operation::Create(_)));
1062    }
1063
1064    #[test]
1065    fn test_classify_operations_explicit_task_id() {
1066        let flat_tasks = vec![FlatTask {
1067            name: "Task".to_string(),
1068            spec: None,
1069            priority: None,
1070            parent_name: None,
1071            depends_on: vec![],
1072            task_id: Some(99), // Explicit task_id
1073        }];
1074
1075        let existing = HashMap::new(); // Not in existing
1076
1077        let operations = classify_operations(&flat_tasks, &existing);
1078
1079        // Should still be update because of explicit task_id
1080        assert_eq!(operations.len(), 1);
1081        assert!(matches!(
1082            operations[0],
1083            Operation::Update { task_id: 99, .. }
1084        ));
1085    }
1086
1087    #[test]
1088    fn test_find_duplicate_names_no_duplicates() {
1089        let tasks = vec![
1090            TaskTree {
1091                name: "Task 1".to_string(),
1092                spec: None,
1093                priority: None,
1094                children: None,
1095                depends_on: None,
1096                task_id: None,
1097            },
1098            TaskTree {
1099                name: "Task 2".to_string(),
1100                spec: None,
1101                priority: None,
1102                children: None,
1103                depends_on: None,
1104                task_id: None,
1105            },
1106        ];
1107
1108        let duplicates = find_duplicate_names(&tasks);
1109        assert_eq!(duplicates.len(), 0);
1110    }
1111
1112    #[test]
1113    fn test_find_duplicate_names_with_duplicates() {
1114        let tasks = vec![
1115            TaskTree {
1116                name: "Duplicate".to_string(),
1117                spec: None,
1118                priority: None,
1119                children: None,
1120                depends_on: None,
1121                task_id: None,
1122            },
1123            TaskTree {
1124                name: "Unique".to_string(),
1125                spec: None,
1126                priority: None,
1127                children: None,
1128                depends_on: None,
1129                task_id: None,
1130            },
1131            TaskTree {
1132                name: "Duplicate".to_string(),
1133                spec: None,
1134                priority: None,
1135                children: None,
1136                depends_on: None,
1137                task_id: None,
1138            },
1139        ];
1140
1141        let duplicates = find_duplicate_names(&tasks);
1142        assert_eq!(duplicates.len(), 1);
1143        assert_eq!(duplicates[0], "Duplicate");
1144    }
1145
1146    #[test]
1147    fn test_find_duplicate_names_nested() {
1148        let tasks = vec![TaskTree {
1149            name: "Parent".to_string(),
1150            spec: None,
1151            priority: None,
1152            children: Some(vec![TaskTree {
1153                name: "Parent".to_string(), // Duplicate name in child
1154                spec: None,
1155                priority: None,
1156                children: None,
1157                depends_on: None,
1158                task_id: None,
1159            }]),
1160            depends_on: None,
1161            task_id: None,
1162        }];
1163
1164        let duplicates = find_duplicate_names(&tasks);
1165        assert_eq!(duplicates.len(), 1);
1166        assert_eq!(duplicates[0], "Parent");
1167    }
1168
1169    #[test]
1170    fn test_flatten_task_tree_empty() {
1171        let tasks: Vec<TaskTree> = vec![];
1172        let flat = flatten_task_tree(&tasks);
1173        assert_eq!(flat.len(), 0);
1174    }
1175
1176    #[test]
1177    fn test_flatten_task_tree_deep_nesting() {
1178        // Create 4-level deep nesting: Root -> L1 -> L2 -> L3
1179        let tasks = vec![TaskTree {
1180            name: "Root".to_string(),
1181            spec: None,
1182            priority: None,
1183            children: Some(vec![TaskTree {
1184                name: "Level1".to_string(),
1185                spec: None,
1186                priority: None,
1187                children: Some(vec![TaskTree {
1188                    name: "Level2".to_string(),
1189                    spec: None,
1190                    priority: None,
1191                    children: Some(vec![TaskTree {
1192                        name: "Level3".to_string(),
1193                        spec: None,
1194                        priority: None,
1195                        children: None,
1196                        depends_on: None,
1197                        task_id: None,
1198                    }]),
1199                    depends_on: None,
1200                    task_id: None,
1201                }]),
1202                depends_on: None,
1203                task_id: None,
1204            }]),
1205            depends_on: None,
1206            task_id: None,
1207        }];
1208
1209        let flat = flatten_task_tree(&tasks);
1210        assert_eq!(flat.len(), 4);
1211
1212        // Check parent relationships
1213        assert_eq!(flat[0].name, "Root");
1214        assert_eq!(flat[0].parent_name, None);
1215
1216        assert_eq!(flat[1].name, "Level1");
1217        assert_eq!(flat[1].parent_name, Some("Root".to_string()));
1218
1219        assert_eq!(flat[2].name, "Level2");
1220        assert_eq!(flat[2].parent_name, Some("Level1".to_string()));
1221
1222        assert_eq!(flat[3].name, "Level3");
1223        assert_eq!(flat[3].parent_name, Some("Level2".to_string()));
1224    }
1225
1226    #[test]
1227    fn test_flatten_task_tree_many_siblings() {
1228        let children: Vec<TaskTree> = (0..10)
1229            .map(|i| TaskTree {
1230                name: format!("Child {}", i),
1231                spec: None,
1232                priority: None,
1233                children: None,
1234                depends_on: None,
1235                task_id: None,
1236            })
1237            .collect();
1238
1239        let tasks = vec![TaskTree {
1240            name: "Parent".to_string(),
1241            spec: None,
1242            priority: None,
1243            children: Some(children),
1244            depends_on: None,
1245            task_id: None,
1246        }];
1247
1248        let flat = flatten_task_tree(&tasks);
1249        assert_eq!(flat.len(), 11); // 1 parent + 10 children
1250
1251        // All children should have same parent
1252        for child in flat.iter().skip(1).take(10) {
1253            assert_eq!(child.parent_name, Some("Parent".to_string()));
1254        }
1255    }
1256
1257    #[test]
1258    fn test_flatten_task_tree_complex_mixed() {
1259        // Complex structure with multiple levels and siblings
1260        let tasks = vec![
1261            TaskTree {
1262                name: "Task 1".to_string(),
1263                spec: None,
1264                priority: None,
1265                children: Some(vec![
1266                    TaskTree {
1267                        name: "Task 1.1".to_string(),
1268                        spec: None,
1269                        priority: None,
1270                        children: None,
1271                        depends_on: None,
1272                        task_id: None,
1273                    },
1274                    TaskTree {
1275                        name: "Task 1.2".to_string(),
1276                        spec: None,
1277                        priority: None,
1278                        children: Some(vec![TaskTree {
1279                            name: "Task 1.2.1".to_string(),
1280                            spec: None,
1281                            priority: None,
1282                            children: None,
1283                            depends_on: None,
1284                            task_id: None,
1285                        }]),
1286                        depends_on: None,
1287                        task_id: None,
1288                    },
1289                ]),
1290                depends_on: None,
1291                task_id: None,
1292            },
1293            TaskTree {
1294                name: "Task 2".to_string(),
1295                spec: None,
1296                priority: None,
1297                children: None,
1298                depends_on: Some(vec!["Task 1".to_string()]),
1299                task_id: None,
1300            },
1301        ];
1302
1303        let flat = flatten_task_tree(&tasks);
1304        assert_eq!(flat.len(), 5);
1305
1306        // Verify structure
1307        assert_eq!(flat[0].name, "Task 1");
1308        assert_eq!(flat[0].parent_name, None);
1309
1310        assert_eq!(flat[1].name, "Task 1.1");
1311        assert_eq!(flat[1].parent_name, Some("Task 1".to_string()));
1312
1313        assert_eq!(flat[2].name, "Task 1.2");
1314        assert_eq!(flat[2].parent_name, Some("Task 1".to_string()));
1315
1316        assert_eq!(flat[3].name, "Task 1.2.1");
1317        assert_eq!(flat[3].parent_name, Some("Task 1.2".to_string()));
1318
1319        assert_eq!(flat[4].name, "Task 2");
1320        assert_eq!(flat[4].parent_name, None);
1321        assert_eq!(flat[4].depends_on, vec!["Task 1"]);
1322    }
1323
1324    #[tokio::test]
1325    async fn test_plan_executor_integration() {
1326        use crate::test_utils::test_helpers::TestContext;
1327
1328        let ctx = TestContext::new().await;
1329
1330        // Create a plan with hierarchy and dependencies
1331        let request = PlanRequest {
1332            tasks: vec![TaskTree {
1333                name: "Integration Test Plan".to_string(),
1334                spec: Some("Test plan execution end-to-end".to_string()),
1335                priority: Some(PriorityValue::High),
1336                children: Some(vec![
1337                    TaskTree {
1338                        name: "Subtask A".to_string(),
1339                        spec: Some("First subtask".to_string()),
1340                        priority: None,
1341                        children: None,
1342                        depends_on: None,
1343                        task_id: None,
1344                    },
1345                    TaskTree {
1346                        name: "Subtask B".to_string(),
1347                        spec: Some("Second subtask depends on A".to_string()),
1348                        priority: None,
1349                        children: None,
1350                        depends_on: Some(vec!["Subtask A".to_string()]),
1351                        task_id: None,
1352                    },
1353                ]),
1354                depends_on: None,
1355                task_id: None,
1356            }],
1357        };
1358
1359        // Execute the plan
1360        let executor = PlanExecutor::new(&ctx.pool);
1361        let result = executor.execute(&request).await.unwrap();
1362
1363        // Verify success
1364        assert!(result.success, "Plan execution should succeed");
1365        assert_eq!(result.created_count, 3, "Should create 3 tasks");
1366        assert_eq!(result.updated_count, 0, "Should not update any tasks");
1367        assert_eq!(result.dependency_count, 1, "Should create 1 dependency");
1368        assert!(result.error.is_none(), "Should have no error");
1369
1370        // Verify task ID map
1371        assert_eq!(result.task_id_map.len(), 3);
1372        assert!(result.task_id_map.contains_key("Integration Test Plan"));
1373        assert!(result.task_id_map.contains_key("Subtask A"));
1374        assert!(result.task_id_map.contains_key("Subtask B"));
1375
1376        // Verify tasks were created in database
1377        let parent_id = *result.task_id_map.get("Integration Test Plan").unwrap();
1378        let subtask_a_id = *result.task_id_map.get("Subtask A").unwrap();
1379        let subtask_b_id = *result.task_id_map.get("Subtask B").unwrap();
1380
1381        // Check parent task
1382        let parent: (String, String, i64, Option<i64>) =
1383            sqlx::query_as("SELECT name, spec, priority, parent_id FROM tasks WHERE id = ?")
1384                .bind(parent_id)
1385                .fetch_one(&ctx.pool)
1386                .await
1387                .unwrap();
1388
1389        assert_eq!(parent.0, "Integration Test Plan");
1390        assert_eq!(parent.1, "Test plan execution end-to-end");
1391        assert_eq!(parent.2, 2); // High priority = 2
1392        assert_eq!(parent.3, None); // No parent
1393
1394        // Check subtask A
1395        let subtask_a: (String, Option<i64>) =
1396            sqlx::query_as("SELECT name, parent_id FROM tasks WHERE id = ?")
1397                .bind(subtask_a_id)
1398                .fetch_one(&ctx.pool)
1399                .await
1400                .unwrap();
1401
1402        assert_eq!(subtask_a.0, "Subtask A");
1403        assert_eq!(subtask_a.1, Some(parent_id)); // Parent should be set
1404
1405        // Check dependency
1406        let dep: (i64, i64) = sqlx::query_as(
1407            "SELECT blocking_task_id, blocked_task_id FROM dependencies WHERE blocked_task_id = ?",
1408        )
1409        .bind(subtask_b_id)
1410        .fetch_one(&ctx.pool)
1411        .await
1412        .unwrap();
1413
1414        assert_eq!(dep.0, subtask_a_id); // Blocking task
1415        assert_eq!(dep.1, subtask_b_id); // Blocked task
1416    }
1417
1418    #[tokio::test]
1419    async fn test_plan_executor_idempotency() {
1420        use crate::test_utils::test_helpers::TestContext;
1421
1422        let ctx = TestContext::new().await;
1423
1424        // Create a plan
1425        let request = PlanRequest {
1426            tasks: vec![TaskTree {
1427                name: "Idempotent Task".to_string(),
1428                spec: Some("Initial spec".to_string()),
1429                priority: Some(PriorityValue::High),
1430                children: Some(vec![
1431                    TaskTree {
1432                        name: "Child 1".to_string(),
1433                        spec: Some("Child spec 1".to_string()),
1434                        priority: None,
1435                        children: None,
1436                        depends_on: None,
1437                        task_id: None,
1438                    },
1439                    TaskTree {
1440                        name: "Child 2".to_string(),
1441                        spec: Some("Child spec 2".to_string()),
1442                        priority: Some(PriorityValue::Low),
1443                        children: None,
1444                        depends_on: None,
1445                        task_id: None,
1446                    },
1447                ]),
1448                depends_on: None,
1449                task_id: None,
1450            }],
1451        };
1452
1453        let executor = PlanExecutor::new(&ctx.pool);
1454
1455        // First execution - should create all tasks
1456        let result1 = executor.execute(&request).await.unwrap();
1457        assert!(result1.success, "First execution should succeed");
1458        assert_eq!(result1.created_count, 3, "Should create 3 tasks");
1459        assert_eq!(result1.updated_count, 0, "Should not update any tasks");
1460        assert_eq!(result1.task_id_map.len(), 3, "Should have 3 task IDs");
1461
1462        // Get task IDs from first execution
1463        let parent_id_1 = *result1.task_id_map.get("Idempotent Task").unwrap();
1464        let child1_id_1 = *result1.task_id_map.get("Child 1").unwrap();
1465        let child2_id_1 = *result1.task_id_map.get("Child 2").unwrap();
1466
1467        // Second execution with same plan - should update all tasks (idempotent)
1468        let result2 = executor.execute(&request).await.unwrap();
1469        assert!(result2.success, "Second execution should succeed");
1470        assert_eq!(result2.created_count, 0, "Should not create any new tasks");
1471        assert_eq!(result2.updated_count, 3, "Should update all 3 tasks");
1472        assert_eq!(result2.task_id_map.len(), 3, "Should still have 3 task IDs");
1473
1474        // Task IDs should remain the same (idempotent)
1475        let parent_id_2 = *result2.task_id_map.get("Idempotent Task").unwrap();
1476        let child1_id_2 = *result2.task_id_map.get("Child 1").unwrap();
1477        let child2_id_2 = *result2.task_id_map.get("Child 2").unwrap();
1478
1479        assert_eq!(parent_id_1, parent_id_2, "Parent ID should not change");
1480        assert_eq!(child1_id_1, child1_id_2, "Child 1 ID should not change");
1481        assert_eq!(child2_id_1, child2_id_2, "Child 2 ID should not change");
1482
1483        // Verify data in database hasn't changed (spec, priority)
1484        let parent: (String, i64) = sqlx::query_as("SELECT spec, priority FROM tasks WHERE id = ?")
1485            .bind(parent_id_2)
1486            .fetch_one(&ctx.pool)
1487            .await
1488            .unwrap();
1489
1490        assert_eq!(parent.0, "Initial spec");
1491        assert_eq!(parent.1, 2); // High priority = 2
1492
1493        // Third execution with modified plan - should update with new values
1494        let modified_request = PlanRequest {
1495            tasks: vec![TaskTree {
1496                name: "Idempotent Task".to_string(),
1497                spec: Some("Updated spec".to_string()), // Changed
1498                priority: Some(PriorityValue::Critical), // Changed
1499                children: Some(vec![
1500                    TaskTree {
1501                        name: "Child 1".to_string(),
1502                        spec: Some("Updated child spec 1".to_string()), // Changed
1503                        priority: None,
1504                        children: None,
1505                        depends_on: None,
1506                        task_id: None,
1507                    },
1508                    TaskTree {
1509                        name: "Child 2".to_string(),
1510                        spec: Some("Child spec 2".to_string()), // Unchanged
1511                        priority: Some(PriorityValue::Low),
1512                        children: None,
1513                        depends_on: None,
1514                        task_id: None,
1515                    },
1516                ]),
1517                depends_on: None,
1518                task_id: None,
1519            }],
1520        };
1521
1522        let result3 = executor.execute(&modified_request).await.unwrap();
1523        assert!(result3.success, "Third execution should succeed");
1524        assert_eq!(result3.created_count, 0, "Should not create any new tasks");
1525        assert_eq!(result3.updated_count, 3, "Should update all 3 tasks");
1526
1527        // Verify updates were applied
1528        let updated_parent: (String, i64) =
1529            sqlx::query_as("SELECT spec, priority FROM tasks WHERE id = ?")
1530                .bind(parent_id_2)
1531                .fetch_one(&ctx.pool)
1532                .await
1533                .unwrap();
1534
1535        assert_eq!(updated_parent.0, "Updated spec");
1536        assert_eq!(updated_parent.1, 1); // Critical priority = 1
1537
1538        let updated_child1: (String,) = sqlx::query_as("SELECT spec FROM tasks WHERE id = ?")
1539            .bind(child1_id_2)
1540            .fetch_one(&ctx.pool)
1541            .await
1542            .unwrap();
1543
1544        assert_eq!(updated_child1.0, "Updated child spec 1");
1545    }
1546
1547    #[tokio::test]
1548    async fn test_plan_executor_dependencies() {
1549        use crate::test_utils::test_helpers::TestContext;
1550
1551        let ctx = TestContext::new().await;
1552
1553        // Create a plan with multiple dependency relationships
1554        let request = PlanRequest {
1555            tasks: vec![
1556                TaskTree {
1557                    name: "Foundation".to_string(),
1558                    spec: Some("Base layer".to_string()),
1559                    priority: Some(PriorityValue::Critical),
1560                    children: None,
1561                    depends_on: None,
1562                    task_id: None,
1563                },
1564                TaskTree {
1565                    name: "Layer 1".to_string(),
1566                    spec: Some("Depends on Foundation".to_string()),
1567                    priority: Some(PriorityValue::High),
1568                    children: None,
1569                    depends_on: Some(vec!["Foundation".to_string()]),
1570                    task_id: None,
1571                },
1572                TaskTree {
1573                    name: "Layer 2".to_string(),
1574                    spec: Some("Depends on Layer 1".to_string()),
1575                    priority: None,
1576                    children: None,
1577                    depends_on: Some(vec!["Layer 1".to_string()]),
1578                    task_id: None,
1579                },
1580                TaskTree {
1581                    name: "Integration".to_string(),
1582                    spec: Some("Depends on both Foundation and Layer 2".to_string()),
1583                    priority: None,
1584                    children: None,
1585                    depends_on: Some(vec!["Foundation".to_string(), "Layer 2".to_string()]),
1586                    task_id: None,
1587                },
1588            ],
1589        };
1590
1591        let executor = PlanExecutor::new(&ctx.pool);
1592        let result = executor.execute(&request).await.unwrap();
1593
1594        assert!(result.success, "Plan execution should succeed");
1595        assert_eq!(result.created_count, 4, "Should create 4 tasks");
1596        assert_eq!(result.dependency_count, 4, "Should create 4 dependencies");
1597
1598        // Get task IDs
1599        let foundation_id = *result.task_id_map.get("Foundation").unwrap();
1600        let layer1_id = *result.task_id_map.get("Layer 1").unwrap();
1601        let layer2_id = *result.task_id_map.get("Layer 2").unwrap();
1602        let integration_id = *result.task_id_map.get("Integration").unwrap();
1603
1604        // Verify dependency: Layer 1 -> Foundation
1605        let deps1: Vec<(i64,)> =
1606            sqlx::query_as("SELECT blocking_task_id FROM dependencies WHERE blocked_task_id = ?")
1607                .bind(layer1_id)
1608                .fetch_all(&ctx.pool)
1609                .await
1610                .unwrap();
1611
1612        assert_eq!(deps1.len(), 1);
1613        assert_eq!(deps1[0].0, foundation_id);
1614
1615        // Verify dependency: Layer 2 -> Layer 1
1616        let deps2: Vec<(i64,)> =
1617            sqlx::query_as("SELECT blocking_task_id FROM dependencies WHERE blocked_task_id = ?")
1618                .bind(layer2_id)
1619                .fetch_all(&ctx.pool)
1620                .await
1621                .unwrap();
1622
1623        assert_eq!(deps2.len(), 1);
1624        assert_eq!(deps2[0].0, layer1_id);
1625
1626        // Verify dependencies: Integration -> Foundation, Layer 2
1627        let deps3: Vec<(i64,)> =
1628            sqlx::query_as("SELECT blocking_task_id FROM dependencies WHERE blocked_task_id = ? ORDER BY blocking_task_id")
1629                .bind(integration_id)
1630                .fetch_all(&ctx.pool)
1631                .await
1632                .unwrap();
1633
1634        assert_eq!(deps3.len(), 2);
1635        let mut blocking_ids: Vec<i64> = deps3.iter().map(|d| d.0).collect();
1636        blocking_ids.sort();
1637
1638        let mut expected_ids = vec![foundation_id, layer2_id];
1639        expected_ids.sort();
1640
1641        assert_eq!(blocking_ids, expected_ids);
1642    }
1643
1644    #[tokio::test]
1645    async fn test_plan_executor_invalid_dependency() {
1646        use crate::test_utils::test_helpers::TestContext;
1647
1648        let ctx = TestContext::new().await;
1649
1650        // Create a plan with an invalid dependency
1651        let request = PlanRequest {
1652            tasks: vec![TaskTree {
1653                name: "Task A".to_string(),
1654                spec: Some("Depends on non-existent task".to_string()),
1655                priority: None,
1656                children: None,
1657                depends_on: Some(vec!["NonExistent".to_string()]),
1658                task_id: None,
1659            }],
1660        };
1661
1662        let executor = PlanExecutor::new(&ctx.pool);
1663        let result = executor.execute(&request).await.unwrap();
1664
1665        assert!(!result.success, "Plan execution should fail");
1666        assert!(result.error.is_some(), "Should have error message");
1667        let error = result.error.unwrap();
1668        assert!(
1669            error.contains("NonExistent"),
1670            "Error should mention the missing dependency: {}",
1671            error
1672        );
1673    }
1674
1675    #[tokio::test]
1676    async fn test_plan_executor_simple_cycle() {
1677        use crate::test_utils::test_helpers::TestContext;
1678
1679        let ctx = TestContext::new().await;
1680
1681        // Create a plan with a simple cycle: A → B → A
1682        let request = PlanRequest {
1683            tasks: vec![
1684                TaskTree {
1685                    name: "Task A".to_string(),
1686                    spec: Some("Depends on B".to_string()),
1687                    priority: None,
1688                    children: None,
1689                    depends_on: Some(vec!["Task B".to_string()]),
1690                    task_id: None,
1691                },
1692                TaskTree {
1693                    name: "Task B".to_string(),
1694                    spec: Some("Depends on A".to_string()),
1695                    priority: None,
1696                    children: None,
1697                    depends_on: Some(vec!["Task A".to_string()]),
1698                    task_id: None,
1699                },
1700            ],
1701        };
1702
1703        let executor = PlanExecutor::new(&ctx.pool);
1704        let result = executor.execute(&request).await.unwrap();
1705
1706        assert!(!result.success, "Plan execution should fail");
1707        assert!(result.error.is_some(), "Should have error message");
1708        let error = result.error.unwrap();
1709        assert!(
1710            error.contains("Circular dependency"),
1711            "Error should mention circular dependency: {}",
1712            error
1713        );
1714        assert!(
1715            error.contains("Task A") && error.contains("Task B"),
1716            "Error should mention both tasks in the cycle: {}",
1717            error
1718        );
1719    }
1720
1721    #[tokio::test]
1722    async fn test_plan_executor_complex_cycle() {
1723        use crate::test_utils::test_helpers::TestContext;
1724
1725        let ctx = TestContext::new().await;
1726
1727        // Create a plan with a complex cycle: A → B → C → A
1728        let request = PlanRequest {
1729            tasks: vec![
1730                TaskTree {
1731                    name: "Task A".to_string(),
1732                    spec: Some("Depends on B".to_string()),
1733                    priority: None,
1734                    children: None,
1735                    depends_on: Some(vec!["Task B".to_string()]),
1736                    task_id: None,
1737                },
1738                TaskTree {
1739                    name: "Task B".to_string(),
1740                    spec: Some("Depends on C".to_string()),
1741                    priority: None,
1742                    children: None,
1743                    depends_on: Some(vec!["Task C".to_string()]),
1744                    task_id: None,
1745                },
1746                TaskTree {
1747                    name: "Task C".to_string(),
1748                    spec: Some("Depends on A".to_string()),
1749                    priority: None,
1750                    children: None,
1751                    depends_on: Some(vec!["Task A".to_string()]),
1752                    task_id: None,
1753                },
1754            ],
1755        };
1756
1757        let executor = PlanExecutor::new(&ctx.pool);
1758        let result = executor.execute(&request).await.unwrap();
1759
1760        assert!(!result.success, "Plan execution should fail");
1761        assert!(result.error.is_some(), "Should have error message");
1762        let error = result.error.unwrap();
1763        assert!(
1764            error.contains("Circular dependency"),
1765            "Error should mention circular dependency: {}",
1766            error
1767        );
1768        assert!(
1769            error.contains("Task A") && error.contains("Task B") && error.contains("Task C"),
1770            "Error should mention all tasks in the cycle: {}",
1771            error
1772        );
1773    }
1774
1775    #[tokio::test]
1776    async fn test_plan_executor_valid_dag() {
1777        use crate::test_utils::test_helpers::TestContext;
1778
1779        let ctx = TestContext::new().await;
1780
1781        // Create a valid DAG: no cycles
1782        //   A
1783        //  / \
1784        // B   C
1785        //  \ /
1786        //   D
1787        let request = PlanRequest {
1788            tasks: vec![
1789                TaskTree {
1790                    name: "Task A".to_string(),
1791                    spec: Some("Root task".to_string()),
1792                    priority: None,
1793                    children: None,
1794                    depends_on: None,
1795                    task_id: None,
1796                },
1797                TaskTree {
1798                    name: "Task B".to_string(),
1799                    spec: Some("Depends on A".to_string()),
1800                    priority: None,
1801                    children: None,
1802                    depends_on: Some(vec!["Task A".to_string()]),
1803                    task_id: None,
1804                },
1805                TaskTree {
1806                    name: "Task C".to_string(),
1807                    spec: Some("Depends on A".to_string()),
1808                    priority: None,
1809                    children: None,
1810                    depends_on: Some(vec!["Task A".to_string()]),
1811                    task_id: None,
1812                },
1813                TaskTree {
1814                    name: "Task D".to_string(),
1815                    spec: Some("Depends on B and C".to_string()),
1816                    priority: None,
1817                    children: None,
1818                    depends_on: Some(vec!["Task B".to_string(), "Task C".to_string()]),
1819                    task_id: None,
1820                },
1821            ],
1822        };
1823
1824        let executor = PlanExecutor::new(&ctx.pool);
1825        let result = executor.execute(&request).await.unwrap();
1826
1827        assert!(
1828            result.success,
1829            "Plan execution should succeed for valid DAG"
1830        );
1831        assert_eq!(result.created_count, 4, "Should create 4 tasks");
1832        assert_eq!(result.dependency_count, 4, "Should create 4 dependencies");
1833    }
1834
1835    #[tokio::test]
1836    async fn test_plan_executor_self_dependency() {
1837        use crate::test_utils::test_helpers::TestContext;
1838
1839        let ctx = TestContext::new().await;
1840
1841        // Create a plan with self-dependency: A → A
1842        let request = PlanRequest {
1843            tasks: vec![TaskTree {
1844                name: "Task A".to_string(),
1845                spec: Some("Depends on itself".to_string()),
1846                priority: None,
1847                children: None,
1848                depends_on: Some(vec!["Task A".to_string()]),
1849                task_id: None,
1850            }],
1851        };
1852
1853        let executor = PlanExecutor::new(&ctx.pool);
1854        let result = executor.execute(&request).await.unwrap();
1855
1856        assert!(
1857            !result.success,
1858            "Plan execution should fail for self-dependency"
1859        );
1860        assert!(result.error.is_some(), "Should have error message");
1861        let error = result.error.unwrap();
1862        assert!(
1863            error.contains("Circular dependency"),
1864            "Error should mention circular dependency: {}",
1865            error
1866        );
1867    }
1868
1869    // Database query tests
1870    #[tokio::test]
1871    async fn test_find_tasks_by_names_empty() {
1872        use crate::test_utils::test_helpers::TestContext;
1873
1874        let ctx = TestContext::new().await;
1875        let executor = PlanExecutor::new(&ctx.pool);
1876
1877        let result = executor.find_tasks_by_names(&[]).await.unwrap();
1878        assert!(result.is_empty(), "Empty input should return empty map");
1879    }
1880
1881    #[tokio::test]
1882    async fn test_find_tasks_by_names_partial() {
1883        use crate::test_utils::test_helpers::TestContext;
1884
1885        let ctx = TestContext::new().await;
1886        let executor = PlanExecutor::new(&ctx.pool);
1887
1888        // Create some tasks first
1889        let request = PlanRequest {
1890            tasks: vec![
1891                TaskTree {
1892                    name: "Task A".to_string(),
1893                    spec: None,
1894                    priority: None,
1895                    children: None,
1896                    depends_on: None,
1897                    task_id: None,
1898                },
1899                TaskTree {
1900                    name: "Task B".to_string(),
1901                    spec: None,
1902                    priority: None,
1903                    children: None,
1904                    depends_on: None,
1905                    task_id: None,
1906                },
1907            ],
1908        };
1909        executor.execute(&request).await.unwrap();
1910
1911        // Query for A, B, and C (C doesn't exist)
1912        let names = vec![
1913            "Task A".to_string(),
1914            "Task B".to_string(),
1915            "Task C".to_string(),
1916        ];
1917        let result = executor.find_tasks_by_names(&names).await.unwrap();
1918
1919        assert_eq!(result.len(), 2, "Should find 2 out of 3 tasks");
1920        assert!(result.contains_key("Task A"));
1921        assert!(result.contains_key("Task B"));
1922        assert!(!result.contains_key("Task C"));
1923    }
1924
1925    // Performance tests
1926    #[tokio::test]
1927    async fn test_plan_1000_tasks_performance() {
1928        use crate::test_utils::test_helpers::TestContext;
1929
1930        let ctx = TestContext::new().await;
1931        let executor = PlanExecutor::new(&ctx.pool);
1932
1933        // Generate 1000 flat tasks
1934        let mut tasks = Vec::new();
1935        for i in 0..1000 {
1936            tasks.push(TaskTree {
1937                name: format!("Task {}", i),
1938                spec: Some(format!("Spec for task {}", i)),
1939                priority: Some(PriorityValue::Medium),
1940                children: None,
1941                depends_on: None,
1942                task_id: None,
1943            });
1944        }
1945
1946        let request = PlanRequest { tasks };
1947
1948        let start = std::time::Instant::now();
1949        let result = executor.execute(&request).await.unwrap();
1950        let duration = start.elapsed();
1951
1952        assert!(result.success);
1953        assert_eq!(result.created_count, 1000);
1954        assert!(
1955            duration.as_secs() < 10,
1956            "Should complete 1000 tasks in under 10 seconds, took {:?}",
1957            duration
1958        );
1959
1960        println!("✅ Created 1000 tasks in {:?}", duration);
1961    }
1962
1963    #[tokio::test]
1964    async fn test_plan_deep_nesting_20_levels() {
1965        use crate::test_utils::test_helpers::TestContext;
1966
1967        let ctx = TestContext::new().await;
1968        let executor = PlanExecutor::new(&ctx.pool);
1969
1970        // Generate deep nesting: 20 levels
1971        fn build_deep_tree(depth: usize, current: usize) -> TaskTree {
1972            TaskTree {
1973                name: format!("Level {}", current),
1974                spec: Some(format!("Task at depth {}", current)),
1975                priority: Some(PriorityValue::Low),
1976                children: if current < depth {
1977                    Some(vec![build_deep_tree(depth, current + 1)])
1978                } else {
1979                    None
1980                },
1981                depends_on: None,
1982                task_id: None,
1983            }
1984        }
1985
1986        let request = PlanRequest {
1987            tasks: vec![build_deep_tree(20, 1)],
1988        };
1989
1990        let start = std::time::Instant::now();
1991        let result = executor.execute(&request).await.unwrap();
1992        let duration = start.elapsed();
1993
1994        assert!(result.success);
1995        assert_eq!(
1996            result.created_count, 20,
1997            "Should create 20 tasks (1 per level)"
1998        );
1999        assert!(
2000            duration.as_secs() < 5,
2001            "Should handle 20-level nesting in under 5 seconds, took {:?}",
2002            duration
2003        );
2004
2005        println!("✅ Created 20-level deep tree in {:?}", duration);
2006    }
2007
2008    #[test]
2009    fn test_flatten_preserves_all_fields() {
2010        let tasks = vec![TaskTree {
2011            name: "Full Task".to_string(),
2012            spec: Some("Detailed spec".to_string()),
2013            priority: Some(PriorityValue::Critical),
2014            children: None,
2015            depends_on: Some(vec!["Dep1".to_string(), "Dep2".to_string()]),
2016            task_id: Some(42),
2017        }];
2018
2019        let flat = flatten_task_tree(&tasks);
2020        assert_eq!(flat.len(), 1);
2021
2022        let task = &flat[0];
2023        assert_eq!(task.name, "Full Task");
2024        assert_eq!(task.spec, Some("Detailed spec".to_string()));
2025        assert_eq!(task.priority, Some(PriorityValue::Critical));
2026        assert_eq!(task.depends_on, vec!["Dep1", "Dep2"]);
2027        assert_eq!(task.task_id, Some(42));
2028    }
2029}