use std::borrow::Cow;
use std::fmt;
use std::hash::{Hash, Hasher};
use std::str::FromStr;

use indexmap::IndexMap;
use ref_cast::RefCast;
use reqwest_retry::policies::ExponentialBackoff;
use tracing::{debug, info};
use uv_warnings::warn_user;

use uv_cache::Cache;
use uv_client::{BaseClient, BaseClientBuilder};
use uv_pep440::{Prerelease, Version};
use uv_platform::{Arch, Libc, Os, Platform};
use uv_preview::Preview;

use crate::discovery::{
    EnvironmentPreference, PythonRequest, find_best_python_installation, find_python_installation,
};
use crate::downloads::{
    DownloadResult, ManagedPythonDownload, ManagedPythonDownloadList, PythonDownloadRequest,
    Reporter,
};
use crate::implementation::LenientImplementationName;
use crate::managed::{ManagedPythonInstallation, ManagedPythonInstallations};
use crate::{
    Error, ImplementationName, Interpreter, MissingPythonHint, PythonDownloads, PythonPreference,
    PythonSource, PythonVariant, PythonVersion, downloads,
};

/// A Python interpreter and accompanying tools.
#[derive(Clone, Debug)]
pub struct PythonInstallation {
    // Public in the crate for test assertions
    pub(crate) source: PythonSource,
    pub(crate) interpreter: Interpreter,
}

impl PythonInstallation {
    /// Create a new [`PythonInstallation`] from a source and interpreter.
    pub fn new(source: PythonSource, interpreter: Interpreter) -> Self {
        Self {
            source,
            interpreter,
        }
    }

    /// Find an installed [`PythonInstallation`].
    ///
    /// This is the standard interface for discovering a Python installation for creating
    /// an environment. If interested in finding an existing environment, see
    /// [`PythonEnvironment::find`] instead.
    ///
    /// Note we still require an [`EnvironmentPreference`] as this can either bypass virtual environments
    /// or prefer them. In most cases, this should be [`EnvironmentPreference::OnlySystem`]
    /// but if you want to allow an interpreter from a virtual environment if it satisfies the request,
    /// then use [`EnvironmentPreference::Any`].
    ///
    /// See [`find_installation`] for implementation details.
    pub fn find(
        request: &PythonRequest,
        environments: EnvironmentPreference,
        preference: PythonPreference,
        download_list: &ManagedPythonDownloadList,
        cache: &Cache,
        preview: Preview,
    ) -> Result<Self, Error> {
        let installation = Self::find_existing(request, environments, preference, cache, preview)?;
        installation.warn_if_outdated_prerelease(request, download_list);
        Ok(installation)
    }

    /// Find an existing [`PythonInstallation`].
    pub fn find_existing(
        request: &PythonRequest,
        environments: EnvironmentPreference,
        preference: PythonPreference,
        cache: &Cache,
        preview: Preview,
    ) -> Result<Self, Error> {
        Ok(find_python_installation(
            request,
            environments,
            preference,
            cache,
            preview,
        )??)
    }

    /// Find or download a [`PythonInstallation`] that satisfies a requested version, if the request
    /// cannot be satisfied, fallback to the best available Python installation.
    pub async fn find_best(
        request: &PythonRequest,
        environments: EnvironmentPreference,
        preference: PythonPreference,
        python_downloads: PythonDownloads,
        client_builder: &BaseClientBuilder<'_>,
        cache: &Cache,
        reporter: Option<&dyn Reporter>,
        python_install_mirror: Option<&str>,
        pypy_install_mirror: Option<&str>,
        python_downloads_json_url: Option<&str>,
        preview: Preview,
    ) -> Result<Self, Error> {
        let downloads_enabled = preference.allows_managed()
            && python_downloads.is_automatic()
            && client_builder.connectivity.is_online();
        let installation = find_best_python_installation(
            request,
            environments,
            preference,
            downloads_enabled,
            client_builder,
            cache,
            reporter,
            python_install_mirror,
            pypy_install_mirror,
            python_downloads_json_url,
            preview,
        )
        .await?;
        installation
            .download_and_warn_if_outdated_prerelease(
                request,
                client_builder,
                python_downloads_json_url,
            )
            .await?;
        Ok(installation)
    }

