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 => expanded += 1,
66 _ => {}
67 }
68 }
69
70 PhaseStats {
71 total,
72 pending,
73 in_progress,
74 done,
75 blocked,
76 expanded,
77 total_complexity,
78 }
79 }
80
81 pub fn get_actionable_tasks(&self) -> Vec<&Task> {
83 self.tasks
84 .iter()
85 .filter(|t| {
86 if t.is_expanded() {
88 return false;
89 }
90 if let Some(ref parent_id) = t.parent_id {
92 self.get_task(parent_id)
94 .map(|p| p.is_expanded())
95 .unwrap_or(false)
96 } else {
97 true
99 }
100 })
101 .collect()
102 }
103
104 pub fn find_next_task(&self) -> Option<&Task> {
107 self.tasks.iter().find(|task| {
108 task.status == super::task::TaskStatus::Pending
109 && task.has_dependencies_met(&self.tasks)
110 })
111 }
112
113 pub fn find_next_task_cross_tag<'a>(&'a self, all_tasks: &[&Task]) -> Option<&'a Task> {
116 self.tasks.iter().find(|task| {
117 task.status == super::task::TaskStatus::Pending
118 && task.has_dependencies_met_refs(all_tasks)
119 })
120 }
121
122 pub fn get_tasks_needing_expansion(&self) -> Vec<&Task> {
123 self.tasks.iter().filter(|t| t.needs_expansion()).collect()
124 }
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct PhaseStats {
129 pub total: usize,
130 pub pending: usize,
131 pub in_progress: usize,
132 pub done: usize,
133 pub blocked: usize,
134 pub expanded: usize,
135 pub total_complexity: u32,
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use crate::models::task::{Task, TaskStatus};
142
143 #[test]
144 fn test_phase_creation() {
145 let phase = Phase::new("phase-1-auth".to_string());
146
147 assert_eq!(phase.name, "phase-1-auth");
148 assert!(phase.tasks.is_empty());
149 }
150
151 #[test]
152 fn test_add_task() {
153 let mut phase = Phase::new("phase-1".to_string());
154 let task = Task::new(
155 "TASK-1".to_string(),
156 "Test Task".to_string(),
157 "Description".to_string(),
158 );
159
160 phase.add_task(task.clone());
161
162 assert_eq!(phase.tasks.len(), 1);
163 assert_eq!(phase.tasks[0].id, "TASK-1");
164 }
165
166 #[test]
167 fn test_get_task() {
168 let mut phase = Phase::new("phase-1".to_string());
169 let task = Task::new(
170 "TASK-1".to_string(),
171 "Test Task".to_string(),
172 "Description".to_string(),
173 );
174 phase.add_task(task);
175
176 let retrieved = phase.get_task("TASK-1");
177 assert!(retrieved.is_some());
178 assert_eq!(retrieved.unwrap().id, "TASK-1");
179
180 let missing = phase.get_task("TASK-99");
181 assert!(missing.is_none());
182 }
183
184 #[test]
185 fn test_get_task_mut() {
186 let mut phase = Phase::new("phase-1".to_string());
187 let task = Task::new(
188 "TASK-1".to_string(),
189 "Test Task".to_string(),
190 "Description".to_string(),
191 );
192 phase.add_task(task);
193
194 {
195 let task_mut = phase.get_task_mut("TASK-1").unwrap();
196 task_mut.set_status(TaskStatus::InProgress);
197 }
198
199 assert_eq!(
200 phase.get_task("TASK-1").unwrap().status,
201 TaskStatus::InProgress
202 );
203 }
204
205 #[test]
206 fn test_remove_task() {
207 let mut phase = Phase::new("phase-1".to_string());
208 let task1 = Task::new(
209 "TASK-1".to_string(),
210 "Task 1".to_string(),
211 "Desc".to_string(),
212 );
213 let task2 = Task::new(
214 "TASK-2".to_string(),
215 "Task 2".to_string(),
216 "Desc".to_string(),
217 );
218 phase.add_task(task1);
219 phase.add_task(task2);
220
221 let removed = phase.remove_task("TASK-1");
222 assert!(removed.is_some());
223 assert_eq!(removed.unwrap().id, "TASK-1");
224 assert_eq!(phase.tasks.len(), 1);
225 assert_eq!(phase.tasks[0].id, "TASK-2");
226
227 let missing = phase.remove_task("TASK-99");
228 assert!(missing.is_none());
229 }
230
231 #[test]
232 fn test_get_stats_empty_phase() {
233 let phase = Phase::new("phase-1".to_string());
234 let stats = phase.get_stats();
235
236 assert_eq!(stats.total, 0);
237 assert_eq!(stats.pending, 0);
238 assert_eq!(stats.in_progress, 0);
239 assert_eq!(stats.done, 0);
240 assert_eq!(stats.blocked, 0);
241 assert_eq!(stats.total_complexity, 0);
242 }
243
244 #[test]
245 fn test_get_stats_with_tasks() {
246 let mut phase = Phase::new("phase-1".to_string());
247
248 let mut task1 = Task::new(
249 "TASK-1".to_string(),
250 "Task 1".to_string(),
251 "Desc".to_string(),
252 );
253 task1.complexity = 3;
254 task1.set_status(TaskStatus::Done);
255
256 let mut task2 = Task::new(
257 "TASK-2".to_string(),
258 "Task 2".to_string(),
259 "Desc".to_string(),
260 );
261 task2.complexity = 5;
262 task2.set_status(TaskStatus::InProgress);
263
264 let mut task3 = Task::new(
265 "TASK-3".to_string(),
266 "Task 3".to_string(),
267 "Desc".to_string(),
268 );
269 task3.complexity = 8;
270 let mut task4 = Task::new(
273 "TASK-4".to_string(),
274 "Task 4".to_string(),
275 "Desc".to_string(),
276 );
277 task4.complexity = 2;
278 task4.set_status(TaskStatus::Blocked);
279
280 phase.add_task(task1);
281 phase.add_task(task2);
282 phase.add_task(task3);
283 phase.add_task(task4);
284
285 let stats = phase.get_stats();
286
287 assert_eq!(stats.total, 4);
288 assert_eq!(stats.pending, 1);
289 assert_eq!(stats.in_progress, 1);
290 assert_eq!(stats.done, 1);
291 assert_eq!(stats.blocked, 1);
292 assert_eq!(stats.total_complexity, 18); }
294
295 #[test]
296 fn test_find_next_task_no_dependencies() {
297 let mut phase = Phase::new("phase-1".to_string());
298
299 let mut task1 = Task::new(
300 "TASK-1".to_string(),
301 "Task 1".to_string(),
302 "Desc".to_string(),
303 );
304 task1.set_status(TaskStatus::Done);
305
306 let task2 = Task::new(
307 "TASK-2".to_string(),
308 "Task 2".to_string(),
309 "Desc".to_string(),
310 );
311 let task3 = Task::new(
314 "TASK-3".to_string(),
315 "Task 3".to_string(),
316 "Desc".to_string(),
317 );
318 phase.add_task(task1);
321 phase.add_task(task2);
322 phase.add_task(task3);
323
324 let next = phase.find_next_task();
325 assert!(next.is_some());
326 assert_eq!(next.unwrap().id, "TASK-2"); }
328
329 #[test]
330 fn test_find_next_task_with_dependencies() {
331 let mut phase = Phase::new("phase-1".to_string());
332
333 let mut task1 = Task::new(
334 "TASK-1".to_string(),
335 "Task 1".to_string(),
336 "Desc".to_string(),
337 );
338 task1.set_status(TaskStatus::Done);
339
340 let task2 = Task::new(
341 "TASK-2".to_string(),
342 "Task 2".to_string(),
343 "Desc".to_string(),
344 );
345 let mut task3 = Task::new(
348 "TASK-3".to_string(),
349 "Task 3".to_string(),
350 "Desc".to_string(),
351 );
352 task3.dependencies = vec!["TASK-1".to_string(), "TASK-2".to_string()];
353 phase.add_task(task1);
356 phase.add_task(task2);
357 phase.add_task(task3);
358
359 let next = phase.find_next_task();
360 assert!(next.is_some());
361 assert_eq!(next.unwrap().id, "TASK-2"); }
363
364 #[test]
365 fn test_find_next_task_dependencies_met() {
366 let mut phase = Phase::new("phase-1".to_string());
367
368 let mut task1 = Task::new(
369 "TASK-1".to_string(),
370 "Task 1".to_string(),
371 "Desc".to_string(),
372 );
373 task1.set_status(TaskStatus::Done);
374
375 let mut task2 = Task::new(
376 "TASK-2".to_string(),
377 "Task 2".to_string(),
378 "Desc".to_string(),
379 );
380 task2.set_status(TaskStatus::Done);
381
382 let mut task3 = Task::new(
383 "TASK-3".to_string(),
384 "Task 3".to_string(),
385 "Desc".to_string(),
386 );
387 task3.dependencies = vec!["TASK-1".to_string(), "TASK-2".to_string()];
388 phase.add_task(task1);
391 phase.add_task(task2);
392 phase.add_task(task3);
393
394 let next = phase.find_next_task();
395 assert!(next.is_some());
396 assert_eq!(next.unwrap().id, "TASK-3"); }
398
399 #[test]
400 fn test_find_next_task_none_available() {
401 let mut phase = Phase::new("phase-1".to_string());
402
403 let mut task1 = Task::new(
404 "TASK-1".to_string(),
405 "Task 1".to_string(),
406 "Desc".to_string(),
407 );
408 task1.set_status(TaskStatus::Done);
409
410 let mut task2 = Task::new(
411 "TASK-2".to_string(),
412 "Task 2".to_string(),
413 "Desc".to_string(),
414 );
415 task2.set_status(TaskStatus::InProgress);
416
417 phase.add_task(task1);
418 phase.add_task(task2);
419
420 let next = phase.find_next_task();
421 assert!(next.is_none()); }
423
424 #[test]
425 fn test_get_tasks_needing_expansion() {
426 let mut phase = Phase::new("phase-1".to_string());
427
428 let mut task1 = Task::new(
429 "TASK-1".to_string(),
430 "Small Task".to_string(),
431 "Desc".to_string(),
432 );
433 task1.complexity = 5;
434
435 let mut task2 = Task::new(
436 "TASK-2".to_string(),
437 "Medium Task".to_string(),
438 "Desc".to_string(),
439 );
440 task2.complexity = 13;
441
442 let mut task3 = Task::new(
443 "TASK-3".to_string(),
444 "Large Task".to_string(),
445 "Desc".to_string(),
446 );
447 task3.complexity = 21;
448
449 let mut task4 = Task::new(
450 "TASK-4".to_string(),
451 "Huge Task".to_string(),
452 "Desc".to_string(),
453 );
454 task4.complexity = 34;
455
456 phase.add_task(task1);
457 phase.add_task(task2);
458 phase.add_task(task3);
459 phase.add_task(task4);
460
461 let needing_expansion = phase.get_tasks_needing_expansion();
462
463 assert_eq!(needing_expansion.len(), 4); assert!(needing_expansion.iter().any(|t| t.id == "TASK-1"));
465 assert!(needing_expansion.iter().any(|t| t.id == "TASK-2"));
466 assert!(needing_expansion.iter().any(|t| t.id == "TASK-3"));
467 assert!(needing_expansion.iter().any(|t| t.id == "TASK-4"));
468 }
469
470 #[test]
471 fn test_phase_serialization() {
472 let mut phase = Phase::new("phase-1".to_string());
473 let task = Task::new(
474 "TASK-1".to_string(),
475 "Test Task".to_string(),
476 "Description".to_string(),
477 );
478 phase.add_task(task);
479
480 let json = serde_json::to_string(&phase).unwrap();
481 let deserialized: Phase = serde_json::from_str(&json).unwrap();
482
483 assert_eq!(phase.name, deserialized.name);
484 assert_eq!(phase.tasks.len(), deserialized.tasks.len());
485 assert_eq!(phase.tasks[0].id, deserialized.tasks[0].id);
486 }
487}