elif_core/container/
visualization.rs

1use std::collections::{HashMap, HashSet};
2use std::fmt::Write;
3
4use crate::container::descriptor::{ServiceDescriptor, ServiceId};
5use crate::container::ioc_container::IocContainer;
6use crate::container::module::ModuleRegistry;
7use crate::container::scope::ServiceScope;
8use crate::errors::CoreError;
9
10/// Dependency graph visualization formats
11#[derive(Debug, Clone, PartialEq)]
12pub enum VisualizationFormat {
13    /// Graphviz DOT format
14    Dot,
15    /// Mermaid diagram format
16    Mermaid,
17    /// ASCII art tree
18    Ascii,
19    /// JSON representation
20    Json,
21    /// HTML interactive format
22    Html,
23}
24
25/// Visualization style configuration
26#[derive(Debug, Clone)]
27pub struct VisualizationStyle {
28    /// Show service lifetimes in visualization
29    pub show_lifetimes: bool,
30    /// Show service names/types
31    pub show_names: bool,
32    /// Color code by lifetime
33    pub color_by_lifetime: bool,
34    /// Group services by module
35    pub group_by_module: bool,
36    /// Show only specific service types
37    pub filter_types: Option<Vec<String>>,
38    /// Maximum depth to visualize
39    pub max_depth: Option<usize>,
40    /// Include service statistics
41    pub include_stats: bool,
42}
43
44impl Default for VisualizationStyle {
45    fn default() -> Self {
46        Self {
47            show_lifetimes: true,
48            show_names: true,
49            color_by_lifetime: true,
50            group_by_module: false,
51            filter_types: None,
52            max_depth: None,
53            include_stats: false,
54        }
55    }
56}
57
58/// Container dependency visualizer
59// Debug removed due to ModuleRegistry not implementing Debug
60pub struct DependencyVisualizer {
61    descriptors: Vec<ServiceDescriptor>,
62    dependency_graph: HashMap<ServiceId, Vec<ServiceId>>,
63    reverse_graph: HashMap<ServiceId, Vec<ServiceId>>,
64    modules: Option<ModuleRegistry>,
65}
66
67impl DependencyVisualizer {
68    /// Create a new visualizer from service descriptors
69    pub fn new(descriptors: Vec<ServiceDescriptor>) -> Self {
70        let mut dependency_graph = HashMap::new();
71        let mut reverse_graph: HashMap<ServiceId, Vec<ServiceId>> = HashMap::new();
72
73        for descriptor in &descriptors {
74            dependency_graph.insert(
75                descriptor.service_id.clone(),
76                descriptor.dependencies.clone(),
77            );
78
79            // Build reverse dependency graph
80            for dependency in &descriptor.dependencies {
81                reverse_graph
82                    .entry(dependency.clone())
83                    .or_default()
84                    .push(descriptor.service_id.clone());
85            }
86        }
87
88        Self {
89            descriptors,
90            dependency_graph,
91            reverse_graph,
92            modules: None,
93        }
94    }
95
96    /// Create visualizer from IoC container
97    pub fn from_container(container: &IocContainer) -> Self {
98        let descriptors = container
99            .registered_services()
100            .into_iter()
101            .filter_map(|_service_id| {
102                // In a real implementation, we'd get the actual descriptors
103                // For now, create minimal descriptors
104                None // Skip creating descriptors for now due to complexity
105            })
106            .collect();
107
108        Self::new(descriptors)
109    }
110
111    /// Add module information for module-based visualization
112    pub fn with_modules(mut self, modules: ModuleRegistry) -> Self {
113        self.modules = Some(modules);
114        self
115    }
116
117    /// Generate visualization in specified format
118    pub fn visualize(
119        &self,
120        format: VisualizationFormat,
121        style: VisualizationStyle,
122    ) -> Result<String, CoreError> {
123        match format {
124            VisualizationFormat::Dot => self.generate_dot(style),
125            VisualizationFormat::Mermaid => self.generate_mermaid(style),
126            VisualizationFormat::Ascii => self.generate_ascii(style),
127            VisualizationFormat::Json => self.generate_json(style),
128            VisualizationFormat::Html => self.generate_html(style),
129        }
130    }
131
132    /// Generate Graphviz DOT format
133    fn generate_dot(&self, style: VisualizationStyle) -> Result<String, CoreError> {
134        let mut dot = String::new();
135        writeln!(dot, "digraph ServiceDependencies {{").unwrap();
136        writeln!(dot, "    rankdir=TB;").unwrap();
137        writeln!(dot, "    node [shape=rectangle];").unwrap();
138        writeln!(dot).unwrap();
139
140        // Define lifetime colors
141        if style.color_by_lifetime {
142            writeln!(dot, "    // Lifetime color scheme").unwrap();
143            writeln!(dot, "    // Singleton: lightblue").unwrap();
144            writeln!(dot, "    // Scoped: lightgreen").unwrap();
145            writeln!(dot, "    // Transient: lightyellow").unwrap();
146            writeln!(dot).unwrap();
147        }
148
149        // Add service nodes
150        for descriptor in &self.descriptors {
151            if let Some(ref filter) = style.filter_types {
152                if !filter
153                    .iter()
154                    .any(|f| descriptor.service_id.type_name().contains(f))
155                {
156                    continue;
157                }
158            }
159
160            let service_name = self.format_service_name(&descriptor.service_id, &style);
161            let mut node_attrs = Vec::new();
162
163            if style.color_by_lifetime {
164                let color = match descriptor.lifetime {
165                    ServiceScope::Singleton => "lightblue",
166                    ServiceScope::Scoped => "lightgreen",
167                    ServiceScope::Transient => "lightyellow",
168                };
169                node_attrs.push(format!("fillcolor={}", color));
170                node_attrs.push("style=filled".to_string());
171            }
172
173            if style.show_lifetimes {
174                let lifetime_text = format!("\\n({:?})", descriptor.lifetime);
175                node_attrs.push(format!("label=\"{}{}\"", service_name, lifetime_text));
176            } else {
177                node_attrs.push(format!("label=\"{}\"", service_name));
178            }
179
180            writeln!(
181                dot,
182                "    \"{}\" [{}];",
183                service_name,
184                node_attrs.join(", ")
185            )
186            .unwrap();
187        }
188
189        writeln!(dot).unwrap();
190
191        // Add dependency edges
192        for (service_id, dependencies) in &self.dependency_graph {
193            if let Some(ref filter) = style.filter_types {
194                if !filter.iter().any(|f| service_id.type_name().contains(f)) {
195                    continue;
196                }
197            }
198
199            for dependency in dependencies {
200                let source_name = self.format_service_name(service_id, &style);
201                let target_name = self.format_service_name(dependency, &style);
202                writeln!(
203                    dot,
204                    "    \"{}\" -> \"{}\";",
205                    source_name,
206                    target_name
207                )
208                .unwrap();
209            }
210        }
211
212        writeln!(dot, "}}").unwrap();
213        Ok(dot)
214    }
215
216    /// Generate Mermaid diagram format
217    fn generate_mermaid(&self, style: VisualizationStyle) -> Result<String, CoreError> {
218        let mut mermaid = String::new();
219        writeln!(mermaid, "graph TD").unwrap();
220
221        // Add service nodes with lifetimes
222        for descriptor in &self.descriptors {
223            if let Some(ref filter) = style.filter_types {
224                if !filter
225                    .iter()
226                    .any(|f| descriptor.service_id.type_name().contains(f))
227                {
228                    continue;
229                }
230            }
231
232            let service_name = self.format_service_name(&descriptor.service_id, &style);
233            let node_id = self.sanitize_id(descriptor.service_id.type_name());
234
235            let lifetime_indicator = if style.show_lifetimes {
236                match descriptor.lifetime {
237                    ServiceScope::Singleton => "●",
238                    ServiceScope::Scoped => "◐",
239                    ServiceScope::Transient => "○",
240                }
241            } else {
242                ""
243            };
244
245            if style.color_by_lifetime {
246                let class_name = match descriptor.lifetime {
247                    ServiceScope::Singleton => "singleton",
248                    ServiceScope::Scoped => "scoped",
249                    ServiceScope::Transient => "transient",
250                };
251                writeln!(
252                    mermaid,
253                    "    {}[\"{} {}\"]::{}",
254                    node_id, lifetime_indicator, service_name, class_name
255                )
256                .unwrap();
257            } else {
258                writeln!(
259                    mermaid,
260                    "    {}[\"{} {}\"]",
261                    node_id, lifetime_indicator, service_name
262                )
263                .unwrap();
264            }
265        }
266
267        writeln!(mermaid).unwrap();
268
269        // Add dependency relationships
270        for (service_id, dependencies) in &self.dependency_graph {
271            if let Some(ref filter) = style.filter_types {
272                if !filter.iter().any(|f| service_id.type_name().contains(f)) {
273                    continue;
274                }
275            }
276
277            let service_node_id = self.sanitize_id(service_id.type_name());
278            for dependency in dependencies {
279                let dep_node_id = self.sanitize_id(dependency.type_name());
280                writeln!(mermaid, "    {} --> {}", service_node_id, dep_node_id).unwrap();
281            }
282        }
283
284        // Add style classes
285        if style.color_by_lifetime {
286            writeln!(mermaid).unwrap();
287            writeln!(mermaid, "    classDef singleton fill:#add8e6").unwrap();
288            writeln!(mermaid, "    classDef scoped fill:#90ee90").unwrap();
289            writeln!(mermaid, "    classDef transient fill:#ffffe0").unwrap();
290        }
291
292        Ok(mermaid)
293    }
294
295    /// Generate ASCII tree format
296    fn generate_ascii(&self, style: VisualizationStyle) -> Result<String, CoreError> {
297        let mut ascii = String::new();
298        writeln!(ascii, "Service Dependency Tree").unwrap();
299        writeln!(ascii, "=======================").unwrap();
300        writeln!(ascii).unwrap();
301
302        // Find root services (services with no dependents or explicitly marked as root)
303        let mut roots = Vec::new();
304        for descriptor in &self.descriptors {
305            if !self.reverse_graph.contains_key(&descriptor.service_id)
306                || self.reverse_graph[&descriptor.service_id].is_empty()
307            {
308                roots.push(&descriptor.service_id);
309            }
310        }
311
312        // If no clear roots, use all services that don't depend on others
313        if roots.is_empty() {
314            for descriptor in &self.descriptors {
315                if descriptor.dependencies.is_empty() {
316                    roots.push(&descriptor.service_id);
317                }
318            }
319        }
320
321        let mut visited = HashSet::new();
322        for root in roots {
323            self.generate_ascii_tree(
324                root,
325                &style,
326                &mut ascii,
327                0,
328                "",
329                &mut visited,
330                style.max_depth,
331            )?;
332        }
333
334        if style.include_stats {
335            writeln!(ascii).unwrap();
336            writeln!(ascii, "Statistics:").unwrap();
337            writeln!(ascii, "-----------").unwrap();
338            writeln!(ascii, "Total services: {}", self.descriptors.len()).unwrap();
339
340            let mut lifetime_counts = HashMap::new();
341            for desc in &self.descriptors {
342                *lifetime_counts.entry(desc.lifetime).or_insert(0) += 1;
343            }
344
345            for (lifetime, count) in lifetime_counts {
346                writeln!(ascii, "{:?}: {}", lifetime, count).unwrap();
347            }
348        }
349
350        Ok(ascii)
351    }
352
353    /// Generate ASCII tree for a specific service
354    fn generate_ascii_tree(
355        &self,
356        service_id: &ServiceId,
357        style: &VisualizationStyle,
358        output: &mut String,
359        depth: usize,
360        prefix: &str,
361        visited: &mut HashSet<ServiceId>,
362        max_depth: Option<usize>,
363    ) -> Result<(), CoreError> {
364        if let Some(max_d) = max_depth {
365            if depth >= max_d {
366                return Ok(());
367            }
368        }
369
370        if visited.contains(service_id) {
371            writeln!(
372                output,
373                "{}├── {} (circular)",
374                prefix,
375                self.format_service_name(service_id, style)
376            )
377            .unwrap();
378            return Ok(());
379        }
380
381        visited.insert(service_id.clone());
382
383        let descriptor = self
384            .descriptors
385            .iter()
386            .find(|d| &d.service_id == service_id);
387
388        let service_display = if let Some(desc) = descriptor {
389            if style.show_lifetimes {
390                format!(
391                    "{} ({:?})",
392                    self.format_service_name(service_id, style),
393                    desc.lifetime
394                )
395            } else {
396                self.format_service_name(service_id, style)
397            }
398        } else {
399            self.format_service_name(service_id, style)
400        };
401
402        writeln!(output, "{}├── {}", prefix, service_display).unwrap();
403
404        if let Some(dependencies) = self.dependency_graph.get(service_id) {
405            for (i, dependency) in dependencies.iter().enumerate() {
406                let is_last = i == dependencies.len() - 1;
407                let new_prefix = if is_last {
408                    format!("{}    ", prefix)
409                } else {
410                    format!("{}│   ", prefix)
411                };
412
413                self.generate_ascii_tree(
414                    dependency,
415                    style,
416                    output,
417                    depth + 1,
418                    &new_prefix,
419                    visited,
420                    max_depth,
421                )?;
422            }
423        }
424
425        visited.remove(service_id);
426        Ok(())
427    }
428
429    /// Generate JSON representation
430    fn generate_json(&self, style: VisualizationStyle) -> Result<String, CoreError> {
431        use std::collections::BTreeMap; // For consistent ordering
432
433        let mut json_data = BTreeMap::new();
434
435        // Services array
436        let mut services = Vec::new();
437        for descriptor in &self.descriptors {
438            let service_name = self.format_service_name(&descriptor.service_id, &style);
439            
440            if let Some(ref filter) = style.filter_types {
441                if !filter
442                    .iter()
443                    .any(|f| service_name.contains(f))
444                {
445                    continue;
446                }
447            }
448
449            let mut service_data = BTreeMap::new();
450            service_data.insert(
451                "id".to_string(),
452                serde_json::Value::String(service_name.clone()),
453            );
454
455            if style.show_names {
456                service_data.insert(
457                    "name".to_string(),
458                    serde_json::Value::String(
459                        self.format_service_name(&descriptor.service_id, &style),
460                    ),
461                );
462            }
463
464            if style.show_lifetimes {
465                service_data.insert(
466                    "lifetime".to_string(),
467                    serde_json::Value::String(format!("{:?}", descriptor.lifetime)),
468                );
469            }
470
471            let deps: Vec<serde_json::Value> = descriptor
472                .dependencies
473                .iter()
474                .map(|dep| serde_json::Value::String(self.format_service_name(dep, &style)))
475                .collect();
476            service_data.insert("dependencies".to_string(), serde_json::Value::Array(deps));
477
478            services.push(serde_json::Value::Object(
479                service_data.into_iter().collect(),
480            ));
481        }
482
483        json_data.insert("services".to_string(), serde_json::Value::Array(services));
484
485        // Dependencies edges
486        let mut edges = Vec::new();
487        for (service_id, dependencies) in &self.dependency_graph {
488            let source_name = self.format_service_name(service_id, &style);
489            
490            if let Some(ref filter) = style.filter_types {
491                if !filter.iter().any(|f| source_name.contains(f)) {
492                    continue;
493                }
494            }
495
496            for dependency in dependencies {
497                let target_name = self.format_service_name(dependency, &style);
498                let mut edge = BTreeMap::new();
499                edge.insert(
500                    "from".to_string(),
501                    serde_json::Value::String(source_name.clone()),
502                );
503                edge.insert(
504                    "to".to_string(),
505                    serde_json::Value::String(target_name),
506                );
507
508                edges.push(serde_json::Value::Object(edge.into_iter().collect()));
509            }
510        }
511
512        json_data.insert("edges".to_string(), serde_json::Value::Array(edges));
513
514        // Statistics if requested
515        if style.include_stats {
516            let mut stats = BTreeMap::new();
517            stats.insert(
518                "total_services".to_string(),
519                serde_json::Value::Number(serde_json::Number::from(self.descriptors.len())),
520            );
521
522            let total_deps: usize = self.dependency_graph.values().map(|deps| deps.len()).sum();
523            stats.insert(
524                "total_dependencies".to_string(),
525                serde_json::Value::Number(serde_json::Number::from(total_deps)),
526            );
527
528            json_data.insert(
529                "statistics".to_string(),
530                serde_json::Value::Object(stats.into_iter().collect()),
531            );
532        }
533
534        let json_value = serde_json::Value::Object(json_data.into_iter().collect());
535        serde_json::to_string_pretty(&json_value).map_err(|e| CoreError::InvalidServiceDescriptor {
536            message: format!("Failed to serialize JSON: {}", e),
537        })
538    }
539
540    /// Generate interactive HTML visualization
541    fn generate_html(&self, style: VisualizationStyle) -> Result<String, CoreError> {
542        let _json_data = self.generate_json(style)?;
543
544        // Simplified HTML placeholder - full interactive D3.js visualization would go here
545        Ok("<html><body><h1>Service Dependencies Visualization</h1><p>Interactive visualization would be generated here</p></body></html>".to_string())
546    }
547
548    /// Format service name according to style
549    fn format_service_name(&self, service_id: &ServiceId, style: &VisualizationStyle) -> String {
550        if !style.show_names {
551            return "Service".to_string();
552        }
553
554        if let Some(name) = &service_id.name {
555            name.clone()
556        } else {
557            // Extract just the type name without module path
558            let type_name = service_id.type_name();
559            type_name
560                .split("::")
561                .last()
562                .unwrap_or(type_name)
563                .to_string()
564        }
565    }
566
567    /// Sanitize ID for use in Mermaid diagrams
568    fn sanitize_id(&self, id: &str) -> String {
569        id.replace("::", "_")
570            .replace("<", "_")
571            .replace(">", "_")
572            .replace(" ", "_")
573            .replace("-", "_")
574    }
575}
576
577/// Service explorer for interactive dependency investigation
578// Debug removed due to DependencyVisualizer not implementing Debug
579pub struct ServiceExplorer {
580    visualizer: DependencyVisualizer,
581}
582
583impl ServiceExplorer {
584    /// Create a new service explorer
585    pub fn new(descriptors: Vec<ServiceDescriptor>) -> Self {
586        Self {
587            visualizer: DependencyVisualizer::new(descriptors),
588        }
589    }
590
591    /// Find all paths between two services
592    pub fn find_paths(&self, from: &ServiceId, to: &ServiceId) -> Vec<Vec<ServiceId>> {
593        let mut paths = Vec::new();
594        let mut current_path = Vec::new();
595        let mut visited = HashSet::new();
596
597        self.find_paths_recursive(from, to, &mut current_path, &mut visited, &mut paths);
598        paths
599    }
600
601    /// Recursive path finding
602    fn find_paths_recursive(
603        &self,
604        current: &ServiceId,
605        target: &ServiceId,
606        path: &mut Vec<ServiceId>,
607        visited: &mut HashSet<ServiceId>,
608        paths: &mut Vec<Vec<ServiceId>>,
609    ) {
610        if visited.contains(current) {
611            return; // Avoid cycles
612        }
613
614        path.push(current.clone());
615        visited.insert(current.clone());
616
617        if current == target {
618            paths.push(path.clone());
619        } else if let Some(dependencies) = self.visualizer.dependency_graph.get(current) {
620            for dependency in dependencies {
621                self.find_paths_recursive(dependency, target, path, visited, paths);
622            }
623        }
624
625        path.pop();
626        visited.remove(current);
627    }
628
629    /// Get services that depend on a given service
630    pub fn get_dependents(&self, service_id: &ServiceId) -> Vec<&ServiceId> {
631        self.visualizer
632            .reverse_graph
633            .get(service_id)
634            .map(|deps| deps.iter().collect())
635            .unwrap_or_default()
636    }
637
638    /// Get dependency depth for a service
639    pub fn get_dependency_depth(&self, service_id: &ServiceId) -> usize {
640        let mut max_depth = 0;
641        let mut visited = HashSet::new();
642
643        self.calculate_depth(service_id, 0, &mut max_depth, &mut visited);
644        max_depth
645    }
646
647    /// Calculate maximum dependency depth recursively
648    fn calculate_depth(
649        &self,
650        service_id: &ServiceId,
651        current_depth: usize,
652        max_depth: &mut usize,
653        visited: &mut HashSet<ServiceId>,
654    ) {
655        if visited.contains(service_id) {
656            return; // Avoid infinite recursion
657        }
658
659        *max_depth = (*max_depth).max(current_depth);
660        visited.insert(service_id.clone());
661
662        if let Some(dependencies) = self.visualizer.dependency_graph.get(service_id) {
663            for dependency in dependencies {
664                self.calculate_depth(dependency, current_depth + 1, max_depth, visited);
665            }
666        }
667
668        visited.remove(service_id);
669    }
670
671    /// Export service information as a report
672    pub fn generate_report(&self) -> String {
673        let mut report = String::new();
674
675        writeln!(report, "Service Dependency Analysis Report").unwrap();
676        writeln!(report, "===================================").unwrap();
677        writeln!(report).unwrap();
678
679        writeln!(report, "Summary:").unwrap();
680        writeln!(report, "--------").unwrap();
681        writeln!(
682            report,
683            "Total services: {}",
684            self.visualizer.descriptors.len()
685        )
686        .unwrap();
687        writeln!(
688            report,
689            "Total dependencies: {}",
690            self.visualizer
691                .dependency_graph
692                .values()
693                .map(|deps| deps.len())
694                .sum::<usize>()
695        )
696        .unwrap();
697        writeln!(report).unwrap();
698
699        // Services with most dependencies
700        let mut services_by_deps: Vec<_> = self
701            .visualizer
702            .descriptors
703            .iter()
704            .map(|desc| (desc, desc.dependencies.len()))
705            .collect();
706        services_by_deps.sort_by_key(|(_, count)| std::cmp::Reverse(*count));
707
708        writeln!(report, "Services with Most Dependencies:").unwrap();
709        writeln!(report, "-------------------------------").unwrap();
710        for (desc, count) in services_by_deps.iter().take(10) {
711            writeln!(
712                report,
713                "{}: {} dependencies",
714                desc.service_id.type_name(),
715                count
716            )
717            .unwrap();
718        }
719        writeln!(report).unwrap();
720
721        // Services with most dependents
722        let mut dependents_count: HashMap<&ServiceId, usize> = HashMap::new();
723        for dependents in self.visualizer.reverse_graph.values() {
724            for dependent in dependents {
725                *dependents_count.entry(dependent).or_insert(0) += 1;
726            }
727        }
728
729        let mut services_by_dependents: Vec<_> = dependents_count.into_iter().collect();
730        services_by_dependents.sort_by_key(|(_, count)| std::cmp::Reverse(*count));
731
732        writeln!(report, "Most Depended-Upon Services:").unwrap();
733        writeln!(report, "---------------------------").unwrap();
734        for (service_id, count) in services_by_dependents.iter().take(10) {
735            writeln!(report, "{}: {} dependents", service_id.type_name(), count).unwrap();
736        }
737
738        report
739    }
740}
741
742#[cfg(test)]
743mod tests {
744    use super::*;
745    use crate::container::descriptor::{ServiceActivationStrategy, ServiceDescriptor};
746    use std::any::{Any, TypeId};
747
748    fn create_test_descriptor(
749        type_name: &str,
750        lifetime: ServiceScope,
751        deps: Vec<&str>,
752    ) -> ServiceDescriptor {
753        let service_id = ServiceId {
754            type_id: TypeId::of::<()>(),
755            type_name: "test_service",
756            name: Some(type_name.to_string()),
757        };
758
759        let dependencies: Vec<ServiceId> = deps
760            .iter()
761            .map(|dep| ServiceId {
762                type_id: TypeId::of::<()>(),
763                type_name: "test_service",
764                name: Some(dep.to_string()),
765            })
766            .collect();
767
768        ServiceDescriptor {
769            service_id,
770            implementation_id: TypeId::of::<()>(),
771            lifetime,
772            dependencies,
773            activation_strategy: ServiceActivationStrategy::Factory(Box::new(|| {
774                Ok(Box::new(()) as Box<dyn Any + Send + Sync>)
775            })),
776        }
777    }
778
779    #[test]
780    fn test_dot_generation() {
781        let descriptors = vec![
782            create_test_descriptor("ServiceA", ServiceScope::Singleton, vec![]),
783            create_test_descriptor("ServiceB", ServiceScope::Scoped, vec!["ServiceA"]),
784        ];
785
786        let visualizer = DependencyVisualizer::new(descriptors);
787        let style = VisualizationStyle::default();
788
789        let dot = visualizer.generate_dot(style).unwrap();
790
791        assert!(dot.contains("digraph ServiceDependencies"));
792        assert!(dot.contains("ServiceA"));
793        assert!(dot.contains("ServiceB"));
794        assert!(dot.contains("ServiceB\" -> \"ServiceA"));
795    }
796
797    #[test]
798    fn test_mermaid_generation() {
799        let descriptors = vec![
800            create_test_descriptor("ServiceA", ServiceScope::Singleton, vec![]),
801            create_test_descriptor("ServiceB", ServiceScope::Transient, vec!["ServiceA"]),
802        ];
803
804        let visualizer = DependencyVisualizer::new(descriptors);
805        let style = VisualizationStyle::default();
806
807        let mermaid = visualizer.generate_mermaid(style).unwrap();
808
809        assert!(mermaid.contains("graph TD"));
810        assert!(mermaid.contains("ServiceA"));
811        assert!(mermaid.contains("ServiceB"));
812        assert!(mermaid.contains("-->"));
813    }
814
815    #[test]
816    fn test_ascii_generation() {
817        let descriptors = vec![
818            create_test_descriptor("ServiceA", ServiceScope::Singleton, vec![]),
819            create_test_descriptor("ServiceB", ServiceScope::Scoped, vec!["ServiceA"]),
820        ];
821
822        let visualizer = DependencyVisualizer::new(descriptors);
823        let style = VisualizationStyle::default();
824
825        let ascii = visualizer.generate_ascii(style).unwrap();
826
827        assert!(ascii.contains("Service Dependency Tree"));
828        assert!(ascii.contains("ServiceA"));
829        assert!(ascii.contains("ServiceB"));
830    }
831
832    #[test]
833    fn test_json_generation() {
834        let descriptors = vec![
835            create_test_descriptor("ServiceA", ServiceScope::Singleton, vec![]),
836            create_test_descriptor("ServiceB", ServiceScope::Transient, vec!["ServiceA"]),
837        ];
838
839        let visualizer = DependencyVisualizer::new(descriptors);
840        let style = VisualizationStyle::default();
841
842        let json = visualizer.generate_json(style).unwrap();
843
844        // Parse JSON to verify it's valid
845        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
846        assert!(parsed.get("services").is_some());
847        assert!(parsed.get("edges").is_some());
848    }
849
850    #[test]
851    fn test_service_explorer() {
852        let descriptors = vec![
853            create_test_descriptor("ServiceA", ServiceScope::Singleton, vec![]),
854            create_test_descriptor("ServiceB", ServiceScope::Scoped, vec!["ServiceA"]),
855            create_test_descriptor("ServiceC", ServiceScope::Transient, vec!["ServiceB"]),
856        ];
857
858        let explorer = ServiceExplorer::new(descriptors);
859
860        let service_a = ServiceId {
861            type_id: TypeId::of::<()>(),
862            type_name: "test_service",
863            name: Some("ServiceA".to_string()),
864        };
865        let service_c = ServiceId {
866            type_id: TypeId::of::<()>(),
867            type_name: "test_service",
868            name: Some("ServiceC".to_string()),
869        };
870
871        // Test path finding
872        let paths = explorer.find_paths(&service_c, &service_a);
873        assert_eq!(paths.len(), 1);
874        assert_eq!(paths[0].len(), 3); // C -> B -> A
875
876        // Test dependency depth
877        let depth = explorer.get_dependency_depth(&service_c);
878        assert_eq!(depth, 2); // C depends on B which depends on A
879    }
880
881    #[test]
882    fn test_style_filtering() {
883        let descriptors = vec![
884            create_test_descriptor("UserService", ServiceScope::Singleton, vec![]),
885            create_test_descriptor("PaymentService", ServiceScope::Scoped, vec!["UserService"]),
886            create_test_descriptor("NotificationService", ServiceScope::Transient, vec![]),
887        ];
888
889        let visualizer = DependencyVisualizer::new(descriptors);
890        let mut style = VisualizationStyle::default();
891        style.filter_types = Some(vec!["User".to_string(), "Payment".to_string()]);
892
893        let json = visualizer.generate_json(style).unwrap();
894        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
895
896        let services = parsed["services"].as_array().unwrap();
897        assert_eq!(services.len(), 2); // Only UserService and PaymentService
898
899        let service_names: Vec<&str> = services.iter().map(|s| s["id"].as_str().unwrap()).collect();
900        assert!(service_names.contains(&"UserService"));
901        assert!(service_names.contains(&"PaymentService"));
902        assert!(!service_names.contains(&"NotificationService"));
903    }
904}