    /// Find or fetch a [`PythonInstallation`].
    ///
    /// Unlike [`PythonInstallation::find`], if the required Python is not installed it will be installed automatically.
    pub async fn find_or_download(
        request: Option<&PythonRequest>,
        environments: EnvironmentPreference,
        preference: PythonPreference,
        python_downloads: PythonDownloads,
        client_builder: &BaseClientBuilder<'_>,
        cache: &Cache,
        reporter: Option<&dyn Reporter>,
        python_install_mirror: Option<&str>,
        pypy_install_mirror: Option<&str>,
        python_downloads_json_url: Option<&str>,
        preview: Preview,
    ) -> Result<Self, Error> {
        let request = request.unwrap_or(&PythonRequest::Default);

        let err = match Self::find_existing(request, environments, preference, cache, preview) {
            Ok(installation) => {
                installation
                    .download_and_warn_if_outdated_prerelease(
                        request,
                        client_builder,
                        python_downloads_json_url,
                    )
                    .await?;
                return Ok(installation);
            }
            Err(err) => err,
        };

        match err {
            // If Python is missing, we should attempt a download
            Error::MissingPython(..) => {}
            // If we raised a non-critical error, we should attempt a download
            Error::Discovery(ref err) if !err.is_critical() => {}
            // Otherwise, this is fatal
            _ => return Err(err),
        }

        // If we can't convert the request to a download, throw the original error
        let Some(download_request) = PythonDownloadRequest::from_request(request) else {
            return Err(err);
        };

        let download_list_client = client_builder.build()?;
        let download_list =
            ManagedPythonDownloadList::new(&download_list_client, python_downloads_json_url)
                .await?;

        let downloads_enabled = preference.allows_managed()
            && python_downloads.is_automatic()
            && client_builder.connectivity.is_online();

        let download = download_request
            .clone()
            .fill()
            .map(|request| download_list.find(&request));

        // Regardless of whether downloads are enabled, we want to determine if the download is
        // available to power error messages. However, if downloads aren't enabled, we don't want to
        // report any errors related to them.
        let download = match download {
            Ok(Ok(download)) => Some(download),
            // If the download cannot be found, return the _original_ discovery error
            Ok(Err(downloads::Error::NoDownloadFound(_))) => {
                if downloads_enabled {
                    debug!("No downloads are available for {request}");
                    if matches!(request, PythonRequest::Default | PythonRequest::Any) {
                        return Err(err);
                    }
                    return Err(err.with_hint(MissingPythonHint::RequiresUpdate));
                }
                None
            }
            Err(err) | Ok(Err(err)) => {
                if downloads_enabled {
                    // We failed to determine the platform information
                    return Err(err.into());
                }
                None
            }
        };

        let Some(download) = download else {
            // N.B. We should only be in this case when downloads are disabled; when downloads are
            // enabled, we should fail eagerly when something goes wrong with the download.
            debug_assert!(!downloads_enabled);
            return Err(err);
        };

        // If the download is available, but not usable, we attach a hint to the original error.
        if !downloads_enabled {
            match python_downloads {
                PythonDownloads::Automatic => {}
                PythonDownloads::Manual => {
                    return Err(err.with_hint(MissingPythonHint::DownloadsManual(request.clone())));
                }
                PythonDownloads::Never => {
                    return Err(err.with_hint(MissingPythonHint::DownloadsNever(request.clone())));
                }
            }

            match preference {
                PythonPreference::OnlySystem => {
                    return Err(
                        err.with_hint(MissingPythonHint::PreferenceOnlySystem(request.clone()))
                    );
                }
                PythonPreference::Managed
                | PythonPreference::OnlyManaged
                | PythonPreference::System => {}
            }

            if !client_builder.connectivity.is_online() {
                return Err(err.with_hint(MissingPythonHint::Offline(request.clone())));
            }

            return Err(err);
        }

        // Python downloads are performing their own retries to catch stream errors, disable the
        // default retries to avoid the middleware performing uncontrolled retries.
        let retry_policy = client_builder.retry_policy();
        let download_client = client_builder.clone().retries(0).build()?;

        let installation = Self::fetch(
            download,
            &download_client,
            &retry_policy,
            cache,
            reporter,
            python_install_mirror,
            pypy_install_mirror,
        )
        .await?;

        installation.warn_if_outdated_prerelease(request, &download_list);

        Ok(installation)
    }

