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: p1,
1003                        ..
1004                    },
1005                    VersionConstraint::Requirement {
1006                        prefix: p2,
1007                        ..
1008                    },
1009                )
1010                | (
1011                    VersionConstraint::Requirement {
1012                        prefix: p1,
1013                        ..
1014                    },
1015                    VersionConstraint::Exact {
1016                        prefix: p2,
1017                        ..
1018                    },
1019                ) => {
1020                    // Different prefixes = different namespaces, no conflict
1021                    if p1 != p2 {
1022                        // Continue to next pair
1023                    }
1024                    // Same prefix - could do more sophisticated conflict detection here
1025                }
1026                _ => {
1027                    // More sophisticated conflict detection could be added here
1028                }
1029            }
1030        }
1031        false
1032    }
1033}
1034
1035/// Manages version constraints for multiple dependencies and resolves them simultaneously.
1036///
1037/// `ConstraintResolver` coordinates version resolution across an entire dependency graph,
1038/// ensuring that all constraints are satisfied and conflicts are detected. It maintains
1039/// separate [`ConstraintSet`]s for each dependency and resolves them against available
1040/// version catalogs.
1041///
1042/// # Multi-Dependency Resolution
1043///
1044/// Unlike [`ConstraintSet`] which manages constraints for a single dependency, the
1045/// `ConstraintResolver` handles multiple dependencies simultaneously:
1046///
1047/// - Each dependency gets its own constraint set
1048/// - Constraints can be added incrementally
1049/// - Resolution happens across the entire dependency graph
1050/// - Missing dependencies are detected and reported
1051///
1052/// # Resolution Process
1053///
1054/// 1. **Collect constraints**: Gather all constraints for each dependency
1055/// 2. **Validate availability**: Ensure versions exist for all dependencies
1056/// 3. **Apply constraint sets**: Use each dependency's constraints to filter versions
1057/// 4. **Select best matches**: Choose optimal versions for each dependency
1058/// 5. **Return resolution map**: Provide final version selections
1059///
1060/// # Examples
1061///
1062/// ## Basic Multi-Dependency Resolution
1063///
1064/// ```rust,no_run
1065/// use agpm_cli::version::constraints::ConstraintResolver;
1066/// use semver::Version;
1067/// use std::collections::HashMap;
1068///
1069/// let mut resolver = ConstraintResolver::new();
1070///
1071/// // Add constraints for multiple dependencies
1072/// resolver.add_constraint("dep1", "^1.0.0")?;
1073/// resolver.add_constraint("dep2", "~2.1.0")?;
1074/// resolver.add_constraint("dep3", "main")?;
1075///
1076/// // Provide available versions for each dependency
1077/// let mut available = HashMap::new();
1078/// available.insert("dep1".to_string(), vec![Version::parse("1.5.0")?]);
1079/// available.insert("dep2".to_string(), vec![Version::parse("2.1.3")?]);
1080/// available.insert("dep3".to_string(), vec![Version::parse("3.0.0")?]);
1081///
1082/// // Resolve all dependencies
1083/// let resolved = resolver.resolve(&available)?;
1084/// assert_eq!(resolved.len(), 3);
1085/// # Ok::<(), anyhow::Error>(())
1086/// ```
1087///
1088/// ## Incremental Constraint Addition
1089///
1090/// ```rust,no_run
1091/// use agpm_cli::version::constraints::ConstraintResolver;
1092///
1093/// let mut resolver = ConstraintResolver::new();
1094///
1095/// // Add multiple constraints for the same dependency
1096/// resolver.add_constraint("my-dep", ">=1.0.0")?;
1097/// resolver.add_constraint("my-dep", "<2.0.0")?;
1098/// resolver.add_constraint("my-dep", "^1.5.0")?;
1099///
1100/// // All constraints will be combined into a single constraint set
1101/// # Ok::<(), anyhow::Error>(())
1102/// ```
1103///
1104/// # Error Conditions
1105///
1106/// The resolver reports several types of errors:
1107///
1108/// - **Missing dependencies**: A constraint exists but no versions are available
1109/// - **Unsatisfiable constraints**: No available version meets all requirements
1110/// - **Conflicting constraints**: Impossible constraint combinations
1111///
1112/// # Use Cases
1113///
1114/// This resolver is particularly useful for:
1115/// - Package managers resolving dependency graphs
1116/// - Build systems selecting compatible versions
1117/// - Configuration management ensuring consistent environments
1118/// - Update analysis determining safe upgrade paths
1119pub struct ConstraintResolver {
1120    constraints: HashMap<String, ConstraintSet>,
1121}
1122
1123impl Default for ConstraintResolver {
1124    fn default() -> Self {
1125        Self::new()
1126    }
1127}
1128
1129impl ConstraintResolver {
1130    /// Creates a new constraint resolver
1131    ///
1132    /// # Returns
1133    ///
1134    /// Returns a new `ConstraintResolver` with empty constraint and resolution maps
1135    #[must_use]
1136    pub fn new() -> Self {
1137        Self {
1138            constraints: HashMap::new(),
1139        }
1140    }
1141
1142    /// Add a version constraint for a specific dependency.
1143    ///
1144    /// This method parses the constraint string and adds it to the constraint set
1145    /// for the named dependency. If this is the first constraint for the dependency,
1146    /// a new constraint set is created. Multiple constraints for the same dependency
1147    /// are combined into a single set with conflict detection.
1148    ///
1149    /// # Arguments
1150    ///
1151    /// * `dependency` - The name of the dependency to constrain
1152    /// * `constraint` - The constraint string to parse and add (e.g., "^1.0.0", "latest")
1153    ///
1154    /// # Returns
1155    ///
1156    /// Returns `Ok(())` if the constraint was added successfully, or `Err` if:
1157    /// - The constraint string is invalid
1158    /// - The constraint conflicts with existing constraints for this dependency
1159    ///
1160    /// # Examples
1161    ///
1162    /// ```rust,no_run
1163    /// use agpm_cli::version::constraints::ConstraintResolver;
1164    ///
1165    /// let mut resolver = ConstraintResolver::new();
1166    ///
1167    /// // Add constraints for different dependencies
1168    /// resolver.add_constraint("web-framework", "^2.0.0")?;
1169    /// resolver.add_constraint("database", "~1.5.0")?;
1170    /// resolver.add_constraint("auth-lib", "main")?;
1171    ///
1172    /// // Add multiple constraints for the same dependency
1173    /// resolver.add_constraint("api-client", ">=1.0.0")?;
1174    /// resolver.add_constraint("api-client", "<2.0.0")?; // Compatible range
1175    ///
1176    /// // This would fail - conflicting exact versions
1177    /// resolver.add_constraint("my-dep", "1.0.0")?;
1178    /// let result = resolver.add_constraint("my-dep", "2.0.0");
1179    /// assert!(result.is_err());
1180    /// # Ok::<(), anyhow::Error>(())
1181    /// ```
1182    ///
1183    /// # Constraint Combination
1184    ///
1185    /// When multiple constraints are added for the same dependency, they are
1186    /// combined using AND logic. The final constraint set requires that all
1187    /// individual constraints be satisfied simultaneously.
1188    pub fn add_constraint(&mut self, dependency: &str, constraint: &str) -> Result<()> {
1189        let parsed = VersionConstraint::parse(constraint)?;
1190
1191        self.constraints.entry(dependency.to_string()).or_default().add(parsed)?;
1192
1193        Ok(())
1194    }
1195
1196    /// Resolve all dependency constraints and return the best version for each.
1197    ///
1198    /// This method performs the core resolution algorithm, taking all accumulated
1199    /// constraints and finding the best matching version for each dependency from
1200    /// the provided catalog of available versions.
1201    ///
1202    /// # Resolution Algorithm
1203    ///
1204    /// For each dependency with constraints:
1205    /// 1. **Verify availability**: Check that versions exist for the dependency
1206    /// 2. **Apply constraints**: Filter versions using the dependency's constraint set
1207    /// 3. **Select best match**: Choose the highest compatible version
1208    /// 4. **Handle prereleases**: Apply prerelease policies appropriately
1209    ///
1210    /// # Arguments
1211    ///
1212    /// * `available_versions` - Map from dependency names to lists of available versions
1213    ///
1214    /// # Returns
1215    ///
1216    /// Returns `Ok(HashMap<String, Version>)` with the resolved version for each
1217    /// dependency, or `Err` if resolution fails.
1218    ///
1219    /// # Error Conditions
1220    ///
1221    /// - **Missing dependency**: Constraint exists but no versions available
1222    /// - **No satisfying version**: Available versions don't meet constraints
1223    /// - **Internal errors**: Constraint set conflicts or parsing failures
1224    ///
1225    /// # Examples
1226    ///
1227    /// ```rust,no_run
1228    /// use agpm_cli::version::constraints::ConstraintResolver;
1229    /// use semver::Version;
1230    /// use std::collections::HashMap;
1231    ///
1232    /// let mut resolver = ConstraintResolver::new();
1233    /// resolver.add_constraint("web-server", "^1.0.0")?;
1234    /// resolver.add_constraint("database", "~2.1.0")?;
1235    ///
1236    /// // Provide version catalog
1237    /// let mut available = HashMap::new();
1238    /// available.insert(
1239    ///     "web-server".to_string(),
1240    ///     vec![
1241    ///         Version::parse("1.0.0")?,
1242    ///         Version::parse("1.2.0")?,
1243    ///         Version::parse("1.5.0")?, // Best match for ^1.0.0
1244    ///         Version::parse("2.0.0")?, // Too new
1245    ///     ],
1246    /// );
1247    /// available.insert(
1248    ///     "database".to_string(),
1249    ///     vec![
1250    ///         Version::parse("2.1.0")?,
1251    ///         Version::parse("2.1.3")?, // Best match for ~2.1.0
1252    ///         Version::parse("2.2.0")?, // Too new
1253    ///     ],
1254    /// );
1255    ///
1256    /// // Resolve dependencies
1257    /// let resolved = resolver.resolve(&available)?;
1258    /// assert_eq!(resolved["web-server"], Version::parse("1.5.0")?);
1259    /// assert_eq!(resolved["database"], Version::parse("2.1.3")?);
1260    /// # Ok::<(), anyhow::Error>(())
1261    /// ```
1262    ///
1263    /// ## Error Handling
1264    ///
1265    /// ```rust,no_run
1266    /// use agpm_cli::version::constraints::ConstraintResolver;
1267    /// use std::collections::HashMap;
1268    ///
1269    /// let mut resolver = ConstraintResolver::new();
1270    /// resolver.add_constraint("missing-dep", "^1.0.0")?;
1271    ///
1272    /// let available = HashMap::new(); // No versions provided
1273    ///
1274    /// let result = resolver.resolve(&available);
1275    /// assert!(result.is_err()); // Missing dependency error
1276    /// # Ok::<(), anyhow::Error>(())
1277    /// ```
1278    ///
1279    /// # Performance Considerations
1280    ///
1281    /// - Resolution is performed independently for each dependency
1282    /// - Version filtering and sorting may be expensive for large version lists
1283    /// - Consider pre-filtering available versions if catalogs are very large
1284    pub fn resolve(
1285        &self,
1286        available_versions: &HashMap<String, Vec<Version>>,
1287    ) -> Result<HashMap<String, Version>> {
1288        let mut resolved = HashMap::new();
1289
1290        for (dep, constraint_set) in &self.constraints {
1291            let versions = available_versions.get(dep).ok_or_else(|| AgpmError::Other {
1292                message: format!("No versions available for dependency: {dep}"),
1293            })?;
1294
1295            let best_match =
1296                constraint_set.find_best_match(versions).ok_or_else(|| AgpmError::Other {
1297                    message: format!("No version satisfies constraints for dependency: {dep}"),
1298                })?;
1299
1300            resolved.insert(dep.clone(), best_match.clone());
1301        }
1302
1303        Ok(resolved)
1304    }
1305}
1306
1307#[cfg(test)]
1308mod tests {
1309    use super::*;
1310
1311    #[test]
1312    fn test_version_constraint_parse() {
1313        // Exact version
1314        let constraint = VersionConstraint::parse("1.0.0").unwrap();
1315        assert!(matches!(constraint, VersionConstraint::Exact { .. }));
1316
1317        // Version with v prefix
1318        let constraint = VersionConstraint::parse("v1.0.0").unwrap();
1319        assert!(matches!(constraint, VersionConstraint::Exact { .. }));
1320
1321        // Caret requirement
1322        let constraint = VersionConstraint::parse("^1.0.0").unwrap();
1323        assert!(matches!(constraint, VersionConstraint::Requirement { .. }));
1324
1325        // Tilde requirement
1326        let constraint = VersionConstraint::parse("~1.2.0").unwrap();
1327        assert!(matches!(constraint, VersionConstraint::Requirement { .. }));
1328
1329        // Range requirement
1330        let constraint = VersionConstraint::parse(">=1.0.0, <2.0.0").unwrap();
1331        assert!(matches!(constraint, VersionConstraint::Requirement { .. }));
1332
1333        // Git refs (including "latest" - it's just a tag name)
1334        let constraint = VersionConstraint::parse("latest").unwrap();
1335        assert!(matches!(constraint, VersionConstraint::GitRef(_)));
1336
1337        let constraint = VersionConstraint::parse("main").unwrap();
1338        assert!(matches!(constraint, VersionConstraint::GitRef(_)));
1339    }
1340
1341    #[test]
1342    fn test_constraint_matching() {
1343        let v100 = Version::parse("1.0.0").unwrap();
1344        let v110 = Version::parse("1.1.0").unwrap();
1345        let v200 = Version::parse("2.0.0").unwrap();
1346
1347        // Exact match
1348        let exact = VersionConstraint::Exact {
1349            prefix: None,
1350            version: v100.clone(),
1351        };
1352        assert!(exact.matches(&v100));
1353        assert!(!exact.matches(&v110));
1354
1355        // Caret requirement
1356        let caret = VersionConstraint::parse("^1.0.0").unwrap();
1357        assert!(caret.matches(&v100));
1358        assert!(caret.matches(&v110));
1359        assert!(!caret.matches(&v200));
1360
1361        // Git refs don't match semantic versions
1362        let git_ref = VersionConstraint::GitRef("latest".to_string());
1363        assert!(!git_ref.matches(&v100));
1364        assert!(!git_ref.matches(&v200));
1365    }
1366
1367    #[test]
1368    fn test_constraint_set() {
1369        let mut set = ConstraintSet::new();
1370        set.add(VersionConstraint::parse(">=1.0.0").unwrap()).unwrap();
1371        set.add(VersionConstraint::parse("<2.0.0").unwrap()).unwrap();
1372
1373        let v090 = Version::parse("0.9.0").unwrap();
1374        let v100 = Version::parse("1.0.0").unwrap();
1375        let v150 = Version::parse("1.5.0").unwrap();
1376        let v200 = Version::parse("2.0.0").unwrap();
1377
1378        assert!(!set.satisfies(&v090));
1379        assert!(set.satisfies(&v100));
1380        assert!(set.satisfies(&v150));
1381        assert!(!set.satisfies(&v200));
1382    }
1383
1384    #[test]
1385    fn test_find_best_match() {
1386        let mut set = ConstraintSet::new();
1387        set.add(VersionConstraint::parse("^1.0.0").unwrap()).unwrap();
1388
1389        let versions = vec![
1390            Version::parse("0.9.0").unwrap(),
1391            Version::parse("1.0.0").unwrap(),
1392            Version::parse("1.2.0").unwrap(),
1393            Version::parse("1.5.0").unwrap(),
1394            Version::parse("2.0.0").unwrap(),
1395        ];
1396
1397        let best = set.find_best_match(&versions).unwrap();
1398        assert_eq!(best, &Version::parse("1.5.0").unwrap());
1399    }
1400
1401    #[test]
1402    fn test_constraint_conflicts() {
1403        let mut set = ConstraintSet::new();
1404
1405        // Add first exact version
1406        set.add(VersionConstraint::Exact {
1407            prefix: None,
1408            version: Version::parse("1.0.0").unwrap(),
1409        })
1410        .unwrap();
1411
1412        // Try to add conflicting exact version
1413        let result = set.add(VersionConstraint::Exact {
1414            prefix: None,
1415            version: Version::parse("2.0.0").unwrap(),
1416        });
1417        assert!(result.is_err());
1418
1419        // Adding the same version should be ok
1420        let result = set.add(VersionConstraint::Exact {
1421            prefix: None,
1422            version: Version::parse("1.0.0").unwrap(),
1423        });
1424        assert!(result.is_ok());
1425    }
1426
1427    #[test]
1428    fn test_constraint_resolver() {
1429        let mut resolver = ConstraintResolver::new();
1430
1431        resolver.add_constraint("dep1", "^1.0.0").unwrap();
1432        resolver.add_constraint("dep2", "~2.1.0").unwrap();
1433
1434        let mut available = HashMap::new();
1435        available.insert(
1436            "dep1".to_string(),
1437            vec![
1438                Version::parse("0.9.0").unwrap(),
1439                Version::parse("1.0.0").unwrap(),
1440                Version::parse("1.5.0").unwrap(),
1441                Version::parse("2.0.0").unwrap(),
1442            ],
1443        );
1444        available.insert(
1445            "dep2".to_string(),
1446            vec![
1447                Version::parse("2.0.0").unwrap(),
1448                Version::parse("2.1.0").unwrap(),
1449                Version::parse("2.1.5").unwrap(),
1450                Version::parse("2.2.0").unwrap(),
1451            ],
1452        );
1453
1454        let resolved = resolver.resolve(&available).unwrap();
1455        assert_eq!(resolved.get("dep1"), Some(&Version::parse("1.5.0").unwrap()));
1456        assert_eq!(resolved.get("dep2"), Some(&Version::parse("2.1.5").unwrap()));
1457    }
1458
1459    #[test]
1460    fn test_allows_prerelease() {
1461        assert!(VersionConstraint::GitRef("main".to_string()).allows_prerelease());
1462        assert!(VersionConstraint::GitRef("latest".to_string()).allows_prerelease()); // Git ref
1463        assert!(
1464            !VersionConstraint::Exact {
1465                prefix: None,
1466                version: Version::parse("1.0.0").unwrap()
1467            }
1468            .allows_prerelease()
1469        );
1470    }
1471
1472    #[test]
1473    fn test_version_constraint_parse_edge_cases() {
1474        // Test latest-prerelease (just a tag name)
1475        let constraint = VersionConstraint::parse("latest-prerelease").unwrap();
1476        assert!(matches!(constraint, VersionConstraint::GitRef(_)));
1477
1478        // Test asterisk wildcard
1479        let constraint = VersionConstraint::parse("*").unwrap();
1480        assert!(matches!(constraint, VersionConstraint::GitRef(_)));
1481
1482        // Test range operators
1483        let constraint = VersionConstraint::parse(">=1.0.0").unwrap();
1484        assert!(matches!(constraint, VersionConstraint::Requirement { .. }));
1485
1486        let constraint = VersionConstraint::parse("<2.0.0").unwrap();
1487        assert!(matches!(constraint, VersionConstraint::Requirement { .. }));
1488
1489        let constraint = VersionConstraint::parse("=1.0.0").unwrap();
1490        assert!(matches!(constraint, VersionConstraint::Requirement { .. }));
1491
1492        // Test git branch names
1493        let constraint = VersionConstraint::parse("feature/new-feature").unwrap();
1494        assert!(matches!(constraint, VersionConstraint::GitRef(_)));
1495
1496        // Test commit hash
1497        let constraint = VersionConstraint::parse("abc123def456").unwrap();
1498        assert!(matches!(constraint, VersionConstraint::GitRef(_)));
1499    }
1500
1501    #[test]
1502    fn test_version_constraint_display() {
1503        let exact = VersionConstraint::Exact {
1504            prefix: None,
1505            version: Version::parse("1.0.0").unwrap(),
1506        };
1507        assert_eq!(format!("{exact}"), "1.0.0");
1508
1509        let req = VersionConstraint::parse("^1.0.0").unwrap();
1510        assert_eq!(format!("{req}"), "^1.0.0");
1511
1512        let git_ref = VersionConstraint::GitRef("main".to_string());
1513        assert_eq!(format!("{git_ref}"), "main");
1514
1515        let latest = VersionConstraint::GitRef("latest".to_string());
1516        assert_eq!(format!("{latest}"), "latest");
1517    }
1518
1519    #[test]
1520    fn test_version_constraint_matches_ref() {
1521        let git_ref = VersionConstraint::GitRef("main".to_string());
1522        assert!(git_ref.matches_ref("main"));
1523        assert!(!git_ref.matches_ref("develop"));
1524
1525        // Other constraint types should return false for ref matching
1526        let exact = VersionConstraint::Exact {
1527            prefix: None,
1528            version: Version::parse("1.0.0").unwrap(),
1529        };
1530        assert!(!exact.matches_ref("v1.0.0"));
1531
1532        let latest = VersionConstraint::GitRef("latest".to_string());
1533        assert!(latest.matches_ref("latest"));
1534    }
1535
1536    #[test]
1537    fn test_version_constraint_to_version_req() {
1538        let exact = VersionConstraint::Exact {
1539            prefix: None,
1540            version: Version::parse("1.0.0").unwrap(),
1541        };
1542        let req = exact.to_version_req().unwrap();
1543        assert!(req.matches(&Version::parse("1.0.0").unwrap()));
1544
1545        let caret = VersionConstraint::parse("^1.0.0").unwrap();
1546        let req = caret.to_version_req().unwrap();
1547        assert!(req.matches(&Version::parse("1.0.0").unwrap()));
1548
1549        let git_ref = VersionConstraint::GitRef("main".to_string());
1550        assert!(git_ref.to_version_req().is_none());
1551
1552        let latest = VersionConstraint::GitRef("latest".to_string());
1553        assert!(latest.to_version_req().is_none()); // Git ref - cannot convert
1554    }
1555
1556    #[test]
1557    fn test_constraint_set_with_prereleases() {
1558        let mut set = ConstraintSet::new();
1559        set.add(VersionConstraint::GitRef("main".to_string())).unwrap();
1560
1561        let v100_pre = Version::parse("1.0.0-alpha.1").unwrap();
1562        let v100 = Version::parse("1.0.0").unwrap();
1563
1564        assert!(set.allows_prerelease());
1565
1566        // Git refs don't match semver versions
1567        let versions = vec![v100_pre.clone(), v100.clone()];
1568        let best = set.find_best_match(&versions);
1569        assert!(best.is_none()); // Git refs don't match semver
1570    }
1571
1572    #[test]
1573    fn test_constraint_set_no_matches() {
1574        let mut set = ConstraintSet::new();
1575        set.add(VersionConstraint::parse(">=2.0.0").unwrap()).unwrap();
1576
1577        let versions = vec![Version::parse("1.0.0").unwrap(), Version::parse("1.5.0").unwrap()];
1578
1579        let best = set.find_best_match(&versions);
1580        assert!(best.is_none());
1581    }
1582
1583    #[test]
1584    fn test_constraint_resolver_missing_dependency() {
1585        let mut resolver = ConstraintResolver::new();
1586        resolver.add_constraint("dep1", "^1.0.0").unwrap();
1587
1588        let available = HashMap::new(); // No versions available
1589
1590        let result = resolver.resolve(&available);
1591        assert!(result.is_err());
1592    }
1593
1594    #[test]
1595    fn test_constraint_resolver_no_satisfying_version() {
1596        let mut resolver = ConstraintResolver::new();
1597        resolver.add_constraint("dep1", "^2.0.0").unwrap();
1598
1599        let mut available = HashMap::new();
1600        available.insert(
1601            "dep1".to_string(),
1602            vec![Version::parse("1.0.0").unwrap()], // Only 1.x available, but we need 2.x
1603        );
1604
1605        let result = resolver.resolve(&available);
1606        assert!(result.is_err());
1607    }
1608
1609    #[test]
1610    fn test_constraint_set_git_ref_conflicts() {
1611        let mut set = ConstraintSet::new();
1612
1613        // Add first git ref
1614        set.add(VersionConstraint::GitRef("main".to_string())).unwrap();
1615
1616        // Try to add conflicting git ref
1617        let result = set.add(VersionConstraint::GitRef("develop".to_string()));
1618        assert!(result.is_err());
1619
1620        // Adding the same ref should be ok
1621        let result = set.add(VersionConstraint::GitRef("main".to_string()));
1622        assert!(result.is_ok());
1623    }
1624
1625    #[test]
1626    fn test_git_ref_constraint_with_versions() {
1627        let git_ref = VersionConstraint::GitRef("latest".to_string());
1628
1629        let v100_pre = Version::parse("1.0.0-alpha.1").unwrap();
1630        let v100 = Version::parse("1.0.0").unwrap();
1631
1632        // Git refs don't match semantic versions
1633        assert!(!git_ref.matches(&v100));
1634        assert!(!git_ref.matches(&v100_pre));
1635    }
1636
1637    #[test]
1638    fn test_git_ref_allows_prereleases() {
1639        let git_ref = VersionConstraint::GitRef("latest".to_string());
1640
1641        // Git refs allow prereleases (they reference commits)
1642        assert!(git_ref.allows_prerelease());
1643
1644        let main_ref = VersionConstraint::GitRef("main".to_string());
1645        assert!(main_ref.allows_prerelease());
1646    }
1647
1648    #[test]
1649    fn test_requirement_constraint_allows_prerelease() {
1650        let req = VersionConstraint::parse("^1.0.0").unwrap();
1651        assert!(!req.allows_prerelease());
1652
1653        let exact = VersionConstraint::Exact {
1654            prefix: None,
1655            version: Version::parse("1.0.0").unwrap(),
1656        };
1657        assert!(!exact.allows_prerelease());
1658    }
1659
1660    #[test]
1661    fn test_constraint_set_prerelease_filtering() {
1662        let mut set = ConstraintSet::new();
1663        set.add(VersionConstraint::parse("^1.0.0").unwrap()).unwrap();
1664
1665        let versions = vec![
1666            Version::parse("1.0.0-alpha.1").unwrap(),
1667            Version::parse("1.0.0").unwrap(),
1668            Version::parse("1.1.0-beta.1").unwrap(),
1669            Version::parse("1.1.0").unwrap(),
1670        ];
1671
1672        let best = set.find_best_match(&versions).unwrap();
1673        assert_eq!(best, &Version::parse("1.1.0").unwrap()); // Should pick highest stable
1674    }
1675
1676    #[test]
1677    fn test_parse_with_whitespace() {
1678        let constraint = VersionConstraint::parse("  1.0.0  ").unwrap();
1679        assert!(matches!(constraint, VersionConstraint::Exact { .. }));
1680
1681        let constraint = VersionConstraint::parse("  latest  ").unwrap();
1682        assert!(matches!(constraint, VersionConstraint::GitRef(_))); // Just a git ref
1683
1684        let constraint = VersionConstraint::parse("  ^1.0.0  ").unwrap();
1685        assert!(matches!(constraint, VersionConstraint::Requirement { .. }));
1686    }
1687
1688    #[test]
1689    fn test_constraint_resolver_add_constraint_error() {
1690        let mut resolver = ConstraintResolver::new();
1691
1692        // Add a valid constraint first
1693        resolver.add_constraint("dep1", "1.0.0").unwrap();
1694
1695        // Add conflicting constraint
1696        let result = resolver.add_constraint("dep1", "2.0.0");
1697        assert!(result.is_err());
1698    }
1699
1700    #[test]
1701    fn test_constraint_set_no_conflict_different_types() {
1702        let mut set = ConstraintSet::new();
1703
1704        // These shouldn't conflict as they are different types
1705        set.add(VersionConstraint::parse("^1.0.0").unwrap()).unwrap();
1706        set.add(VersionConstraint::GitRef("main".to_string())).unwrap();
1707
1708        // Should have 2 constraints
1709        assert_eq!(set.constraints.len(), 2);
1710    }
1711
1712    #[test]
1713    fn test_git_ref_to_version_req() {
1714        let git_ref = VersionConstraint::GitRef("latest".to_string());
1715        // Git refs cannot be converted to version requirements
1716        assert!(git_ref.to_version_req().is_none());
1717
1718        let main_ref = VersionConstraint::GitRef("main".to_string());
1719        assert!(main_ref.to_version_req().is_none());
1720    }
1721
1722    // ========== Prefix Support Tests ==========
1723
1724    #[test]
1725    fn test_prefixed_constraint_parsing() {
1726        // Prefixed exact version
1727        let constraint = VersionConstraint::parse("agents-v1.0.0").unwrap();
1728        match constraint {
1729            VersionConstraint::Exact {
1730                prefix,
1731                version,
1732            } => {
1733                assert_eq!(prefix, Some("agents".to_string()));
1734                assert_eq!(version, Version::parse("1.0.0").unwrap());
1735            }
1736            _ => panic!("Expected Exact constraint"),
1737        }
1738
1739        // Prefixed requirement
1740        let constraint = VersionConstraint::parse("agents-^v1.0.0").unwrap();
1741        match constraint {
1742            VersionConstraint::Requirement {
1743                prefix,
1744                req,
1745            } => {
1746                assert_eq!(prefix, Some("agents".to_string()));
1747                assert!(req.matches(&Version::parse("1.5.0").unwrap()));
1748                assert!(!req.matches(&Version::parse("2.0.0").unwrap()));
1749            }
1750            _ => panic!("Expected Requirement constraint"),
1751        }
1752
1753        // Unprefixed constraint (backward compatible)
1754        let constraint = VersionConstraint::parse("^1.0.0").unwrap();
1755        match constraint {
1756            VersionConstraint::Requirement {
1757                prefix,
1758                ..
1759            } => {
1760                assert_eq!(prefix, None);
1761            }
1762            _ => panic!("Expected Requirement constraint"),
1763        }
1764    }
1765
1766    #[test]
1767    fn test_prefixed_constraint_display() {
1768        let prefixed_exact = VersionConstraint::Exact {
1769            prefix: Some("agents".to_string()),
1770            version: Version::parse("1.0.0").unwrap(),
1771        };
1772        assert_eq!(prefixed_exact.to_string(), "agents-1.0.0");
1773
1774        let unprefixed_exact = VersionConstraint::Exact {
1775            prefix: None,
1776            version: Version::parse("1.0.0").unwrap(),
1777        };
1778        assert_eq!(unprefixed_exact.to_string(), "1.0.0");
1779
1780        let prefixed_req = VersionConstraint::parse("snippets-^v2.0.0").unwrap();
1781        let display = prefixed_req.to_string();
1782        assert!(display.starts_with("snippets-"));
1783    }
1784
1785    #[test]
1786    fn test_matches_version_info() {
1787        use crate::version::VersionInfo;
1788
1789        // Prefixed constraint matching prefixed version
1790        let constraint = VersionConstraint::parse("agents-^v1.0.0").unwrap();
1791        let version_info = VersionInfo {
1792            prefix: Some("agents".to_string()),
1793            version: Version::parse("1.2.0").unwrap(),
1794            tag: "agents-v1.2.0".to_string(),
1795            prerelease: false,
1796        };
1797        assert!(constraint.matches_version_info(&version_info));
1798
1799        // Prefixed constraint NOT matching different prefix
1800        let wrong_prefix = VersionInfo {
1801            prefix: Some("snippets".to_string()),
1802            version: Version::parse("1.2.0").unwrap(),
1803            tag: "snippets-v1.2.0".to_string(),
1804            prerelease: false,
1805        };
1806        assert!(!constraint.matches_version_info(&wrong_prefix));
1807
1808        // Unprefixed constraint matching unprefixed version
1809        let unprefixed_constraint = VersionConstraint::parse("^1.0.0").unwrap();
1810        let unprefixed_version = VersionInfo {
1811            prefix: None,
1812            version: Version::parse("1.5.0").unwrap(),
1813            tag: "v1.5.0".to_string(),
1814            prerelease: false,
1815        };
1816        assert!(unprefixed_constraint.matches_version_info(&unprefixed_version));
1817
1818        // Unprefixed constraint NOT matching prefixed version
1819        assert!(!unprefixed_constraint.matches_version_info(&version_info));
1820    }
1821
1822    #[test]
1823    fn test_prefixed_constraint_conflicts() {
1824        let mut set = ConstraintSet::new();
1825
1826        // Add prefixed constraint
1827        set.add(VersionConstraint::parse("agents-^v1.0.0").unwrap()).unwrap();
1828
1829        // Different prefix should not conflict
1830        let result = set.add(VersionConstraint::parse("snippets-^v1.0.0").unwrap());
1831        assert!(result.is_ok());
1832
1833        // Same prefix but compatible constraints should not conflict
1834        let result = set.add(VersionConstraint::parse("agents-~v1.2.0").unwrap());
1835        assert!(result.is_ok());
1836
1837        // Different prefixes for Exact constraints
1838        let mut exact_set = ConstraintSet::new();
1839        exact_set.add(VersionConstraint::parse("agents-v1.0.0").unwrap()).unwrap();
1840
1841        // Different prefix, same version - should not conflict
1842        let result = exact_set.add(VersionConstraint::parse("snippets-v1.0.0").unwrap());
1843        assert!(result.is_ok());
1844    }
1845
1846    #[test]
1847    fn test_prefix_with_hyphens() {
1848        // Multiple hyphens in prefix
1849        let constraint = VersionConstraint::parse("my-cool-agent-v1.0.0").unwrap();
1850        match constraint {
1851            VersionConstraint::Exact {
1852                prefix,
1853                version,
1854            } => {
1855                assert_eq!(prefix, Some("my-cool-agent".to_string()));
1856                assert_eq!(version, Version::parse("1.0.0").unwrap());
1857            }
1858            _ => panic!("Expected Exact constraint"),
1859        }
1860
1861        // Prefix ending with 'v'
1862        let constraint = VersionConstraint::parse("tool-v-v1.0.0").unwrap();
1863        match constraint {
1864            VersionConstraint::Exact {
1865                prefix,
1866                version,
1867            } => {
1868                assert_eq!(prefix, Some("tool-v".to_string()));
1869                assert_eq!(version, Version::parse("1.0.0").unwrap());
1870            }
1871            _ => panic!("Expected Exact constraint"),
1872        }
1873    }
1874}