1use super::task::Task;
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
5pub struct Phase {
6 pub name: String,
7 pub tasks: Vec<Task>,
8}
9
10impl Phase {
11 pub fn new(name: String) -> Self {
12 Phase {
13 name,
14 tasks: Vec::new(),
15 }
16 }
17
18 pub fn add_task(&mut self, task: Task) {
19 self.tasks.push(task);
20 }
21
22 pub fn get_task(&self, task_id: &str) -> Option<&Task> {
23 self.tasks.iter().find(|t| t.id == task_id)
24 }
25
26 pub fn get_task_mut(&mut self, task_id: &str) -> Option<&mut Task> {
27 self.tasks.iter_mut().find(|t| t.id == task_id)
28 }
29
30 pub fn remove_task(&mut self, task_id: &str) -> Option<Task> {
31 self.tasks
32 .iter()
33 .position(|t| t.id == task_id)
34 .map(|idx| self.tasks.remove(idx))
35 }
36
37 pub fn get_stats(&self) -> PhaseStats {
38 let mut total = 0;
39 let mut pending = 0;
40 let mut in_progress = 0;
41 let mut done = 0;
42 let mut blocked = 0;
43 let mut expanded = 0;
44 let mut total_complexity = 0;
45
46 for task in &self.tasks {
47 if task.is_subtask() {
49 continue;
50 }
51
52 total += 1;
53
54 if !task.is_expanded() {
57 total_complexity += task.complexity;
58 }
59
60 match task.status {
61 super::task::TaskStatus::Pending => pending += 1,
62 super::task::TaskStatus::InProgress => in_progress += 1,
63 super::task::TaskStatus::Done => done += 1,
64 super::task::TaskStatus::Blocked => blocked += 1,
65 super::task::TaskStatus::Expanded => {
66 let all_subtasks_done = task.subtasks.iter().all(|subtask_id| {
68 self.get_task(subtask_id)
69 .map(|st| st.status == super::task::TaskStatus::Done)
70 .unwrap_or(false)
71 });
72 if all_subtasks_done && !task.subtasks.is_empty() {
73 done += 1;
74 } else {
75 expanded += 1;
76 }
77 }
78 _ => {}
79 }
80 }
81
82 PhaseStats {
83 total,
84 pending,
85 in_progress,
86 done,
87 blocked,
88 expanded,
89 total_complexity,
90 }
91 }
92
93 pub fn get_actionable_tasks(&self) -> Vec<&Task> {
95 self.tasks
96 .iter()
97 .filter(|t| {
98 if t.is_expanded() {
100 return false;
101 }
102 if let Some(ref parent_id) = t.parent_id {
104 self.get_task(parent_id)
106 .map(|p| p.is_expanded())
107 .unwrap_or(false)
108 } else {
109 true
111 }
112 })
113 .collect()
114 }
115
116 pub fn find_next_task(&self) -> Option<&Task> {
119 self.tasks.iter().find(|task| {
120 task.status == super::task::TaskStatus::Pending
121 && task.has_dependencies_met(&self.tasks)
122 })
123 }
124
125 pub fn find_next_task_cross_tag<'a>(&'a self, all_tasks: &[&Task]) -> Option<&'a Task> {
128 self.tasks.iter().find(|task| {
129 task.status == super::task::TaskStatus::Pending
130 && task.has_dependencies_met_refs(all_tasks)
131 })
132 }
133
134 pub fn get_tasks_needing_expansion(&self) -> Vec<&Task> {
135 self.tasks.iter().filter(|t| t.needs_expansion()).collect()
136 }
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct PhaseStats {
141 pub total: usize,
142 pub pending: usize,
143 pub in_progress: usize,
144 pub done: usize,
145 pub blocked: usize,
146 pub expanded: usize,
147 pub total_complexity: u32,
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153 use crate::models::task::{Task, TaskStatus};
154
155 #[test]
156 fn test_phase_creation() {
157 let phase = Phase::new("phase-1-auth".to_string());
158
159 assert_eq!(phase.name, "phase-1-auth");
160 assert!(phase.tasks.is_empty());
161 }
162
163 #[test]
164 fn test_add_task() {
165 let mut phase = Phase::new("phase-1".to_string());
166 let task = Task::new(
167 "TASK-1".to_string(),
168 "Test Task".to_string(),
169 "Description".to_string(),
170 );
171
172 phase.add_task(task.clone());
173
174 assert_eq!(phase.tasks.len(), 1);
175 assert_eq!(phase.tasks[0].id, "TASK-1");
176 }
177
178 #[test]
179 fn test_get_task() {
180 let mut phase = Phase::new("phase-1".to_string());
181 let task = Task::new(
182 "TASK-1".to_string(),
183 "Test Task".to_string(),
184 "Description".to_string(),
185 );
186 phase.add_task(task);
187
188 let retrieved = phase.get_task("TASK-1");
189 assert!(retrieved.is_some());
190 assert_eq!(retrieved.unwrap().id, "TASK-1");
191
192 let missing = phase.get_task("TASK-99");
193 assert!(missing.is_none());
194 }
195
196 #[test]
197 fn test_get_task_mut() {
198 let mut phase = Phase::new("phase-1".to_string());
199 let task = Task::new(
200 "TASK-1".to_string(),
201 "Test Task".to_string(),
202 "Description".to_string(),
203 );
204 phase.add_task(task);
205
206 {
207 let task_mut = phase.get_task_mut("TASK-1").unwrap();
208 task_mut.set_status(TaskStatus::InProgress);
209 }
210
211 assert_eq!(
212 phase.get_task("TASK-1").unwrap().status,
213 TaskStatus::InProgress
214 );
215 }
216
217 #[test]
218 fn test_remove_task() {
219 let mut phase = Phase::new("phase-1".to_string());
220 let task1 = Task::new(
221 "TASK-1".to_string(),
222 "Task 1".to_string(),
223 "Desc".to_string(),
224 );
225 let task2 = Task::new(
226 "TASK-2".to_string(),
227 "Task 2".to_string(),
228 "Desc".to_string(),
229 );
230 phase.add_task(task1);
231 phase.add_task(task2);
232
233 let removed = phase.remove_task("TASK-1");
234 assert!(removed.is_some());
235 assert_eq!(removed.unwrap().id, "TASK-1");
236 assert_eq!(phase.tasks.len(), 1);
237 assert_eq!(phase.tasks[0].id, "TASK-2");
238
239 let missing = phase.remove_task("TASK-99");
240 assert!(missing.is_none());
241 }
242
243 #[test]
244 fn test_get_stats_empty_phase() {
245 let phase = Phase::new("phase-1".to_string());
246 let stats = phase.get_stats();
247
248 assert_eq!(stats.total, 0);
249 assert_eq!(stats.pending, 0);
250 assert_eq!(stats.in_progress, 0);
251 assert_eq!(stats.done, 0);
252 assert_eq!(stats.blocked, 0);
253 assert_eq!(stats.total_complexity, 0);
254 }
255
256 #[test]
257 fn test_get_stats_with_tasks() {
258 let mut phase = Phase::new("phase-1".to_string());
259
260 let mut task1 = Task::new(
261 "TASK-1".to_string(),
262 "Task 1".to_string(),
263 "Desc".to_string(),
264 );
265 task1.complexity = 3;
266 task1.set_status(TaskStatus::Done);
267
268 let mut task2 = Task::new(
269 "TASK-2".to_string(),
270 "Task 2".to_string(),
271 "Desc".to_string(),
272 );
273 task2.complexity = 5;
274 task2.set_status(TaskStatus::InProgress);
275
276 let mut task3 = Task::new(
277 "TASK-3".to_string(),
278 "Task 3".to_string(),
279 "Desc".to_string(),
280 );
281 task3.complexity = 8;
282 let mut task4 = Task::new(
285 "TASK-4".to_string(),
286 "Task 4".to_string(),
287 "Desc".to_string(),
288 );
289 task4.complexity = 2;
290 task4.set_status(TaskStatus::Blocked);
291
292 phase.add_task(task1);
293 phase.add_task(task2);
294 phase.add_task(task3);
295 phase.add_task(task4);
296
297 let stats = phase.get_stats();
298
299 assert_eq!(stats.total, 4);
300 assert_eq!(stats.pending, 1);
301 assert_eq!(stats.in_progress, 1);
302 assert_eq!(stats.done, 1);
303 assert_eq!(stats.blocked, 1);
304 assert_eq!(stats.total_complexity, 18); }
306
307 #[test]
308 fn test_find_next_task_no_dependencies() {
309 let mut phase = Phase::new("phase-1".to_string());
310
311 let mut task1 = Task::new(
312 "TASK-1".to_string(),
313 "Task 1".to_string(),
314 "Desc".to_string(),
315 );
316 task1.set_status(TaskStatus::Done);
317
318 let task2 = Task::new(
319 "TASK-2".to_string(),
320 "Task 2".to_string(),
321 "Desc".to_string(),
322 );
323 let task3 = Task::new(
326 "TASK-3".to_string(),
327 "Task 3".to_string(),
328 "Desc".to_string(),
329 );
330 phase.add_task(task1);
333 phase.add_task(task2);
334 phase.add_task(task3);
335
336 let next = phase.find_next_task();
337 assert!(next.is_some());
338 assert_eq!(next.unwrap().id, "TASK-2"); }
340
341 #[test]
342 fn test_find_next_task_with_dependencies() {
343 let mut phase = Phase::new("phase-1".to_string());
344
345 let mut task1 = Task::new(
346 "TASK-1".to_string(),
347 "Task 1".to_string(),
348 "Desc".to_string(),
349 );
350 task1.set_status(TaskStatus::Done);
351
352 let task2 = Task::new(
353 "TASK-2".to_string(),
354 "Task 2".to_string(),
355 "Desc".to_string(),
356 );
357 let mut task3 = Task::new(
360 "TASK-3".to_string(),
361 "Task 3".to_string(),
362 "Desc".to_string(),
363 );
364 task3.dependencies = vec!["TASK-1".to_string(), "TASK-2".to_string()];
365 phase.add_task(task1);
368 phase.add_task(task2);
369 phase.add_task(task3);
370
371 let next = phase.find_next_task();
372 assert!(next.is_some());
373 assert_eq!(next.unwrap().id, "TASK-2"); }
375
376 #[test]
377 fn test_find_next_task_dependencies_met() {
378 let mut phase = Phase::new("phase-1".to_string());
379
380 let mut task1 = Task::new(
381 "TASK-1".to_string(),
382 "Task 1".to_string(),
383 "Desc".to_string(),
384 );
385 task1.set_status(TaskStatus::Done);
386
387 let mut task2 = Task::new(
388 "TASK-2".to_string(),
389 "Task 2".to_string(),
390 "Desc".to_string(),
391 );
392 task2.set_status(TaskStatus::Done);
393
394 let mut task3 = Task::new(
395 "TASK-3".to_string(),
396 "Task 3".to_string(),
397 "Desc".to_string(),
398 );
399 task3.dependencies = vec!["TASK-1".to_string(), "TASK-2".to_string()];
400 phase.add_task(task1);
403 phase.add_task(task2);
404 phase.add_task(task3);
405
406 let next = phase.find_next_task();
407 assert!(next.is_some());
408 assert_eq!(next.unwrap().id, "TASK-3"); }
410
411 #[test]
412 fn test_find_next_task_none_available() {
413 let mut phase = Phase::new("phase-1".to_string());
414
415 let mut task1 = Task::new(
416 "TASK-1".to_string(),
417 "Task 1".to_string(),
418 "Desc".to_string(),
419 );
420 task1.set_status(TaskStatus::Done);
421
422 let mut task2 = Task::new(
423 "TASK-2".to_string(),
424 "Task 2".to_string(),
425 "Desc".to_string(),
426 );
427 task2.set_status(TaskStatus::InProgress);
428
429 phase.add_task(task1);
430 phase.add_task(task2);
431
432 let next = phase.find_next_task();
433 assert!(next.is_none()); }
435
436 #[test]
437 fn test_get_tasks_needing_expansion() {
438 let mut phase = Phase::new("phase-1".to_string());
439
440 let mut task1 = Task::new(
441 "TASK-1".to_string(),
442 "Small Task".to_string(),
443 "Desc".to_string(),
444 );
445 task1.complexity = 5;
446
447 let mut task2 = Task::new(
448 "TASK-2".to_string(),
449 "Medium Task".to_string(),
450 "Desc".to_string(),
451 );
452 task2.complexity = 13;
453
454 let mut task3 = Task::new(
455 "TASK-3".to_string(),
456 "Large Task".to_string(),
457 "Desc".to_string(),
458 );
459 task3.complexity = 21;
460
461 let mut task4 = Task::new(
462 "TASK-4".to_string(),
463 "Huge Task".to_string(),
464 "Desc".to_string(),
465 );
466 task4.complexity = 34;
467
468 phase.add_task(task1);
469 phase.add_task(task2);
470 phase.add_task(task3);
471 phase.add_task(task4);
472
473 let needing_expansion = phase.get_tasks_needing_expansion();
474
475 assert_eq!(needing_expansion.len(), 4); assert!(needing_expansion.iter().any(|t| t.id == "TASK-1"));
477 assert!(needing_expansion.iter().any(|t| t.id == "TASK-2"));
478 assert!(needing_expansion.iter().any(|t| t.id == "TASK-3"));
479 assert!(needing_expansion.iter().any(|t| t.id == "TASK-4"));
480 }
481
482 #[test]
483 fn test_phase_serialization() {
484 let mut phase = Phase::new("phase-1".to_string());
485 let task = Task::new(
486 "TASK-1".to_string(),
487 "Test Task".to_string(),
488 "Description".to_string(),
489 );
490 phase.add_task(task);
491
492 let json = serde_json::to_string(&phase).unwrap();
493 let deserialized: Phase = serde_json::from_str(&json).unwrap();
494
495 assert_eq!(phase.name, deserialized.name);
496 assert_eq!(phase.tasks.len(), deserialized.tasks.len());
497 assert_eq!(phase.tasks[0].id, deserialized.tasks[0].id);
498 }
499
500 #[test]
501 fn test_get_stats_expanded_with_all_subtasks_done() {
502 let mut phase = Phase::new("phase-1".to_string());
503
504 let mut parent = Task::new(
506 "TASK-1".to_string(),
507 "Parent Task".to_string(),
508 "Desc".to_string(),
509 );
510 parent.complexity = 8;
511 parent.set_status(TaskStatus::Expanded);
512 parent.subtasks = vec!["TASK-1.1".to_string(), "TASK-1.2".to_string()];
513
514 let mut subtask1 = Task::new(
516 "TASK-1.1".to_string(),
517 "Subtask 1".to_string(),
518 "Desc".to_string(),
519 );
520 subtask1.parent_id = Some("TASK-1".to_string());
521 subtask1.set_status(TaskStatus::Done);
522
523 let mut subtask2 = Task::new(
524 "TASK-1.2".to_string(),
525 "Subtask 2".to_string(),
526 "Desc".to_string(),
527 );
528 subtask2.parent_id = Some("TASK-1".to_string());
529 subtask2.set_status(TaskStatus::Done);
530
531 let mut task2 = Task::new(
533 "TASK-2".to_string(),
534 "Regular Task".to_string(),
535 "Desc".to_string(),
536 );
537 task2.complexity = 3;
538 task2.set_status(TaskStatus::Done);
539
540 phase.add_task(parent);
541 phase.add_task(subtask1);
542 phase.add_task(subtask2);
543 phase.add_task(task2);
544
545 let stats = phase.get_stats();
546
547 assert_eq!(stats.total, 2);
549 assert_eq!(stats.done, 2);
551 assert_eq!(stats.expanded, 0);
553 }
554
555 #[test]
556 fn test_get_stats_expanded_with_incomplete_subtasks() {
557 let mut phase = Phase::new("phase-1".to_string());
558
559 let mut parent = Task::new(
561 "TASK-1".to_string(),
562 "Parent Task".to_string(),
563 "Desc".to_string(),
564 );
565 parent.complexity = 8;
566 parent.set_status(TaskStatus::Expanded);
567 parent.subtasks = vec!["TASK-1.1".to_string(), "TASK-1.2".to_string()];
568
569 let mut subtask1 = Task::new(
571 "TASK-1.1".to_string(),
572 "Subtask 1".to_string(),
573 "Desc".to_string(),
574 );
575 subtask1.parent_id = Some("TASK-1".to_string());
576 subtask1.set_status(TaskStatus::Done);
577
578 let mut subtask2 = Task::new(
579 "TASK-1.2".to_string(),
580 "Subtask 2".to_string(),
581 "Desc".to_string(),
582 );
583 subtask2.parent_id = Some("TASK-1".to_string());
584 phase.add_task(parent);
587 phase.add_task(subtask1);
588 phase.add_task(subtask2);
589
590 let stats = phase.get_stats();
591
592 assert_eq!(stats.total, 1);
594 assert_eq!(stats.expanded, 1);
596 assert_eq!(stats.done, 0);
597 }
598}