    /// Download and install the requested installation.
    pub async fn fetch(
        download: &ManagedPythonDownload,
        client: &BaseClient,
        retry_policy: &ExponentialBackoff,
        cache: &Cache,
        reporter: Option<&dyn Reporter>,
        python_install_mirror: Option<&str>,
        pypy_install_mirror: Option<&str>,
    ) -> Result<Self, Error> {
        let installations = ManagedPythonInstallations::from_settings(None)?.init()?;
        let installations_dir = installations.root();
        let scratch_dir = installations.scratch();
        let _lock = installations.lock().await?;

        info!("Fetching requested Python...");
        let result = download
            .fetch_with_retry(
                client,
                retry_policy,
                installations_dir,
                &scratch_dir,
                false,
                python_install_mirror,
                pypy_install_mirror,
                reporter,
            )
            .await?;

        let path = match result {
            DownloadResult::AlreadyAvailable(path) => path,
            DownloadResult::Fetched(path) => path,
        };

        let installed = ManagedPythonInstallation::new(path, download);
        installed.ensure_externally_managed()?;
        installed.ensure_sysconfig_patched()?;
        installed.ensure_canonical_executables()?;
        installed.ensure_build_file()?;

        let minor_version = installed.minor_version_key();
        let highest_patch = installations
            .find_all()?
            .filter(|installation| installation.minor_version_key() == minor_version)
            .filter_map(|installation| installation.version().patch())
            .fold(0, std::cmp::max);
        if installed
            .version()
            .patch()
            .is_some_and(|p| p >= highest_patch)
        {
            installed.ensure_minor_version_link()?;
        }

        if let Err(e) = installed.ensure_dylib_patched() {
            e.warn_user(&installed);
        }

        Ok(Self {
            source: PythonSource::Managed,
            interpreter: Interpreter::query(installed.executable(false), cache)?,
        })
    }

    /// Create a [`PythonInstallation`] from an existing [`Interpreter`].
    pub fn from_interpreter(interpreter: Interpreter) -> Self {
        Self {
            source: PythonSource::ProvidedPath,
            interpreter,
        }
    }

    /// Return the [`PythonSource`] of the Python installation, indicating where it was found.
    pub fn source(&self) -> &PythonSource {
        &self.source
    }

    pub fn key(&self) -> PythonInstallationKey {
        self.interpreter.key()
    }

    /// Return the Python [`Version`] of the Python installation as reported by its interpreter.
    pub fn python_version(&self) -> &Version {
        self.interpreter.python_version()
    }

    /// Return the [`LenientImplementationName`] of the Python installation as reported by its interpreter.
    pub fn implementation(&self) -> LenientImplementationName {
        LenientImplementationName::from(self.interpreter.implementation_name())
    }

    /// Returns `true` if this is a managed (uv-installed) Python installation.
    ///
    /// Uses the source as a fast path, then falls back to checking the interpreter's base prefix.
    pub fn is_managed(&self) -> bool {
        self.source.is_managed() || self.interpreter.is_managed()
    }

    /// Whether this is a CPython installation.
    ///
    /// Returns false if it is an alternative implementation, e.g., PyPy.
    pub(crate) fn is_alternative_implementation(&self) -> bool {
        !matches!(
            self.implementation(),
            LenientImplementationName::Known(ImplementationName::CPython)
        ) || self.os().is_emscripten()
    }

    /// Return the [`Arch`] of the Python installation as reported by its interpreter.
    pub fn arch(&self) -> Arch {
        self.interpreter.arch()
    }

    /// Return the [`Libc`] of the Python installation as reported by its interpreter.
    pub fn libc(&self) -> Libc {
        self.interpreter.libc()
    }

    /// Return the [`Os`] of the Python installation as reported by its interpreter.
    pub fn os(&self) -> Os {
        self.interpreter.os()
    }

