agpm_cli/version/
constraints.rs

1//! Version constraint parsing and resolution for AGPM dependencies.
2//!
3//! This module provides comprehensive version constraint handling for AGPM dependencies,
4//! supporting semantic versioning, Git references, and various constraint types. It enables
5//! dependency resolution with conflict detection and version matching.
6//!
7//! # Version Constraint Types
8//!
9//! AGPM supports several types of version constraints:
10//!
11//! - **Exact versions**: `"1.0.0"` - Matches exactly the specified version
12//! - **Semantic version ranges**: `"^1.0.0"`, `"~1.2.0"`, `">=1.0.0"` - Uses semver ranges
13//! - **Git references**: `"main"`, `"feature/branch"`, `"abc123"`, `"latest"` - Git branches, tags, or commits
14//!
15//! # Constraint Resolution
16//!
17//! The constraint system provides:
18//! - **Conflict detection**: Prevents incompatible constraints for the same dependency
19//! - **Version resolution**: Finds best matching versions from available options
20//! - **Prerelease handling**: Manages alpha, beta, RC versions appropriately
21//! - **Precedence rules**: Resolves multiple constraints consistently
22//!
23//! # Examples
24//!
25//! ## Basic Constraint Parsing
26//!
27//! ```rust,no_run
28//! use agpm_cli::version::constraints::VersionConstraint;
29//!
30//! // Parse different constraint types
31//! let exact = VersionConstraint::parse("1.0.0")?;
32//! let caret = VersionConstraint::parse("^1.0.0")?;
33//! let latest = VersionConstraint::parse("latest")?;
34//! let branch = VersionConstraint::parse("main")?;
35//! # Ok::<(), anyhow::Error>(())
36//! ```
37//!
38//! ## Constraint Set Management
39//!
40//! ```rust,no_run
41//! use agpm_cli::version::constraints::{ConstraintSet, VersionConstraint};
42//! use semver::Version;
43//!
44//! let mut set = ConstraintSet::new();
45//! set.add(VersionConstraint::parse(">=1.0.0")?)?;
46//! set.add(VersionConstraint::parse("<2.0.0")?)?;
47//!
48//! let versions = vec![
49//!     Version::parse("0.9.0")?,
50//!     Version::parse("1.5.0")?,
51//!     Version::parse("2.0.0")?,
52//! ];
53//!
54//! let best = set.find_best_match(&versions).unwrap();
55//! assert_eq!(best, &Version::parse("1.5.0")?);
56//! # Ok::<(), anyhow::Error>(())
57//! ```
58//!
59//! ## Dependency Resolution
60//!
61//! ```rust,no_run
62//! use agpm_cli::version::constraints::ConstraintResolver;
63//! use semver::Version;
64//! use std::collections::HashMap;
65//!
66//! let mut resolver = ConstraintResolver::new();
67//! resolver.add_constraint("dep1", "^1.0.0")?;
68//! resolver.add_constraint("dep2", "~2.1.0")?;
69//!
70//! let mut available = HashMap::new();
71//! available.insert("dep1".to_string(), vec![Version::parse("1.5.0")?]);
72//! available.insert("dep2".to_string(), vec![Version::parse("2.1.3")?]);
73//!
74//! let resolved = resolver.resolve(&available)?;
75//! # Ok::<(), anyhow::Error>(())
76//! ```
77//!
78//! # Constraint Syntax Reference
79//!
80//! | Syntax | Description | Example |
81//! |--------|-------------|----------|
82//! | `1.0.0` | Exact version | `"1.0.0"` |
83//! | `^1.0.0` | Compatible within major version | `"^1.0.0"` matches `1.x.x` |
84//! | `~1.2.0` | Compatible within minor version | `"~1.2.0"` matches `1.2.x` |
85//! | `>=1.0.0` | Greater than or equal | `">=1.0.0"` |
86//! | `<2.0.0` | Less than | `"<2.0.0"` |
87//! | `>=1.0.0, <2.0.0` | Range constraint | Multiple constraints |
88//! | `main` | Git branch reference | Branch name |
89//! | `latest` | Git tag or branch name | Just a regular ref |
90//! | `v1.0.0` | Git tag reference | Tag name |
91//! | `abc123` | Git commit reference | Commit hash (full or abbreviated) |
92//!
93//! # Version Resolution Precedence
94//!
95//! When resolving versions, AGPM follows this precedence:
96//!
97//! 1. **Exact matches** take highest priority
98//! 2. **Semantic version requirements** are resolved to highest compatible version
99//! 3. **Stable versions** are preferred over prereleases (unless explicitly allowed)
100//! 4. **Newer versions** are preferred when multiple versions satisfy constraints
101//! 5. **Git references** bypass semantic versioning and use exact ref matching
102//!
103//! # Prerelease Version Handling
104//!
105//! - **Default behavior**: Prereleases (alpha, beta, RC) are excluded from resolution
106//! - **Explicit inclusion**: Use Git references to include prereleases
107//! - **Version ranges**: Prereleases only match if explicitly specified in range
108//! - **Constraint sets**: If any constraint allows prereleases, the entire set does
109//!
110//! # Error Conditions
111//!
112//! The constraint system handles these error conditions:
113//! - **Conflicting constraints**: Same dependency with incompatible requirements
114//! - **Invalid version strings**: Malformed semantic version specifications
115//! - **Resolution failures**: No available version satisfies all constraints
116//! - **Missing dependencies**: Required dependency not found in available versions
117
118use anyhow::Result;
119use semver::{Version, VersionReq};
120use std::collections::HashMap;
121use std::fmt;
122
123use crate::core::AgpmError;
124
125/// A version constraint that defines acceptable versions for a dependency.
126///
127/// Version constraints in AGPM support multiple formats to handle different
128/// versioning strategies and Git-based dependencies. Each constraint type
129/// provides specific matching behavior for version resolution.
130///
131/// # Constraint Types
132///
133/// - [`Exact`](Self::Exact): Matches exactly one specific semantic version
134/// - [`Requirement`](Self::Requirement): Matches versions using semver ranges
135/// - [`GitRef`](Self::GitRef): Matches specific Git branches, tags, or commit hashes (including "latest")
136///
137/// # Examples
138///
139/// ```rust,no_run
140/// use agpm_cli::version::constraints::VersionConstraint;
141/// use semver::Version;
142///
143/// // Parse various constraint formats
144/// let exact = VersionConstraint::parse("1.0.0")?;
145/// let caret = VersionConstraint::parse("^1.0.0")?; // Compatible versions
146/// let tilde = VersionConstraint::parse("~1.2.0")?; // Patch-level compatible
147/// let range = VersionConstraint::parse(">=1.0.0, <2.0.0")?; // Version range
148/// let branch = VersionConstraint::parse("main")?;
149/// let latest_tag = VersionConstraint::parse("latest")?; // Just a tag name
150/// let commit = VersionConstraint::parse("abc123def")?;
151///
152/// // Test version matching
153/// let version = Version::parse("1.2.3")?;
154/// assert!(caret.matches(&version));
155/// # Ok::<(), anyhow::Error>(())
156/// ```
157///
158/// # Prerelease Handling
159///
160/// By default, most constraints exclude prerelease versions to ensure stability:
161/// - `GitRef` constraints (including "latest" tag names) may reference any commit
162///
163/// # Git Reference Matching
164///
165/// Git references are matched by name rather than semantic version:
166/// - Branch names: `"main"`, `"develop"`, `"feature/auth"`
167/// - Tag names: `"v1.0.0"`, `"release-2023-01"`
168/// - Commit hashes: `"abc123def456"` (full or abbreviated)
169///
170/// # Prefix Support (Monorepo Versioning)
171///
172/// Constraints can include optional prefixes for monorepo-style versioning:
173/// - `"agents-v1.0.0"`: Exact version with prefix
174/// - `"agents-^v1.0.0"`: Compatible version range with prefix
175/// - Prefixed constraints only match tags with the same prefix
176#[derive(Debug, Clone)]
177pub enum VersionConstraint {
178    /// Exact version match with optional prefix (e.g., "1.0.0", "agents-v1.0.0")
179    Exact {
180        prefix: Option<String>,
181        version: Version,
182    },
183
184    /// Semantic version requirement with optional prefix (e.g., "^1.0.0", "agents-^v1.0.0")
185    Requirement {
186        prefix: Option<String>,
187        req: VersionReq,
188    },
189
190    /// Git tag or branch name (including "latest" - it's just a tag name)
191    GitRef(String),
192}
193
194impl VersionConstraint {
195    /// Parse a constraint string into a [`VersionConstraint`].
196    ///
197    /// This method intelligently determines the constraint type based on the input format.
198    /// It handles various syntaxes including semantic versions, version ranges, special
199    /// keywords, and Git references.
200    ///
201    /// # Parsing Logic
202    ///
203    /// 1. **Special keywords**: `"*"` (wildcard for any version)
204    /// 2. **Exact versions**: `"1.0.0"`, `"v1.0.0"` (without range operators)
205    /// 3. **Version requirements**: `"^1.0.0"`, `"~1.2.0"`, `">=1.0.0"`, `"<2.0.0"`
206    /// 4. **Git references**: Any string that doesn't match the above patterns (including "latest")
207    ///
208    /// # Arguments
209    ///
210    /// * `constraint` - The constraint string to parse (whitespace is trimmed)
211    ///
212    /// # Returns
213    ///
214    /// Returns `Ok(VersionConstraint)` on successful parsing, or `Err` if the
215    /// semantic version parsing fails (Git references always succeed).
216    ///
217    /// # Examples
218    ///
219    /// ```rust,no_run
220    /// use agpm_cli::version::constraints::VersionConstraint;
221    ///
222    /// // Exact version matching
223    /// let exact = VersionConstraint::parse("1.0.0")?;
224    /// let exact_with_v = VersionConstraint::parse("v1.0.0")?;
225    ///
226    /// // Semantic version ranges
227    /// let caret = VersionConstraint::parse("^1.0.0")?;      // 1.x.x compatible
228    /// let tilde = VersionConstraint::parse("~1.2.0")?;      // 1.2.x compatible
229    /// let gte = VersionConstraint::parse(">=1.0.0")?;       // Greater or equal
230    /// let range = VersionConstraint::parse(">1.0.0, <2.0.0")?; // Range
231    ///
232    /// // Special keywords
233    /// let any = VersionConstraint::parse("*")?;             // Any version
234    ///
235    /// // Git references
236    /// let branch = VersionConstraint::parse("main")?;       // Branch name
237    /// let tag = VersionConstraint::parse("release-v1")?;    // Tag name
238    /// let latest = VersionConstraint::parse("latest")?;     // Just a tag/branch name
239    /// let commit = VersionConstraint::parse("abc123def")?;  // Commit hash
240    /// # Ok::<(), anyhow::Error>(())
241    /// ```
242    ///
243    /// # Error Handling
244    ///
245    /// This method only returns errors for malformed semantic version strings.
246    /// Git references and special keywords always parse successfully.
247    pub fn parse(constraint: &str) -> Result<Self> {
248        let trimmed = constraint.trim();
249
250        // Extract prefix from constraint first (e.g., "agents-^v1.0.0" → (Some("agents"), "^v1.0.0"))
251        let (prefix, version_str) = crate::version::split_prefix_and_version(trimmed);
252
253        // Check for wildcard in the version portion (supports both "*" and "agents-*")
254        if version_str == "*" {
255            // Wildcard means any version - treat as a GitRef that matches everything
256            return Ok(Self::GitRef(trimmed.to_string()));
257        }
258
259        // Try to parse as exact version (with or without 'v' prefix)
260        let cleaned_version_str = version_str.strip_prefix('v').unwrap_or(version_str);
261        if let Ok(version) = Version::parse(cleaned_version_str) {
262            // Check if it's a range operator
263            if !version_str.starts_with('^')
264                && !version_str.starts_with('~')
265                && !version_str.starts_with('>')
266                && !version_str.starts_with('<')
267                && !version_str.starts_with('=')
268            {
269                return Ok(Self::Exact {
270                    prefix,
271                    version,
272                });
273            }
274        }
275
276        // Try to parse as version requirement (with v-prefix normalization)
277        match crate::version::parse_version_req(version_str) {
278            Ok(req) => {
279                return Ok(Self::Requirement {
280                    prefix,
281                    req,
282                });
283            }
284            Err(e) => {
285                // If it looks like a semver constraint but failed to parse, return error
286                if version_str.starts_with('^')
287                    || version_str.starts_with('~')
288                    || version_str.starts_with('=')
289                    || version_str.starts_with('>')
290                    || version_str.starts_with('<')
291                {
292                    return Err(anyhow::anyhow!("Invalid semver constraint '{trimmed}': {e}"));
293                }
294                // Otherwise it might be a git ref, continue
295            }
296        }
297
298        // Otherwise treat as git ref
299        Ok(Self::GitRef(trimmed.to_string()))
300    }
301
302    /// Check if a semantic version satisfies this constraint.
303    ///
304    /// This method tests whether a given semantic version matches the requirements
305    /// of this constraint. Different constraint types use different matching logic:
306    ///
307    /// - **Exact**: Version must match exactly
308    /// - **Requirement**: Version must satisfy the semver range
309    /// - **`GitRef`**: Never matches semantic versions (Git refs are matched separately)
310    ///
311    /// # Arguments
312    ///
313    /// * `version` - The semantic version to test against this constraint
314    ///
315    /// # Returns
316    ///
317    /// Returns `true` if the version satisfies the constraint, `false` otherwise.
318    ///
319    /// # Examples
320    ///
321    /// ```rust,no_run
322    /// use agpm_cli::version::constraints::VersionConstraint;
323    /// use semver::Version;
324    ///
325    /// let constraint = VersionConstraint::parse("^1.0.0")?;
326    /// let version = Version::parse("1.2.3")?;
327    ///
328    /// assert!(constraint.matches(&version)); // 1.2.3 is compatible with ^1.0.0
329    /// # Ok::<(), anyhow::Error>(())
330    /// ```
331    ///
332    /// # Note
333    ///
334    /// Git reference constraints always return `false` for this method since they
335    /// operate on Git refs rather than semantic versions. Use [`matches_ref`](Self::matches_ref)
336    /// to test Git reference matching.
337    #[must_use]
338    pub fn matches(&self, version: &Version) -> bool {
339        match self {
340            Self::Exact {
341                version: v,
342                ..
343            } => v == version,
344            Self::Requirement {
345                req,
346                ..
347            } => req.matches(version),
348            Self::GitRef(_) => false, // Git refs don't match semver versions
349        }
350    }
351
352    /// Check if a Git reference satisfies this constraint.
353    ///
354    /// This method tests whether a Git reference (branch, tag, or commit hash)
355    /// matches a Git reference constraint. Only [`GitRef`](Self::GitRef) constraints
356    /// can match Git references - all other constraint types return `false`.
357    ///
358    /// # Arguments
359    ///
360    /// * `git_ref` - The Git reference string to test (branch, tag, or commit)
361    ///
362    /// # Returns
363    ///
364    /// Returns `true` if this is a `GitRef` constraint with matching reference name,
365    /// `false` otherwise.
366    ///
367    /// # Examples
368    ///
369    /// ```rust,no_run
370    /// use agpm_cli::version::constraints::VersionConstraint;
371    ///
372    /// let branch_constraint = VersionConstraint::parse("main")?;
373    /// assert!(branch_constraint.matches_ref("main"));
374    /// assert!(!branch_constraint.matches_ref("develop"));
375    ///
376    /// let version_constraint = VersionConstraint::parse("^1.0.0")?;
377    /// assert!(!version_constraint.matches_ref("main")); // Version constraints don't match refs
378    /// # Ok::<(), anyhow::Error>(())
379    /// ```
380    ///
381    /// # Use Cases
382    ///
383    /// This method is primarily used during dependency resolution to match
384    /// dependencies that specify Git branches, tags, or commit hashes rather
385    /// than semantic versions.
386    #[must_use]
387    pub fn matches_ref(&self, git_ref: &str) -> bool {
388        match self {
389            Self::GitRef(ref_name) => ref_name == git_ref,
390            _ => false,
391        }
392    }
393
394    /// Check if a VersionInfo satisfies this constraint, including prefix matching.
395    ///
396    /// This method performs comprehensive matching that considers both the prefix
397    /// (for monorepo-style versioning) and the semantic version. It's the preferred
398    /// method for version resolution when working with potentially prefixed versions.
399    ///
400    /// # Matching Rules
401    ///
402    /// - **Prefix matching**: Constraint and version must have the same prefix (both None, or same String)
403    /// - **Version matching**: After prefix check, applies standard semver matching rules
404    /// - **Prerelease handling**: Follows same rules as [`matches`](Self::matches)
405    ///
406    /// # Arguments
407    ///
408    /// * `version_info` - The version information to test, including prefix and semver
409    ///
410    /// # Returns
411    ///
412    /// Returns `true` if both the prefix matches AND the version satisfies the constraint.
413    ///
414    /// # Examples
415    ///
416    /// ```rust,no_run
417    /// use agpm_cli::version::constraints::VersionConstraint;
418    /// use agpm_cli::version::VersionInfo;
419    /// use semver::Version;
420    ///
421    /// // Prefixed version matching
422    /// let constraint = VersionConstraint::parse("agents-^v1.0.0")?;
423    /// let version = VersionInfo {
424    ///     prefix: Some("agents".to_string()),
425    ///     version: Version::parse("1.2.0")?,
426    ///     tag: "agents-v1.2.0".to_string(),
427    ///     prerelease: false,
428    /// };
429    /// assert!(constraint.matches_version_info(&version));
430    ///
431    /// // Prefix mismatch
432    /// let wrong_prefix = VersionInfo {
433    ///     prefix: Some("snippets".to_string()),
434    ///     version: Version::parse("1.2.0")?,
435    ///     tag: "snippets-v1.2.0".to_string(),
436    ///     prerelease: false,
437    /// };
438    /// assert!(!constraint.matches_version_info(&wrong_prefix));
439    ///
440    /// // Unprefixed constraint only matches unprefixed versions
441    /// let no_prefix_constraint = VersionConstraint::parse("^1.0.0")?;
442    /// let no_prefix_version = VersionInfo {
443    ///     prefix: None,
444    ///     version: Version::parse("1.2.0")?,
445    ///     tag: "v1.2.0".to_string(),
446    ///     prerelease: false,
447    /// };
448    /// assert!(no_prefix_constraint.matches_version_info(&no_prefix_version));
449    /// assert!(!no_prefix_constraint.matches_version_info(&version)); // Has prefix
450    /// # Ok::<(), anyhow::Error>(())
451    /// ```
452    #[inline]
453    #[must_use]
454    pub fn matches_version_info(&self, version_info: &crate::version::VersionInfo) -> bool {
455        // Check prefix first
456        let constraint_prefix = match self {
457            Self::Exact {
458                prefix,
459                ..
460            }
461            | Self::Requirement {
462                prefix,
463                ..
464            } => prefix.as_ref(),
465            _ => None,
466        };
467
468        // Prefix must match (both None or both Some with same value)
469        if constraint_prefix != version_info.prefix.as_ref() {
470            return false;
471        }
472
473        // Then check version using existing logic
474        self.matches(&version_info.version)
475    }
476
477    /// Convert this constraint to a semantic version requirement if applicable.
478    ///
479    /// This method converts version-based constraints into [`VersionReq`] objects
480    /// that can be used with the semver crate for version matching. Git reference
481    /// constraints cannot be converted since they don't represent version ranges.
482    ///
483    /// # Returns
484    ///
485    /// Returns `Some(VersionReq)` for constraints that can be expressed as semantic
486    /// version requirements, or `None` for Git reference constraints.
487    ///
488    /// # Conversion Rules
489    ///
490    /// - **Exact**: Converted to `=1.0.0` requirement
491    /// - **Requirement**: Returns the inner `VersionReq` directly
492    /// - **`GitRef`**: Returns `None` (cannot be converted)
493    ///
494    /// # Examples
495    ///
496    /// ```rust,no_run
497    /// use agpm_cli::version::constraints::VersionConstraint;
498    /// use semver::Version;
499    ///
500    /// let exact = VersionConstraint::parse("1.0.0")?;
501    /// let req = exact.to_version_req().unwrap();
502    /// assert!(req.matches(&Version::parse("1.0.0")?));
503    ///
504    /// let caret = VersionConstraint::parse("^1.0.0")?;
505    /// let req = caret.to_version_req().unwrap();
506    /// assert!(req.matches(&Version::parse("1.2.0")?));
507    ///
508    /// let git_ref = VersionConstraint::parse("main")?;
509    /// assert!(git_ref.to_version_req().is_none()); // Git refs can't be converted
510    /// # Ok::<(), anyhow::Error>(())
511    /// ```
512    ///
513    /// # Use Cases
514    ///
515    /// This method is useful for integrating with existing semver-based tooling
516    /// or for performing version calculations that require `VersionReq` objects.
517    #[must_use]
518    pub fn to_version_req(&self) -> Option<VersionReq> {
519        match self {
520            Self::Exact {
521                version,
522                ..
523            } => {
524                // Create an exact version requirement
525                VersionReq::parse(&format!("={version}")).ok()
526            }
527            Self::Requirement {
528                req,
529                ..
530            } => Some(req.clone()),
531            Self::GitRef(_) => None, // Git refs cannot be converted to version requirements
532        }
533    }
534
535    /// Check if this constraint allows prerelease versions.
536    ///
537    /// Prerelease versions contain identifiers like `-alpha`, `-beta`, `-rc` that
538    /// indicate pre-release status. This method determines whether the constraint
539    /// should consider such versions during resolution.
540    ///
541    /// # Prerelease Policy
542    ///
543    /// - **`GitRef`**: Allows prereleases (Git refs may point to any commit)
544    /// - **Exact/Requirement**: Excludes prereleases unless explicitly specified
545    ///
546    /// # Returns
547    ///
548    /// Returns `true` if prerelease versions should be considered, `false` if only
549    /// stable versions should be considered.
550    ///
551    /// # Examples
552    ///
553    /// ```rust,no_run
554    /// use agpm_cli::version::constraints::VersionConstraint;
555    ///
556    /// let branch = VersionConstraint::parse("main")?;
557    /// assert!(branch.allows_prerelease()); // Git refs may be any version
558    ///
559    /// let latest = VersionConstraint::parse("latest")?;
560    /// assert!(latest.allows_prerelease()); // Git ref - just a tag name
561    ///
562    /// let exact = VersionConstraint::parse("1.0.0")?;
563    /// assert!(!exact.allows_prerelease()); // Exact stable version
564    /// # Ok::<(), anyhow::Error>(())
565    /// ```
566    ///
567    /// # Impact on Resolution
568    ///
569    /// During version resolution, if any constraint in a set allows prereleases,
570    /// the entire constraint set will consider prerelease versions as candidates.
571    #[must_use]
572    pub const fn allows_prerelease(&self) -> bool {
573        matches!(self, Self::GitRef(_))
574    }
575}
576
577impl fmt::Display for VersionConstraint {
578    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
579        match self {
580            Self::Exact {
581                prefix,
582                version,
583            } => {
584                if let Some(p) = prefix {
585                    write!(f, "{p}-{version}")
586                } else {
587                    write!(f, "{version}")
588                }
589            }
590            Self::Requirement {
591                prefix,
592                req,
593            } => {
594                if let Some(p) = prefix {
595                    write!(f, "{p}-{req}")
596                } else {
597                    write!(f, "{req}")
598                }
599            }
600            Self::GitRef(ref_name) => write!(f, "{ref_name}"),
601        }
602    }
603}
604
605/// A collection of version constraints that must all be satisfied simultaneously.
606///
607/// `ConstraintSet` manages multiple [`VersionConstraint`]s for a single dependency,
608/// ensuring that all constraints are compatible and can be resolved together.
609/// It provides conflict detection, version matching, and best-match selection.
610///
611/// # Constraint Combination
612///
613/// When multiple constraints are added to a set, they create an intersection
614/// of requirements. For example:
615/// - `>=1.0.0` AND `<2.0.0` = versions in range `[1.0.0, 2.0.0)`
616/// - `^1.0.0` AND `~1.2.0` = versions compatible with both (e.g., `1.2.x`)
617///
618/// # Conflict Detection
619///
620/// The constraint set detects and prevents conflicting constraints:
621/// - Multiple exact versions: `1.0.0` AND `2.0.0` (impossible to satisfy)
622/// - Conflicting Git refs: `main` AND `develop` (can't be both branches)
623///
624/// # Resolution Strategy
625///
626/// When selecting from available versions, the set:
627/// 1. Filters versions that satisfy ALL constraints
628/// 2. Excludes prereleases unless explicitly allowed
629/// 3. Selects the highest remaining version
630///
631/// # Examples
632///
633/// ## Basic Usage
634///
635/// ```rust,no_run
636/// use agpm_cli::version::constraints::{ConstraintSet, VersionConstraint};
637/// use semver::Version;
638///
639/// let mut set = ConstraintSet::new();
640/// set.add(VersionConstraint::parse(">=1.0.0")?)?;
641/// set.add(VersionConstraint::parse("<2.0.0")?)?;
642///
643/// let version = Version::parse("1.5.0")?;
644/// assert!(set.satisfies(&version));
645///
646/// let version = Version::parse("2.0.0")?;
647/// assert!(!set.satisfies(&version)); // Outside range
648/// # Ok::<(), anyhow::Error>(())
649/// ```
650///
651/// ## Best Match Selection
652///
653/// ```rust,no_run
654/// use agpm_cli::version::constraints::{ConstraintSet, VersionConstraint};
655/// use semver::Version;
656///
657/// let mut set = ConstraintSet::new();
658/// set.add(VersionConstraint::parse("^1.0.0")?)?;
659///
660/// let versions = vec![
661///     Version::parse("0.9.0")?,  // Too old
662///     Version::parse("1.0.0")?,  // Matches
663///     Version::parse("1.5.0")?,  // Matches, higher
664///     Version::parse("2.0.0")?,  // Too new
665/// ];
666///
667/// let best = set.find_best_match(&versions).unwrap();
668/// assert_eq!(best, &Version::parse("1.5.0")?); // Highest compatible
669/// # Ok::<(), anyhow::Error>(())
670/// ```
671///
672/// ## Conflict Detection
673///
674/// ```rust,no_run
675/// use agpm_cli::version::constraints::{ConstraintSet, VersionConstraint};
676/// use semver::Version;
677///
678/// let mut set = ConstraintSet::new();
679/// set.add(VersionConstraint::parse("1.0.0")?)?; // Exact version
680///
681/// // This will fail - can't have two different exact versions
682/// let result = set.add(VersionConstraint::parse("2.0.0")?);
683/// assert!(result.is_err());
684/// # Ok::<(), anyhow::Error>(())
685/// ```
686#[derive(Debug, Clone)]
687pub struct ConstraintSet {
688    constraints: Vec<VersionConstraint>,
689}
690
691impl Default for ConstraintSet {
692    fn default() -> Self {
693        Self::new()
694    }
695}
696
697impl ConstraintSet {
698    /// Creates a new empty constraint set
699    ///
700    /// # Returns
701    ///
702    /// Returns a new `ConstraintSet` with no constraints
703    #[must_use]
704    pub const fn new() -> Self {
705        Self {
706            constraints: Vec::new(),
707        }
708    }
709
710    /// Add a constraint to this set with conflict detection.
711    ///
712    /// This method adds a new constraint to the set after checking for conflicts
713    /// with existing constraints. If the new constraint would create an impossible
714    /// situation (like requiring two different exact versions), an error is returned.
715    ///
716    /// # Arguments
717    ///
718    /// * `constraint` - The [`VersionConstraint`] to add to this set
719    ///
720    /// # Returns
721    ///
722    /// Returns `Ok(())` if the constraint was added successfully, or `Err` if it
723    /// conflicts with existing constraints.
724    ///
725    /// # Conflict Detection
726    ///
727    /// Current conflict detection covers:
728    /// - **Exact version conflicts**: Different exact versions for the same dependency
729    /// - **Git ref conflicts**: Different Git references for the same dependency
730    ///
731    /// Future versions may add more sophisticated conflict detection for semantic
732    /// version ranges.
733    ///
734    /// # Examples
735    ///
736    /// ```rust,no_run
737    /// use agpm_cli::version::constraints::{ConstraintSet, VersionConstraint};
738    ///
739    /// let mut set = ConstraintSet::new();
740    ///
741    /// // These constraints are compatible
742    /// set.add(VersionConstraint::parse(">=1.0.0")?)?;
743    /// set.add(VersionConstraint::parse("<2.0.0")?)?;
744    ///
745    /// // This would conflict with exact versions
746    /// set.add(VersionConstraint::parse("1.5.0")?)?;
747    /// let result = set.add(VersionConstraint::parse("1.6.0")?);
748    /// assert!(result.is_err()); // Conflict: can't be both 1.5.0 AND 1.6.0
749    /// # Ok::<(), anyhow::Error>(())
750    /// ```
751    pub fn add(&mut self, constraint: VersionConstraint) -> Result<()> {
752        // Check for conflicting constraints
753        if self.has_conflict(&constraint) {
754            return Err(AgpmError::Other {
755                message: format!("Constraint {constraint} conflicts with existing constraints"),
756            }
757            .into());
758        }
759
760        self.constraints.push(constraint);
761        Ok(())
762    }
763
764    /// Check if a version satisfies all constraints in this set.
765    ///
766    /// This method tests whether a given version passes all the constraints
767    /// in this set. For the version to be acceptable, it must satisfy every
768    /// single constraint - this represents a logical AND operation.
769    ///
770    /// # Arguments
771    ///
772    /// * `version` - The semantic version to test against all constraints
773    ///
774    /// # Returns
775    ///
776    /// Returns `true` if the version satisfies ALL constraints, `false` if it
777    /// fails to satisfy any constraint.
778    ///
779    /// # Examples
780    ///
781    /// ```rust,no_run
782    /// use agpm_cli::version::constraints::{ConstraintSet, VersionConstraint};
783    /// use semver::Version;
784    ///
785    /// let mut set = ConstraintSet::new();
786    /// set.add(VersionConstraint::parse(">=1.0.0")?)?; // Must be at least 1.0.0
787    /// set.add(VersionConstraint::parse("<2.0.0")?)?;  // Must be less than 2.0.0
788    /// set.add(VersionConstraint::parse("^1.0.0")?)?;  // Must be compatible with 1.0.0
789    ///
790    /// assert!(set.satisfies(&Version::parse("1.5.0")?)); // Satisfies all three
791    /// assert!(!set.satisfies(&Version::parse("0.9.0")?)); // Fails >=1.0.0
792    /// assert!(!set.satisfies(&Version::parse("2.0.0")?)); // Fails <2.0.0
793    /// # Ok::<(), anyhow::Error>(())
794    /// ```
795    ///
796    /// # Performance Note
797    ///
798    /// This method short-circuits on the first constraint that fails, making it
799    /// efficient even with many constraints.
800    #[must_use]
801    pub fn satisfies(&self, version: &Version) -> bool {
802        self.constraints.iter().all(|c| c.matches(version))
803    }
804
805    /// Find the best matching version from a list of available versions.
806    ///
807    /// This method filters the provided versions to find those that satisfy all
808    /// constraints, then selects the "best" match according to AGPM's resolution
809    /// strategy. The selection prioritizes newer versions while respecting prerelease
810    /// preferences.
811    ///
812    /// # Resolution Strategy
813    ///
814    /// 1. **Filter candidates**: Keep only versions that satisfy all constraints
815    /// 2. **Sort by version**: Order candidates from highest to lowest version
816    /// 3. **Apply prerelease policy**: Remove prereleases unless explicitly allowed
817    /// 4. **Select best**: Return the highest remaining version
818    ///
819    /// # Arguments
820    ///
821    /// * `versions` - Slice of available versions to choose from
822    ///
823    /// # Returns
824    ///
825    /// Returns `Some(&Version)` with the best matching version, or `None` if no
826    /// version satisfies all constraints.
827    ///
828    /// # Examples
829    ///
830    /// ```rust,no_run
831    /// use agpm_cli::version::constraints::{ConstraintSet, VersionConstraint};
832    /// use semver::Version;
833    ///
834    /// let mut set = ConstraintSet::new();
835    /// set.add(VersionConstraint::parse("^1.0.0")?)?;
836    ///
837    /// let versions = vec![
838    ///     Version::parse("0.9.0")?,    // Too old
839    ///     Version::parse("1.0.0")?,    // Compatible
840    ///     Version::parse("1.2.0")?,    // Compatible, newer
841    ///     Version::parse("1.5.0")?,    // Compatible, newest
842    ///     Version::parse("2.0.0")?,    // Too new
843    /// ];
844    ///
845    /// let best = set.find_best_match(&versions).unwrap();
846    /// assert_eq!(best, &Version::parse("1.5.0")?); // Highest compatible version
847    /// # Ok::<(), anyhow::Error>(())
848    /// ```
849    ///
850    /// ## Prerelease Handling
851    ///
852    /// ```rust,no_run
853    /// use agpm_cli::version::constraints::{ConstraintSet, VersionConstraint};
854    /// use semver::Version;
855    ///
856    /// let mut set = ConstraintSet::new();
857    /// set.add(VersionConstraint::parse("^1.0.0")?)?; // Doesn't allow prereleases
858    ///
859    /// let versions = vec![
860    ///     Version::parse("1.0.0")?,
861    ///     Version::parse("1.1.0-alpha.1")?,  // Prerelease
862    ///     Version::parse("1.1.0")?,           // Stable
863    /// ];
864    ///
865    /// let best = set.find_best_match(&versions).unwrap();
866    /// assert_eq!(best, &Version::parse("1.1.0")?); // Stable version preferred
867    /// # Ok::<(), anyhow::Error>(())
868    /// ```
869    #[must_use]
870    pub fn find_best_match<'a>(&self, versions: &'a [Version]) -> Option<&'a Version> {
871        let mut candidates: Vec<&Version> = versions.iter().filter(|v| self.satisfies(v)).collect();
872
873        // Sort by version (highest first)
874        candidates.sort_by(|a, b| b.cmp(a));
875
876        // If we don't allow prereleases, filter them out
877        if !self.allows_prerelease() {
878            candidates.retain(|v| v.pre.is_empty());
879        }
880
881        candidates.first().copied()
882    }
883
884    /// Check if any constraint in this set allows prerelease versions.
885    ///
886    /// This method determines the prerelease policy for the entire constraint set.
887    /// If ANY constraint in the set allows prereleases, the entire set is considered
888    /// to allow prereleases. This ensures that explicit prerelease constraints
889    /// (like `latest-prerelease` or Git refs) are respected.
890    ///
891    /// # Returns
892    ///
893    /// Returns `true` if any constraint allows prereleases, `false` if all constraints
894    /// exclude prereleases.
895    ///
896    /// # Examples
897    ///
898    /// ```rust,no_run
899    /// use agpm_cli::version::constraints::{ConstraintSet, VersionConstraint};
900    ///
901    /// let mut stable_set = ConstraintSet::new();
902    /// stable_set.add(VersionConstraint::parse("^1.0.0")?)?;
903    /// stable_set.add(VersionConstraint::parse("~1.2.0")?)?;
904    /// assert!(!stable_set.allows_prerelease()); // All constraints exclude prereleases
905    ///
906    /// let mut prerelease_set = ConstraintSet::new();
907    /// prerelease_set.add(VersionConstraint::parse("^1.0.0")?)?;
908    /// prerelease_set.add(VersionConstraint::parse("main")?)?; // Git ref allows prereleases
909    /// assert!(prerelease_set.allows_prerelease()); // One constraint allows prereleases
910    /// # Ok::<(), anyhow::Error>(())
911    /// ```
912    ///
913    /// # Impact on Resolution
914    ///
915    /// This setting affects [`find_best_match`](Self::find_best_match) behavior:
916    /// - If `false`: Prerelease versions are filtered out before selection
917    /// - If `true`: Prerelease versions are included in selection
918    #[must_use]
919    pub fn allows_prerelease(&self) -> bool {
920        self.constraints.iter().any(VersionConstraint::allows_prerelease)
921    }
922
923    /// Check if a new constraint would conflict with existing constraints.
924    ///
925    /// This method performs conflict detection to prevent adding incompatible
926    /// constraints to the same set. It currently detects basic conflicts but
927    /// could be enhanced with more sophisticated analysis in the future.
928    ///
929    /// # Current Conflict Detection
930    ///
931    /// - **Exact version conflicts**: Two different exact versions (`1.0.0` vs `2.0.0`)
932    /// - **Git reference conflicts**: Two different Git refs (`main` vs `develop`)
933    ///
934    /// # Arguments
935    ///
936    /// * `new_constraint` - The constraint to test for conflicts
937    ///
938    /// # Returns
939    ///
940    /// Returns `true` if the constraint conflicts with existing ones, `false` if
941    /// it's compatible.
942    ///
943    /// # Future Enhancements
944    ///
945    /// Future versions could detect more sophisticated conflicts:
946    /// - Impossible version ranges (e.g., `>2.0.0` AND `<1.0.0`)
947    /// - Contradictory semver requirements
948    /// - Mixed version and Git reference constraints
949    ///
950    /// # Examples
951    ///
952    /// ```rust,no_run,ignore
953    /// use agpm_cli::version::constraints::{ConstraintSet, VersionConstraint};
954    ///
955    /// let mut set = ConstraintSet::new();
956    /// set.add(VersionConstraint::parse("1.0.0")?)?;
957    ///
958    /// // This would conflict (different exact versions)
959    /// let conflicting = VersionConstraint::parse("2.0.0")?;
960    /// assert!(set.has_conflict(&conflicting));
961    ///
962    /// // This would not conflict (same exact version)
963    /// let compatible = VersionConstraint::parse("1.0.0")?;
964    /// assert!(!set.has_conflict(&compatible));
965    /// # Ok::<(), anyhow::Error>(())
966    /// ```
967    fn has_conflict(&self, new_constraint: &VersionConstraint) -> bool {
968        // Simple conflict detection - can be enhanced
969        for existing in &self.constraints {
970            match (existing, new_constraint) {
971                (
972                    VersionConstraint::Exact {
973                        prefix: p1,
974                        version: v1,
975                    },
976                    VersionConstraint::Exact {
977                        prefix: p2,
978                        version: v2,
979                    },
980                ) => {
981                    // Different prefixes = different namespaces, no conflict
982                    if p1 != p2 {
983                        continue;
984                    }
985                    // Same prefix (or both None), conflict if different versions
986                    if v1 != v2 {
987                        return true;
988                    }
989                }
990                (VersionConstraint::GitRef(r1), VersionConstraint::GitRef(r2)) => {
991                    if r1 != r2 {
992                        return true;
993                    }
994                }
995                // For Requirement constraints, different prefixes = no conflict
996                (
997                    VersionConstraint::Exact {
998                        prefix: p1,
999                        ..
1000                    },
1001                    VersionConstraint::Requirement {
1002                        prefix: p2,
1003                        ..
1004                    },
1005                )
1006                | (
1007                    VersionConstraint::Requirement {
1008                        prefix: p1,
1009                        ..
1010                    },
1011                    VersionConstraint::Exact {
1012                        prefix: p2,
1013                        ..
1014                    },
1015                )
1016                | (
1017                    VersionConstraint::Requirement {
1018                        prefix: p1,
1019                        ..
1020                    },
1021                    VersionConstraint::Requirement {
1022                        prefix: p2,
1023                        ..
1024                    },
1025                ) => {
1026                    // Different prefixes = different namespaces, no conflict
1027                    if p1 != p2 {
1028                        continue;
1029                    }
1030                    // Same prefix - could do more sophisticated conflict detection here
1031                }
1032                _ => {
1033                    // More sophisticated conflict detection could be added here
1034                }
1035            }
1036        }
1037        false
1038    }
1039}
1040
1041/// Manages version constraints for multiple dependencies and resolves them simultaneously.
1042///
1043/// `ConstraintResolver` coordinates version resolution across an entire dependency graph,
1044/// ensuring that all constraints are satisfied and conflicts are detected. It maintains
1045/// separate [`ConstraintSet`]s for each dependency and resolves them against available
1046/// version catalogs.
1047///
1048/// # Multi-Dependency Resolution
1049///
1050/// Unlike [`ConstraintSet`] which manages constraints for a single dependency, the
1051/// `ConstraintResolver` handles multiple dependencies simultaneously:
1052///
1053/// - Each dependency gets its own constraint set
1054/// - Constraints can be added incrementally
1055/// - Resolution happens across the entire dependency graph
1056/// - Missing dependencies are detected and reported
1057///
1058/// # Resolution Process
1059///
1060/// 1. **Collect constraints**: Gather all constraints for each dependency
1061/// 2. **Validate availability**: Ensure versions exist for all dependencies
1062/// 3. **Apply constraint sets**: Use each dependency's constraints to filter versions
1063/// 4. **Select best matches**: Choose optimal versions for each dependency
1064/// 5. **Return resolution map**: Provide final version selections
1065///
1066/// # Examples
1067///
1068/// ## Basic Multi-Dependency Resolution
1069///
1070/// ```rust,no_run
1071/// use agpm_cli::version::constraints::ConstraintResolver;
1072/// use semver::Version;
1073/// use std::collections::HashMap;
1074///
1075/// let mut resolver = ConstraintResolver::new();
1076///
1077/// // Add constraints for multiple dependencies
1078/// resolver.add_constraint("dep1", "^1.0.0")?;
1079/// resolver.add_constraint("dep2", "~2.1.0")?;
1080/// resolver.add_constraint("dep3", "main")?;
1081///
1082/// // Provide available versions for each dependency
1083/// let mut available = HashMap::new();
1084/// available.insert("dep1".to_string(), vec![Version::parse("1.5.0")?]);
1085/// available.insert("dep2".to_string(), vec![Version::parse("2.1.3")?]);
1086/// available.insert("dep3".to_string(), vec![Version::parse("3.0.0")?]);
1087///
1088/// // Resolve all dependencies
1089/// let resolved = resolver.resolve(&available)?;
1090/// assert_eq!(resolved.len(), 3);
1091/// # Ok::<(), anyhow::Error>(())
1092/// ```
1093///
1094/// ## Incremental Constraint Addition
1095///
1096/// ```rust,no_run
1097/// use agpm_cli::version::constraints::ConstraintResolver;
1098///
1099/// let mut resolver = ConstraintResolver::new();
1100///
1101/// // Add multiple constraints for the same dependency
1102/// resolver.add_constraint("my-dep", ">=1.0.0")?;
1103/// resolver.add_constraint("my-dep", "<2.0.0")?;
1104/// resolver.add_constraint("my-dep", "^1.5.0")?;
1105///
1106/// // All constraints will be combined into a single constraint set
1107/// # Ok::<(), anyhow::Error>(())
1108/// ```
1109///
1110/// # Error Conditions
1111///
1112/// The resolver reports several types of errors:
1113///
1114/// - **Missing dependencies**: A constraint exists but no versions are available
1115/// - **Unsatisfiable constraints**: No available version meets all requirements
1116/// - **Conflicting constraints**: Impossible constraint combinations
1117///
1118/// # Use Cases
1119///
1120/// This resolver is particularly useful for:
1121/// - Package managers resolving dependency graphs
1122/// - Build systems selecting compatible versions
1123/// - Configuration management ensuring consistent environments
1124/// - Update analysis determining safe upgrade paths
1125pub struct ConstraintResolver {
1126    constraints: HashMap<String, ConstraintSet>,
1127}
1128
1129impl Default for ConstraintResolver {
1130    fn default() -> Self {
1131        Self::new()
1132    }
1133}
1134
1135impl ConstraintResolver {
1136    /// Creates a new constraint resolver
1137    ///
1138    /// # Returns
1139    ///
1140    /// Returns a new `ConstraintResolver` with empty constraint and resolution maps
1141    #[must_use]
1142    pub fn new() -> Self {
1143        Self {
1144            constraints: HashMap::new(),
1145        }
1146    }
1147
1148    /// Add a version constraint for a specific dependency.
1149    ///
1150    /// This method parses the constraint string and adds it to the constraint set
1151    /// for the named dependency. If this is the first constraint for the dependency,
1152    /// a new constraint set is created. Multiple constraints for the same dependency
1153    /// are combined into a single set with conflict detection.
1154    ///
1155    /// # Arguments
1156    ///
1157    /// * `dependency` - The name of the dependency to constrain
1158    /// * `constraint` - The constraint string to parse and add (e.g., "^1.0.0", "latest")
1159    ///
1160    /// # Returns
1161    ///
1162    /// Returns `Ok(())` if the constraint was added successfully, or `Err` if:
1163    /// - The constraint string is invalid
1164    /// - The constraint conflicts with existing constraints for this dependency
1165    ///
1166    /// # Examples
1167    ///
1168    /// ```rust,no_run
1169    /// use agpm_cli::version::constraints::ConstraintResolver;
1170    ///
1171    /// let mut resolver = ConstraintResolver::new();
1172    ///
1173    /// // Add constraints for different dependencies
1174    /// resolver.add_constraint("web-framework", "^2.0.0")?;
1175    /// resolver.add_constraint("database", "~1.5.0")?;
1176    /// resolver.add_constraint("auth-lib", "main")?;
1177    ///
1178    /// // Add multiple constraints for the same dependency
1179    /// resolver.add_constraint("api-client", ">=1.0.0")?;
1180    /// resolver.add_constraint("api-client", "<2.0.0")?; // Compatible range
1181    ///
1182    /// // This would fail - conflicting exact versions
1183    /// resolver.add_constraint("my-dep", "1.0.0")?;
1184    /// let result = resolver.add_constraint("my-dep", "2.0.0");
1185    /// assert!(result.is_err());
1186    /// # Ok::<(), anyhow::Error>(())
1187    /// ```
1188    ///
1189    /// # Constraint Combination
1190    ///
1191    /// When multiple constraints are added for the same dependency, they are
1192    /// combined using AND logic. The final constraint set requires that all
1193    /// individual constraints be satisfied simultaneously.
1194    pub fn add_constraint(&mut self, dependency: &str, constraint: &str) -> Result<()> {
1195        let parsed = VersionConstraint::parse(constraint)?;
1196
1197        self.constraints.entry(dependency.to_string()).or_default().add(parsed)?;
1198
1199        Ok(())
1200    }
1201
1202    /// Resolve all dependency constraints and return the best version for each.
1203    ///
1204    /// This method performs the core resolution algorithm, taking all accumulated
1205    /// constraints and finding the best matching version for each dependency from
1206    /// the provided catalog of available versions.
1207    ///
1208    /// # Resolution Algorithm
1209    ///
1210    /// For each dependency with constraints:
1211    /// 1. **Verify availability**: Check that versions exist for the dependency
1212    /// 2. **Apply constraints**: Filter versions using the dependency's constraint set
1213    /// 3. **Select best match**: Choose the highest compatible version
1214    /// 4. **Handle prereleases**: Apply prerelease policies appropriately
1215    ///
1216    /// # Arguments
1217    ///
1218    /// * `available_versions` - Map from dependency names to lists of available versions
1219    ///
1220    /// # Returns
1221    ///
1222    /// Returns `Ok(HashMap<String, Version>)` with the resolved version for each
1223    /// dependency, or `Err` if resolution fails.
1224    ///
1225    /// # Error Conditions
1226    ///
1227    /// - **Missing dependency**: Constraint exists but no versions available
1228    /// - **No satisfying version**: Available versions don't meet constraints
1229    /// - **Internal errors**: Constraint set conflicts or parsing failures
1230    ///
1231    /// # Examples
1232    ///
1233    /// ```rust,no_run
1234    /// use agpm_cli::version::constraints::ConstraintResolver;
1235    /// use semver::Version;
1236    /// use std::collections::HashMap;
1237    ///
1238    /// let mut resolver = ConstraintResolver::new();
1239    /// resolver.add_constraint("web-server", "^1.0.0")?;
1240    /// resolver.add_constraint("database", "~2.1.0")?;
1241    ///
1242    /// // Provide version catalog
1243    /// let mut available = HashMap::new();
1244    /// available.insert(
1245    ///     "web-server".to_string(),
1246    ///     vec![
1247    ///         Version::parse("1.0.0")?,
1248    ///         Version::parse("1.2.0")?,
1249    ///         Version::parse("1.5.0")?, // Best match for ^1.0.0
1250    ///         Version::parse("2.0.0")?, // Too new
1251    ///     ],
1252    /// );
1253    /// available.insert(
1254    ///     "database".to_string(),
1255    ///     vec![
1256    ///         Version::parse("2.1.0")?,
1257    ///         Version::parse("2.1.3")?, // Best match for ~2.1.0
1258    ///         Version::parse("2.2.0")?, // Too new
1259    ///     ],
1260    /// );
1261    ///
1262    /// // Resolve dependencies
1263    /// let resolved = resolver.resolve(&available)?;
1264    /// assert_eq!(resolved["web-server"], Version::parse("1.5.0")?);
1265    /// assert_eq!(resolved["database"], Version::parse("2.1.3")?);
1266    /// # Ok::<(), anyhow::Error>(())
1267    /// ```
1268    ///
1269    /// ## Error Handling
1270    ///
1271    /// ```rust,no_run
1272    /// use agpm_cli::version::constraints::ConstraintResolver;
1273    /// use std::collections::HashMap;
1274    ///
1275    /// let mut resolver = ConstraintResolver::new();
1276    /// resolver.add_constraint("missing-dep", "^1.0.0")?;
1277    ///
1278    /// let available = HashMap::new(); // No versions provided
1279    ///
1280    /// let result = resolver.resolve(&available);
1281    /// assert!(result.is_err()); // Missing dependency error
1282    /// # Ok::<(), anyhow::Error>(())
1283    /// ```
1284    ///
1285    /// # Performance Considerations
1286    ///
1287    /// - Resolution is performed independently for each dependency
1288    /// - Version filtering and sorting may be expensive for large version lists
1289    /// - Consider pre-filtering available versions if catalogs are very large
1290    pub fn resolve(
1291        &self,
1292        available_versions: &HashMap<String, Vec<Version>>,
1293    ) -> Result<HashMap<String, Version>> {
1294        let mut resolved = HashMap::new();
1295
1296        for (dep, constraint_set) in &self.constraints {
1297            let versions = available_versions.get(dep).ok_or_else(|| AgpmError::Other {
1298                message: format!("No versions available for dependency: {dep}"),
1299            })?;
1300
1301            let best_match =
1302                constraint_set.find_best_match(versions).ok_or_else(|| AgpmError::Other {
1303                    message: format!("No version satisfies constraints for dependency: {dep}"),
1304                })?;
1305
1306            resolved.insert(dep.clone(), best_match.clone());
1307        }
1308
1309        Ok(resolved)
1310    }
1311}
1312
1313#[cfg(test)]
1314mod tests {
1315    use super::*;
1316
1317    #[test]
1318    fn test_version_constraint_parse() {
1319        // Exact version
1320        let constraint = VersionConstraint::parse("1.0.0").unwrap();
1321        assert!(matches!(constraint, VersionConstraint::Exact { .. }));
1322
1323        // Version with v prefix
1324        let constraint = VersionConstraint::parse("v1.0.0").unwrap();
1325        assert!(matches!(constraint, VersionConstraint::Exact { .. }));
1326
1327        // Caret requirement
1328        let constraint = VersionConstraint::parse("^1.0.0").unwrap();
1329        assert!(matches!(constraint, VersionConstraint::Requirement { .. }));
1330
1331        // Tilde requirement
1332        let constraint = VersionConstraint::parse("~1.2.0").unwrap();
1333        assert!(matches!(constraint, VersionConstraint::Requirement { .. }));
1334
1335        // Range requirement
1336        let constraint = VersionConstraint::parse(">=1.0.0, <2.0.0").unwrap();
1337        assert!(matches!(constraint, VersionConstraint::Requirement { .. }));
1338
1339        // Git refs (including "latest" - it's just a tag name)
1340        let constraint = VersionConstraint::parse("latest").unwrap();
1341        assert!(matches!(constraint, VersionConstraint::GitRef(_)));
1342
1343        let constraint = VersionConstraint::parse("main").unwrap();
1344        assert!(matches!(constraint, VersionConstraint::GitRef(_)));
1345    }
1346
1347    #[test]
1348    fn test_constraint_matching() {
1349        let v100 = Version::parse("1.0.0").unwrap();
1350        let v110 = Version::parse("1.1.0").unwrap();
1351        let v200 = Version::parse("2.0.0").unwrap();
1352
1353        // Exact match
1354        let exact = VersionConstraint::Exact {
1355            prefix: None,
1356            version: v100.clone(),
1357        };
1358        assert!(exact.matches(&v100));
1359        assert!(!exact.matches(&v110));
1360
1361        // Caret requirement
1362        let caret = VersionConstraint::parse("^1.0.0").unwrap();
1363        assert!(caret.matches(&v100));
1364        assert!(caret.matches(&v110));
1365        assert!(!caret.matches(&v200));
1366
1367        // Git refs don't match semantic versions
1368        let git_ref = VersionConstraint::GitRef("latest".to_string());
1369        assert!(!git_ref.matches(&v100));
1370        assert!(!git_ref.matches(&v200));
1371    }
1372
1373    #[test]
1374    fn test_constraint_set() {
1375        let mut set = ConstraintSet::new();
1376        set.add(VersionConstraint::parse(">=1.0.0").unwrap()).unwrap();
1377        set.add(VersionConstraint::parse("<2.0.0").unwrap()).unwrap();
1378
1379        let v090 = Version::parse("0.9.0").unwrap();
1380        let v100 = Version::parse("1.0.0").unwrap();
1381        let v150 = Version::parse("1.5.0").unwrap();
1382        let v200 = Version::parse("2.0.0").unwrap();
1383
1384        assert!(!set.satisfies(&v090));
1385        assert!(set.satisfies(&v100));
1386        assert!(set.satisfies(&v150));
1387        assert!(!set.satisfies(&v200));
1388    }
1389
1390    #[test]
1391    fn test_find_best_match() {
1392        let mut set = ConstraintSet::new();
1393        set.add(VersionConstraint::parse("^1.0.0").unwrap()).unwrap();
1394
1395        let versions = vec![
1396            Version::parse("0.9.0").unwrap(),
1397            Version::parse("1.0.0").unwrap(),
1398            Version::parse("1.2.0").unwrap(),
1399            Version::parse("1.5.0").unwrap(),
1400            Version::parse("2.0.0").unwrap(),
1401        ];
1402
1403        let best = set.find_best_match(&versions).unwrap();
1404        assert_eq!(best, &Version::parse("1.5.0").unwrap());
1405    }
1406
1407    #[test]
1408    fn test_constraint_conflicts() {
1409        let mut set = ConstraintSet::new();
1410
1411        // Add first exact version
1412        set.add(VersionConstraint::Exact {
1413            prefix: None,
1414            version: Version::parse("1.0.0").unwrap(),
1415        })
1416        .unwrap();
1417
1418        // Try to add conflicting exact version
1419        let result = set.add(VersionConstraint::Exact {
1420            prefix: None,
1421            version: Version::parse("2.0.0").unwrap(),
1422        });
1423        assert!(result.is_err());
1424
1425        // Adding the same version should be ok
1426        let result = set.add(VersionConstraint::Exact {
1427            prefix: None,
1428            version: Version::parse("1.0.0").unwrap(),
1429        });
1430        assert!(result.is_ok());
1431    }
1432
1433    #[test]
1434    fn test_constraint_resolver() {
1435        let mut resolver = ConstraintResolver::new();
1436
1437        resolver.add_constraint("dep1", "^1.0.0").unwrap();
1438        resolver.add_constraint("dep2", "~2.1.0").unwrap();
1439
1440        let mut available = HashMap::new();
1441        available.insert(
1442            "dep1".to_string(),
1443            vec![
1444                Version::parse("0.9.0").unwrap(),
1445                Version::parse("1.0.0").unwrap(),
1446                Version::parse("1.5.0").unwrap(),
1447                Version::parse("2.0.0").unwrap(),
1448            ],
1449        );
1450        available.insert(
1451            "dep2".to_string(),
1452            vec![
1453                Version::parse("2.0.0").unwrap(),
1454                Version::parse("2.1.0").unwrap(),
1455                Version::parse("2.1.5").unwrap(),
1456                Version::parse("2.2.0").unwrap(),
1457            ],
1458        );
1459
1460        let resolved = resolver.resolve(&available).unwrap();
1461        assert_eq!(resolved.get("dep1"), Some(&Version::parse("1.5.0").unwrap()));
1462        assert_eq!(resolved.get("dep2"), Some(&Version::parse("2.1.5").unwrap()));
1463    }
1464
1465    #[test]
1466    fn test_allows_prerelease() {
1467        assert!(VersionConstraint::GitRef("main".to_string()).allows_prerelease());
1468        assert!(VersionConstraint::GitRef("latest".to_string()).allows_prerelease()); // Git ref
1469        assert!(
1470            !VersionConstraint::Exact {
1471                prefix: None,
1472                version: Version::parse("1.0.0").unwrap()
1473            }
1474            .allows_prerelease()
1475        );
1476    }
1477
1478    #[test]
1479    fn test_version_constraint_parse_edge_cases() {
1480        // Test latest-prerelease (just a tag name)
1481        let constraint = VersionConstraint::parse("latest-prerelease").unwrap();
1482        assert!(matches!(constraint, VersionConstraint::GitRef(_)));
1483
1484        // Test asterisk wildcard
1485        let constraint = VersionConstraint::parse("*").unwrap();
1486        assert!(matches!(constraint, VersionConstraint::GitRef(_)));
1487
1488        // Test range operators
1489        let constraint = VersionConstraint::parse(">=1.0.0").unwrap();
1490        assert!(matches!(constraint, VersionConstraint::Requirement { .. }));
1491
1492        let constraint = VersionConstraint::parse("<2.0.0").unwrap();
1493        assert!(matches!(constraint, VersionConstraint::Requirement { .. }));
1494
1495        let constraint = VersionConstraint::parse("=1.0.0").unwrap();
1496        assert!(matches!(constraint, VersionConstraint::Requirement { .. }));
1497
1498        // Test git branch names
1499        let constraint = VersionConstraint::parse("feature/new-feature").unwrap();
1500        assert!(matches!(constraint, VersionConstraint::GitRef(_)));
1501
1502        // Test commit hash
1503        let constraint = VersionConstraint::parse("abc123def456").unwrap();
1504        assert!(matches!(constraint, VersionConstraint::GitRef(_)));
1505    }
1506
1507    #[test]
1508    fn test_version_constraint_display() {
1509        let exact = VersionConstraint::Exact {
1510            prefix: None,
1511            version: Version::parse("1.0.0").unwrap(),
1512        };
1513        assert_eq!(format!("{exact}"), "1.0.0");
1514
1515        let req = VersionConstraint::parse("^1.0.0").unwrap();
1516        assert_eq!(format!("{req}"), "^1.0.0");
1517
1518        let git_ref = VersionConstraint::GitRef("main".to_string());
1519        assert_eq!(format!("{git_ref}"), "main");
1520
1521        let latest = VersionConstraint::GitRef("latest".to_string());
1522        assert_eq!(format!("{latest}"), "latest");
1523    }
1524
1525    #[test]
1526    fn test_version_constraint_matches_ref() {
1527        let git_ref = VersionConstraint::GitRef("main".to_string());
1528        assert!(git_ref.matches_ref("main"));
1529        assert!(!git_ref.matches_ref("develop"));
1530
1531        // Other constraint types should return false for ref matching
1532        let exact = VersionConstraint::Exact {
1533            prefix: None,
1534            version: Version::parse("1.0.0").unwrap(),
1535        };
1536        assert!(!exact.matches_ref("v1.0.0"));
1537
1538        let latest = VersionConstraint::GitRef("latest".to_string());
1539        assert!(latest.matches_ref("latest"));
1540    }
1541
1542    #[test]
1543    fn test_version_constraint_to_version_req() {
1544        let exact = VersionConstraint::Exact {
1545            prefix: None,
1546            version: Version::parse("1.0.0").unwrap(),
1547        };
1548        let req = exact.to_version_req().unwrap();
1549        assert!(req.matches(&Version::parse("1.0.0").unwrap()));
1550
1551        let caret = VersionConstraint::parse("^1.0.0").unwrap();
1552        let req = caret.to_version_req().unwrap();
1553        assert!(req.matches(&Version::parse("1.0.0").unwrap()));
1554
1555        let git_ref = VersionConstraint::GitRef("main".to_string());
1556        assert!(git_ref.to_version_req().is_none());
1557
1558        let latest = VersionConstraint::GitRef("latest".to_string());
1559        assert!(latest.to_version_req().is_none()); // Git ref - cannot convert
1560    }
1561
1562    #[test]
1563    fn test_constraint_set_with_prereleases() {
1564        let mut set = ConstraintSet::new();
1565        set.add(VersionConstraint::GitRef("main".to_string())).unwrap();
1566
1567        let v100_pre = Version::parse("1.0.0-alpha.1").unwrap();
1568        let v100 = Version::parse("1.0.0").unwrap();
1569
1570        assert!(set.allows_prerelease());
1571
1572        // Git refs don't match semver versions
1573        let versions = vec![v100_pre.clone(), v100.clone()];
1574        let best = set.find_best_match(&versions);
1575        assert!(best.is_none()); // Git refs don't match semver
1576    }
1577
1578    #[test]
1579    fn test_constraint_set_no_matches() {
1580        let mut set = ConstraintSet::new();
1581        set.add(VersionConstraint::parse(">=2.0.0").unwrap()).unwrap();
1582
1583        let versions = vec![Version::parse("1.0.0").unwrap(), Version::parse("1.5.0").unwrap()];
1584
1585        let best = set.find_best_match(&versions);
1586        assert!(best.is_none());
1587    }
1588
1589    #[test]
1590    fn test_constraint_resolver_missing_dependency() {
1591        let mut resolver = ConstraintResolver::new();
1592        resolver.add_constraint("dep1", "^1.0.0").unwrap();
1593
1594        let available = HashMap::new(); // No versions available
1595
1596        let result = resolver.resolve(&available);
1597        assert!(result.is_err());
1598    }
1599
1600    #[test]
1601    fn test_constraint_resolver_no_satisfying_version() {
1602        let mut resolver = ConstraintResolver::new();
1603        resolver.add_constraint("dep1", "^2.0.0").unwrap();
1604
1605        let mut available = HashMap::new();
1606        available.insert(
1607            "dep1".to_string(),
1608            vec![Version::parse("1.0.0").unwrap()], // Only 1.x available, but we need 2.x
1609        );
1610
1611        let result = resolver.resolve(&available);
1612        assert!(result.is_err());
1613    }
1614
1615    #[test]
1616    fn test_constraint_set_git_ref_conflicts() {
1617        let mut set = ConstraintSet::new();
1618
1619        // Add first git ref
1620        set.add(VersionConstraint::GitRef("main".to_string())).unwrap();
1621
1622        // Try to add conflicting git ref
1623        let result = set.add(VersionConstraint::GitRef("develop".to_string()));
1624        assert!(result.is_err());
1625
1626        // Adding the same ref should be ok
1627        let result = set.add(VersionConstraint::GitRef("main".to_string()));
1628        assert!(result.is_ok());
1629    }
1630
1631    #[test]
1632    fn test_git_ref_constraint_with_versions() {
1633        let git_ref = VersionConstraint::GitRef("latest".to_string());
1634
1635        let v100_pre = Version::parse("1.0.0-alpha.1").unwrap();
1636        let v100 = Version::parse("1.0.0").unwrap();
1637
1638        // Git refs don't match semantic versions
1639        assert!(!git_ref.matches(&v100));
1640        assert!(!git_ref.matches(&v100_pre));
1641    }
1642
1643    #[test]
1644    fn test_git_ref_allows_prereleases() {
1645        let git_ref = VersionConstraint::GitRef("latest".to_string());
1646
1647        // Git refs allow prereleases (they reference commits)
1648        assert!(git_ref.allows_prerelease());
1649
1650        let main_ref = VersionConstraint::GitRef("main".to_string());
1651        assert!(main_ref.allows_prerelease());
1652    }
1653
1654    #[test]
1655    fn test_requirement_constraint_allows_prerelease() {
1656        let req = VersionConstraint::parse("^1.0.0").unwrap();
1657        assert!(!req.allows_prerelease());
1658
1659        let exact = VersionConstraint::Exact {
1660            prefix: None,
1661            version: Version::parse("1.0.0").unwrap(),
1662        };
1663        assert!(!exact.allows_prerelease());
1664    }
1665
1666    #[test]
1667    fn test_constraint_set_prerelease_filtering() {
1668        let mut set = ConstraintSet::new();
1669        set.add(VersionConstraint::parse("^1.0.0").unwrap()).unwrap();
1670
1671        let versions = vec![
1672            Version::parse("1.0.0-alpha.1").unwrap(),
1673            Version::parse("1.0.0").unwrap(),
1674            Version::parse("1.1.0-beta.1").unwrap(),
1675            Version::parse("1.1.0").unwrap(),
1676        ];
1677
1678        let best = set.find_best_match(&versions).unwrap();
1679        assert_eq!(best, &Version::parse("1.1.0").unwrap()); // Should pick highest stable
1680    }
1681
1682    #[test]
1683    fn test_parse_with_whitespace() {
1684        let constraint = VersionConstraint::parse("  1.0.0  ").unwrap();
1685        assert!(matches!(constraint, VersionConstraint::Exact { .. }));
1686
1687        let constraint = VersionConstraint::parse("  latest  ").unwrap();
1688        assert!(matches!(constraint, VersionConstraint::GitRef(_))); // Just a git ref
1689
1690        let constraint = VersionConstraint::parse("  ^1.0.0  ").unwrap();
1691        assert!(matches!(constraint, VersionConstraint::Requirement { .. }));
1692    }
1693
1694    #[test]
1695    fn test_constraint_resolver_add_constraint_error() {
1696        let mut resolver = ConstraintResolver::new();
1697
1698        // Add a valid constraint first
1699        resolver.add_constraint("dep1", "1.0.0").unwrap();
1700
1701        // Add conflicting constraint
1702        let result = resolver.add_constraint("dep1", "2.0.0");
1703        assert!(result.is_err());
1704    }
1705
1706    #[test]
1707    fn test_constraint_set_no_conflict_different_types() {
1708        let mut set = ConstraintSet::new();
1709
1710        // These shouldn't conflict as they are different types
1711        set.add(VersionConstraint::parse("^1.0.0").unwrap()).unwrap();
1712        set.add(VersionConstraint::GitRef("main".to_string())).unwrap();
1713
1714        // Should have 2 constraints
1715        assert_eq!(set.constraints.len(), 2);
1716    }
1717
1718    #[test]
1719    fn test_git_ref_to_version_req() {
1720        let git_ref = VersionConstraint::GitRef("latest".to_string());
1721        // Git refs cannot be converted to version requirements
1722        assert!(git_ref.to_version_req().is_none());
1723
1724        let main_ref = VersionConstraint::GitRef("main".to_string());
1725        assert!(main_ref.to_version_req().is_none());
1726    }
1727
1728    // ========== Prefix Support Tests ==========
1729
1730    #[test]
1731    fn test_prefixed_constraint_parsing() {
1732        // Prefixed exact version
1733        let constraint = VersionConstraint::parse("agents-v1.0.0").unwrap();
1734        match constraint {
1735            VersionConstraint::Exact {
1736                prefix,
1737                version,
1738            } => {
1739                assert_eq!(prefix, Some("agents".to_string()));
1740                assert_eq!(version, Version::parse("1.0.0").unwrap());
1741            }
1742            _ => panic!("Expected Exact constraint"),
1743        }
1744
1745        // Prefixed requirement
1746        let constraint = VersionConstraint::parse("agents-^v1.0.0").unwrap();
1747        match constraint {
1748            VersionConstraint::Requirement {
1749                prefix,
1750                req,
1751            } => {
1752                assert_eq!(prefix, Some("agents".to_string()));
1753                assert!(req.matches(&Version::parse("1.5.0").unwrap()));
1754                assert!(!req.matches(&Version::parse("2.0.0").unwrap()));
1755            }
1756            _ => panic!("Expected Requirement constraint"),
1757        }
1758
1759        // Unprefixed constraint (backward compatible)
1760        let constraint = VersionConstraint::parse("^1.0.0").unwrap();
1761        match constraint {
1762            VersionConstraint::Requirement {
1763                prefix,
1764                ..
1765            } => {
1766                assert_eq!(prefix, None);
1767            }
1768            _ => panic!("Expected Requirement constraint"),
1769        }
1770    }
1771
1772    #[test]
1773    fn test_prefixed_constraint_display() {
1774        let prefixed_exact = VersionConstraint::Exact {
1775            prefix: Some("agents".to_string()),
1776            version: Version::parse("1.0.0").unwrap(),
1777        };
1778        assert_eq!(prefixed_exact.to_string(), "agents-1.0.0");
1779
1780        let unprefixed_exact = VersionConstraint::Exact {
1781            prefix: None,
1782            version: Version::parse("1.0.0").unwrap(),
1783        };
1784        assert_eq!(unprefixed_exact.to_string(), "1.0.0");
1785
1786        let prefixed_req = VersionConstraint::parse("snippets-^v2.0.0").unwrap();
1787        let display = prefixed_req.to_string();
1788        assert!(display.starts_with("snippets-"));
1789    }
1790
1791    #[test]
1792    fn test_matches_version_info() {
1793        use crate::version::VersionInfo;
1794
1795        // Prefixed constraint matching prefixed version
1796        let constraint = VersionConstraint::parse("agents-^v1.0.0").unwrap();
1797        let version_info = VersionInfo {
1798            prefix: Some("agents".to_string()),
1799            version: Version::parse("1.2.0").unwrap(),
1800            tag: "agents-v1.2.0".to_string(),
1801            prerelease: false,
1802        };
1803        assert!(constraint.matches_version_info(&version_info));
1804
1805        // Prefixed constraint NOT matching different prefix
1806        let wrong_prefix = VersionInfo {
1807            prefix: Some("snippets".to_string()),
1808            version: Version::parse("1.2.0").unwrap(),
1809            tag: "snippets-v1.2.0".to_string(),
1810            prerelease: false,
1811        };
1812        assert!(!constraint.matches_version_info(&wrong_prefix));
1813
1814        // Unprefixed constraint matching unprefixed version
1815        let unprefixed_constraint = VersionConstraint::parse("^1.0.0").unwrap();
1816        let unprefixed_version = VersionInfo {
1817            prefix: None,
1818            version: Version::parse("1.5.0").unwrap(),
1819            tag: "v1.5.0".to_string(),
1820            prerelease: false,
1821        };
1822        assert!(unprefixed_constraint.matches_version_info(&unprefixed_version));
1823
1824        // Unprefixed constraint NOT matching prefixed version
1825        assert!(!unprefixed_constraint.matches_version_info(&version_info));
1826    }
1827
1828    #[test]
1829    fn test_prefixed_constraint_conflicts() {
1830        let mut set = ConstraintSet::new();
1831
1832        // Add prefixed constraint
1833        set.add(VersionConstraint::parse("agents-^v1.0.0").unwrap()).unwrap();
1834
1835        // Different prefix should not conflict
1836        let result = set.add(VersionConstraint::parse("snippets-^v1.0.0").unwrap());
1837        assert!(result.is_ok());
1838
1839        // Same prefix but compatible constraints should not conflict
1840        let result = set.add(VersionConstraint::parse("agents-~v1.2.0").unwrap());
1841        assert!(result.is_ok());
1842
1843        // Different prefixes for Exact constraints
1844        let mut exact_set = ConstraintSet::new();
1845        exact_set.add(VersionConstraint::parse("agents-v1.0.0").unwrap()).unwrap();
1846
1847        // Different prefix, same version - should not conflict
1848        let result = exact_set.add(VersionConstraint::parse("snippets-v1.0.0").unwrap());
1849        assert!(result.is_ok());
1850    }
1851
1852    #[test]
1853    fn test_prefix_with_hyphens() {
1854        // Multiple hyphens in prefix
1855        let constraint = VersionConstraint::parse("my-cool-agent-v1.0.0").unwrap();
1856        match constraint {
1857            VersionConstraint::Exact {
1858                prefix,
1859                version,
1860            } => {
1861                assert_eq!(prefix, Some("my-cool-agent".to_string()));
1862                assert_eq!(version, Version::parse("1.0.0").unwrap());
1863            }
1864            _ => panic!("Expected Exact constraint"),
1865        }
1866
1867        // Prefix ending with 'v'
1868        let constraint = VersionConstraint::parse("tool-v-v1.0.0").unwrap();
1869        match constraint {
1870            VersionConstraint::Exact {
1871                prefix,
1872                version,
1873            } => {
1874                assert_eq!(prefix, Some("tool-v".to_string()));
1875                assert_eq!(version, Version::parse("1.0.0").unwrap());
1876            }
1877            _ => panic!("Expected Exact constraint"),
1878        }
1879    }
1880}