metis_core/application/services/document/
creation.rs

1use crate::domain::documents::initiative::Complexity;
2use crate::domain::documents::strategy::RiskLevel;
3use crate::domain::documents::traits::Document;
4use crate::domain::documents::types::{DocumentId, DocumentType, Phase, Tag, ParentReference};
5use crate::domain::configuration::FlightLevelConfig;
6use crate::Result;
7use crate::{Adr, Initiative, MetisError, Strategy, Task, Vision};
8use std::fs;
9use std::path::{Path, PathBuf};
10
11/// Service for creating new documents with proper defaults and validation
12pub struct DocumentCreationService {
13    workspace_dir: PathBuf,
14}
15
16/// Configuration for creating a new document
17#[derive(Debug, Clone)]
18pub struct DocumentCreationConfig {
19    pub title: String,
20    pub description: Option<String>,
21    pub parent_id: Option<DocumentId>,
22    pub tags: Vec<Tag>,
23    pub phase: Option<Phase>,
24    pub complexity: Option<Complexity>,
25    pub risk_level: Option<RiskLevel>,
26}
27
28/// Result of document creation
29#[derive(Debug)]
30pub struct CreationResult {
31    pub document_id: DocumentId,
32    pub document_type: DocumentType,
33    pub file_path: PathBuf,
34}
35
36impl DocumentCreationService {
37    /// Create a new document creation service for a workspace
38    pub fn new<P: AsRef<Path>>(workspace_dir: P) -> Self {
39        Self {
40            workspace_dir: workspace_dir.as_ref().to_path_buf(),
41        }
42    }
43
44    /// Create a new vision document
45    pub async fn create_vision(&self, config: DocumentCreationConfig) -> Result<CreationResult> {
46        // Vision documents go directly in the workspace root
47        let file_path = self.workspace_dir.join("vision.md");
48
49        // Check if vision already exists
50        if file_path.exists() {
51            return Err(MetisError::ValidationFailed {
52                message: "Vision document already exists".to_string(),
53            });
54        }
55
56        // Create vision with defaults
57        let mut tags = vec![
58            Tag::Label("vision".to_string()),
59            Tag::Phase(config.phase.unwrap_or(Phase::Draft)),
60        ];
61        tags.extend(config.tags);
62
63        let vision = Vision::new(
64            config.title.clone(),
65            tags,
66            false, // not archived
67        )
68        .map_err(|e| MetisError::InvalidDocument(e.to_string()))?;
69
70        // Create parent directory if needed
71        if let Some(parent) = file_path.parent() {
72            fs::create_dir_all(parent).map_err(|e| MetisError::FileSystem(e.to_string()))?;
73        }
74
75        // Write to file
76        vision
77            .to_file(&file_path)
78            .await
79            .map_err(|e| MetisError::InvalidDocument(e.to_string()))?;
80
81        Ok(CreationResult {
82            document_id: vision.id(),
83            document_type: DocumentType::Vision,
84            file_path,
85        })
86    }
87
88    /// Create a new strategy document
89    pub async fn create_strategy(&self, config: DocumentCreationConfig) -> Result<CreationResult> {
90        // Generate strategy ID from title
91        let strategy_id = self.generate_id_from_title(&config.title);
92        let strategy_dir = self.workspace_dir.join("strategies").join(&strategy_id);
93        let file_path = strategy_dir.join("strategy.md");
94
95        // Check if strategy already exists
96        if file_path.exists() {
97            return Err(MetisError::ValidationFailed {
98                message: format!("Strategy with ID '{}' already exists", strategy_id),
99            });
100        }
101
102        // Create strategy with defaults
103        let mut tags = vec![
104            Tag::Label("strategy".to_string()),
105            Tag::Phase(config.phase.unwrap_or(Phase::Shaping)),
106        ];
107        tags.extend(config.tags);
108
109        let strategy = Strategy::new(
110            config.title.clone(),
111            config.parent_id,
112            Vec::new(), // blocked_by
113            tags,
114            false,                                          // not archived
115            config.risk_level.unwrap_or(RiskLevel::Medium), // use config risk_level or default to Medium
116            Vec::new(),                                     // stakeholders - empty by default
117        )
118        .map_err(|e| MetisError::InvalidDocument(e.to_string()))?;
119
120        // Create parent directory
121        fs::create_dir_all(&strategy_dir).map_err(|e| MetisError::FileSystem(e.to_string()))?;
122
123        // Write to file
124        strategy
125            .to_file(&file_path)
126            .await
127            .map_err(|e| MetisError::InvalidDocument(e.to_string()))?;
128
129        Ok(CreationResult {
130            document_id: strategy.id(),
131            document_type: DocumentType::Strategy,
132            file_path,
133        })
134    }
135
136    /// Create a new initiative document (legacy method)
137    pub async fn create_initiative(
138        &self,
139        config: DocumentCreationConfig,
140        strategy_id: &str,
141    ) -> Result<CreationResult> {
142        // Use full configuration for backward compatibility
143        self.create_initiative_with_config(config, strategy_id, &FlightLevelConfig::full()).await
144    }
145
146    /// Create a new initiative document with flight level configuration
147    pub async fn create_initiative_with_config(
148        &self,
149        config: DocumentCreationConfig,
150        strategy_id: &str,
151        flight_config: &FlightLevelConfig,
152    ) -> Result<CreationResult> {
153        // Validate that initiatives are enabled in this configuration
154        if !flight_config.initiatives_enabled {
155            let enabled_types: Vec<String> = flight_config.enabled_document_types().iter().map(|t| t.to_string()).collect();
156            return Err(MetisError::ValidationFailed {
157                message: format!(
158                    "Initiative creation is disabled in current configuration ({} mode). Available document types: {}. To enable initiatives, use 'metis config set --preset full' or 'metis config set --initiatives true'",
159                    flight_config.preset_name(),
160                    enabled_types.join(", ")
161                ),
162            });
163        }
164
165        // Generate initiative ID from title
166        let initiative_id = self.generate_id_from_title(&config.title);
167        
168        // Determine directory structure using consistent NULL-based paths
169        let (parent_ref, effective_strategy_id) = if flight_config.strategies_enabled {
170            // Full configuration: use actual strategy_id
171            if strategy_id == "NULL" {
172                return Err(MetisError::ValidationFailed {
173                    message: format!(
174                        "Cannot create initiative with NULL strategy when strategies are enabled in {} configuration. Provide a valid strategy_id",
175                        flight_config.preset_name()
176                    ),
177                });
178            }
179            
180            // Validate parent strategy exists
181            let strategy_file = self
182                .workspace_dir
183                .join("strategies")
184                .join(strategy_id)
185                .join("strategy.md");
186            if !strategy_file.exists() {
187                return Err(MetisError::NotFound(format!(
188                    "Parent strategy '{}' not found",
189                    strategy_id
190                )));
191            }
192            
193            (ParentReference::Some(DocumentId::from(strategy_id)), strategy_id)
194        } else {
195            // Streamlined configuration: use NULL as strategy placeholder
196            (ParentReference::Null, "NULL")
197        };
198        
199        // Consistent directory structure: strategies/{strategy_id}/initiatives/{initiative_id}
200        let initiative_dir = self
201            .workspace_dir
202            .join("strategies")
203            .join(effective_strategy_id)
204            .join("initiatives")
205            .join(&initiative_id);
206
207        let file_path = initiative_dir.join("initiative.md");
208
209        // Check if initiative already exists
210        if file_path.exists() {
211            return Err(MetisError::ValidationFailed {
212                message: format!("Initiative with ID '{}' already exists", initiative_id),
213            });
214        }
215
216        // Create initiative with defaults
217        let mut tags = vec![
218            Tag::Label("initiative".to_string()),
219            Tag::Phase(config.phase.unwrap_or(Phase::Discovery)),
220        ];
221        tags.extend(config.tags);
222
223        // Use the parent reference from configuration, or explicit parent_id from config
224        let parent_id = config.parent_id.map(ParentReference::Some).unwrap_or(parent_ref);
225
226        let initiative = Initiative::new(
227            config.title.clone(),
228            parent_id.parent_id().cloned(), // Extract actual parent ID for document creation
229            Some(DocumentId::from(effective_strategy_id)), // strategy_id from configuration
230            Vec::new(), // blocked_by
231            tags,
232            false,                                      // not archived
233            config.complexity.unwrap_or(Complexity::M), // use config complexity or default to M
234        )
235        .map_err(|e| MetisError::InvalidDocument(e.to_string()))?;
236
237        // Create parent directory
238        fs::create_dir_all(&initiative_dir).map_err(|e| MetisError::FileSystem(e.to_string()))?;
239
240        // Write to file
241        initiative
242            .to_file(&file_path)
243            .await
244            .map_err(|e| MetisError::InvalidDocument(e.to_string()))?;
245
246        Ok(CreationResult {
247            document_id: initiative.id(),
248            document_type: DocumentType::Initiative,
249            file_path,
250        })
251    }
252
253    /// Create a new task document (legacy method)
254    pub async fn create_task(
255        &self,
256        config: DocumentCreationConfig,
257        strategy_id: &str,
258        initiative_id: &str,
259    ) -> Result<CreationResult> {
260        // Use full configuration for backward compatibility
261        self.create_task_with_config(config, strategy_id, initiative_id, &FlightLevelConfig::full()).await
262    }
263
264    /// Create a new task document with flight level configuration
265    pub async fn create_task_with_config(
266        &self,
267        config: DocumentCreationConfig,
268        strategy_id: &str,
269        initiative_id: &str,
270        flight_config: &FlightLevelConfig,
271    ) -> Result<CreationResult> {
272        // Generate task ID from title
273        let task_id = self.generate_id_from_title(&config.title);
274        
275        // Determine directory structure using consistent NULL-based paths
276        let (parent_ref, parent_title, effective_strategy_id, effective_initiative_id) = if flight_config.initiatives_enabled {
277            // Initiatives are enabled, tasks go under initiatives
278            if initiative_id == "NULL" {
279                return Err(MetisError::ValidationFailed {
280                    message: format!(
281                        "Cannot create task with NULL initiative when initiatives are enabled in {} configuration. Provide a valid initiative_id or create the task as a backlog item",
282                        flight_config.preset_name()
283                    ),
284                });
285            }
286
287            let eff_strategy_id = if flight_config.strategies_enabled {
288                // Full configuration: use actual strategy_id
289                if strategy_id == "NULL" {
290                    return Err(MetisError::ValidationFailed {
291                        message: format!(
292                            "Cannot create task with NULL strategy when strategies are enabled in {} configuration. Provide a valid strategy_id or create the task as a backlog item",
293                            flight_config.preset_name()
294                        ),
295                    });
296                }
297                strategy_id
298            } else {
299                // Streamlined configuration: use NULL as strategy placeholder
300                "NULL"
301            };
302
303            // Validate parent initiative exists using the consistent path structure
304            let initiative_file = self
305                .workspace_dir
306                .join("strategies")
307                .join(eff_strategy_id)
308                .join("initiatives")
309                .join(initiative_id)
310                .join("initiative.md");
311            
312            if !initiative_file.exists() {
313                return Err(MetisError::NotFound(format!(
314                    "Parent initiative '{}' not found",
315                    initiative_id
316                )));
317            }
318
319            (ParentReference::Some(DocumentId::from(initiative_id)), Some(initiative_id.to_string()), eff_strategy_id, initiative_id)
320        } else {
321            // Direct configuration: use NULL placeholders for both strategy and initiative
322            (ParentReference::Null, None, "NULL", "NULL")
323        };
324
325        // Consistent directory structure: strategies/{strategy_id}/initiatives/{initiative_id}/tasks/{task_id}
326        let task_dir = self
327            .workspace_dir
328            .join("strategies")
329            .join(effective_strategy_id)
330            .join("initiatives")
331            .join(effective_initiative_id)
332            .join("tasks");
333
334        let file_path = task_dir.join(format!("{}.md", task_id));
335
336        // Check if task already exists
337        if file_path.exists() {
338            return Err(MetisError::ValidationFailed {
339                message: format!("Task with ID '{}' already exists", task_id),
340            });
341        }
342
343        // Create task with defaults
344        let mut tags = vec![
345            Tag::Label("task".to_string()),
346            Tag::Phase(config.phase.unwrap_or(Phase::Todo)),
347        ];
348        tags.extend(config.tags);
349
350        // Use the parent reference from configuration, or explicit parent_id from config
351        let parent_id = config.parent_id.map(ParentReference::Some).unwrap_or(parent_ref);
352
353        let task = Task::new(
354            config.title.clone(),
355            parent_id.parent_id().cloned(), // Extract actual parent ID for document creation
356            parent_title,                   // parent title for template
357            if effective_strategy_id == "NULL" { None } else { Some(DocumentId::from(effective_strategy_id)) },
358            if effective_initiative_id == "NULL" { None } else { Some(DocumentId::from(effective_initiative_id)) },
359            Vec::new(),                     // blocked_by
360            tags,
361            false, // not archived
362        )
363        .map_err(|e| MetisError::InvalidDocument(e.to_string()))?;
364
365        // Create parent directory if needed
366        if !task_dir.exists() {
367            fs::create_dir_all(&task_dir)
368                .map_err(|e| MetisError::FileSystem(e.to_string()))?;
369        }
370
371        // Write to file
372        task.to_file(&file_path)
373            .await
374            .map_err(|e| MetisError::InvalidDocument(e.to_string()))?;
375
376        Ok(CreationResult {
377            document_id: task.id(),
378            document_type: DocumentType::Task,
379            file_path,
380        })
381    }
382
383    /// Create a new backlog item (task without parent)
384    pub async fn create_backlog_item(&self, config: DocumentCreationConfig) -> Result<CreationResult> {
385        // Generate task ID from title
386        let task_id = self.generate_id_from_title(&config.title);
387        
388        // Create backlog directory structure based on tags
389        let backlog_dir = self.determine_backlog_directory(&config.tags);
390        let file_path = backlog_dir.join(format!("{}.md", task_id));
391
392        // Check if backlog item already exists
393        if file_path.exists() {
394            return Err(MetisError::ValidationFailed {
395                message: format!("Backlog item with ID '{}' already exists", task_id),
396            });
397        }
398
399        // Create backlog item with defaults - no parent, Backlog phase
400        let mut tags = vec![
401            Tag::Label("task".to_string()),
402            Tag::Phase(config.phase.unwrap_or(Phase::Backlog)),
403        ];
404        tags.extend(config.tags);
405
406        let task = Task::new(
407            config.title.clone(),
408            None,                            // No parent for backlog items
409            None,                            // No parent title for template
410            None,                            // No strategy for backlog items
411            None,                            // No initiative for backlog items
412            Vec::new(),                      // blocked_by
413            tags,
414            false, // not archived
415        )
416        .map_err(|e| MetisError::InvalidDocument(e.to_string()))?;
417
418        // Create parent directory if needed
419        if !backlog_dir.exists() {
420            fs::create_dir_all(&backlog_dir)
421                .map_err(|e| MetisError::FileSystem(e.to_string()))?;
422        }
423
424        // Write to file
425        task.to_file(&file_path)
426            .await
427            .map_err(|e| MetisError::InvalidDocument(e.to_string()))?;
428
429        Ok(CreationResult {
430            document_id: task.id(),
431            document_type: DocumentType::Task,
432            file_path,
433        })
434    }
435
436    /// Determine the backlog directory based on tags
437    fn determine_backlog_directory(&self, tags: &[Tag]) -> PathBuf {
438        let base_backlog_dir = self.workspace_dir.join("backlog");
439        
440        // Check for type tags to determine subdirectory
441        for tag in tags {
442            if let Tag::Label(label) = tag {
443                match label.as_str() {
444                    "bug" => return base_backlog_dir.join("bugs"),
445                    "feature" => return base_backlog_dir.join("features"),
446                    "tech-debt" => return base_backlog_dir.join("tech-debt"),
447                    _ => {}
448                }
449            }
450        }
451        
452        // Default to general backlog if no specific type found
453        base_backlog_dir
454    }
455
456    /// Create a new ADR document
457    pub async fn create_adr(&self, config: DocumentCreationConfig) -> Result<CreationResult> {
458        // Find the next ADR number
459        let adr_number = self.get_next_adr_number()?;
460        let adr_slug = self.generate_id_from_title(&config.title);
461        let adr_filename = format!("{:03}-{}.md", adr_number, adr_slug);
462        let adrs_dir = self.workspace_dir.join("adrs");
463        let file_path = adrs_dir.join(&adr_filename);
464
465        // Check if ADR already exists
466        if file_path.exists() {
467            return Err(MetisError::ValidationFailed {
468                message: format!("ADR with filename '{}' already exists", adr_filename),
469            });
470        }
471
472        // Create ADR with defaults
473        let mut tags = vec![
474            Tag::Label("adr".to_string()),
475            Tag::Phase(config.phase.unwrap_or(Phase::Draft)),
476        ];
477        tags.extend(config.tags);
478
479        let adr = Adr::new(
480            adr_number,
481            config.title.clone(),
482            String::new(), // decision_maker - will be set when transitioning to decided
483            None,          // decision_date - will be set when transitioning to decided
484            config.parent_id,
485            tags,
486            false, // not archived
487        )
488        .map_err(|e| MetisError::InvalidDocument(e.to_string()))?;
489
490        // Create parent directory
491        fs::create_dir_all(&adrs_dir).map_err(|e| MetisError::FileSystem(e.to_string()))?;
492
493        // Write to file
494        adr.to_file(&file_path)
495            .await
496            .map_err(|e| MetisError::InvalidDocument(e.to_string()))?;
497
498        Ok(CreationResult {
499            document_id: adr.id(),
500            document_type: DocumentType::Adr,
501            file_path,
502        })
503    }
504
505    /// Generate a slugified ID from a title (same as DocumentId::title_to_slug)
506    fn generate_id_from_title(&self, title: &str) -> String {
507        use crate::domain::documents::types::DocumentId;
508        DocumentId::title_to_slug(title)
509    }
510
511    /// Get the next ADR number by examining existing ADRs
512    fn get_next_adr_number(&self) -> Result<u32> {
513        let adrs_dir = self.workspace_dir.join("adrs");
514
515        if !adrs_dir.exists() {
516            return Ok(1);
517        }
518
519        let mut max_number = 0;
520        for entry in fs::read_dir(&adrs_dir).map_err(|e| MetisError::FileSystem(e.to_string()))? {
521            let entry = entry.map_err(|e| MetisError::FileSystem(e.to_string()))?;
522            let filename = entry.file_name().to_string_lossy().to_string();
523
524            if filename.ends_with(".md") {
525                // Parse number from filename like "001-title.md"
526                if let Some(number_str) = filename.split('-').next() {
527                    if let Ok(number) = number_str.parse::<u32>() {
528                        max_number = max_number.max(number);
529                    }
530                }
531            }
532        }
533
534        Ok(max_number + 1)
535    }
536}
537
538#[cfg(test)]
539mod tests {
540    use super::*;
541    use tempfile::tempdir;
542
543    #[tokio::test]
544    async fn test_create_vision_document() {
545        let temp_dir = tempdir().unwrap();
546        let workspace_dir = temp_dir.path().join(".metis");
547        fs::create_dir_all(&workspace_dir).unwrap();
548
549        let service = DocumentCreationService::new(&workspace_dir);
550        let config = DocumentCreationConfig {
551            title: "Test Vision".to_string(),
552            description: Some("A test vision document".to_string()),
553            parent_id: None,
554            tags: vec![],
555            phase: None,
556            complexity: None,
557            risk_level: None,
558        };
559
560        let result = service.create_vision(config).await.unwrap();
561
562        assert_eq!(result.document_type, DocumentType::Vision);
563        assert!(result.file_path.exists());
564
565        // Verify we can read it back
566        let vision = Vision::from_file(&result.file_path).await.unwrap();
567        assert_eq!(vision.title(), "Test Vision");
568    }
569
570    #[tokio::test]
571    async fn test_create_strategy_document() {
572        let temp_dir = tempdir().unwrap();
573        let workspace_dir = temp_dir.path().join(".metis");
574        fs::create_dir_all(&workspace_dir).unwrap();
575
576        let service = DocumentCreationService::new(&workspace_dir);
577        let config = DocumentCreationConfig {
578            title: "Test Strategy".to_string(),
579            description: Some("A test strategy document".to_string()),
580            parent_id: None,
581            tags: vec![],
582            phase: None,
583            complexity: None,
584            risk_level: None,
585        };
586
587        let result = service.create_strategy(config).await.unwrap();
588
589        assert_eq!(result.document_type, DocumentType::Strategy);
590        assert!(result.file_path.exists());
591
592        // Verify we can read it back
593        let strategy = Strategy::from_file(&result.file_path).await.unwrap();
594        assert_eq!(strategy.title(), "Test Strategy");
595    }
596
597    #[tokio::test]
598    async fn test_create_initiative_document() {
599        let temp_dir = tempdir().unwrap();
600        let workspace_dir = temp_dir.path().join(".metis");
601        fs::create_dir_all(&workspace_dir).unwrap();
602
603        let service = DocumentCreationService::new(&workspace_dir);
604
605        // First create a parent strategy
606        let strategy_config = DocumentCreationConfig {
607            title: "Parent Strategy".to_string(),
608            description: Some("A parent strategy".to_string()),
609            parent_id: None,
610            tags: vec![],
611            phase: None,
612            complexity: None,
613            risk_level: None,
614        };
615        let strategy_result = service.create_strategy(strategy_config).await.unwrap();
616        let strategy_id = strategy_result.document_id.to_string();
617
618        // Now create an initiative
619        let initiative_config = DocumentCreationConfig {
620            title: "Test Initiative".to_string(),
621            description: Some("A test initiative document".to_string()),
622            parent_id: Some(strategy_result.document_id),
623            tags: vec![],
624            phase: None,
625            complexity: None,
626            risk_level: None,
627        };
628
629        let result = service
630            .create_initiative(initiative_config, &strategy_id)
631            .await
632            .unwrap();
633
634        assert_eq!(result.document_type, DocumentType::Initiative);
635        assert!(result.file_path.exists());
636
637        // Verify we can read it back
638        let initiative = Initiative::from_file(&result.file_path).await.unwrap();
639        assert_eq!(initiative.title(), "Test Initiative");
640    }
641
642    #[tokio::test]
643    async fn test_generate_id_from_title() {
644        let temp_dir = tempdir().unwrap();
645        let workspace_dir = temp_dir.path().join(".metis");
646
647        let service = DocumentCreationService::new(&workspace_dir);
648
649        assert_eq!(
650            service.generate_id_from_title("Test Strategy"),
651            "test-strategy"
652        );
653        assert_eq!(
654            service.generate_id_from_title("My Complex Title!"),
655            "my-complex-title"
656        );
657        assert_eq!(
658            service.generate_id_from_title("Multiple   Spaces"),
659            "multiple-spaces"
660        );
661    }
662
663    #[tokio::test]
664    async fn test_get_next_adr_number() {
665        let temp_dir = tempdir().unwrap();
666        let workspace_dir = temp_dir.path().join(".metis");
667        let adrs_dir = workspace_dir.join("adrs");
668        fs::create_dir_all(&adrs_dir).unwrap();
669
670        let service = DocumentCreationService::new(&workspace_dir);
671
672        // Should start at 1 when no ADRs exist
673        assert_eq!(service.get_next_adr_number().unwrap(), 1);
674
675        // Create some ADR files
676        fs::write(adrs_dir.join("001-first-adr.md"), "content").unwrap();
677        fs::write(adrs_dir.join("002-second-adr.md"), "content").unwrap();
678
679        // Should return 3 as next number
680        assert_eq!(service.get_next_adr_number().unwrap(), 3);
681    }
682
683    // Flexible flight levels tests
684
685    fn setup_test_service_temp() -> (DocumentCreationService, tempfile::TempDir) {
686        let temp_dir = tempfile::TempDir::new().expect("Failed to create temp directory");
687        let service = DocumentCreationService::new(temp_dir.path());
688        (service, temp_dir)
689    }
690
691    #[tokio::test]
692    async fn test_create_initiative_full_configuration() {
693        let (service, _temp) = setup_test_service_temp();
694        let flight_config = FlightLevelConfig::full();
695
696        // First create a strategy
697        let strategy_config = DocumentCreationConfig {
698            title: "Test Strategy".to_string(),
699            description: None,
700            parent_id: None,
701            tags: vec![],
702            phase: None,
703            complexity: None,
704            risk_level: None,
705        };
706
707        let strategy_result = service.create_strategy(strategy_config).await.unwrap();
708
709        // Now create an initiative under the strategy
710        let initiative_config = DocumentCreationConfig {
711            title: "Test Initiative".to_string(),
712            description: None,
713            parent_id: None,
714            tags: vec![],
715            phase: None,
716            complexity: None,
717            risk_level: None,
718        };
719
720        let result = service
721            .create_initiative_with_config(
722                initiative_config,
723                &strategy_result.document_id.to_string(),
724                &flight_config,
725            )
726            .await
727            .unwrap();
728
729        assert_eq!(result.document_type, DocumentType::Initiative);
730        assert!(result.file_path.exists());
731        
732        // Verify the path structure for full configuration
733        assert!(result.file_path.to_string_lossy().contains("strategies"));
734        assert!(result.file_path.to_string_lossy().contains("initiatives"));
735    }
736
737    #[tokio::test]
738    async fn test_create_initiative_streamlined_configuration() {
739        let (service, _temp) = setup_test_service_temp();
740        let flight_config = FlightLevelConfig::streamlined();
741
742        let initiative_config = DocumentCreationConfig {
743            title: "Test Initiative".to_string(),
744            description: None,
745            parent_id: None,
746            tags: vec![],
747            phase: None,
748            complexity: None,
749            risk_level: None,
750        };
751
752        // In streamlined config, we pass "NULL" as strategy_id
753        let result = service
754            .create_initiative_with_config(initiative_config, "NULL", &flight_config)
755            .await
756            .unwrap();
757
758        assert_eq!(result.document_type, DocumentType::Initiative);
759        assert!(result.file_path.exists());
760        
761        // Verify the NULL-based path structure for streamlined configuration
762        assert!(result.file_path.to_string_lossy().contains("initiatives"));
763        assert!(result.file_path.to_string_lossy().contains("strategies/NULL"));
764    }
765
766    #[tokio::test]
767    async fn test_create_initiative_disabled_in_direct_configuration() {
768        let (service, _temp) = setup_test_service_temp();
769        let flight_config = FlightLevelConfig::direct();
770
771        let initiative_config = DocumentCreationConfig {
772            title: "Test Initiative".to_string(),
773            description: None,
774            parent_id: None,
775            tags: vec![],
776            phase: None,
777            complexity: None,
778            risk_level: None,
779        };
780
781        // In direct config, initiatives are disabled
782        let result = service
783            .create_initiative_with_config(initiative_config, "NULL", &flight_config)
784            .await;
785
786        assert!(result.is_err());
787        assert!(result.unwrap_err().to_string().contains("Initiative creation is disabled"));
788    }
789
790    #[tokio::test]
791    async fn test_create_task_direct_configuration() {
792        let (service, _temp) = setup_test_service_temp();
793        let flight_config = FlightLevelConfig::direct();
794
795        let task_config = DocumentCreationConfig {
796            title: "Test Task".to_string(),
797            description: None,
798            parent_id: None,
799            tags: vec![],
800            phase: None,
801            complexity: None,
802            risk_level: None,
803        };
804
805        // In direct config, tasks go directly under workspace
806        let result = service
807            .create_task_with_config(task_config, "NULL", "NULL", &flight_config)
808            .await
809            .unwrap();
810
811        assert_eq!(result.document_type, DocumentType::Task);
812        assert!(result.file_path.exists());
813        
814        // Verify the NULL-based path structure for direct configuration
815        assert!(result.file_path.to_string_lossy().contains("tasks"));
816        assert!(result.file_path.to_string_lossy().contains("strategies/NULL/initiatives/NULL"));
817    }
818}