    /// Return the [`Interpreter`] for the Python installation.
    pub fn interpreter(&self) -> &Interpreter {
        &self.interpreter
    }

    /// Consume the [`PythonInstallation`] and return the [`Interpreter`].
    pub fn into_interpreter(self) -> Interpreter {
        self.interpreter
    }

    /// Return `true` when checking for an outdated managed prerelease warning may be necessary.
    fn should_check_outdated_prerelease_warning(&self, request: &PythonRequest) -> bool {
        if request.allows_prereleases() {
            return false;
        }

        let interpreter = self.interpreter();

        if interpreter.python_version().pre().is_none() {
            return false;
        }

        if !interpreter.is_managed() {
            return false;
        }

        // Transparent upgrades only exist for CPython, so skip the warning for other
        // managed implementations.
        //
        // See: https://github.com/astral-sh/uv/issues/16675
        if !interpreter
            .implementation_name()
            .eq_ignore_ascii_case("cpython")
        {
            return false;
        }

        true
    }

    /// Emit a warning when the interpreter is a managed prerelease and a matching stable
    /// build can be installed via `uv python upgrade`.
    fn warn_if_outdated_prerelease(
        &self,
        request: &PythonRequest,
        download_list: &ManagedPythonDownloadList,
    ) {
        if !self.should_check_outdated_prerelease_warning(request) {
            return;
        }

        let interpreter = self.interpreter();
        let version = interpreter.python_version();

        let release = version.only_release();

        let Ok(download_request) = PythonDownloadRequest::try_from(&interpreter.key()) else {
            return;
        };

        let download_request = download_request.with_prereleases(false);

        let has_stable_download = {
            let mut downloads = download_list.iter_matching(&download_request);

            downloads.any(|download| {
                let download_version = download.key().version().into_version();
                download_version.pre().is_none() && download_version.only_release() >= release
            })
        };

        if !has_stable_download {
            return;
        }

        if let Some(upgrade_request) = download_request
            .unset_defaults()
            .without_patch()
            .simplified_display()
        {
            warn_user!(
                "You're using a pre-release version of Python ({}) but a stable version is available. Use `uv python upgrade {}` to upgrade.",
                version,
                upgrade_request
            );
        } else {
            warn_user!(
                "You're using a pre-release version of Python ({}) but a stable version is available. Run `uv python upgrade` to update your managed interpreters.",
                version,
            );
        }
    }

    /// Emit a warning when the interpreter is a managed prerelease and a matching stable
    /// build can be installed via `uv python upgrade`.
    ///
    /// Avoids loading the Python download list unless the discovered interpreter could require
    /// the warning.
    pub async fn download_and_warn_if_outdated_prerelease(
        &self,
        request: &PythonRequest,
        client_builder: &BaseClientBuilder<'_>,
        python_downloads_json_url: Option<&str>,
    ) -> Result<(), Error> {
        if !self.should_check_outdated_prerelease_warning(request) {
            return Ok(());
        }

        let download_list_client = client_builder.build()?;
        let download_list =
            ManagedPythonDownloadList::new(&download_list_client, python_downloads_json_url)
                .await?;
        self.warn_if_outdated_prerelease(request, &download_list);

        Ok(())
    }
}

#[derive(Error, Debug)]
pub enum PythonInstallationKeyError {
    #[error("Failed to parse Python installation key `{0}`: {1}")]
    ParseError(String, String),
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct PythonInstallationKey {
    pub(crate) implementation: LenientImplementationName,
    pub(crate) major: u8,
    pub(crate) minor: u8,
    pub(crate) patch: u8,
    pub(crate) prerelease: Option<Prerelease>,
    pub(crate) platform: Platform,
    pub(crate) variant: PythonVariant,
}

impl PythonInstallationKey {
    pub fn new(
        implementation: LenientImplementationName,
        major: u8,
        minor: u8,
        patch: u8,
        prerelease: Option<Prerelease>,
        platform: Platform,
        variant: PythonVariant,
    ) -> Self {
        Self {
            implementation,
            major,
            minor,
            patch,
            prerelease,
            platform,
            variant,
        }
    }

