Skip to main content

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, Default, Serialize, Deserialize)]
29#[serde(rename_all = "lowercase")]
30pub enum DependencySource {
31    #[default]
32    Registry,
33    Git {
34        url: String,
35        rev: Option<String>,
36    },
37    Path {
38        path: String,
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            downloads: 0,
339        }
340    }
341
342    fn create_dependency(name: &str, version_req: &str) -> Dependency {
343        Dependency {
344            name: name.to_string(),
345            version_req: version_req.to_string(),
346            optional: false,
347            features: vec![],
348            source: DependencySource::Registry,
349        }
350    }
351
352    // Dependency struct tests
353    #[test]
354    fn test_dependency_clone() {
355        let dep = Dependency {
356            name: "test-dep".to_string(),
357            version_req: "^1.0.0".to_string(),
358            optional: true,
359            features: vec!["feature1".to_string(), "feature2".to_string()],
360            source: DependencySource::Registry,
361        };
362
363        let cloned = dep.clone();
364        assert_eq!(dep.name, cloned.name);
365        assert_eq!(dep.version_req, cloned.version_req);
366        assert_eq!(dep.optional, cloned.optional);
367        assert_eq!(dep.features.len(), cloned.features.len());
368    }
369
370    #[test]
371    fn test_dependency_debug() {
372        let dep = create_dependency("my-dep", "^2.0");
373        let debug = format!("{:?}", dep);
374        assert!(debug.contains("Dependency"));
375        assert!(debug.contains("my-dep"));
376    }
377
378    #[test]
379    fn test_dependency_serialize() {
380        let dep = create_dependency("test-dep", ">=1.0.0");
381        let json = serde_json::to_string(&dep).unwrap();
382        assert!(json.contains("\"name\":\"test-dep\""));
383        assert!(json.contains("\"version_req\":\">=1.0.0\""));
384    }
385
386    #[test]
387    fn test_dependency_deserialize() {
388        let json = r#"{
389            "name": "parsed-dep",
390            "version_req": "~1.2.0",
391            "optional": true,
392            "features": ["async"],
393            "source": "registry"
394        }"#;
395
396        let dep: Dependency = serde_json::from_str(json).unwrap();
397        assert_eq!(dep.name, "parsed-dep");
398        assert_eq!(dep.version_req, "~1.2.0");
399        assert!(dep.optional);
400        assert_eq!(dep.features, vec!["async"]);
401    }
402
403    #[test]
404    fn test_dependency_with_features() {
405        let dep = Dependency {
406            name: "feature-dep".to_string(),
407            version_req: "1.0.0".to_string(),
408            optional: false,
409            features: vec!["serde".to_string(), "async".to_string(), "full".to_string()],
410            source: DependencySource::Registry,
411        };
412
413        assert_eq!(dep.features.len(), 3);
414        assert!(dep.features.contains(&"serde".to_string()));
415    }
416
417    // DependencySource tests
418    #[test]
419    fn test_dependency_source_default() {
420        let source = DependencySource::default();
421        assert!(matches!(source, DependencySource::Registry));
422    }
423
424    #[test]
425    fn test_dependency_source_registry_serialize() {
426        let source = DependencySource::Registry;
427        let json = serde_json::to_string(&source).unwrap();
428        assert_eq!(json, "\"registry\"");
429    }
430
431    #[test]
432    fn test_dependency_source_git_serialize() {
433        let source = DependencySource::Git {
434            url: "https://github.com/test/repo".to_string(),
435            rev: Some("main".to_string()),
436        };
437        let json = serde_json::to_string(&source).unwrap();
438        assert!(json.contains("git"));
439        assert!(json.contains("github.com"));
440        assert!(json.contains("main"));
441    }
442
443    #[test]
444    fn test_dependency_source_git_without_rev() {
445        let source = DependencySource::Git {
446            url: "https://github.com/test/repo".to_string(),
447            rev: None,
448        };
449        let json = serde_json::to_string(&source).unwrap();
450        assert!(json.contains("git"));
451    }
452
453    #[test]
454    fn test_dependency_source_path_serialize() {
455        let source = DependencySource::Path {
456            path: "/local/path/to/dep".to_string(),
457        };
458        let json = serde_json::to_string(&source).unwrap();
459        assert!(json.contains("path"));
460        assert!(json.contains("/local/path"));
461    }
462
463    #[test]
464    fn test_dependency_source_deserialize_registry() {
465        let source: DependencySource = serde_json::from_str("\"registry\"").unwrap();
466        assert!(matches!(source, DependencySource::Registry));
467    }
468
469    #[test]
470    fn test_dependency_source_deserialize_git() {
471        let json = r#"{"git": {"url": "https://github.com/test/repo", "rev": "v1.0.0"}}"#;
472        let source: DependencySource = serde_json::from_str(json).unwrap();
473        match source {
474            DependencySource::Git { url, rev } => {
475                assert!(url.contains("github.com"));
476                assert_eq!(rev, Some("v1.0.0".to_string()));
477            }
478            _ => panic!("Expected Git source"),
479        }
480    }
481
482    #[test]
483    fn test_dependency_source_clone() {
484        let source = DependencySource::Git {
485            url: "https://test.com".to_string(),
486            rev: Some("abc123".to_string()),
487        };
488        let cloned = source.clone();
489        match cloned {
490            DependencySource::Git { url, rev } => {
491                assert_eq!(url, "https://test.com");
492                assert_eq!(rev, Some("abc123".to_string()));
493            }
494            _ => panic!("Expected Git source"),
495        }
496    }
497
498    #[test]
499    fn test_dependency_source_debug() {
500        let source = DependencySource::Path {
501            path: "./local".to_string(),
502        };
503        let debug = format!("{:?}", source);
504        assert!(debug.contains("Path"));
505    }
506
507    // ResolvedDependency tests
508    #[test]
509    fn test_resolved_dependency_clone() {
510        let resolved = ResolvedDependency {
511            name: "resolved-pkg".to_string(),
512            version: Version::parse("1.2.3").unwrap(),
513            dependencies: vec![create_dependency("dep-a", "^1.0")],
514        };
515
516        let cloned = resolved.clone();
517        assert_eq!(resolved.name, cloned.name);
518        assert_eq!(resolved.version, cloned.version);
519        assert_eq!(resolved.dependencies.len(), cloned.dependencies.len());
520    }
521
522    #[test]
523    fn test_resolved_dependency_debug() {
524        let resolved = ResolvedDependency {
525            name: "test-pkg".to_string(),
526            version: Version::parse("2.0.0").unwrap(),
527            dependencies: vec![],
528        };
529
530        let debug = format!("{:?}", resolved);
531        assert!(debug.contains("ResolvedDependency"));
532        assert!(debug.contains("test-pkg"));
533    }
534
535    // DependencyResolver tests
536    #[test]
537    fn test_dependency_resolver_new() {
538        let _resolver = DependencyResolver::new();
539        // DependencyResolver created successfully
540    }
541
542    #[test]
543    fn test_dependency_resolver_default() {
544        let _resolver = DependencyResolver::default();
545        // DependencyResolver::default() works
546    }
547
548    #[test]
549    fn test_dependency_resolver_add_package_versions() {
550        let mut resolver = DependencyResolver::new();
551
552        resolver.add_package_versions(
553            "my-package".to_string(),
554            vec![
555                create_version_entry("1.0.0", false),
556                create_version_entry("1.1.0", false),
557            ],
558        );
559
560        // Can add another package
561        resolver.add_package_versions(
562            "other-package".to_string(),
563            vec![create_version_entry("2.0.0", false)],
564        );
565    }
566
567    #[test]
568    fn test_dependency_resolution() {
569        let mut resolver = DependencyResolver::new();
570
571        // Add package A with versions
572        resolver.add_package_versions(
573            "package-a".to_string(),
574            vec![
575                create_version_entry("1.0.0", false),
576                create_version_entry("1.1.0", false),
577            ],
578        );
579
580        let deps = vec![create_dependency("package-a", "^1.0")];
581
582        let root_version = Version::parse("1.0.0").unwrap();
583        let resolved = resolver.resolve("root", &root_version, deps);
584
585        assert!(resolved.is_ok());
586        let resolved = resolved.unwrap();
587        assert_eq!(resolved.len(), 1);
588        assert_eq!(resolved[0].name, "package-a");
589        assert_eq!(resolved[0].version, Version::parse("1.1.0").unwrap());
590    }
591
592    #[test]
593    fn test_dependency_resolution_exact_version() {
594        let mut resolver = DependencyResolver::new();
595
596        resolver.add_package_versions(
597            "exact-pkg".to_string(),
598            vec![
599                create_version_entry("1.0.0", false),
600                create_version_entry("1.1.0", false),
601                create_version_entry("2.0.0", false),
602            ],
603        );
604
605        let deps = vec![create_dependency("exact-pkg", "=1.0.0")];
606        let root_version = Version::parse("1.0.0").unwrap();
607        let resolved = resolver.resolve("root", &root_version, deps).unwrap();
608
609        assert_eq!(resolved[0].version, Version::parse("1.0.0").unwrap());
610    }
611
612    #[test]
613    fn test_dependency_resolution_yanked_excluded() {
614        let mut resolver = DependencyResolver::new();
615
616        resolver.add_package_versions(
617            "yanked-pkg".to_string(),
618            vec![
619                create_version_entry("1.0.0", false),
620                create_version_entry("1.1.0", true), // yanked
621                create_version_entry("1.0.5", false),
622            ],
623        );
624
625        let deps = vec![create_dependency("yanked-pkg", "^1.0")];
626        let root_version = Version::parse("1.0.0").unwrap();
627        let resolved = resolver.resolve("root", &root_version, deps).unwrap();
628
629        // Should pick 1.0.5 (highest non-yanked that matches)
630        assert_eq!(resolved[0].version, Version::parse("1.0.5").unwrap());
631    }
632
633    #[test]
634    fn test_dependency_resolution_package_not_found() {
635        let resolver = DependencyResolver::new();
636
637        let deps = vec![create_dependency("nonexistent", "^1.0")];
638        let root_version = Version::parse("1.0.0").unwrap();
639        let result = resolver.resolve("root", &root_version, deps);
640
641        assert!(result.is_err());
642    }
643
644    #[test]
645    fn test_dependency_resolution_no_compatible_version() {
646        let mut resolver = DependencyResolver::new();
647
648        resolver.add_package_versions(
649            "old-pkg".to_string(),
650            vec![create_version_entry("0.5.0", false)],
651        );
652
653        let deps = vec![create_dependency("old-pkg", "^1.0")];
654        let root_version = Version::parse("1.0.0").unwrap();
655        let result = resolver.resolve("root", &root_version, deps);
656
657        assert!(result.is_err());
658    }
659
660    #[test]
661    fn test_dependency_resolution_invalid_version_req() {
662        let mut resolver = DependencyResolver::new();
663
664        resolver
665            .add_package_versions("pkg".to_string(), vec![create_version_entry("1.0.0", false)]);
666
667        let deps = vec![create_dependency("pkg", "invalid-req")];
668        let root_version = Version::parse("1.0.0").unwrap();
669        let result = resolver.resolve("root", &root_version, deps);
670
671        assert!(result.is_err());
672    }
673
674    #[test]
675    fn test_dependency_resolution_multiple_deps() {
676        let mut resolver = DependencyResolver::new();
677
678        resolver
679            .add_package_versions("pkg-a".to_string(), vec![create_version_entry("1.0.0", false)]);
680        resolver
681            .add_package_versions("pkg-b".to_string(), vec![create_version_entry("2.0.0", false)]);
682        resolver
683            .add_package_versions("pkg-c".to_string(), vec![create_version_entry("3.0.0", false)]);
684
685        let deps = vec![
686            create_dependency("pkg-a", "^1.0"),
687            create_dependency("pkg-b", "^2.0"),
688            create_dependency("pkg-c", "^3.0"),
689        ];
690
691        let root_version = Version::parse("1.0.0").unwrap();
692        let resolved = resolver.resolve("root", &root_version, deps).unwrap();
693
694        assert_eq!(resolved.len(), 3);
695    }
696
697    #[test]
698    fn test_circular_dependency_detection() {
699        let resolver = DependencyResolver::new();
700
701        // Create circular dependency: A -> B -> A
702        let resolved = vec![
703            ResolvedDependency {
704                name: "package-a".to_string(),
705                version: Version::parse("1.0.0").unwrap(),
706                dependencies: vec![create_dependency("package-b", "1.0")],
707            },
708            ResolvedDependency {
709                name: "package-b".to_string(),
710                version: Version::parse("1.0.0").unwrap(),
711                dependencies: vec![create_dependency("package-a", "1.0")],
712            },
713        ];
714
715        let result = resolver.check_circular_dependencies(&resolved);
716        assert!(result.is_err());
717    }
718
719    #[test]
720    fn test_no_circular_dependency() {
721        let resolver = DependencyResolver::new();
722
723        // Linear dependency: A -> B -> C (no cycle)
724        let resolved = vec![
725            ResolvedDependency {
726                name: "package-a".to_string(),
727                version: Version::parse("1.0.0").unwrap(),
728                dependencies: vec![create_dependency("package-b", "1.0")],
729            },
730            ResolvedDependency {
731                name: "package-b".to_string(),
732                version: Version::parse("1.0.0").unwrap(),
733                dependencies: vec![create_dependency("package-c", "1.0")],
734            },
735            ResolvedDependency {
736                name: "package-c".to_string(),
737                version: Version::parse("1.0.0").unwrap(),
738                dependencies: vec![],
739            },
740        ];
741
742        let result = resolver.check_circular_dependencies(&resolved);
743        assert!(result.is_ok());
744    }
745
746    #[test]
747    fn test_circular_dependency_three_node_cycle() {
748        let resolver = DependencyResolver::new();
749
750        // A -> B -> C -> A
751        let resolved = vec![
752            ResolvedDependency {
753                name: "a".to_string(),
754                version: Version::parse("1.0.0").unwrap(),
755                dependencies: vec![create_dependency("b", "1.0")],
756            },
757            ResolvedDependency {
758                name: "b".to_string(),
759                version: Version::parse("1.0.0").unwrap(),
760                dependencies: vec![create_dependency("c", "1.0")],
761            },
762            ResolvedDependency {
763                name: "c".to_string(),
764                version: Version::parse("1.0.0").unwrap(),
765                dependencies: vec![create_dependency("a", "1.0")],
766            },
767        ];
768
769        let result = resolver.check_circular_dependencies(&resolved);
770        assert!(result.is_err());
771    }
772
773    // calculate_install_order tests
774    #[test]
775    fn test_calculate_install_order_simple() {
776        let resolver = DependencyResolver::new();
777
778        let resolved = vec![
779            ResolvedDependency {
780                name: "root".to_string(),
781                version: Version::parse("1.0.0").unwrap(),
782                dependencies: vec![create_dependency("leaf", "1.0")],
783            },
784            ResolvedDependency {
785                name: "leaf".to_string(),
786                version: Version::parse("1.0.0").unwrap(),
787                dependencies: vec![],
788            },
789        ];
790
791        let order = resolver.calculate_install_order(&resolved).unwrap();
792        assert_eq!(order.len(), 2);
793        // leaf should come before root (dependencies first)
794        let leaf_pos = order.iter().position(|x| x == "leaf").unwrap();
795        let root_pos = order.iter().position(|x| x == "root").unwrap();
796        assert!(leaf_pos < root_pos);
797    }
798
799    #[test]
800    fn test_calculate_install_order_chain() {
801        let resolver = DependencyResolver::new();
802
803        // A depends on B, B depends on C
804        let resolved = vec![
805            ResolvedDependency {
806                name: "a".to_string(),
807                version: Version::parse("1.0.0").unwrap(),
808                dependencies: vec![create_dependency("b", "1.0")],
809            },
810            ResolvedDependency {
811                name: "b".to_string(),
812                version: Version::parse("1.0.0").unwrap(),
813                dependencies: vec![create_dependency("c", "1.0")],
814            },
815            ResolvedDependency {
816                name: "c".to_string(),
817                version: Version::parse("1.0.0").unwrap(),
818                dependencies: vec![],
819            },
820        ];
821
822        let order = resolver.calculate_install_order(&resolved).unwrap();
823        let c_pos = order.iter().position(|x| x == "c").unwrap();
824        let b_pos = order.iter().position(|x| x == "b").unwrap();
825        let a_pos = order.iter().position(|x| x == "a").unwrap();
826
827        assert!(c_pos < b_pos);
828        assert!(b_pos < a_pos);
829    }
830
831    #[test]
832    fn test_calculate_install_order_no_deps() {
833        let resolver = DependencyResolver::new();
834
835        let resolved = vec![ResolvedDependency {
836            name: "standalone".to_string(),
837            version: Version::parse("1.0.0").unwrap(),
838            dependencies: vec![],
839        }];
840
841        let order = resolver.calculate_install_order(&resolved).unwrap();
842        assert_eq!(order, vec!["standalone"]);
843    }
844
845    #[test]
846    fn test_calculate_install_order_empty() {
847        let resolver = DependencyResolver::new();
848        let resolved: Vec<ResolvedDependency> = vec![];
849
850        let order = resolver.calculate_install_order(&resolved).unwrap();
851        assert!(order.is_empty());
852    }
853
854    // DependencyConflict tests
855    #[test]
856    fn test_dependency_conflict_clone() {
857        let conflict = DependencyConflict {
858            package: "conflicting-pkg".to_string(),
859            required_by: vec![
860                ConflictRequirement {
861                    package: "pkg-a".to_string(),
862                    version_req: "^1.0".to_string(),
863                },
864                ConflictRequirement {
865                    package: "pkg-b".to_string(),
866                    version_req: "^2.0".to_string(),
867                },
868            ],
869        };
870
871        let cloned = conflict.clone();
872        assert_eq!(conflict.package, cloned.package);
873        assert_eq!(conflict.required_by.len(), cloned.required_by.len());
874    }
875
876    #[test]
877    fn test_dependency_conflict_debug() {
878        let conflict = DependencyConflict {
879            package: "test-conflict".to_string(),
880            required_by: vec![],
881        };
882
883        let debug = format!("{:?}", conflict);
884        assert!(debug.contains("DependencyConflict"));
885        assert!(debug.contains("test-conflict"));
886    }
887
888    #[test]
889    fn test_dependency_conflict_serialize() {
890        let conflict = DependencyConflict {
891            package: "pkg".to_string(),
892            required_by: vec![ConflictRequirement {
893                package: "requirer".to_string(),
894                version_req: "^1.0".to_string(),
895            }],
896        };
897
898        let json = serde_json::to_string(&conflict).unwrap();
899        assert!(json.contains("\"package\":\"pkg\""));
900        assert!(json.contains("requirer"));
901    }
902
903    // ConflictRequirement tests
904    #[test]
905    fn test_conflict_requirement_clone() {
906        let req = ConflictRequirement {
907            package: "req-pkg".to_string(),
908            version_req: ">=1.0.0".to_string(),
909        };
910
911        let cloned = req.clone();
912        assert_eq!(req.package, cloned.package);
913        assert_eq!(req.version_req, cloned.version_req);
914    }
915
916    #[test]
917    fn test_conflict_requirement_debug() {
918        let req = ConflictRequirement {
919            package: "debug-pkg".to_string(),
920            version_req: "~1.2".to_string(),
921        };
922
923        let debug = format!("{:?}", req);
924        assert!(debug.contains("ConflictRequirement"));
925    }
926
927    #[test]
928    fn test_conflict_requirement_serialize() {
929        let req = ConflictRequirement {
930            package: "ser-pkg".to_string(),
931            version_req: "*".to_string(),
932        };
933
934        let json = serde_json::to_string(&req).unwrap();
935        assert!(json.contains("\"package\":\"ser-pkg\""));
936        assert!(json.contains("\"version_req\":\"*\""));
937    }
938
939    #[test]
940    fn test_conflict_requirement_deserialize() {
941        let json = r#"{"package": "de-pkg", "version_req": "^3.0"}"#;
942        let req: ConflictRequirement = serde_json::from_str(json).unwrap();
943        assert_eq!(req.package, "de-pkg");
944        assert_eq!(req.version_req, "^3.0");
945    }
946
947    // Transitive dependency tests
948    #[test]
949    fn test_transitive_dependency_resolution() {
950        let mut resolver = DependencyResolver::new();
951
952        // pkg-a depends on pkg-b (via dependencies field in VersionEntry)
953        let mut deps_map = HashMap::new();
954        deps_map.insert("pkg-b".to_string(), "^1.0".to_string());
955
956        resolver.add_package_versions(
957            "pkg-a".to_string(),
958            vec![VersionEntry {
959                version: "1.0.0".to_string(),
960                download_url: "https://example.com/a.tar.gz".to_string(),
961                checksum: "abc".to_string(),
962                size: 1000,
963                published_at: "2025-01-01".to_string(),
964                yanked: false,
965                min_mockforge_version: None,
966                dependencies: deps_map,
967                downloads: 0,
968            }],
969        );
970
971        resolver
972            .add_package_versions("pkg-b".to_string(), vec![create_version_entry("1.0.0", false)]);
973
974        let deps = vec![create_dependency("pkg-a", "^1.0")];
975        let root_version = Version::parse("1.0.0").unwrap();
976        let resolved = resolver.resolve("root", &root_version, deps).unwrap();
977
978        // Should resolve both pkg-a and pkg-b
979        assert_eq!(resolved.len(), 2);
980        let names: Vec<&str> = resolved.iter().map(|r| r.name.as_str()).collect();
981        assert!(names.contains(&"pkg-a"));
982        assert!(names.contains(&"pkg-b"));
983    }
984}