1use std::collections::HashMap;
8
9use chrono::Utc;
10use ito_domain::changes::{
11 Change, ChangeRepository, ChangeStatus, ChangeSummary, ChangeTargetResolution,
12 ResolveTargetOptions,
13};
14use ito_domain::errors::{DomainError, DomainResult};
15use ito_domain::modules::{Module, ModuleRepository, ModuleSummary};
16use ito_domain::tasks::{ProgressInfo, TaskRepository, TasksFormat, TasksParseResult};
17
18#[derive(Debug, Clone, Default)]
24pub struct MockTaskRepository {
25 tasks: HashMap<String, TasksParseResult>,
26}
27
28impl MockTaskRepository {
29 pub fn new() -> Self {
31 Self::default()
32 }
33
34 pub fn with_tasks(mut self, change_id: &str, result: TasksParseResult) -> Self {
36 self.tasks.insert(change_id.to_string(), result);
37 self
38 }
39}
40
41impl TaskRepository for MockTaskRepository {
42 fn load_tasks(&self, change_id: &str) -> DomainResult<TasksParseResult> {
43 Ok(self
44 .tasks
45 .get(change_id)
46 .cloned()
47 .unwrap_or_else(TasksParseResult::empty))
48 }
49}
50
51#[derive(Debug, Clone, Default)]
60pub struct MockChangeRepository {
61 changes: HashMap<String, Change>,
62 summaries: Vec<ChangeSummary>,
63}
64
65impl MockChangeRepository {
66 pub fn new() -> Self {
68 Self::default()
69 }
70
71 pub fn with_change(mut self, change: Change) -> Self {
73 let progress = &change.tasks.progress;
74 let summary = ChangeSummary {
75 id: change.id.clone(),
76 module_id: change.module_id.clone(),
77 completed_tasks: progress.complete as u32,
78 shelved_tasks: progress.shelved as u32,
79 in_progress_tasks: progress.in_progress as u32,
80 pending_tasks: progress.pending as u32,
81 total_tasks: progress.total as u32,
82 last_modified: change.last_modified,
83 has_proposal: change.proposal.is_some(),
84 has_design: change.design.is_some(),
85 has_specs: !change.specs.is_empty(),
86 has_tasks: progress.total > 0,
87 };
88 self.summaries.push(summary);
89 self.changes.insert(change.id.clone(), change);
90 self
91 }
92
93 pub fn with_summary(mut self, summary: ChangeSummary) -> Self {
95 self.summaries.push(summary);
96 self
97 }
98}
99
100impl ChangeRepository for MockChangeRepository {
101 fn resolve_target_with_options(
102 &self,
103 input: &str,
104 _options: ResolveTargetOptions,
105 ) -> ChangeTargetResolution {
106 let matches: Vec<&String> = self.changes.keys().filter(|k| k.contains(input)).collect();
107 match matches.len() {
108 0 => ChangeTargetResolution::NotFound,
109 1 => ChangeTargetResolution::Unique(matches[0].clone()),
110 _ => ChangeTargetResolution::Ambiguous(matches.into_iter().cloned().collect()),
111 }
112 }
113
114 fn suggest_targets(&self, input: &str, max: usize) -> Vec<String> {
115 self.changes
116 .keys()
117 .filter(|k| k.contains(input))
118 .take(max)
119 .cloned()
120 .collect()
121 }
122
123 fn exists(&self, id: &str) -> bool {
124 self.changes.contains_key(id)
125 }
126
127 fn get(&self, id: &str) -> DomainResult<Change> {
128 self.changes
129 .get(id)
130 .cloned()
131 .ok_or_else(|| DomainError::not_found("change", id))
132 }
133
134 fn list(&self) -> DomainResult<Vec<ChangeSummary>> {
135 Ok(self.summaries.clone())
136 }
137
138 fn list_by_module(&self, module_id: &str) -> DomainResult<Vec<ChangeSummary>> {
139 Ok(self
140 .summaries
141 .iter()
142 .filter(|s| s.module_id.as_deref() == Some(module_id))
143 .cloned()
144 .collect())
145 }
146
147 fn list_incomplete(&self) -> DomainResult<Vec<ChangeSummary>> {
148 Ok(self
149 .summaries
150 .iter()
151 .filter(|s| {
152 let status = s.status();
153 match status {
154 ChangeStatus::Complete => false,
155 ChangeStatus::NoTasks => true,
156 ChangeStatus::InProgress => true,
157 }
158 })
159 .cloned()
160 .collect())
161 }
162
163 fn list_complete(&self) -> DomainResult<Vec<ChangeSummary>> {
164 Ok(self
165 .summaries
166 .iter()
167 .filter(|s| {
168 let status = s.status();
169 match status {
170 ChangeStatus::Complete => true,
171 ChangeStatus::NoTasks => false,
172 ChangeStatus::InProgress => false,
173 }
174 })
175 .cloned()
176 .collect())
177 }
178
179 fn get_summary(&self, id: &str) -> DomainResult<ChangeSummary> {
180 self.summaries
181 .iter()
182 .find(|s| s.id == id)
183 .cloned()
184 .ok_or_else(|| DomainError::not_found("change", id))
185 }
186}
187
188#[derive(Debug, Clone, Default)]
194pub struct MockModuleRepository {
195 modules: HashMap<String, Module>,
196 summaries: Vec<ModuleSummary>,
197}
198
199impl MockModuleRepository {
200 pub fn new() -> Self {
202 Self::default()
203 }
204
205 pub fn with_module(mut self, module: Module) -> Self {
207 let summary = ModuleSummary {
208 id: module.id.clone(),
209 name: module.name.clone(),
210 change_count: 0,
211 };
212 self.summaries.push(summary);
213 self.modules.insert(module.id.clone(), module);
214 self
215 }
216
217 pub fn with_module_and_count(mut self, module: Module, change_count: u32) -> Self {
219 let summary = ModuleSummary {
220 id: module.id.clone(),
221 name: module.name.clone(),
222 change_count,
223 };
224 self.summaries.push(summary);
225 self.modules.insert(module.id.clone(), module);
226 self
227 }
228}
229
230impl ModuleRepository for MockModuleRepository {
231 fn exists(&self, id: &str) -> bool {
232 self.modules.contains_key(id)
233 }
234
235 fn get(&self, id_or_name: &str) -> DomainResult<Module> {
236 if let Some(m) = self.modules.get(id_or_name) {
238 return Ok(m.clone());
239 }
240 for m in self.modules.values() {
241 if m.name == id_or_name {
242 return Ok(m.clone());
243 }
244 }
245 Err(DomainError::not_found("module", id_or_name))
246 }
247
248 fn list(&self) -> DomainResult<Vec<ModuleSummary>> {
249 Ok(self.summaries.clone())
250 }
251}
252
253pub fn make_change(id: &str) -> Change {
259 Change {
260 id: id.to_string(),
261 module_id: None,
262 path: std::path::PathBuf::from(format!("/tmp/test/{id}")),
263 proposal: None,
264 design: None,
265 specs: Vec::new(),
266 tasks: TasksParseResult::empty(),
267 last_modified: Utc::now(),
268 }
269}
270
271pub fn make_change_with_progress(
273 id: &str,
274 module_id: Option<&str>,
275 total: usize,
276 complete: usize,
277) -> Change {
278 let mut change = make_change(id);
279 change.module_id = module_id.map(String::from);
280 change.tasks = TasksParseResult {
281 format: TasksFormat::Checkbox,
282 tasks: Vec::new(),
283 waves: Vec::new(),
284 diagnostics: Vec::new(),
285 progress: ProgressInfo {
286 total,
287 complete,
288 shelved: 0,
289 in_progress: 0,
290 pending: total.saturating_sub(complete),
291 remaining: total.saturating_sub(complete),
292 },
293 };
294 change
295}
296
297pub fn make_change_summary(id: &str) -> ChangeSummary {
299 ChangeSummary {
300 id: id.to_string(),
301 module_id: None,
302 completed_tasks: 0,
303 shelved_tasks: 0,
304 in_progress_tasks: 0,
305 pending_tasks: 0,
306 total_tasks: 0,
307 last_modified: Utc::now(),
308 has_proposal: false,
309 has_design: false,
310 has_specs: false,
311 has_tasks: false,
312 }
313}
314
315pub fn make_module(id: &str, name: &str) -> Module {
317 Module {
318 id: id.to_string(),
319 name: name.to_string(),
320 description: None,
321 path: std::path::PathBuf::from(format!("/tmp/test/modules/{id}")),
322 }
323}
324
325pub fn make_tasks_result(total: usize, complete: usize) -> TasksParseResult {
327 TasksParseResult {
328 format: TasksFormat::Checkbox,
329 tasks: Vec::new(),
330 waves: Vec::new(),
331 diagnostics: Vec::new(),
332 progress: ProgressInfo {
333 total,
334 complete,
335 shelved: 0,
336 in_progress: 0,
337 pending: total.saturating_sub(complete),
338 remaining: total.saturating_sub(complete),
339 },
340 }
341}