    pub(crate) fn new_from_version(
        implementation: LenientImplementationName,
        version: &PythonVersion,
        platform: Platform,
        variant: PythonVariant,
    ) -> Self {
        Self {
            implementation,
            major: version.major(),
            minor: version.minor(),
            patch: version.patch().unwrap_or_default(),
            prerelease: version.pre(),
            platform,
            variant,
        }
    }

    pub fn implementation(&self) -> Cow<'_, LenientImplementationName> {
        if self.os().is_emscripten() {
            Cow::Owned(LenientImplementationName::from(ImplementationName::Pyodide))
        } else {
            Cow::Borrowed(&self.implementation)
        }
    }

    pub fn version(&self) -> PythonVersion {
        PythonVersion::from_str(&format!(
            "{}.{}.{}{}",
            self.major,
            self.minor,
            self.patch,
            self.prerelease
                .map(|pre| pre.to_string())
                .unwrap_or_default()
        ))
        .expect("Python installation keys must have valid Python versions")
    }

    /// The version in `x.y.z` format.
    #[cfg(windows)]
    pub(crate) fn sys_version(&self) -> String {
        format!("{}.{}.{}", self.major, self.minor, self.patch)
    }

    pub fn major(&self) -> u8 {
        self.major
    }

    pub fn minor(&self) -> u8 {
        self.minor
    }

    pub fn prerelease(&self) -> Option<Prerelease> {
        self.prerelease
    }

    pub fn platform(&self) -> &Platform {
        &self.platform
    }

    pub fn arch(&self) -> &Arch {
        &self.platform.arch
    }

    pub fn os(&self) -> &Os {
        &self.platform.os
    }

    pub fn libc(&self) -> &Libc {
        &self.platform.libc
    }

    pub fn variant(&self) -> &PythonVariant {
        &self.variant
    }

    /// Return a canonical name for a minor versioned executable.
    pub fn executable_name_minor(&self) -> String {
        format!(
            "{name}{maj}.{min}{var}{exe}",
            name = self.implementation().executable_install_name(),
            maj = self.major,
            min = self.minor,
            var = self.variant.executable_suffix(),
            exe = std::env::consts::EXE_SUFFIX
        )
    }

    /// Return a canonical name for a major versioned executable.
    pub fn executable_name_major(&self) -> String {
        format!(
            "{name}{maj}{var}{exe}",
            name = self.implementation().executable_install_name(),
            maj = self.major,
            var = self.variant.executable_suffix(),
            exe = std::env::consts::EXE_SUFFIX
        )
    }

    /// Return a canonical name for an un-versioned executable.
    pub fn executable_name(&self) -> String {
        format!(
            "{name}{var}{exe}",
            name = self.implementation().executable_install_name(),
            var = self.variant.executable_suffix(),
            exe = std::env::consts::EXE_SUFFIX
        )
    }
}

impl fmt::Display for PythonInstallationKey {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let variant = match self.variant {
            PythonVariant::Default => String::new(),
            _ => format!("+{}", self.variant),
        };
        write!(
            f,
            "{}-{}.{}.{}{}{}-{}",
            self.implementation(),
            self.major,
            self.minor,
            self.patch,
            self.prerelease
                .map(|pre| pre.to_string())
                .unwrap_or_default(),
            variant,
            self.platform
        )
    }
}

impl FromStr for PythonInstallationKey {
    type Err = PythonInstallationKeyError;

    fn from_str(key: &str) -> Result<Self, Self::Err> {
        let parts = key.split('-').collect::<Vec<_>>();

        // We need exactly implementation-version-os-arch-libc
        if parts.len() != 5 {
            return Err(PythonInstallationKeyError::ParseError(
                key.to_string(),
                format!(
                    "expected exactly 5 `-`-separated values, got {}",
                    parts.len()
                ),
            ));
        }

        let [implementation_str, version_str, os, arch, libc] = parts.as_slice() else {
            unreachable!()
        };

        let implementation = LenientImplementationName::from(*implementation_str);

        let (version, variant) = match version_str.split_once('+') {
            Some((version, variant)) => {
                let variant = PythonVariant::from_str(variant).map_err(|()| {
                    PythonInstallationKeyError::ParseError(
                        key.to_string(),
                        format!("invalid Python variant: {variant}"),
                    )
                })?;
                (version, variant)
            }
            None => (*version_str, PythonVariant::Default),
        };

        let version = PythonVersion::from_str(version).map_err(|err| {
            PythonInstallationKeyError::ParseError(
                key.to_string(),
                format!("invalid Python version: {err}"),
            )
        })?;

        let platform = Platform::from_parts(os, arch, libc).map_err(|err| {
            PythonInstallationKeyError::ParseError(
                key.to_string(),
                format!("invalid platform: {err}"),
            )
        })?;

        Ok(Self {
            implementation,
            major: version.major(),
            minor: version.minor(),
            patch: version.patch().unwrap_or_default(),
            prerelease: version.pre(),
            platform,
            variant,
        })
    }
}

