agpm_cli/version/
conflict.rs

1//! Version conflict detection and reporting.
2//!
3//! This module handles detection and reporting of version conflicts that can occur
4//! when multiple dependencies require incompatible versions of the same resource.
5//! It provides detailed conflict information to help users resolve dependency issues.
6
7use anyhow::Result;
8use semver::{Op, Version, VersionReq};
9use std::collections::{HashMap, HashSet};
10use std::fmt;
11
12use crate::core::AgpmError;
13use pubgrub::Ranges;
14
15/// Represents a version conflict between dependencies
16#[derive(Debug, Clone)]
17pub struct VersionConflict {
18    pub resource: String,
19    pub conflicting_requirements: Vec<ConflictingRequirement>,
20}
21
22#[derive(Debug, Clone)]
23pub struct ConflictingRequirement {
24    pub required_by: String,
25    pub requirement: String,
26    pub resolved_version: Option<Version>,
27}
28
29impl fmt::Display for VersionConflict {
30    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31        writeln!(f, "Version conflict for '{}':", self.resource)?;
32        for req in &self.conflicting_requirements {
33            writeln!(f, "  - {} requires {}", req.required_by, req.requirement)?;
34            if let Some(v) = &req.resolved_version {
35                writeln!(f, "    (resolved to {v})")?;
36            }
37        }
38        Ok(())
39    }
40}
41
42/// Detects and resolves version conflicts
43pub struct ConflictDetector {
44    requirements: HashMap<String, Vec<(String, String)>>, // resource -> [(requirer, requirement)]
45}
46
47impl Default for ConflictDetector {
48    fn default() -> Self {
49        Self::new()
50    }
51}
52
53impl ConflictDetector {
54    pub fn new() -> Self {
55        Self {
56            requirements: HashMap::new(),
57        }
58    }
59
60    /// Add a dependency requirement
61    pub fn add_requirement(&mut self, resource: &str, required_by: &str, requirement: &str) {
62        self.requirements
63            .entry(resource.to_string())
64            .or_default()
65            .push((required_by.to_string(), requirement.to_string()));
66    }
67
68    /// Detect conflicts in the current requirements
69    pub fn detect_conflicts(&self) -> Vec<VersionConflict> {
70        let mut conflicts = Vec::new();
71
72        for (resource, requirements) in &self.requirements {
73            if requirements.len() <= 1 {
74                continue; // No conflict possible with single requirement
75            }
76
77            // Check if requirements are compatible
78            let compatible = self.are_requirements_compatible(requirements);
79            if !compatible {
80                let conflict = VersionConflict {
81                    resource: resource.clone(),
82                    conflicting_requirements: requirements
83                        .iter()
84                        .map(|(requirer, req)| ConflictingRequirement {
85                            required_by: requirer.clone(),
86                            requirement: req.clone(),
87                            resolved_version: None,
88                        })
89                        .collect(),
90                };
91                conflicts.push(conflict);
92            }
93        }
94
95        conflicts
96    }
97
98    /// Check if a set of requirements are compatible
99    fn are_requirements_compatible(&self, requirements: &[(String, String)]) -> bool {
100        // Check for HEAD (unspecified version) mixed with specific versions
101        let has_head = requirements.iter().any(|(_, req)| req == "HEAD");
102        let has_specific = requirements.iter().any(|(_, req)| req != "HEAD");
103
104        if has_head && has_specific {
105            // HEAD mixed with specific versions is a conflict
106            return false;
107        }
108
109        // Parse all requirements (with v-prefix normalization)
110        let parsed_reqs: Vec<_> = requirements
111            .iter()
112            .filter_map(|(_, req)| {
113                if req == "*" {
114                    Some(VersionReq::parse("*").unwrap())
115                } else if req == "HEAD" {
116                    // HEAD is handled above
117                    None
118                } else {
119                    crate::version::parse_version_req(req).ok()
120                }
121            })
122            .collect();
123
124        if parsed_reqs.len() != requirements.len() {
125            // Some requirements couldn't be parsed as semver
126            // Check if we have BOTH semver and git refs - that's a conflict
127            let has_semver = !parsed_reqs.is_empty();
128            let has_git_refs = parsed_reqs.len() < requirements.len();
129
130            if has_semver && has_git_refs {
131                // Mixed semver and git refs - incompatible!
132                return false;
133            }
134
135            // All git refs - check if they're compatible
136            return self.check_git_ref_compatibility(requirements);
137        }
138
139        // All semver - check if ranges intersect
140        self.can_satisfy_all(&parsed_reqs)
141    }
142
143    /// Check if git references are compatible
144    ///
145    /// Normalizes refs to lowercase to handle case-insensitive filesystems (Windows, macOS)
146    /// where "main" and "Main" refer to the same branch.
147    fn check_git_ref_compatibility(&self, requirements: &[(String, String)]) -> bool {
148        let refs: HashSet<_> = requirements
149            .iter()
150            .filter_map(|(_, req)| {
151                if !req.starts_with('^')
152                    && !req.starts_with('~')
153                    && !req.starts_with('>')
154                    && !req.starts_with('<')
155                    && !req.starts_with('=')
156                    && req != "HEAD"
157                    && req != "*"
158                {
159                    // Normalize to lowercase for case-insensitive comparison
160                    // (handles "main" vs "Main" on case-insensitive filesystems)
161                    Some(req.to_lowercase())
162                } else {
163                    None
164                }
165            })
166            .collect();
167
168        // All git refs must be the same (after normalization)
169        refs.len() <= 1
170    }
171
172    /// Check if all requirements can be satisfied by some version
173    ///
174    /// Uses pubgrub's Ranges type for proper range intersection, avoiding heuristics.
175    fn can_satisfy_all(&self, requirements: &[VersionReq]) -> bool {
176        if requirements.is_empty() {
177            return true;
178        }
179
180        // Convert all VersionReq to Ranges and compute intersection
181        let mut intersection: Option<Ranges<Version>> = None;
182
183        for req in requirements {
184            let range = self.version_req_to_ranges(req);
185
186            intersection = match intersection {
187                None => Some(range),
188                Some(current) => Some(current.intersection(&range)),
189            };
190
191            // Early exit if intersection becomes empty
192            if let Some(ref i) = intersection
193                && i.is_empty()
194            {
195                return false;
196            }
197        }
198
199        // If we have a non-empty intersection, requirements are compatible
200        intersection.is_none_or(|i| !i.is_empty())
201    }
202
203    /// Convert a `semver::VersionReq` to `pubgrub::Ranges`<Version>
204    ///
205    /// In semver, multiple comparators in a single requirement are `ANDed` together (intersection),
206    /// not `ORed` (union). For example, ">=5.0.0, <6.0.0" means [5.0.0, 6.0.0), not the entire number line.
207    fn version_req_to_ranges(&self, req: &VersionReq) -> Ranges<Version> {
208        let comparators = &req.comparators;
209
210        // Wildcard: matches all versions
211        if comparators.is_empty() {
212            return Ranges::full();
213        }
214
215        // Start with full range and intersect each comparator (AND semantics)
216        let mut ranges = Ranges::full();
217
218        for comp in comparators {
219            // Build Version with prerelease and build metadata
220            let base_version = if comp.pre.is_empty() {
221                Version::new(comp.major, comp.minor.unwrap_or(0), comp.patch.unwrap_or(0))
222            } else {
223                Version {
224                    major: comp.major,
225                    minor: comp.minor.unwrap_or(0),
226                    patch: comp.patch.unwrap_or(0),
227                    pre: comp.pre.clone(),
228                    build: Default::default(),
229                }
230            };
231
232            let comp_range = match comp.op {
233                Op::Exact => {
234                    // =x.y.z → exactly that version
235                    Ranges::singleton(base_version)
236                }
237                Op::Greater => {
238                    // >x.y.z → strictly higher than x.y.z
239                    Ranges::strictly_higher_than(base_version)
240                }
241                Op::GreaterEq => {
242                    // >=x.y.z → x.y.z or higher
243                    Ranges::higher_than(base_version)
244                }
245                Op::Less => {
246                    // <x.y.z → strictly lower than x.y.z
247                    Ranges::strictly_lower_than(base_version)
248                }
249                Op::LessEq => {
250                    // <=x.y.z → x.y.z or lower
251                    Ranges::lower_than(base_version)
252                }
253                Op::Tilde => {
254                    // Tilde operator: allows patch updates
255                    // ~1.2.3 → >=1.2.3, <1.3.0
256                    // ~1.2 → >=1.2.0, <1.3.0
257                    // ~1 → >=1.0.0, <2.0.0 (allows minor and patch updates)
258                    let upper = if comp.minor.is_none() {
259                        // ~1 → [1.0.0, 2.0.0)
260                        Version::new(comp.major + 1, 0, 0)
261                    } else {
262                        // ~1.2 or ~1.2.3 → [1.2.x, 1.3.0)
263                        Version::new(comp.major, comp.minor.unwrap() + 1, 0)
264                    };
265                    Ranges::between(base_version, upper)
266                }
267                Op::Caret => {
268                    // Caret operator: compatible updates (no breaking changes)
269                    // ^1.2.3 → >=1.2.3, <2.0.0 (major != 0: allow minor and patch)
270                    // ^0.2.3 → >=0.2.3, <0.3.0 (major == 0: allow only patch)
271                    // ^0.0.3 → >=0.0.3, <0.0.4 (major == 0 && minor == 0: allow only exact patch)
272                    // ^0.0 → >=0.0.0, <0.1.0
273                    // ^0 → >=0.0.0, <1.0.0
274
275                    if base_version.major > 0 {
276                        // ^x.y.z (x>0) → [x.y.z, (x+1).0.0)
277                        let upper = Version::new(base_version.major + 1, 0, 0);
278                        Ranges::between(base_version, upper)
279                    } else if base_version.minor > 0 {
280                        // ^0.y.z (y>0) → [0.y.z, 0.(y+1).0)
281                        let upper = Version::new(0, base_version.minor + 1, 0);
282                        Ranges::between(base_version, upper)
283                    } else if comp.patch.is_some() && base_version.patch > 0 {
284                        // ^0.0.z (z>0) → [0.0.z, 0.0.(z+1))
285                        let upper = Version::new(0, 0, base_version.patch + 1);
286                        Ranges::between(base_version, upper)
287                    } else if comp.patch.is_none() && comp.minor.is_some() {
288                        // ^0.0 → [0.0.0, 0.1.0)
289                        let upper = Version::new(0, 1, 0);
290                        Ranges::between(base_version, upper)
291                    } else if comp.minor.is_none() {
292                        // ^0 → [0.0.0, 1.0.0)
293                        let upper = Version::new(1, 0, 0);
294                        Ranges::between(base_version, upper)
295                    } else {
296                        // ^0.0.0 → [0.0.0, 0.0.1)
297                        let upper = Version::new(0, 0, 1);
298                        Ranges::between(base_version, upper)
299                    }
300                }
301                Op::Wildcard => {
302                    // x.* or x.y.* → all versions in that major/minor
303                    if comp.minor.is_none() {
304                        // x.* → >=x.0.0, <(x+1).0.0
305                        let lower = Version::new(comp.major, 0, 0);
306                        let upper = Version::new(comp.major + 1, 0, 0);
307                        Ranges::between(lower, upper)
308                    } else if comp.patch.is_none() {
309                        // x.y.* → >=x.y.0, <x.(y+1).0
310                        let lower = Version::new(comp.major, comp.minor.unwrap(), 0);
311                        let upper = Version::new(comp.major, comp.minor.unwrap() + 1, 0);
312                        Ranges::between(lower, upper)
313                    } else {
314                        // Full version specified - shouldn't happen with Wildcard op
315                        Ranges::singleton(base_version)
316                    }
317                }
318                _ => {
319                    // Unknown operator - treat as full range (overly permissive)
320                    Ranges::full()
321                }
322            };
323
324            // Intersect with accumulated range (AND semantics)
325            ranges = ranges.intersection(&comp_range);
326        }
327
328        ranges
329    }
330
331    /// Try to resolve conflicts by finding compatible versions
332    pub fn resolve_conflicts(
333        &self,
334        available_versions: &HashMap<String, Vec<Version>>,
335    ) -> Result<HashMap<String, Version>> {
336        let mut resolved = HashMap::new();
337        let conflicts = self.detect_conflicts();
338
339        if !conflicts.is_empty() {
340            let conflict_messages: Vec<String> =
341                conflicts.iter().map(std::string::ToString::to_string).collect();
342
343            return Err(AgpmError::Other {
344                message: format!(
345                    "Unable to resolve version conflicts:\n{}",
346                    conflict_messages.join("\n")
347                ),
348            }
349            .into());
350        }
351
352        // Resolve each resource to its best version
353        for (resource, requirements) in &self.requirements {
354            let versions = available_versions.get(resource).ok_or_else(|| AgpmError::Other {
355                message: format!("No versions available for resource: {resource}"),
356            })?;
357
358            let best_version = self.find_best_version(versions, requirements)?;
359            resolved.insert(resource.clone(), best_version);
360        }
361
362        Ok(resolved)
363    }
364
365    /// Find the best version that satisfies all requirements
366    fn find_best_version(
367        &self,
368        available: &[Version],
369        requirements: &[(String, String)],
370    ) -> Result<Version> {
371        let mut candidates = available.to_vec();
372
373        // Filter by each requirement
374        for (_, req_str) in requirements {
375            if req_str == "latest" || req_str == "*" {
376                continue; // These match everything
377            }
378
379            if let Ok(req) = crate::version::parse_version_req(req_str) {
380                candidates.retain(|v| req.matches(v));
381            }
382        }
383
384        if candidates.is_empty() {
385            return Err(AgpmError::Other {
386                message: format!("No version satisfies all requirements: {requirements:?}"),
387            }
388            .into());
389        }
390
391        // Sort and return the highest version
392        candidates.sort_by(|a, b| b.cmp(a));
393        Ok(candidates[0].clone())
394    }
395}
396
397/// Analyzes dependency graphs for circular dependencies
398pub struct CircularDependencyDetector {
399    graph: HashMap<String, HashSet<String>>,
400}
401
402impl Default for CircularDependencyDetector {
403    fn default() -> Self {
404        Self::new()
405    }
406}
407
408impl CircularDependencyDetector {
409    pub fn new() -> Self {
410        Self {
411            graph: HashMap::new(),
412        }
413    }
414
415    /// Add a dependency edge
416    pub fn add_dependency(&mut self, from: &str, to: &str) {
417        self.graph.entry(from.to_string()).or_default().insert(to.to_string());
418    }
419
420    /// Detect circular dependencies
421    pub fn detect_cycles(&self) -> Vec<Vec<String>> {
422        let mut cycles = Vec::new();
423        let mut visited = HashSet::new();
424        let mut rec_stack = HashSet::new();
425        let mut path = Vec::new();
426
427        for node in self.graph.keys() {
428            if !visited.contains(node) {
429                self.dfs_detect_cycle(node, &mut visited, &mut rec_stack, &mut path, &mut cycles);
430            }
431        }
432
433        cycles
434    }
435
436    fn dfs_detect_cycle(
437        &self,
438        node: &str,
439        visited: &mut HashSet<String>,
440        rec_stack: &mut HashSet<String>,
441        path: &mut Vec<String>,
442        cycles: &mut Vec<Vec<String>>,
443    ) {
444        visited.insert(node.to_string());
445        rec_stack.insert(node.to_string());
446        path.push(node.to_string());
447
448        if let Some(neighbors) = self.graph.get(node) {
449            for neighbor in neighbors {
450                if !visited.contains(neighbor) {
451                    self.dfs_detect_cycle(neighbor, visited, rec_stack, path, cycles);
452                } else if rec_stack.contains(neighbor) {
453                    // Found a cycle
454                    let cycle_start = path.iter().position(|n| n == neighbor).unwrap();
455                    let cycle = path[cycle_start..].to_vec();
456                    cycles.push(cycle);
457                }
458            }
459        }
460
461        path.pop();
462        rec_stack.remove(node);
463    }
464}
465
466#[cfg(test)]
467mod tests {
468    use super::*;
469
470    #[test]
471    fn test_conflict_detection() {
472        let mut detector = ConflictDetector::new();
473
474        // Add compatible requirements
475        detector.add_requirement("lib1", "app1", "^1.0.0");
476        detector.add_requirement("lib1", "app2", "^1.2.0");
477
478        let conflicts = detector.detect_conflicts();
479        assert_eq!(conflicts.len(), 0); // These are compatible
480
481        // Add incompatible requirements
482        detector.add_requirement("lib2", "app1", "^1.0.0");
483        detector.add_requirement("lib2", "app2", "^2.0.0");
484
485        let conflicts = detector.detect_conflicts();
486        assert_eq!(conflicts.len(), 1);
487        assert_eq!(conflicts[0].resource, "lib2");
488    }
489
490    #[test]
491    fn test_git_ref_compatibility() {
492        let mut detector = ConflictDetector::new();
493
494        // Same git ref - compatible
495        detector.add_requirement("lib1", "app1", "main");
496        detector.add_requirement("lib1", "app2", "main");
497
498        let conflicts = detector.detect_conflicts();
499        assert_eq!(conflicts.len(), 0);
500
501        // Different git refs - incompatible
502        detector.add_requirement("lib2", "app1", "main");
503        detector.add_requirement("lib2", "app2", "develop");
504
505        let conflicts = detector.detect_conflicts();
506        assert_eq!(conflicts.len(), 1);
507    }
508
509    #[test]
510    fn test_git_ref_case_insensitive() {
511        let mut detector = ConflictDetector::new();
512
513        // Git refs differing only by case should be treated as the same
514        // (important for case-insensitive filesystems like Windows and macOS)
515        detector.add_requirement("lib1", "app1", "main");
516        detector.add_requirement("lib1", "app2", "Main");
517        detector.add_requirement("lib1", "app3", "MAIN");
518
519        let conflicts = detector.detect_conflicts();
520        assert_eq!(
521            conflicts.len(),
522            0,
523            "Git refs differing only by case should be compatible (case-insensitive filesystems)"
524        );
525
526        // Mixed case with different branch names should still conflict
527        let mut detector2 = ConflictDetector::new();
528        detector2.add_requirement("lib2", "app1", "Main");
529        detector2.add_requirement("lib2", "app2", "Develop");
530
531        let conflicts2 = detector2.detect_conflicts();
532        assert_eq!(
533            conflicts2.len(),
534            1,
535            "Different branch names should conflict regardless of case"
536        );
537    }
538
539    #[test]
540    fn test_resolve_conflicts() {
541        let mut detector = ConflictDetector::new();
542        detector.add_requirement("lib1", "app1", "^1.0.0");
543        detector.add_requirement("lib1", "app2", "^1.2.0");
544
545        let mut available = HashMap::new();
546        available.insert(
547            "lib1".to_string(),
548            vec![
549                Version::parse("1.0.0").unwrap(),
550                Version::parse("1.2.0").unwrap(),
551                Version::parse("1.5.0").unwrap(),
552                Version::parse("2.0.0").unwrap(),
553            ],
554        );
555
556        let resolved = detector.resolve_conflicts(&available).unwrap();
557        assert_eq!(resolved.get("lib1"), Some(&Version::parse("1.5.0").unwrap()));
558    }
559
560    #[test]
561    fn test_circular_dependency_detection() {
562        let mut detector = CircularDependencyDetector::new();
563
564        // Create a cycle: A -> B -> C -> A
565        detector.add_dependency("A", "B");
566        detector.add_dependency("B", "C");
567        detector.add_dependency("C", "A");
568
569        let cycles = detector.detect_cycles();
570        assert_eq!(cycles.len(), 1);
571        assert!(cycles[0].contains(&"A".to_string()));
572        assert!(cycles[0].contains(&"B".to_string()));
573        assert!(cycles[0].contains(&"C".to_string()));
574    }
575
576    #[test]
577    fn test_no_circular_dependencies() {
578        let mut detector = CircularDependencyDetector::new();
579
580        // Create a DAG: A -> B -> C
581        detector.add_dependency("A", "B");
582        detector.add_dependency("B", "C");
583        detector.add_dependency("A", "C");
584
585        let cycles = detector.detect_cycles();
586        assert_eq!(cycles.len(), 0);
587    }
588
589    #[test]
590    fn test_conflict_display() {
591        let conflict = VersionConflict {
592            resource: "test-lib".to_string(),
593            conflicting_requirements: vec![
594                ConflictingRequirement {
595                    required_by: "app1".to_string(),
596                    requirement: "^1.0.0".to_string(),
597                    resolved_version: Some(Version::parse("1.5.0").unwrap()),
598                },
599                ConflictingRequirement {
600                    required_by: "app2".to_string(),
601                    requirement: "^2.0.0".to_string(),
602                    resolved_version: None,
603                },
604            ],
605        };
606
607        let display = format!("{}", conflict);
608        assert!(display.contains("test-lib"));
609        assert!(display.contains("app1"));
610        assert!(display.contains("^1.0.0"));
611        assert!(display.contains("1.5.0"));
612    }
613
614    #[test]
615    fn test_head_with_specific_version_conflict() {
616        let mut detector = ConflictDetector::new();
617
618        // HEAD (unspecified) mixed with specific version should conflict
619        detector.add_requirement("lib1", "app1", "HEAD");
620        detector.add_requirement("lib1", "app2", "^1.0.0");
621
622        let conflicts = detector.detect_conflicts();
623        assert_eq!(conflicts.len(), 1, "HEAD mixed with specific version should conflict");
624
625        // "*" with any specific range is compatible (intersection is non-empty)
626        let mut detector2 = ConflictDetector::new();
627        detector2.add_requirement("lib2", "app1", "*");
628        detector2.add_requirement("lib2", "app2", "^1.0.0");
629
630        let conflicts = detector2.detect_conflicts();
631        assert_eq!(
632            conflicts.len(),
633            0,
634            "* should be compatible with ^1.0.0 (intersection is [1.0.0, 2.0.0))"
635        );
636
637        // "*" with ~2.1.0 is also compatible (intersection is [2.1.0, 2.2.0))
638        let mut detector3 = ConflictDetector::new();
639        detector3.add_requirement("lib3", "app1", "*");
640        detector3.add_requirement("lib3", "app2", "~2.1.0");
641
642        let conflicts = detector3.detect_conflicts();
643        assert_eq!(
644            conflicts.len(),
645            0,
646            "* should be compatible with ~2.1.0 (intersection is [2.1.0, 2.2.0))"
647        );
648    }
649
650    #[test]
651    fn test_mixed_semver_and_git_refs() {
652        let mut detector = ConflictDetector::new();
653
654        // Mix of semver and git branch - should be incompatible
655        detector.add_requirement("lib1", "app1", "^1.0.0");
656        detector.add_requirement("lib1", "app2", "main");
657
658        let conflicts = detector.detect_conflicts();
659        assert_eq!(conflicts.len(), 1, "Mixed semver and git ref should be detected as conflict");
660
661        // Test with exact version and git tag
662        let mut detector2 = ConflictDetector::new();
663        detector2.add_requirement("lib2", "app1", "v1.0.0");
664        detector2.add_requirement("lib2", "app2", "develop");
665
666        let conflicts2 = detector2.detect_conflicts();
667        assert_eq!(conflicts2.len(), 1, "Exact version with git branch should conflict");
668    }
669
670    #[test]
671    fn test_duplicate_requirements_same_version() {
672        let mut detector = ConflictDetector::new();
673
674        // Multiple resources requiring the same exact version
675        detector.add_requirement("lib1", "app1", "v1.0.0");
676        detector.add_requirement("lib1", "app2", "v1.0.0");
677        detector.add_requirement("lib1", "app3", "v1.0.0");
678
679        let conflicts = detector.detect_conflicts();
680        assert_eq!(conflicts.len(), 0, "Same version requirements should not conflict");
681    }
682
683    #[test]
684    fn test_exact_version_conflicts() {
685        let mut detector = ConflictDetector::new();
686
687        // Different exact versions - definitely incompatible
688        detector.add_requirement("lib1", "app1", "v1.0.0");
689        detector.add_requirement("lib1", "app2", "v2.0.0");
690
691        let conflicts = detector.detect_conflicts();
692        assert_eq!(conflicts.len(), 1, "Different exact versions must conflict");
693        assert_eq!(conflicts[0].conflicting_requirements.len(), 2);
694    }
695
696    #[test]
697    fn test_resolve_conflicts_missing_resource() {
698        let mut detector = ConflictDetector::new();
699        detector.add_requirement("lib1", "app1", "^1.0.0");
700
701        let available = HashMap::new(); // Empty - missing lib1
702
703        let result = detector.resolve_conflicts(&available);
704        assert!(result.is_err(), "Should error when resource not in available versions");
705        let err_msg = result.unwrap_err().to_string();
706        assert!(err_msg.contains("No versions available"), "Error should mention missing versions");
707    }
708
709    #[test]
710    fn test_resolve_conflicts_with_incompatible_ranges() {
711        let mut detector = ConflictDetector::new();
712        detector.add_requirement("lib1", "app1", "^1.0.0");
713        detector.add_requirement("lib1", "app2", "^2.0.0");
714
715        let mut available = HashMap::new();
716        available.insert(
717            "lib1".to_string(),
718            vec![Version::parse("1.5.0").unwrap(), Version::parse("2.3.0").unwrap()],
719        );
720
721        let result = detector.resolve_conflicts(&available);
722        assert!(result.is_err(), "Should error when requirements are incompatible");
723        let err_msg = result.unwrap_err().to_string();
724        assert!(
725            err_msg.contains("Unable to resolve version conflicts"),
726            "Error should mention conflict resolution failure"
727        );
728    }
729
730    #[test]
731    fn test_resolve_conflicts_no_matching_version() {
732        let mut detector = ConflictDetector::new();
733        detector.add_requirement("lib1", "app1", "^3.0.0"); // Requires 3.x
734
735        let mut available = HashMap::new();
736        available.insert(
737            "lib1".to_string(),
738            vec![Version::parse("1.0.0").unwrap(), Version::parse("2.0.0").unwrap()],
739        );
740
741        let result = detector.resolve_conflicts(&available);
742        assert!(result.is_err(), "Should error when no version satisfies requirement");
743        let err_msg = result.unwrap_err().to_string();
744        assert!(
745            err_msg.contains("No version satisfies"),
746            "Error should mention no matching version: {}",
747            err_msg
748        );
749    }
750
751    #[test]
752    fn test_conflict_aggregated_error_message() {
753        let mut detector = ConflictDetector::new();
754        detector.add_requirement("lib1", "app1", "^1.0.0");
755        detector.add_requirement("lib1", "app2", "^2.0.0");
756        detector.add_requirement("lib2", "app1", "main");
757        detector.add_requirement("lib2", "app3", "develop");
758
759        let conflicts = detector.detect_conflicts();
760        assert_eq!(conflicts.len(), 2, "Should detect both conflicts");
761
762        // Verify the conflicts contain proper information
763        let lib1_conflict = conflicts.iter().find(|c| c.resource == "lib1");
764        assert!(lib1_conflict.is_some(), "Should have lib1 conflict");
765        assert_eq!(
766            lib1_conflict.unwrap().conflicting_requirements.len(),
767            2,
768            "lib1 should have 2 conflicting requirements"
769        );
770
771        let lib2_conflict = conflicts.iter().find(|c| c.resource == "lib2");
772        assert!(lib2_conflict.is_some(), "Should have lib2 conflict");
773        assert_eq!(
774            lib2_conflict.unwrap().conflicting_requirements.len(),
775            2,
776            "lib2 should have 2 conflicting requirements"
777        );
778    }
779
780    #[test]
781    fn test_multi_comparator_compatible() {
782        let mut detector = ConflictDetector::new();
783
784        // ">=5.0.0, <6.0.0" should be compatible with ">=5.5.0"
785        // Intersection is [5.5.0, 6.0.0)
786        detector.add_requirement("lib1", "app1", ">=5.0.0, <6.0.0");
787        detector.add_requirement("lib1", "app2", ">=5.5.0");
788
789        let conflicts = detector.detect_conflicts();
790        assert_eq!(
791            conflicts.len(),
792            0,
793            "Multi-comparator ranges with non-empty intersection should be compatible"
794        );
795    }
796
797    #[test]
798    fn test_multi_comparator_incompatible() {
799        let mut detector = ConflictDetector::new();
800
801        // ">=5.0.0, <6.0.0" should conflict with ">=7.0.0"
802        // Intersection is empty
803        detector.add_requirement("lib1", "app1", ">=5.0.0, <6.0.0");
804        detector.add_requirement("lib1", "app2", ">=7.0.0");
805
806        let conflicts = detector.detect_conflicts();
807        assert_eq!(
808            conflicts.len(),
809            1,
810            "Multi-comparator ranges with empty intersection should conflict"
811        );
812    }
813
814    #[test]
815    fn test_tilde_operator_variants() {
816        let mut detector1 = ConflictDetector::new();
817
818        // ~1 means [1.0.0, 2.0.0) - should be compatible with ^1.5.0 [1.5.0, 2.0.0)
819        detector1.add_requirement("lib1", "app1", "~1");
820        detector1.add_requirement("lib1", "app2", "^1.5.0");
821
822        let conflicts1 = detector1.detect_conflicts();
823        assert_eq!(
824            conflicts1.len(),
825            0,
826            "~1 should be compatible with ^1.5.0 (intersection is [1.5.0, 2.0.0))"
827        );
828
829        let mut detector2 = ConflictDetector::new();
830
831        // ~1.2 means [1.2.0, 1.3.0) - should conflict with ^1.5.0 [1.5.0, 2.0.0)
832        detector2.add_requirement("lib2", "app1", "~1.2");
833        detector2.add_requirement("lib2", "app2", "^1.5.0");
834
835        let conflicts2 = detector2.detect_conflicts();
836        assert_eq!(conflicts2.len(), 1, "~1.2 should conflict with ^1.5.0 (disjoint ranges)");
837
838        let mut detector3 = ConflictDetector::new();
839
840        // ~1.2.3 means [1.2.3, 1.3.0) - should be compatible with >=1.2.0
841        detector3.add_requirement("lib3", "app1", "~1.2.3");
842        detector3.add_requirement("lib3", "app2", ">=1.2.0");
843
844        let conflicts3 = detector3.detect_conflicts();
845        assert_eq!(conflicts3.len(), 0, "~1.2.3 should be compatible with >=1.2.0");
846    }
847
848    #[test]
849    fn test_caret_zero_zero_patch() {
850        let mut detector1 = ConflictDetector::new();
851
852        // ^0.0.3 means [0.0.3, 0.0.4) - should be compatible with >=0.0.3, <0.0.5
853        detector1.add_requirement("lib1", "app1", "^0.0.3");
854        detector1.add_requirement("lib1", "app2", ">=0.0.3, <0.0.5");
855
856        let conflicts1 = detector1.detect_conflicts();
857        assert_eq!(
858            conflicts1.len(),
859            0,
860            "^0.0.3 should be compatible with >=0.0.3, <0.0.5 (intersection is [0.0.3, 0.0.4))"
861        );
862
863        let mut detector2 = ConflictDetector::new();
864
865        // ^0.0.3 means [0.0.3, 0.0.4) - should conflict with ^0.0.5 [0.0.5, 0.0.6)
866        detector2.add_requirement("lib2", "app1", "^0.0.3");
867        detector2.add_requirement("lib2", "app2", "^0.0.5");
868
869        let conflicts2 = detector2.detect_conflicts();
870        assert_eq!(conflicts2.len(), 1, "^0.0.3 should conflict with ^0.0.5 (disjoint ranges)");
871    }
872
873    #[test]
874    fn test_caret_zero_variants() {
875        let mut detector1 = ConflictDetector::new();
876
877        // ^0 means [0.0.0, 1.0.0) - should be compatible with ^0.5.0 [0.5.0, 0.6.0)
878        detector1.add_requirement("lib1", "app1", "^0");
879        detector1.add_requirement("lib1", "app2", "^0.5.0");
880
881        let conflicts1 = detector1.detect_conflicts();
882        assert_eq!(
883            conflicts1.len(),
884            0,
885            "^0 should be compatible with ^0.5.0 (intersection is [0.5.0, 0.6.0))"
886        );
887
888        let mut detector2 = ConflictDetector::new();
889
890        // ^0.0 means [0.0.0, 0.1.0) - should conflict with ^0.5.0 [0.5.0, 0.6.0)
891        detector2.add_requirement("lib2", "app1", "^0.0");
892        detector2.add_requirement("lib2", "app2", "^0.5.0");
893
894        let conflicts2 = detector2.detect_conflicts();
895        assert_eq!(conflicts2.len(), 1, "^0.0 should conflict with ^0.5.0 (disjoint ranges)");
896    }
897
898    #[test]
899    fn test_prerelease_versions() {
900        let mut detector1 = ConflictDetector::new();
901
902        // =1.0.0-beta.1 should conflict with =1.0.0 (different versions)
903        detector1.add_requirement("lib1", "app1", "=1.0.0-beta.1");
904        detector1.add_requirement("lib1", "app2", "=1.0.0");
905
906        let conflicts1 = detector1.detect_conflicts();
907        assert_eq!(
908            conflicts1.len(),
909            1,
910            "=1.0.0-beta.1 should conflict with =1.0.0 (different prerelease)"
911        );
912
913        let mut detector2 = ConflictDetector::new();
914
915        // =1.0.0-beta.1 should be compatible with itself
916        detector2.add_requirement("lib2", "app1", "=1.0.0-beta.1");
917        detector2.add_requirement("lib2", "app2", "=1.0.0-beta.1");
918
919        let conflicts2 = detector2.detect_conflicts();
920        assert_eq!(conflicts2.len(), 0, "Same prerelease version should be compatible");
921
922        let mut detector3 = ConflictDetector::new();
923
924        // >=1.0.0-beta should be compatible with >=1.0.0-alpha (intersection exists)
925        detector3.add_requirement("lib3", "app1", ">=1.0.0-beta");
926        detector3.add_requirement("lib3", "app2", ">=1.0.0-alpha");
927
928        let conflicts3 = detector3.detect_conflicts();
929        assert_eq!(conflicts3.len(), 0, ">=1.0.0-beta should be compatible with >=1.0.0-alpha");
930    }
931
932    #[test]
933    fn test_high_version_ranges() {
934        let mut detector = ConflictDetector::new();
935
936        // Test ranges well above typical test versions (>3.0.0)
937        detector.add_requirement("lib1", "app1", ">=5.0.0, <10.0.0");
938        detector.add_requirement("lib1", "app2", "^7.5.0");
939
940        let conflicts = detector.detect_conflicts();
941        assert_eq!(
942            conflicts.len(),
943            0,
944            "High version ranges should work correctly (intersection is [7.5.0, 8.0.0))"
945        );
946
947        let mut detector2 = ConflictDetector::new();
948
949        // Test conflicting high version ranges
950        detector2.add_requirement("lib2", "app1", ">=100.0.0");
951        detector2.add_requirement("lib2", "app2", "<50.0.0");
952
953        let conflicts2 = detector2.detect_conflicts();
954        assert_eq!(conflicts2.len(), 1, "Disjoint high version ranges should conflict");
955    }
956}