mockforge_plugin_registry/
dependencies.rs

1//! Plugin dependency resolution and management
2
3use crate::{RegistryError, Result, VersionEntry};
4use semver::{Version, VersionReq};
5use serde::{Deserialize, Serialize};
6use std::collections::{HashMap, HashSet, VecDeque};
7
8/// Dependency specification
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct Dependency {
11    /// Package name
12    pub name: String,
13
14    /// Version requirement (semver)
15    pub version_req: String,
16
17    /// Optional dependency
18    pub optional: bool,
19
20    /// Features to enable
21    pub features: Vec<String>,
22
23    /// Registry source
24    pub source: DependencySource,
25}
26
27/// Dependency source
28#[derive(Debug, Clone, Serialize, Deserialize)]
29#[serde(rename_all = "lowercase")]
30pub enum DependencySource {
31    Registry,
32    Git { url: String, rev: Option<String> },
33    Path { path: String },
34}
35
36impl Default for DependencySource {
37    fn default() -> Self {
38        Self::Registry
39    }
40}
41
42/// Resolved dependency
43#[derive(Debug, Clone)]
44pub struct ResolvedDependency {
45    pub name: String,
46    pub version: Version,
47    pub dependencies: Vec<Dependency>,
48}
49
50/// Dependency graph node
51#[allow(dead_code)]
52#[derive(Debug, Clone)]
53struct DependencyNode {
54    name: String,
55    version: Version,
56    dependencies: Vec<String>, // List of dependent package names
57}
58
59/// Dependency resolver
60pub struct DependencyResolver {
61    /// Available package versions
62    available_versions: HashMap<String, Vec<VersionEntry>>,
63}
64
65impl DependencyResolver {
66    /// Create a new dependency resolver
67    pub fn new() -> Self {
68        Self {
69            available_versions: HashMap::new(),
70        }
71    }
72
73    /// Add available versions for a package
74    pub fn add_package_versions(&mut self, name: String, versions: Vec<VersionEntry>) {
75        self.available_versions.insert(name, versions);
76    }
77
78    /// Resolve dependencies for a package
79    pub fn resolve(
80        &self,
81        root_package: &str,
82        _root_version: &Version,
83        dependencies: Vec<Dependency>,
84    ) -> Result<Vec<ResolvedDependency>> {
85        let mut resolved = Vec::new();
86        let mut visited = HashSet::new();
87        let mut queue = VecDeque::new();
88
89        // Start with root dependencies
90        queue.push_back((root_package.to_string(), dependencies));
91
92        while let Some((_parent, deps)) = queue.pop_front() {
93            for dep in deps {
94                // Skip if already resolved
95                if visited.contains(&dep.name) {
96                    continue;
97                }
98
99                // Parse version requirement
100                let version_req = VersionReq::parse(&dep.version_req).map_err(|e| {
101                    RegistryError::InvalidVersion(format!(
102                        "Invalid version requirement '{}': {}",
103                        dep.version_req, e
104                    ))
105                })?;
106
107                // Find compatible version
108                let compatible_version =
109                    self.find_compatible_version(&dep.name, &version_req)?.ok_or_else(|| {
110                        RegistryError::InvalidVersion(format!(
111                            "No compatible version found for {} with requirement {}",
112                            dep.name, dep.version_req
113                        ))
114                    })?;
115
116                // Get version entry
117                let version_entry =
118                    self.get_version_entry(&dep.name, &compatible_version).ok_or_else(|| {
119                        RegistryError::PluginNotFound(format!(
120                            "{} {}",
121                            dep.name, compatible_version
122                        ))
123                    })?;
124
125                // Parse transitive dependencies
126                let transitive_deps: Vec<Dependency> = version_entry
127                    .dependencies
128                    .iter()
129                    .map(|(name, version_req)| Dependency {
130                        name: name.clone(),
131                        version_req: version_req.clone(),
132                        optional: false,
133                        features: vec![],
134                        source: DependencySource::Registry,
135                    })
136                    .collect();
137
138                // Add to resolved
139                resolved.push(ResolvedDependency {
140                    name: dep.name.clone(),
141                    version: compatible_version.clone(),
142                    dependencies: transitive_deps.clone(),
143                });
144
145                visited.insert(dep.name.clone());
146
147                // Queue transitive dependencies
148                if !transitive_deps.is_empty() {
149                    queue.push_back((dep.name.clone(), transitive_deps));
150                }
151            }
152        }
153
154        // Check for circular dependencies
155        self.check_circular_dependencies(&resolved)?;
156
157        Ok(resolved)
158    }
159
160    /// Find a compatible version for a package
161    fn find_compatible_version(
162        &self,
163        package: &str,
164        version_req: &VersionReq,
165    ) -> Result<Option<Version>> {
166        let versions = self
167            .available_versions
168            .get(package)
169            .ok_or_else(|| RegistryError::PluginNotFound(package.to_string()))?;
170
171        // Filter out yanked versions and parse semver
172        let mut compatible_versions: Vec<Version> = versions
173            .iter()
174            .filter(|v| !v.yanked)
175            .filter_map(|v| Version::parse(&v.version).ok())
176            .filter(|v| version_req.matches(v))
177            .collect();
178
179        // Sort by version (highest first)
180        compatible_versions.sort();
181        compatible_versions.reverse();
182
183        Ok(compatible_versions.first().cloned())
184    }
185
186    /// Get version entry for a specific version
187    fn get_version_entry(&self, package: &str, version: &Version) -> Option<&VersionEntry> {
188        self.available_versions
189            .get(package)?
190            .iter()
191            .find(|v| Version::parse(&v.version).ok().map(|v| &v == version).unwrap_or(false))
192    }
193
194    /// Check for circular dependencies
195    fn check_circular_dependencies(&self, resolved: &[ResolvedDependency]) -> Result<()> {
196        let mut graph: HashMap<String, Vec<String>> = HashMap::new();
197
198        // Build adjacency list
199        for dep in resolved {
200            let deps: Vec<String> = dep.dependencies.iter().map(|d| d.name.clone()).collect();
201            graph.insert(dep.name.clone(), deps);
202        }
203
204        // DFS to detect cycles
205        let mut visited = HashSet::new();
206        let mut rec_stack = HashSet::new();
207
208        for node in graph.keys() {
209            if Self::has_cycle_impl(&graph, node, &mut visited, &mut rec_stack) {
210                return Err(RegistryError::InvalidManifest(format!(
211                    "Circular dependency detected involving package: {}",
212                    node
213                )));
214            }
215        }
216
217        Ok(())
218    }
219
220    /// Check if there's a cycle starting from a node
221    fn has_cycle_impl(
222        graph: &HashMap<String, Vec<String>>,
223        node: &str,
224        visited: &mut HashSet<String>,
225        rec_stack: &mut HashSet<String>,
226    ) -> bool {
227        if rec_stack.contains(node) {
228            return true;
229        }
230
231        if visited.contains(node) {
232            return false;
233        }
234
235        visited.insert(node.to_string());
236        rec_stack.insert(node.to_string());
237
238        if let Some(neighbors) = graph.get(node) {
239            for neighbor in neighbors {
240                if Self::has_cycle_impl(graph, neighbor, visited, rec_stack) {
241                    return true;
242                }
243            }
244        }
245
246        rec_stack.remove(node);
247        false
248    }
249
250    /// Calculate installation order (topological sort)
251    pub fn calculate_install_order(&self, resolved: &[ResolvedDependency]) -> Result<Vec<String>> {
252        let mut graph: HashMap<String, Vec<String>> = HashMap::new();
253        let mut in_degree: HashMap<String, usize> = HashMap::new();
254
255        // Build graph
256        for dep in resolved {
257            in_degree.entry(dep.name.clone()).or_insert(0);
258
259            for child_dep in &dep.dependencies {
260                graph.entry(dep.name.clone()).or_default().push(child_dep.name.clone());
261
262                *in_degree.entry(child_dep.name.clone()).or_insert(0) += 1;
263            }
264        }
265
266        // Kahn's algorithm for topological sort
267        let mut queue: VecDeque<String> = in_degree
268            .iter()
269            .filter(|(_, &degree)| degree == 0)
270            .map(|(name, _)| name.clone())
271            .collect();
272
273        let mut order = Vec::new();
274
275        while let Some(node) = queue.pop_front() {
276            order.push(node.clone());
277
278            if let Some(neighbors) = graph.get(&node) {
279                for neighbor in neighbors {
280                    if let Some(degree) = in_degree.get_mut(neighbor) {
281                        *degree -= 1;
282                        if *degree == 0 {
283                            queue.push_back(neighbor.clone());
284                        }
285                    }
286                }
287            }
288        }
289
290        // Check if all nodes are in the order (no cycles)
291        if order.len() != in_degree.len() {
292            return Err(RegistryError::InvalidManifest(
293                "Circular dependency detected during install order calculation".to_string(),
294            ));
295        }
296
297        // Reverse to get correct install order (dependencies first)
298        order.reverse();
299
300        Ok(order)
301    }
302}
303
304impl Default for DependencyResolver {
305    fn default() -> Self {
306        Self::new()
307    }
308}
309
310/// Dependency conflict
311#[derive(Debug, Clone, Serialize, Deserialize)]
312pub struct DependencyConflict {
313    pub package: String,
314    pub required_by: Vec<ConflictRequirement>,
315}
316
317/// Conflict requirement
318#[derive(Debug, Clone, Serialize, Deserialize)]
319pub struct ConflictRequirement {
320    pub package: String,
321    pub version_req: String,
322}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327
328    fn create_version_entry(version: &str, yanked: bool) -> VersionEntry {
329        VersionEntry {
330            version: version.to_string(),
331            download_url: format!("https://example.com/pkg-{}.tar.gz", version),
332            checksum: "abc123".to_string(),
333            size: 1000,
334            published_at: "2025-01-01".to_string(),
335            yanked,
336            min_mockforge_version: None,
337            dependencies: HashMap::new(),
338        }
339    }
340
341    fn create_dependency(name: &str, version_req: &str) -> Dependency {
342        Dependency {
343            name: name.to_string(),
344            version_req: version_req.to_string(),
345            optional: false,
346            features: vec![],
347            source: DependencySource::Registry,
348        }
349    }
350
351    // Dependency struct tests
352    #[test]
353    fn test_dependency_clone() {
354        let dep = Dependency {
355            name: "test-dep".to_string(),
356            version_req: "^1.0.0".to_string(),
357            optional: true,
358            features: vec!["feature1".to_string(), "feature2".to_string()],
359            source: DependencySource::Registry,
360        };
361
362        let cloned = dep.clone();
363        assert_eq!(dep.name, cloned.name);
364        assert_eq!(dep.version_req, cloned.version_req);
365        assert_eq!(dep.optional, cloned.optional);
366        assert_eq!(dep.features.len(), cloned.features.len());
367    }
368
369    #[test]
370    fn test_dependency_debug() {
371        let dep = create_dependency("my-dep", "^2.0");
372        let debug = format!("{:?}", dep);
373        assert!(debug.contains("Dependency"));
374        assert!(debug.contains("my-dep"));
375    }
376
377    #[test]
378    fn test_dependency_serialize() {
379        let dep = create_dependency("test-dep", ">=1.0.0");
380        let json = serde_json::to_string(&dep).unwrap();
381        assert!(json.contains("\"name\":\"test-dep\""));
382        assert!(json.contains("\"version_req\":\">=1.0.0\""));
383    }
384
385    #[test]
386    fn test_dependency_deserialize() {
387        let json = r#"{
388            "name": "parsed-dep",
389            "version_req": "~1.2.0",
390            "optional": true,
391            "features": ["async"],
392            "source": "registry"
393        }"#;
394
395        let dep: Dependency = serde_json::from_str(json).unwrap();
396        assert_eq!(dep.name, "parsed-dep");
397        assert_eq!(dep.version_req, "~1.2.0");
398        assert!(dep.optional);
399        assert_eq!(dep.features, vec!["async"]);
400    }
401
402    #[test]
403    fn test_dependency_with_features() {
404        let dep = Dependency {
405            name: "feature-dep".to_string(),
406            version_req: "1.0.0".to_string(),
407            optional: false,
408            features: vec!["serde".to_string(), "async".to_string(), "full".to_string()],
409            source: DependencySource::Registry,
410        };
411
412        assert_eq!(dep.features.len(), 3);
413        assert!(dep.features.contains(&"serde".to_string()));
414    }
415
416    // DependencySource tests
417    #[test]
418    fn test_dependency_source_default() {
419        let source = DependencySource::default();
420        assert!(matches!(source, DependencySource::Registry));
421    }
422
423    #[test]
424    fn test_dependency_source_registry_serialize() {
425        let source = DependencySource::Registry;
426        let json = serde_json::to_string(&source).unwrap();
427        assert_eq!(json, "\"registry\"");
428    }
429
430    #[test]
431    fn test_dependency_source_git_serialize() {
432        let source = DependencySource::Git {
433            url: "https://github.com/test/repo".to_string(),
434            rev: Some("main".to_string()),
435        };
436        let json = serde_json::to_string(&source).unwrap();
437        assert!(json.contains("git"));
438        assert!(json.contains("github.com"));
439        assert!(json.contains("main"));
440    }
441
442    #[test]
443    fn test_dependency_source_git_without_rev() {
444        let source = DependencySource::Git {
445            url: "https://github.com/test/repo".to_string(),
446            rev: None,
447        };
448        let json = serde_json::to_string(&source).unwrap();
449        assert!(json.contains("git"));
450    }
451
452    #[test]
453    fn test_dependency_source_path_serialize() {
454        let source = DependencySource::Path {
455            path: "/local/path/to/dep".to_string(),
456        };
457        let json = serde_json::to_string(&source).unwrap();
458        assert!(json.contains("path"));
459        assert!(json.contains("/local/path"));
460    }
461
462    #[test]
463    fn test_dependency_source_deserialize_registry() {
464        let source: DependencySource = serde_json::from_str("\"registry\"").unwrap();
465        assert!(matches!(source, DependencySource::Registry));
466    }
467
468    #[test]
469    fn test_dependency_source_deserialize_git() {
470        let json = r#"{"git": {"url": "https://github.com/test/repo", "rev": "v1.0.0"}}"#;
471        let source: DependencySource = serde_json::from_str(json).unwrap();
472        match source {
473            DependencySource::Git { url, rev } => {
474                assert!(url.contains("github.com"));
475                assert_eq!(rev, Some("v1.0.0".to_string()));
476            }
477            _ => panic!("Expected Git source"),
478        }
479    }
480
481    #[test]
482    fn test_dependency_source_clone() {
483        let source = DependencySource::Git {
484            url: "https://test.com".to_string(),
485            rev: Some("abc123".to_string()),
486        };
487        let cloned = source.clone();
488        match cloned {
489            DependencySource::Git { url, rev } => {
490                assert_eq!(url, "https://test.com");
491                assert_eq!(rev, Some("abc123".to_string()));
492            }
493            _ => panic!("Expected Git source"),
494        }
495    }
496
497    #[test]
498    fn test_dependency_source_debug() {
499        let source = DependencySource::Path {
500            path: "./local".to_string(),
501        };
502        let debug = format!("{:?}", source);
503        assert!(debug.contains("Path"));
504    }
505
506    // ResolvedDependency tests
507    #[test]
508    fn test_resolved_dependency_clone() {
509        let resolved = ResolvedDependency {
510            name: "resolved-pkg".to_string(),
511            version: Version::parse("1.2.3").unwrap(),
512            dependencies: vec![create_dependency("dep-a", "^1.0")],
513        };
514
515        let cloned = resolved.clone();
516        assert_eq!(resolved.name, cloned.name);
517        assert_eq!(resolved.version, cloned.version);
518        assert_eq!(resolved.dependencies.len(), cloned.dependencies.len());
519    }
520
521    #[test]
522    fn test_resolved_dependency_debug() {
523        let resolved = ResolvedDependency {
524            name: "test-pkg".to_string(),
525            version: Version::parse("2.0.0").unwrap(),
526            dependencies: vec![],
527        };
528
529        let debug = format!("{:?}", resolved);
530        assert!(debug.contains("ResolvedDependency"));
531        assert!(debug.contains("test-pkg"));
532    }
533
534    // DependencyResolver tests
535    #[test]
536    fn test_dependency_resolver_new() {
537        let _resolver = DependencyResolver::new();
538        // DependencyResolver created successfully
539    }
540
541    #[test]
542    fn test_dependency_resolver_default() {
543        let _resolver = DependencyResolver::default();
544        // DependencyResolver::default() works
545    }
546
547    #[test]
548    fn test_dependency_resolver_add_package_versions() {
549        let mut resolver = DependencyResolver::new();
550
551        resolver.add_package_versions(
552            "my-package".to_string(),
553            vec![
554                create_version_entry("1.0.0", false),
555                create_version_entry("1.1.0", false),
556            ],
557        );
558
559        // Can add another package
560        resolver.add_package_versions(
561            "other-package".to_string(),
562            vec![create_version_entry("2.0.0", false)],
563        );
564    }
565
566    #[test]
567    fn test_dependency_resolution() {
568        let mut resolver = DependencyResolver::new();
569
570        // Add package A with versions
571        resolver.add_package_versions(
572            "package-a".to_string(),
573            vec![
574                create_version_entry("1.0.0", false),
575                create_version_entry("1.1.0", false),
576            ],
577        );
578
579        let deps = vec![create_dependency("package-a", "^1.0")];
580
581        let root_version = Version::parse("1.0.0").unwrap();
582        let resolved = resolver.resolve("root", &root_version, deps);
583
584        assert!(resolved.is_ok());
585        let resolved = resolved.unwrap();
586        assert_eq!(resolved.len(), 1);
587        assert_eq!(resolved[0].name, "package-a");
588        assert_eq!(resolved[0].version, Version::parse("1.1.0").unwrap());
589    }
590
591    #[test]
592    fn test_dependency_resolution_exact_version() {
593        let mut resolver = DependencyResolver::new();
594
595        resolver.add_package_versions(
596            "exact-pkg".to_string(),
597            vec![
598                create_version_entry("1.0.0", false),
599                create_version_entry("1.1.0", false),
600                create_version_entry("2.0.0", false),
601            ],
602        );
603
604        let deps = vec![create_dependency("exact-pkg", "=1.0.0")];
605        let root_version = Version::parse("1.0.0").unwrap();
606        let resolved = resolver.resolve("root", &root_version, deps).unwrap();
607
608        assert_eq!(resolved[0].version, Version::parse("1.0.0").unwrap());
609    }
610
611    #[test]
612    fn test_dependency_resolution_yanked_excluded() {
613        let mut resolver = DependencyResolver::new();
614
615        resolver.add_package_versions(
616            "yanked-pkg".to_string(),
617            vec![
618                create_version_entry("1.0.0", false),
619                create_version_entry("1.1.0", true), // yanked
620                create_version_entry("1.0.5", false),
621            ],
622        );
623
624        let deps = vec![create_dependency("yanked-pkg", "^1.0")];
625        let root_version = Version::parse("1.0.0").unwrap();
626        let resolved = resolver.resolve("root", &root_version, deps).unwrap();
627
628        // Should pick 1.0.5 (highest non-yanked that matches)
629        assert_eq!(resolved[0].version, Version::parse("1.0.5").unwrap());
630    }
631
632    #[test]
633    fn test_dependency_resolution_package_not_found() {
634        let resolver = DependencyResolver::new();
635
636        let deps = vec![create_dependency("nonexistent", "^1.0")];
637        let root_version = Version::parse("1.0.0").unwrap();
638        let result = resolver.resolve("root", &root_version, deps);
639
640        assert!(result.is_err());
641    }
642
643    #[test]
644    fn test_dependency_resolution_no_compatible_version() {
645        let mut resolver = DependencyResolver::new();
646
647        resolver.add_package_versions(
648            "old-pkg".to_string(),
649            vec![create_version_entry("0.5.0", false)],
650        );
651
652        let deps = vec![create_dependency("old-pkg", "^1.0")];
653        let root_version = Version::parse("1.0.0").unwrap();
654        let result = resolver.resolve("root", &root_version, deps);
655
656        assert!(result.is_err());
657    }
658
659    #[test]
660    fn test_dependency_resolution_invalid_version_req() {
661        let mut resolver = DependencyResolver::new();
662
663        resolver
664            .add_package_versions("pkg".to_string(), vec![create_version_entry("1.0.0", false)]);
665
666        let deps = vec![create_dependency("pkg", "invalid-req")];
667        let root_version = Version::parse("1.0.0").unwrap();
668        let result = resolver.resolve("root", &root_version, deps);
669
670        assert!(result.is_err());
671    }
672
673    #[test]
674    fn test_dependency_resolution_multiple_deps() {
675        let mut resolver = DependencyResolver::new();
676
677        resolver
678            .add_package_versions("pkg-a".to_string(), vec![create_version_entry("1.0.0", false)]);
679        resolver
680            .add_package_versions("pkg-b".to_string(), vec![create_version_entry("2.0.0", false)]);
681        resolver
682            .add_package_versions("pkg-c".to_string(), vec![create_version_entry("3.0.0", false)]);
683
684        let deps = vec![
685            create_dependency("pkg-a", "^1.0"),
686            create_dependency("pkg-b", "^2.0"),
687            create_dependency("pkg-c", "^3.0"),
688        ];
689
690        let root_version = Version::parse("1.0.0").unwrap();
691        let resolved = resolver.resolve("root", &root_version, deps).unwrap();
692
693        assert_eq!(resolved.len(), 3);
694    }
695
696    #[test]
697    fn test_circular_dependency_detection() {
698        let resolver = DependencyResolver::new();
699
700        // Create circular dependency: A -> B -> A
701        let resolved = vec![
702            ResolvedDependency {
703                name: "package-a".to_string(),
704                version: Version::parse("1.0.0").unwrap(),
705                dependencies: vec![create_dependency("package-b", "1.0")],
706            },
707            ResolvedDependency {
708                name: "package-b".to_string(),
709                version: Version::parse("1.0.0").unwrap(),
710                dependencies: vec![create_dependency("package-a", "1.0")],
711            },
712        ];
713
714        let result = resolver.check_circular_dependencies(&resolved);
715        assert!(result.is_err());
716    }
717
718    #[test]
719    fn test_no_circular_dependency() {
720        let resolver = DependencyResolver::new();
721
722        // Linear dependency: A -> B -> C (no cycle)
723        let resolved = vec![
724            ResolvedDependency {
725                name: "package-a".to_string(),
726                version: Version::parse("1.0.0").unwrap(),
727                dependencies: vec![create_dependency("package-b", "1.0")],
728            },
729            ResolvedDependency {
730                name: "package-b".to_string(),
731                version: Version::parse("1.0.0").unwrap(),
732                dependencies: vec![create_dependency("package-c", "1.0")],
733            },
734            ResolvedDependency {
735                name: "package-c".to_string(),
736                version: Version::parse("1.0.0").unwrap(),
737                dependencies: vec![],
738            },
739        ];
740
741        let result = resolver.check_circular_dependencies(&resolved);
742        assert!(result.is_ok());
743    }
744
745    #[test]
746    fn test_circular_dependency_three_node_cycle() {
747        let resolver = DependencyResolver::new();
748
749        // A -> B -> C -> A
750        let resolved = vec![
751            ResolvedDependency {
752                name: "a".to_string(),
753                version: Version::parse("1.0.0").unwrap(),
754                dependencies: vec![create_dependency("b", "1.0")],
755            },
756            ResolvedDependency {
757                name: "b".to_string(),
758                version: Version::parse("1.0.0").unwrap(),
759                dependencies: vec![create_dependency("c", "1.0")],
760            },
761            ResolvedDependency {
762                name: "c".to_string(),
763                version: Version::parse("1.0.0").unwrap(),
764                dependencies: vec![create_dependency("a", "1.0")],
765            },
766        ];
767
768        let result = resolver.check_circular_dependencies(&resolved);
769        assert!(result.is_err());
770    }
771
772    // calculate_install_order tests
773    #[test]
774    fn test_calculate_install_order_simple() {
775        let resolver = DependencyResolver::new();
776
777        let resolved = vec![
778            ResolvedDependency {
779                name: "root".to_string(),
780                version: Version::parse("1.0.0").unwrap(),
781                dependencies: vec![create_dependency("leaf", "1.0")],
782            },
783            ResolvedDependency {
784                name: "leaf".to_string(),
785                version: Version::parse("1.0.0").unwrap(),
786                dependencies: vec![],
787            },
788        ];
789
790        let order = resolver.calculate_install_order(&resolved).unwrap();
791        assert_eq!(order.len(), 2);
792        // leaf should come before root (dependencies first)
793        let leaf_pos = order.iter().position(|x| x == "leaf").unwrap();
794        let root_pos = order.iter().position(|x| x == "root").unwrap();
795        assert!(leaf_pos < root_pos);
796    }
797
798    #[test]
799    fn test_calculate_install_order_chain() {
800        let resolver = DependencyResolver::new();
801
802        // A depends on B, B depends on C
803        let resolved = vec![
804            ResolvedDependency {
805                name: "a".to_string(),
806                version: Version::parse("1.0.0").unwrap(),
807                dependencies: vec![create_dependency("b", "1.0")],
808            },
809            ResolvedDependency {
810                name: "b".to_string(),
811                version: Version::parse("1.0.0").unwrap(),
812                dependencies: vec![create_dependency("c", "1.0")],
813            },
814            ResolvedDependency {
815                name: "c".to_string(),
816                version: Version::parse("1.0.0").unwrap(),
817                dependencies: vec![],
818            },
819        ];
820
821        let order = resolver.calculate_install_order(&resolved).unwrap();
822        let c_pos = order.iter().position(|x| x == "c").unwrap();
823        let b_pos = order.iter().position(|x| x == "b").unwrap();
824        let a_pos = order.iter().position(|x| x == "a").unwrap();
825
826        assert!(c_pos < b_pos);
827        assert!(b_pos < a_pos);
828    }
829
830    #[test]
831    fn test_calculate_install_order_no_deps() {
832        let resolver = DependencyResolver::new();
833
834        let resolved = vec![ResolvedDependency {
835            name: "standalone".to_string(),
836            version: Version::parse("1.0.0").unwrap(),
837            dependencies: vec![],
838        }];
839
840        let order = resolver.calculate_install_order(&resolved).unwrap();
841        assert_eq!(order, vec!["standalone"]);
842    }
843
844    #[test]
845    fn test_calculate_install_order_empty() {
846        let resolver = DependencyResolver::new();
847        let resolved: Vec<ResolvedDependency> = vec![];
848
849        let order = resolver.calculate_install_order(&resolved).unwrap();
850        assert!(order.is_empty());
851    }
852
853    // DependencyConflict tests
854    #[test]
855    fn test_dependency_conflict_clone() {
856        let conflict = DependencyConflict {
857            package: "conflicting-pkg".to_string(),
858            required_by: vec![
859                ConflictRequirement {
860                    package: "pkg-a".to_string(),
861                    version_req: "^1.0".to_string(),
862                },
863                ConflictRequirement {
864                    package: "pkg-b".to_string(),
865                    version_req: "^2.0".to_string(),
866                },
867            ],
868        };
869
870        let cloned = conflict.clone();
871        assert_eq!(conflict.package, cloned.package);
872        assert_eq!(conflict.required_by.len(), cloned.required_by.len());
873    }
874
875    #[test]
876    fn test_dependency_conflict_debug() {
877        let conflict = DependencyConflict {
878            package: "test-conflict".to_string(),
879            required_by: vec![],
880        };
881
882        let debug = format!("{:?}", conflict);
883        assert!(debug.contains("DependencyConflict"));
884        assert!(debug.contains("test-conflict"));
885    }
886
887    #[test]
888    fn test_dependency_conflict_serialize() {
889        let conflict = DependencyConflict {
890            package: "pkg".to_string(),
891            required_by: vec![ConflictRequirement {
892                package: "requirer".to_string(),
893                version_req: "^1.0".to_string(),
894            }],
895        };
896
897        let json = serde_json::to_string(&conflict).unwrap();
898        assert!(json.contains("\"package\":\"pkg\""));
899        assert!(json.contains("requirer"));
900    }
901
902    // ConflictRequirement tests
903    #[test]
904    fn test_conflict_requirement_clone() {
905        let req = ConflictRequirement {
906            package: "req-pkg".to_string(),
907            version_req: ">=1.0.0".to_string(),
908        };
909
910        let cloned = req.clone();
911        assert_eq!(req.package, cloned.package);
912        assert_eq!(req.version_req, cloned.version_req);
913    }
914
915    #[test]
916    fn test_conflict_requirement_debug() {
917        let req = ConflictRequirement {
918            package: "debug-pkg".to_string(),
919            version_req: "~1.2".to_string(),
920        };
921
922        let debug = format!("{:?}", req);
923        assert!(debug.contains("ConflictRequirement"));
924    }
925
926    #[test]
927    fn test_conflict_requirement_serialize() {
928        let req = ConflictRequirement {
929            package: "ser-pkg".to_string(),
930            version_req: "*".to_string(),
931        };
932
933        let json = serde_json::to_string(&req).unwrap();
934        assert!(json.contains("\"package\":\"ser-pkg\""));
935        assert!(json.contains("\"version_req\":\"*\""));
936    }
937
938    #[test]
939    fn test_conflict_requirement_deserialize() {
940        let json = r#"{"package": "de-pkg", "version_req": "^3.0"}"#;
941        let req: ConflictRequirement = serde_json::from_str(json).unwrap();
942        assert_eq!(req.package, "de-pkg");
943        assert_eq!(req.version_req, "^3.0");
944    }
945
946    // Transitive dependency tests
947    #[test]
948    fn test_transitive_dependency_resolution() {
949        let mut resolver = DependencyResolver::new();
950
951        // pkg-a depends on pkg-b (via dependencies field in VersionEntry)
952        let mut deps_map = HashMap::new();
953        deps_map.insert("pkg-b".to_string(), "^1.0".to_string());
954
955        resolver.add_package_versions(
956            "pkg-a".to_string(),
957            vec![VersionEntry {
958                version: "1.0.0".to_string(),
959                download_url: "https://example.com/a.tar.gz".to_string(),
960                checksum: "abc".to_string(),
961                size: 1000,
962                published_at: "2025-01-01".to_string(),
963                yanked: false,
964                min_mockforge_version: None,
965                dependencies: deps_map,
966            }],
967        );
968
969        resolver
970            .add_package_versions("pkg-b".to_string(), vec![create_version_entry("1.0.0", false)]);
971
972        let deps = vec![create_dependency("pkg-a", "^1.0")];
973        let root_version = Version::parse("1.0.0").unwrap();
974        let resolved = resolver.resolve("root", &root_version, deps).unwrap();
975
976        // Should resolve both pkg-a and pkg-b
977        assert_eq!(resolved.len(), 2);
978        let names: Vec<&str> = resolved.iter().map(|r| r.name.as_str()).collect();
979        assert!(names.contains(&"pkg-a"));
980        assert!(names.contains(&"pkg-b"));
981    }
982}