impl PartialOrd for PythonInstallationKey {
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for PythonInstallationKey {
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
        self.implementation
            .cmp(&other.implementation)
            .then_with(|| self.version().cmp(&other.version()))
            // Platforms are sorted in preferred order for the target
            .then_with(|| self.platform.cmp(&other.platform).reverse())
            // Python variants are sorted in preferred order, with `Default` first
            .then_with(|| self.variant.cmp(&other.variant).reverse())
    }
}

/// A view into a [`PythonInstallationKey`] that excludes the patch and prerelease versions.
#[derive(Clone, Eq, Ord, PartialOrd, RefCast)]
#[repr(transparent)]
pub struct PythonInstallationMinorVersionKey(PythonInstallationKey);

impl PythonInstallationMinorVersionKey {
    /// Cast a `&PythonInstallationKey` to a `&PythonInstallationMinorVersionKey` using ref-cast.
    #[inline]
    pub fn ref_cast(key: &PythonInstallationKey) -> &Self {
        RefCast::ref_cast(key)
    }

    /// Takes an [`IntoIterator`] of [`ManagedPythonInstallation`]s and returns an [`FxHashMap`] from
    /// [`PythonInstallationMinorVersionKey`] to the installation with highest [`PythonInstallationKey`]
    /// for that minor version key.
    #[inline]
    pub fn highest_installations_by_minor_version_key<'a, I>(
        installations: I,
    ) -> IndexMap<Self, ManagedPythonInstallation>
    where
        I: IntoIterator<Item = &'a ManagedPythonInstallation>,
    {
        let mut minor_versions = IndexMap::default();
        for installation in installations {
            minor_versions
                .entry(installation.minor_version_key().clone())
                .and_modify(|high_installation: &mut ManagedPythonInstallation| {
                    if installation.key() >= high_installation.key() {
                        *high_installation = installation.clone();
                    }
                })
                .or_insert_with(|| installation.clone());
        }
        minor_versions
    }
}

impl fmt::Display for PythonInstallationMinorVersionKey {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        // Display every field on the wrapped key except the patch
        // and prerelease (with special formatting for the variant).
        let variant = match self.0.variant {
            PythonVariant::Default => String::new(),
            _ => format!("+{}", self.0.variant),
        };
        write!(
            f,
            "{}-{}.{}{}-{}",
            self.0.implementation, self.0.major, self.0.minor, variant, self.0.platform,
        )
    }
}

impl fmt::Debug for PythonInstallationMinorVersionKey {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        // Display every field on the wrapped key except the patch
        // and prerelease.
        f.debug_struct("PythonInstallationMinorVersionKey")
            .field("implementation", &self.0.implementation)
            .field("major", &self.0.major)
            .field("minor", &self.0.minor)
            .field("variant", &self.0.variant)
            .field("os", &self.0.platform.os)
            .field("arch", &self.0.platform.arch)
            .field("libc", &self.0.platform.libc)
            .finish()
    }
}

impl PartialEq for PythonInstallationMinorVersionKey {
    fn eq(&self, other: &Self) -> bool {
        // Compare every field on the wrapped key except the patch
        // and prerelease.
        self.0.implementation == other.0.implementation
            && self.0.major == other.0.major
            && self.0.minor == other.0.minor
            && self.0.platform == other.0.platform
            && self.0.variant == other.0.variant
    }
}

