1use super::task::{Task, TaskStatus};
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
6#[serde(rename_all = "lowercase")]
7pub enum IdFormat {
8 #[default]
10 Sequential,
11 Uuid,
13}
14
15impl IdFormat {
16 pub fn as_str(&self) -> &'static str {
18 match self {
19 IdFormat::Sequential => "sequential",
20 IdFormat::Uuid => "uuid",
21 }
22 }
23
24 pub fn parse(s: &str) -> Self {
26 match s.to_lowercase().as_str() {
27 "uuid" => IdFormat::Uuid,
28 _ => IdFormat::Sequential,
29 }
30 }
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct Phase {
35 pub name: String,
36 pub tasks: Vec<Task>,
37 #[serde(default)]
39 pub id_format: IdFormat,
40}
41
42impl Phase {
43 pub fn new(name: String) -> Self {
44 Phase {
45 name,
46 tasks: Vec::new(),
47 id_format: IdFormat::default(),
48 }
49 }
50
51 pub fn add_task(&mut self, task: Task) {
52 self.tasks.push(task);
53 }
54
55 pub fn get_task(&self, task_id: &str) -> Option<&Task> {
56 self.tasks.iter().find(|t| t.id == task_id)
57 }
58
59 pub fn get_task_mut(&mut self, task_id: &str) -> Option<&mut Task> {
60 self.tasks.iter_mut().find(|t| t.id == task_id)
61 }
62
63 pub fn remove_task(&mut self, task_id: &str) -> Option<Task> {
64 self.tasks
65 .iter()
66 .position(|t| t.id == task_id)
67 .map(|idx| self.tasks.remove(idx))
68 }
69
70 pub fn get_stats(&self) -> PhaseStats {
71 let mut total = 0;
72 let mut pending = 0;
73 let mut in_progress = 0;
74 let mut done = 0;
75 let mut blocked = 0;
76 let mut expanded = 0;
77 let mut total_complexity = 0;
78
79 for task in &self.tasks {
80 if task.is_subtask() {
82 continue;
83 }
84
85 total += 1;
86
87 if !task.is_expanded() {
90 total_complexity += task.complexity;
91 }
92
93 match task.status {
94 TaskStatus::Pending => pending += 1,
95 TaskStatus::InProgress => in_progress += 1,
96 TaskStatus::Done => done += 1,
97 TaskStatus::Blocked => blocked += 1,
98 TaskStatus::Expanded => {
99 let all_subtasks_done = task.subtasks.iter().all(|subtask_id| {
101 self.get_task(subtask_id)
102 .map(|st| st.status == TaskStatus::Done)
103 .unwrap_or(false)
104 });
105 if all_subtasks_done && !task.subtasks.is_empty() {
106 done += 1;
107 } else {
108 expanded += 1;
109 }
110 }
111 _ => {}
112 }
113 }
114
115 PhaseStats {
116 total,
117 pending,
118 in_progress,
119 done,
120 blocked,
121 expanded,
122 total_complexity,
123 }
124 }
125
126 pub fn get_actionable_tasks(&self) -> Vec<&Task> {
128 self.tasks
129 .iter()
130 .filter(|t| {
131 if t.is_expanded() {
133 return false;
134 }
135 if let Some(ref parent_id) = t.parent_id {
137 self.get_task(parent_id)
139 .map(|p| p.is_expanded())
140 .unwrap_or(false)
141 } else {
142 true
144 }
145 })
146 .collect()
147 }
148
149 pub fn find_next_task(&self) -> Option<&Task> {
152 self.tasks.iter().find(|task| {
153 task.status == TaskStatus::Pending && task.has_dependencies_met(&self.tasks)
154 })
155 }
156
157 pub fn find_next_task_cross_tag<'a>(&'a self, all_tasks: &[&Task]) -> Option<&'a Task> {
160 self.tasks.iter().find(|task| {
161 task.status == TaskStatus::Pending && task.has_dependencies_met_refs(all_tasks)
162 })
163 }
164
165 pub fn get_tasks_needing_expansion(&self) -> Vec<&Task> {
166 self.tasks.iter().filter(|t| t.needs_expansion()).collect()
167 }
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct PhaseStats {
172 pub total: usize,
173 pub pending: usize,
174 pub in_progress: usize,
175 pub done: usize,
176 pub blocked: usize,
177 pub expanded: usize,
178 pub total_complexity: u32,
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184 use crate::models::task::{Task, TaskStatus};
185
186 #[test]
187 fn test_phase_creation() {
188 let phase = Phase::new("phase-1-auth".to_string());
189
190 assert_eq!(phase.name, "phase-1-auth");
191 assert!(phase.tasks.is_empty());
192 }
193
194 #[test]
195 fn test_add_task() {
196 let mut phase = Phase::new("phase-1".to_string());
197 let task = Task::new(
198 "TASK-1".to_string(),
199 "Test Task".to_string(),
200 "Description".to_string(),
201 );
202
203 phase.add_task(task.clone());
204
205 assert_eq!(phase.tasks.len(), 1);
206 assert_eq!(phase.tasks[0].id, "TASK-1");
207 }
208
209 #[test]
210 fn test_get_task() {
211 let mut phase = Phase::new("phase-1".to_string());
212 let task = Task::new(
213 "TASK-1".to_string(),
214 "Test Task".to_string(),
215 "Description".to_string(),
216 );
217 phase.add_task(task);
218
219 let retrieved = phase.get_task("TASK-1");
220 assert!(retrieved.is_some());
221 assert_eq!(retrieved.unwrap().id, "TASK-1");
222
223 let missing = phase.get_task("TASK-99");
224 assert!(missing.is_none());
225 }
226
227 #[test]
228 fn test_get_task_mut() {
229 let mut phase = Phase::new("phase-1".to_string());
230 let task = Task::new(
231 "TASK-1".to_string(),
232 "Test Task".to_string(),
233 "Description".to_string(),
234 );
235 phase.add_task(task);
236
237 {
238 let task_mut = phase.get_task_mut("TASK-1").unwrap();
239 task_mut.set_status(TaskStatus::InProgress);
240 }
241
242 assert_eq!(
243 phase.get_task("TASK-1").unwrap().status,
244 TaskStatus::InProgress
245 );
246 }
247
248 #[test]
249 fn test_remove_task() {
250 let mut phase = Phase::new("phase-1".to_string());
251 let task1 = Task::new(
252 "TASK-1".to_string(),
253 "Task 1".to_string(),
254 "Desc".to_string(),
255 );
256 let task2 = Task::new(
257 "TASK-2".to_string(),
258 "Task 2".to_string(),
259 "Desc".to_string(),
260 );
261 phase.add_task(task1);
262 phase.add_task(task2);
263
264 let removed = phase.remove_task("TASK-1");
265 assert!(removed.is_some());
266 assert_eq!(removed.unwrap().id, "TASK-1");
267 assert_eq!(phase.tasks.len(), 1);
268 assert_eq!(phase.tasks[0].id, "TASK-2");
269
270 let missing = phase.remove_task("TASK-99");
271 assert!(missing.is_none());
272 }
273
274 #[test]
275 fn test_get_stats_empty_phase() {
276 let phase = Phase::new("phase-1".to_string());
277 let stats = phase.get_stats();
278
279 assert_eq!(stats.total, 0);
280 assert_eq!(stats.pending, 0);
281 assert_eq!(stats.in_progress, 0);
282 assert_eq!(stats.done, 0);
283 assert_eq!(stats.blocked, 0);
284 assert_eq!(stats.total_complexity, 0);
285 }
286
287 #[test]
288 fn test_get_stats_with_tasks() {
289 let mut phase = Phase::new("phase-1".to_string());
290
291 let mut task1 = Task::new(
292 "TASK-1".to_string(),
293 "Task 1".to_string(),
294 "Desc".to_string(),
295 );
296 task1.complexity = 3;
297 task1.set_status(TaskStatus::Done);
298
299 let mut task2 = Task::new(
300 "TASK-2".to_string(),
301 "Task 2".to_string(),
302 "Desc".to_string(),
303 );
304 task2.complexity = 5;
305 task2.set_status(TaskStatus::InProgress);
306
307 let mut task3 = Task::new(
308 "TASK-3".to_string(),
309 "Task 3".to_string(),
310 "Desc".to_string(),
311 );
312 task3.complexity = 8;
313 let mut task4 = Task::new(
316 "TASK-4".to_string(),
317 "Task 4".to_string(),
318 "Desc".to_string(),
319 );
320 task4.complexity = 2;
321 task4.set_status(TaskStatus::Blocked);
322
323 phase.add_task(task1);
324 phase.add_task(task2);
325 phase.add_task(task3);
326 phase.add_task(task4);
327
328 let stats = phase.get_stats();
329
330 assert_eq!(stats.total, 4);
331 assert_eq!(stats.pending, 1);
332 assert_eq!(stats.in_progress, 1);
333 assert_eq!(stats.done, 1);
334 assert_eq!(stats.blocked, 1);
335 assert_eq!(stats.total_complexity, 18); }
337
338 #[test]
339 fn test_find_next_task_no_dependencies() {
340 let mut phase = Phase::new("phase-1".to_string());
341
342 let mut task1 = Task::new(
343 "TASK-1".to_string(),
344 "Task 1".to_string(),
345 "Desc".to_string(),
346 );
347 task1.set_status(TaskStatus::Done);
348
349 let task2 = Task::new(
350 "TASK-2".to_string(),
351 "Task 2".to_string(),
352 "Desc".to_string(),
353 );
354 phase.add_task(task1);
357 phase.add_task(task2);
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_with_dependencies() {
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 task2 = Task::new(
376 "TASK-2".to_string(),
377 "Task 2".to_string(),
378 "Desc".to_string(),
379 );
380 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-2"); }
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_phase_serialization() {
426 let mut phase = Phase::new("phase-1".to_string());
427 let task = Task::new(
428 "TASK-1".to_string(),
429 "Test Task".to_string(),
430 "Description".to_string(),
431 );
432 phase.add_task(task);
433
434 let json = serde_json::to_string(&phase).unwrap();
435 let deserialized: Phase = serde_json::from_str(&json).unwrap();
436
437 assert_eq!(phase.name, deserialized.name);
438 assert_eq!(phase.tasks.len(), deserialized.tasks.len());
439 assert_eq!(phase.tasks[0].id, deserialized.tasks[0].id);
440 }
441
442 #[test]
443 fn test_id_format_parse() {
444 assert_eq!(IdFormat::parse("sequential"), IdFormat::Sequential);
445 assert_eq!(IdFormat::parse("uuid"), IdFormat::Uuid);
446 assert_eq!(IdFormat::parse("UUID"), IdFormat::Uuid);
447 assert_eq!(IdFormat::parse("unknown"), IdFormat::Sequential); }
449
450 #[test]
451 fn test_id_format_as_str() {
452 assert_eq!(IdFormat::Sequential.as_str(), "sequential");
453 assert_eq!(IdFormat::Uuid.as_str(), "uuid");
454 }
455}