impl Hash for PythonInstallationMinorVersionKey {
    fn hash<H: Hasher>(&self, state: &mut H) {
        // Hash every field on the wrapped key except the patch
        // and prerelease.
        self.0.implementation.hash(state);
        self.0.major.hash(state);
        self.0.minor.hash(state);
        self.0.platform.hash(state);
        self.0.variant.hash(state);
    }
}

impl From<PythonInstallationKey> for PythonInstallationMinorVersionKey {
    fn from(key: PythonInstallationKey) -> Self {
        Self(key)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use uv_platform::ArchVariant;

    #[test]
    fn test_python_installation_key_from_str() {
        // Test basic parsing
        let key = PythonInstallationKey::from_str("cpython-3.12.0-linux-x86_64-gnu").unwrap();
        assert_eq!(
            key.implementation,
            LenientImplementationName::Known(ImplementationName::CPython)
        );
        assert_eq!(key.major, 3);
        assert_eq!(key.minor, 12);
        assert_eq!(key.patch, 0);
        assert_eq!(
            key.platform.os,
            Os::new(target_lexicon::OperatingSystem::Linux)
        );
        assert_eq!(
            key.platform.arch,
            Arch::new(target_lexicon::Architecture::X86_64, None)
        );
        assert_eq!(
            key.platform.libc,
            Libc::Some(target_lexicon::Environment::Gnu)
        );

        // Test with architecture variant
        let key = PythonInstallationKey::from_str("cpython-3.11.2-linux-x86_64_v3-musl").unwrap();
        assert_eq!(
            key.implementation,
            LenientImplementationName::Known(ImplementationName::CPython)
        );
        assert_eq!(key.major, 3);
        assert_eq!(key.minor, 11);
        assert_eq!(key.patch, 2);
        assert_eq!(
            key.platform.os,
            Os::new(target_lexicon::OperatingSystem::Linux)
        );
        assert_eq!(
            key.platform.arch,
            Arch::new(target_lexicon::Architecture::X86_64, Some(ArchVariant::V3))
        );
        assert_eq!(
            key.platform.libc,
            Libc::Some(target_lexicon::Environment::Musl)
        );

        // Test with Python variant (freethreaded)
        let key = PythonInstallationKey::from_str("cpython-3.13.0+freethreaded-macos-aarch64-none")
            .unwrap();
        assert_eq!(
            key.implementation,
            LenientImplementationName::Known(ImplementationName::CPython)
        );
        assert_eq!(key.major, 3);
        assert_eq!(key.minor, 13);
        assert_eq!(key.patch, 0);
        assert_eq!(key.variant, PythonVariant::Freethreaded);
        assert_eq!(
            key.platform.os,
            Os::new(target_lexicon::OperatingSystem::Darwin(None))
        );
        assert_eq!(
            key.platform.arch,
            Arch::new(
                target_lexicon::Architecture::Aarch64(target_lexicon::Aarch64Architecture::Aarch64),
                None
            )
        );
        assert_eq!(key.platform.libc, Libc::None);

        // Test error cases
        assert!(PythonInstallationKey::from_str("cpython-3.12.0-linux-x86_64").is_err());
        assert!(PythonInstallationKey::from_str("cpython-3.12.0").is_err());
        assert!(PythonInstallationKey::from_str("cpython").is_err());
    }

    #[test]
    fn test_python_installation_key_display() {
        let key = PythonInstallationKey {
            implementation: LenientImplementationName::from("cpython"),
            major: 3,
            minor: 12,
            patch: 0,
            prerelease: None,
            platform: Platform::from_str("linux-x86_64-gnu").unwrap(),
            variant: PythonVariant::Default,
        };
        assert_eq!(key.to_string(), "cpython-3.12.0-linux-x86_64-gnu");

        let key_with_variant = PythonInstallationKey {
            implementation: LenientImplementationName::from("cpython"),
            major: 3,
            minor: 13,
            patch: 0,
            prerelease: None,
            platform: Platform::from_str("macos-aarch64-none").unwrap(),
            variant: PythonVariant::Freethreaded,
        };
        assert_eq!(
            key_with_variant.to_string(),
            "cpython-3.13.0+freethreaded-macos-aarch64-none"
        );
    }
}
