// SPDX-License-Identifier: MIT

use crate::types::{PackageName, Version};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::str::FromStr;

/// A security vulnerability affecting a Python package.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Vulnerability {
    /// Unique vulnerability identifier (GHSA-*, CVE-*, etc.)
    pub id: String,

    /// Vulnerability summary/title
    pub summary: String,

    /// Detailed description
    pub description: Option<String>,

    /// Severity level
    pub severity: Severity,

    /// Affected package versions
    pub affected_versions: Vec<VersionRange>,

    /// Fixed versions
    pub fixed_versions: Vec<Version>,

    /// Reference URLs
    pub references: Vec<String>,

    /// CVSS score if available
    pub cvss_score: Option<f32>,

    /// CVSS version used for the score (2, 3, or 4; 0 = unknown version)
    pub cvss_version: Option<u8>,

    /// Date when vulnerability was published
    pub published: Option<DateTime<Utc>>,

    /// Date when vulnerability was last modified
    pub modified: Option<DateTime<Utc>>,

    /// Source of the vulnerability data (e.g., "pypa", "pypi", "osv")
    pub source: Option<String>,

    /// Date when vulnerability was withdrawn (if applicable)
    pub withdrawn: Option<DateTime<Utc>>,

    /// Aliases for this vulnerability (e.g., CVE-*, GHSA-*, PYSEC-*)
    #[serde(default)]
    pub aliases: Vec<String>,
}

/// Severity levels for vulnerabilities.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Severity {
    Unknown,
    Low,
    Medium,
    High,
    Critical,
}

impl Severity {
    pub fn from_cvss_score(score: f32) -> Self {
        match score {
            s if s >= 9.0 => Self::Critical,
            s if s >= 7.0 => Self::High,
            s if s >= 4.0 => Self::Medium,
            // CVSS 0.0 ("None" per FIRST.org) maps to Low: it represents a known
            // assessment of zero impact, not an unknown severity.
            _ => Self::Low,
        }
    }
}

/// A version range constraint for vulnerability matching.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct VersionRange {
    /// Minimum version (inclusive)
    pub min: Option<Version>,
    /// Maximum version (exclusive)
    pub max: Option<Version>,
    /// Version constraint string
    pub constraint: String,
}

impl VersionRange {
    /// Check if a version is within this range
    pub fn contains(&self, version: &Version) -> bool {
        let min_satisfied = self.min.as_ref().map_or(true, |min| version >= min);
        let max_satisfied = self.max.as_ref().map_or(true, |max| version < max);
        min_satisfied && max_satisfied
    }

    /// Create a new version range
    pub fn new(min: Option<Version>, max: Option<Version>, constraint: String) -> Self {
        Self {
            min,
            max,
            constraint,
        }
    }
}

/// A vulnerability match found during scanning.
#[derive(Debug, Clone)]
pub struct VulnerabilityMatch {
    /// The package that has the vulnerability
    pub package_name: PackageName,
    /// The installed version
    pub installed_version: Version,
    /// The vulnerability details
    pub vulnerability: Vulnerability,
    /// Whether this is a direct or transitive dependency
    pub is_direct: bool,
}

/// A vulnerability database containing advisories and indexed lookups.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VulnerabilityDatabase {
    /// All advisories in the database
    pub advisories: Vec<Vulnerability>,
    /// Index mapping package names to advisory indices
    pub package_index: HashMap<PackageName, Vec<usize>>,
}

impl Default for VulnerabilityDatabase {
    fn default() -> Self {
        Self::new()
    }
}

impl VulnerabilityDatabase {
    /// Create a new empty vulnerability database
    pub fn new() -> Self {
        Self {
            advisories: Vec::new(),
            package_index: HashMap::new(),
        }
    }

    /// Get advisories for a specific package
    #[cfg_attr(feature = "hotpath", hotpath::measure)]
    pub fn get_advisories_for_package(&self, package_name: &PackageName) -> Vec<&Vulnerability> {
        if let Some(indices) = self.package_index.get(package_name) {
            indices
                .iter()
                .filter_map(|&index| self.advisories.get(index))
                .collect()
        } else {
            Vec::new()
        }
    }

    /// Get the total number of advisories
    pub fn len(&self) -> usize {
        self.advisories.len()
    }

    /// Check if the database is empty
    pub fn is_empty(&self) -> bool {
        self.advisories.is_empty()
    }

    /// Create a database from a package-to-vulnerabilities mapping
    pub fn from_package_map(map: HashMap<String, Vec<Vulnerability>>) -> Self {
        let mut advisories = Vec::new();
        let mut package_index = HashMap::new();

        for (package_name, vulns) in map {
            let package = PackageName::from_str(&package_name).unwrap(); // Never fails due to Infallible error type
            let start_idx = advisories.len();
            for vuln in vulns {
                advisories.push(vuln);
            }
            let end_idx = advisories.len();
            if start_idx < end_idx {
                package_index.insert(package, (start_idx..end_idx).collect());
            }
        }

        Self {
            advisories,
            package_index,
        }
    }

    /// Merge multiple vulnerability databases into a single database.
    /// Vulnerabilities with the same ID **or shared aliases** are merged,
    /// combining their sources, CVSS data, and metadata.
    pub fn merge(databases: Vec<Self>) -> Self {
        if databases.is_empty() {
            return Self::new();
        }

        // Maps any known ID or alias to the canonical ID for that vulnerability group
        let mut canonical_map: HashMap<String, String> = HashMap::new();
        // The merged vulnerability keyed by canonical ID
        let mut vuln_by_canonical: HashMap<String, Vulnerability> = HashMap::new();
        // Package-name → set of canonical IDs
        let mut package_to_canonical: HashMap<PackageName, HashSet<String>> = HashMap::new();

        for database in &databases {
            for (package_name, indices) in &database.package_index {
                for &index in indices {
                    if let Some(vuln) = database.advisories.get(index) {
                        let canonical =
                            Self::resolve_canonical(&mut canonical_map, &vuln.id, &vuln.aliases);

                        if let Some(existing) = vuln_by_canonical.get_mut(&canonical) {
                            Self::merge_into(existing, vuln);
                            // Register any new aliases from the incoming vuln
                            Self::register_aliases(
                                &mut canonical_map,
                                &canonical,
                                &vuln.id,
                                &vuln.aliases,
                            );
                        } else {
                            vuln_by_canonical.insert(canonical.clone(), vuln.clone());
                        }

                        package_to_canonical
                            .entry(package_name.clone())
                            .or_default()
                            .insert(canonical);
                    }
                }
            }
        }

        // Build the final database structure
        let mut all_advisories = Vec::new();
        let mut package_index = HashMap::new();
        let mut added_ids: HashSet<String> = HashSet::new();

        for (package_name, canonical_ids) in package_to_canonical {
            let start_index = all_advisories.len();
            let mut added_any = false;

            for canonical_id in canonical_ids {
                if let Some(vuln) = vuln_by_canonical.get(&canonical_id) {
                    if added_ids.insert(canonical_id) {
                        all_advisories.push(vuln.clone());
                        added_any = true;
                    }
                }
            }

            let end_index = all_advisories.len();
            if added_any && start_index < end_index {
                package_index.insert(package_name, (start_index..end_index).collect());
            }
        }

        Self {
            advisories: all_advisories,
            package_index,
        }
    }

    /// Resolve a vulnerability's canonical ID via alias lookup.
    /// If the ID or any alias is already known, returns the existing canonical.
    /// Otherwise creates a new canonical entry and registers all aliases.
    fn resolve_canonical(
        canonical_map: &mut HashMap<String, String>,
        id: &str,
        aliases: &[String],
    ) -> String {
        // Fast path: exact ID match
        if let Some(canonical) = canonical_map.get(id) {
            return canonical.clone();
        }

        // Check aliases for an existing canonical
        for alias in aliases {
            if let Some(canonical) = canonical_map.get(alias.as_str()) {
                let canonical = canonical.clone();
                canonical_map.insert(id.to_string(), canonical.clone());
                return canonical;
            }
        }

        // New group — this ID becomes canonical
        let canonical = id.to_string();
        canonical_map.insert(id.to_string(), canonical.clone());
        for alias in aliases {
            canonical_map
                .entry(alias.clone())
                .or_insert_with(|| canonical.clone());
        }
        canonical
    }

    /// Register additional aliases for an existing canonical ID.
    fn register_aliases(
        canonical_map: &mut HashMap<String, String>,
        canonical: &str,
        id: &str,
        aliases: &[String],
    ) {
        canonical_map
            .entry(id.to_string())
            .or_insert_with(|| canonical.to_string());
        for alias in aliases {
            canonical_map
                .entry(alias.clone())
                .or_insert_with(|| canonical.to_string());
        }
    }

    /// Merge `incoming` vulnerability data into `existing`.
    fn merge_into(existing: &mut Vulnerability, incoming: &Vulnerability) {
        // Merge sources
        if let (Some(existing_source), Some(new_source)) = (&existing.source, &incoming.source) {
            let mut sources: Vec<&str> = existing_source.split(',').collect();
            if !sources.contains(&new_source.as_str()) {
                sources.push(new_source);
                sources.sort();
                existing.source = Some(sources.join(","));
            }
        } else if existing.source.is_none() && incoming.source.is_some() {
            existing.source = incoming.source.clone();
        }

        // Merge CVSS: prefer highest version, then highest score
        let existing_ver = existing.cvss_version.unwrap_or(0);
        let new_ver = incoming.cvss_version.unwrap_or(0);

        let replace_cvss = match (existing.cvss_score, incoming.cvss_score) {
            (_, None) => false,
            (None, Some(_)) => true,
            (Some(existing_score), Some(new_score)) => match new_ver.cmp(&existing_ver) {
                std::cmp::Ordering::Greater => true,
                std::cmp::Ordering::Equal => {
                    new_score.total_cmp(&existing_score) == std::cmp::Ordering::Greater
                }
                std::cmp::Ordering::Less => false,
            },
        };

        if replace_cvss {
            existing.cvss_score = incoming.cvss_score;
            existing.cvss_version = incoming.cvss_version;
        }

        // Re-derive severity from CVSS score for consistency
        match existing.cvss_score {
            Some(score) => {
                existing.severity = Severity::from_cvss_score(score);
            }
            None => {
                if incoming.severity > existing.severity {
                    existing.severity = incoming.severity;
                }
            }
        }

        // Merge other fields (prefer non-empty values)
        if existing.description.is_none() && incoming.description.is_some() {
            existing.description = incoming.description.clone();
        }
        if existing.published.is_none() && incoming.published.is_some() {
            existing.published = incoming.published;
        }
        if existing.modified.is_none() && incoming.modified.is_some() {
            existing.modified = incoming.modified;
        }

        // Merge references (remove duplicates)
        for reference in &incoming.references {
            if !existing.references.contains(reference) {
                existing.references.push(reference.clone());
            }
        }
        existing.references.sort();

        // Merge affected and fixed versions (remove duplicates)
        for affected_version in &incoming.affected_versions {
            if !existing.affected_versions.contains(affected_version) {
                existing.affected_versions.push(affected_version.clone());
            }
        }
        for fixed_version in &incoming.fixed_versions {
            if !existing.fixed_versions.contains(fixed_version) {
                existing.fixed_versions.push(fixed_version.clone());
            }
        }

        // Merge aliases (union of both sets, excluding the existing ID itself)
        for alias in &incoming.aliases {
            if alias != &existing.id && !existing.aliases.contains(alias) {
                existing.aliases.push(alias.clone());
            }
        }
        // The incoming vuln's own ID becomes an alias of the canonical entry
        if incoming.id != existing.id && !existing.aliases.contains(&incoming.id) {
            existing.aliases.push(incoming.id.clone());
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use chrono::Utc;

    fn create_test_vulnerability(id: &str, source: Option<&str>) -> Vulnerability {
        Vulnerability {
            id: id.to_string(),
            summary: format!("Test vulnerability {id}"),
            description: Some(format!("Description for {id}")),
            severity: Severity::Medium,
            affected_versions: vec![],
            fixed_versions: vec![],
            references: vec![],
            cvss_score: None,
            cvss_version: None,
            published: Some(Utc::now()),
            modified: None,
            source: source.map(|s| s.to_string()),
            withdrawn: None,
            aliases: vec![],
        }
    }

    #[test]
    fn test_merge_empty_databases() {
        let databases = vec![];
        let merged = VulnerabilityDatabase::merge(databases);
        assert!(merged.is_empty());
    }

    #[test]
    fn test_merge_single_database() {
        let mut db = VulnerabilityDatabase::new();
        let vuln = create_test_vulnerability("GHSA-1234", Some("test"));
        db.advisories.push(vuln.clone());

        let package = PackageName::from("test-package");
        db.package_index.insert(package, vec![0]);

        let databases = vec![db.clone()];
        let merged = VulnerabilityDatabase::merge(databases);

        assert_eq!(merged.len(), 1);
        assert_eq!(merged.advisories[0].id, "GHSA-1234");
        assert_eq!(merged.advisories[0].source, Some("test".to_string()));
    }

    #[test]
    fn test_merge_different_vulnerabilities() {
        let mut db1 = VulnerabilityDatabase::new();
        let vuln1 = create_test_vulnerability("GHSA-1234", Some("source1"));
        db1.advisories.push(vuln1);
        let package1 = PackageName::from("package1");
        db1.package_index.insert(package1, vec![0]);

        let mut db2 = VulnerabilityDatabase::new();
        let vuln2 = create_test_vulnerability("GHSA-5678", Some("source2"));
        db2.advisories.push(vuln2);
        let package2 = PackageName::from("package2");
        db2.package_index.insert(package2, vec![0]);

        let databases = vec![db1, db2];
        let merged = VulnerabilityDatabase::merge(databases);

        assert_eq!(merged.len(), 2);

        let ids: Vec<_> = merged.advisories.iter().map(|v| &v.id).collect();
        assert!(ids.contains(&&"GHSA-1234".to_string()));
        assert!(ids.contains(&&"GHSA-5678".to_string()));
    }

    #[test]
    fn test_merge_duplicate_vulnerability_ids() {
        let mut db1 = VulnerabilityDatabase::new();
        let mut vuln1 = create_test_vulnerability("GHSA-1234", Some("source1"));
        vuln1.references = vec!["ref1".to_string()];
        db1.advisories.push(vuln1);
        let package = PackageName::from("test-package");
        db1.package_index.insert(package.clone(), vec![0]);

        let mut db2 = VulnerabilityDatabase::new();
        let mut vuln2 = create_test_vulnerability("GHSA-1234", Some("source2"));
        vuln2.references = vec!["ref2".to_string()];
        vuln2.cvss_score = Some(7.5);
        db2.advisories.push(vuln2);
        db2.package_index.insert(package, vec![0]);

        let databases = vec![db1, db2];
        let merged = VulnerabilityDatabase::merge(databases);

        assert_eq!(merged.len(), 1);
        let merged_vuln = &merged.advisories[0];

        assert_eq!(merged_vuln.id, "GHSA-1234");
        assert_eq!(merged_vuln.source, Some("source1,source2".to_string()));
        assert_eq!(merged_vuln.references.len(), 2);
        assert!(merged_vuln.references.contains(&"ref1".to_string()));
        assert!(merged_vuln.references.contains(&"ref2".to_string()));
        assert_eq!(merged_vuln.cvss_score, Some(7.5));
    }

    #[test]
    fn test_merge_source_deduplication() {
        let mut db1 = VulnerabilityDatabase::new();
        let vuln1 = create_test_vulnerability("GHSA-1234", Some("source1"));
        db1.advisories.push(vuln1);
        let package = PackageName::from("test-package");
        db1.package_index.insert(package.clone(), vec![0]);

        let mut db2 = VulnerabilityDatabase::new();
        let vuln2 = create_test_vulnerability("GHSA-1234", Some("source1")); // Same source
        db2.advisories.push(vuln2);
        db2.package_index.insert(package, vec![0]);

        let databases = vec![db1, db2];
        let merged = VulnerabilityDatabase::merge(databases);

        assert_eq!(merged.len(), 1);
        let merged_vuln = &merged.advisories[0];

        assert_eq!(merged_vuln.id, "GHSA-1234");
        assert_eq!(merged_vuln.source, Some("source1".to_string())); // No duplication
    }

    #[test]
    fn test_merge_preserves_package_associations() {
        let mut db1 = VulnerabilityDatabase::new();
        let vuln1 = create_test_vulnerability("GHSA-1234", Some("source1"));
        db1.advisories.push(vuln1);
        let package1 = PackageName::from("package1");
        db1.package_index.insert(package1.clone(), vec![0]);

        let mut db2 = VulnerabilityDatabase::new();
        let vuln2 = create_test_vulnerability("GHSA-5678", Some("source2"));
        db2.advisories.push(vuln2);
        let package2 = PackageName::from("package2");
        db2.package_index.insert(package2.clone(), vec![0]);

        let databases = vec![db1, db2];
        let merged = VulnerabilityDatabase::merge(databases);

        assert_eq!(merged.package_index.len(), 2);
        assert!(merged.package_index.contains_key(&package1));
        assert!(merged.package_index.contains_key(&package2));

        let package1_vulns = merged.get_advisories_for_package(&package1);
        let package2_vulns = merged.get_advisories_for_package(&package2);

        assert_eq!(package1_vulns.len(), 1);
        assert_eq!(package2_vulns.len(), 1);
        assert_eq!(package1_vulns[0].id, "GHSA-1234");
        assert_eq!(package2_vulns[0].id, "GHSA-5678");
    }

    #[test]
    fn test_merge_prefers_higher_severity() {
        let mut db1 = VulnerabilityDatabase::new();
        let mut vuln1 = create_test_vulnerability("GHSA-1234", Some("source1"));
        vuln1.severity = Severity::Low;
        db1.advisories.push(vuln1);
        let package = PackageName::from("test-package");
        db1.package_index.insert(package.clone(), vec![0]);

        let mut db2 = VulnerabilityDatabase::new();
        let mut vuln2 = create_test_vulnerability("GHSA-1234", Some("source2"));
        vuln2.severity = Severity::Critical;
        db2.advisories.push(vuln2);
        db2.package_index.insert(package, vec![0]);

        let databases = vec![db1, db2];
        let merged = VulnerabilityDatabase::merge(databases);

        assert_eq!(merged.len(), 1);
        let merged_vuln = &merged.advisories[0];
        assert_eq!(merged_vuln.severity, Severity::Critical);
    }

    #[test]
    fn test_merge_prefers_higher_cvss_score() {
        let mut db1 = VulnerabilityDatabase::new();
        let mut vuln1 = create_test_vulnerability("GHSA-1234", Some("source1"));
        vuln1.cvss_score = Some(5.0);
        db1.advisories.push(vuln1);
        let package = PackageName::from("test-package");
        db1.package_index.insert(package.clone(), vec![0]);

        let mut db2 = VulnerabilityDatabase::new();
        let mut vuln2 = create_test_vulnerability("GHSA-1234", Some("source2"));
        vuln2.cvss_score = Some(9.8);
        db2.advisories.push(vuln2);
        db2.package_index.insert(package, vec![0]);

        let databases = vec![db1, db2];
        let merged = VulnerabilityDatabase::merge(databases);

        assert_eq!(merged.len(), 1);
        let merged_vuln = &merged.advisories[0];
        assert_eq!(merged_vuln.cvss_score, Some(9.8));
    }

    #[test]
    fn test_merge_keeps_cvss_when_other_is_none() {
        let mut db1 = VulnerabilityDatabase::new();
        let mut vuln1 = create_test_vulnerability("GHSA-1234", Some("source1"));
        vuln1.cvss_score = Some(7.5);
        db1.advisories.push(vuln1);
        let package = PackageName::from("test-package");
        db1.package_index.insert(package.clone(), vec![0]);

        let mut db2 = VulnerabilityDatabase::new();
        let mut vuln2 = create_test_vulnerability("GHSA-1234", Some("source2"));
        vuln2.cvss_score = None;
        db2.advisories.push(vuln2);
        db2.package_index.insert(package, vec![0]);

        let databases = vec![db1, db2];
        let merged = VulnerabilityDatabase::merge(databases);

        assert_eq!(merged.len(), 1);
        let merged_vuln = &merged.advisories[0];
        assert_eq!(merged_vuln.cvss_score, Some(7.5));
    }

    #[test]
    fn test_merge_fills_cvss_from_none() {
        let mut db1 = VulnerabilityDatabase::new();
        let mut vuln1 = create_test_vulnerability("GHSA-1234", Some("source1"));
        vuln1.cvss_score = None;
        db1.advisories.push(vuln1);
        let package = PackageName::from("test-package");
        db1.package_index.insert(package.clone(), vec![0]);

        let mut db2 = VulnerabilityDatabase::new();
        let mut vuln2 = create_test_vulnerability("GHSA-1234", Some("source2"));
        vuln2.cvss_score = Some(8.5);
        db2.advisories.push(vuln2);
        db2.package_index.insert(package, vec![0]);

        let databases = vec![db1, db2];
        let merged = VulnerabilityDatabase::merge(databases);

        assert_eq!(merged.len(), 1);
        let merged_vuln = &merged.advisories[0];
        assert_eq!(merged_vuln.cvss_score, Some(8.5));
    }

    #[test]
    fn test_merge_known_severity_overrides_unknown() {
        let mut db1 = VulnerabilityDatabase::new();
        let mut vuln1 = create_test_vulnerability("GHSA-1234", Some("source1"));
        vuln1.severity = Severity::Unknown;
        db1.advisories.push(vuln1);
        let package = PackageName::from("test-package");
        db1.package_index.insert(package.clone(), vec![0]);

        let mut db2 = VulnerabilityDatabase::new();
        let mut vuln2 = create_test_vulnerability("GHSA-1234", Some("source2"));
        vuln2.severity = Severity::High;
        db2.advisories.push(vuln2);
        db2.package_index.insert(package, vec![0]);

        let databases = vec![db1, db2];
        let merged = VulnerabilityDatabase::merge(databases);

        assert_eq!(merged.len(), 1);
        assert_eq!(merged.advisories[0].severity, Severity::High);
    }

    #[test]
    fn test_merge_unknown_does_not_override_known() {
        let mut db1 = VulnerabilityDatabase::new();
        let mut vuln1 = create_test_vulnerability("GHSA-1234", Some("source1"));
        vuln1.severity = Severity::High;
        db1.advisories.push(vuln1);
        let package = PackageName::from("test-package");
        db1.package_index.insert(package.clone(), vec![0]);

        let mut db2 = VulnerabilityDatabase::new();
        let mut vuln2 = create_test_vulnerability("GHSA-1234", Some("source2"));
        vuln2.severity = Severity::Unknown;
        db2.advisories.push(vuln2);
        db2.package_index.insert(package, vec![0]);

        let databases = vec![db1, db2];
        let merged = VulnerabilityDatabase::merge(databases);

        assert_eq!(merged.len(), 1);
        assert_eq!(merged.advisories[0].severity, Severity::High);
    }

    #[test]
    fn test_merge_prefers_higher_cvss_version() {
        let mut db1 = VulnerabilityDatabase::new();
        let mut vuln1 = create_test_vulnerability("GHSA-1234", Some("source1"));
        vuln1.cvss_score = Some(9.0);
        vuln1.cvss_version = Some(2);
        db1.advisories.push(vuln1);
        let package = PackageName::from("test-package");
        db1.package_index.insert(package.clone(), vec![0]);

        let mut db2 = VulnerabilityDatabase::new();
        let mut vuln2 = create_test_vulnerability("GHSA-1234", Some("source2"));
        vuln2.cvss_score = Some(8.5);
        vuln2.cvss_version = Some(3);
        db2.advisories.push(vuln2);
        db2.package_index.insert(package, vec![0]);

        let databases = vec![db1, db2];
        let merged = VulnerabilityDatabase::merge(databases);

        assert_eq!(merged.len(), 1);
        let merged_vuln = &merged.advisories[0];
        // CVSS v3 wins over v2 even with lower score
        assert_eq!(merged_vuln.cvss_score, Some(8.5));
        assert_eq!(merged_vuln.cvss_version, Some(3));
    }

    #[test]
    fn test_merge_rederives_severity_from_cvss() {
        let mut db1 = VulnerabilityDatabase::new();
        let mut vuln1 = create_test_vulnerability("GHSA-1234", Some("source1"));
        vuln1.severity = Severity::Critical;
        vuln1.cvss_score = Some(5.0);
        vuln1.cvss_version = Some(3);
        db1.advisories.push(vuln1);
        let package = PackageName::from("test-package");
        db1.package_index.insert(package.clone(), vec![0]);

        let mut db2 = VulnerabilityDatabase::new();
        let mut vuln2 = create_test_vulnerability("GHSA-1234", Some("source2"));
        vuln2.severity = Severity::Low;
        vuln2.cvss_score = None;
        db2.advisories.push(vuln2);
        db2.package_index.insert(package, vec![0]);

        let databases = vec![db1, db2];
        let merged = VulnerabilityDatabase::merge(databases);

        assert_eq!(merged.len(), 1);
        let merged_vuln = &merged.advisories[0];
        // Severity re-derived from CVSS 5.0 → Medium, not the original Critical
        assert_eq!(merged_vuln.severity, Severity::Medium);
        assert_eq!(merged_vuln.cvss_score, Some(5.0));
    }

    // --- Alias-based deduplication tests ---

    #[test]
    fn test_merge_deduplicates_by_alias_pysec_and_ghsa() {
        // Simulates: OSV returns PYSEC-123 (no CVSS, aliases GHSA-456) and
        // GHSA-456 (has CVSS) for the same package
        let mut db = VulnerabilityDatabase::new();

        let mut pysec = create_test_vulnerability("PYSEC-123", Some("osv"));
        pysec.severity = Severity::Unknown;
        pysec.cvss_score = None;
        pysec.aliases = vec!["CVE-2023-99999".to_string(), "GHSA-456".to_string()];
        db.advisories.push(pysec);

        let mut ghsa = create_test_vulnerability("GHSA-456", Some("osv"));
        ghsa.severity = Severity::Critical;
        ghsa.cvss_score = Some(9.8);
        ghsa.cvss_version = Some(3);
        ghsa.aliases = vec!["CVE-2023-99999".to_string(), "PYSEC-123".to_string()];
        db.advisories.push(ghsa);

        let package = PackageName::from("test-package");
        db.package_index.insert(package, vec![0, 1]);

        let merged = VulnerabilityDatabase::merge(vec![db]);

        assert_eq!(
            merged.len(),
            1,
            "Alias duplicates should be merged into one"
        );
        let merged_vuln = &merged.advisories[0];
        assert_eq!(merged_vuln.cvss_score, Some(9.8));
        assert_eq!(merged_vuln.severity, Severity::Critical);
    }

    #[test]
    fn test_merge_deduplicates_across_databases_via_shared_cve() {
        // OSV returns GHSA-456 (with CVSS), PyPA returns PYSEC-123 (no CVSS)
        // Both share CVE-2023-99999 as an alias
        let mut db_osv = VulnerabilityDatabase::new();
        let mut ghsa = create_test_vulnerability("GHSA-456", Some("osv"));
        ghsa.cvss_score = Some(7.5);
        ghsa.cvss_version = Some(3);
        ghsa.severity = Severity::High;
        ghsa.aliases = vec!["CVE-2023-99999".to_string()];
        db_osv.advisories.push(ghsa);
        let package = PackageName::from("test-package");
        db_osv.package_index.insert(package.clone(), vec![0]);

        let mut db_pypa = VulnerabilityDatabase::new();
        let mut pysec = create_test_vulnerability("PYSEC-123", Some("pypa"));
        pysec.severity = Severity::Unknown;
        pysec.cvss_score = None;
        pysec.aliases = vec!["CVE-2023-99999".to_string()];
        db_pypa.advisories.push(pysec);
        db_pypa.package_index.insert(package, vec![0]);

        let merged = VulnerabilityDatabase::merge(vec![db_osv, db_pypa]);

        assert_eq!(
            merged.len(),
            1,
            "Cross-database alias duplicates should merge"
        );
        let merged_vuln = &merged.advisories[0];
        assert_eq!(merged_vuln.cvss_score, Some(7.5));
        assert_eq!(merged_vuln.severity, Severity::High);
        let source = merged_vuln.source.as_deref().unwrap();
        assert!(source.contains("osv"));
        assert!(source.contains("pypa"));
    }

    #[test]
    fn test_merge_no_aliases_stays_separate() {
        let mut db = VulnerabilityDatabase::new();
        let vuln1 = create_test_vulnerability("GHSA-aaa", Some("osv"));
        let vuln2 = create_test_vulnerability("GHSA-bbb", Some("osv"));
        db.advisories.push(vuln1);
        db.advisories.push(vuln2);
        let package = PackageName::from("test-package");
        db.package_index.insert(package, vec![0, 1]);

        let merged = VulnerabilityDatabase::merge(vec![db]);

        assert_eq!(merged.len(), 2, "Unrelated vulns must stay separate");
    }

    #[test]
    fn test_merge_three_way_alias_dedup() {
        // OSV PYSEC (no CVSS), OSV GHSA (has CVSS), PyPA PYSEC (no CVSS)
        // All three refer to the same CVE
        let mut db_osv = VulnerabilityDatabase::new();

        let mut pysec_osv = create_test_vulnerability("PYSEC-100", Some("osv"));
        pysec_osv.severity = Severity::Unknown;
        pysec_osv.cvss_score = None;
        pysec_osv.aliases = vec!["CVE-2024-11111".to_string(), "GHSA-xyz".to_string()];
        db_osv.advisories.push(pysec_osv);

        let mut ghsa_osv = create_test_vulnerability("GHSA-xyz", Some("osv"));
        ghsa_osv.cvss_score = Some(8.1);
        ghsa_osv.cvss_version = Some(3);
        ghsa_osv.severity = Severity::High;
        ghsa_osv.aliases = vec!["CVE-2024-11111".to_string(), "PYSEC-100".to_string()];
        db_osv.advisories.push(ghsa_osv);

        let package = PackageName::from("test-package");
        db_osv.package_index.insert(package.clone(), vec![0, 1]);

        let mut db_pypa = VulnerabilityDatabase::new();
        let mut pysec_pypa = create_test_vulnerability("PYSEC-100", Some("pypa"));
        pysec_pypa.severity = Severity::Unknown;
        pysec_pypa.cvss_score = None;
        pysec_pypa.aliases = vec!["CVE-2024-11111".to_string()];
        db_pypa.advisories.push(pysec_pypa);
        db_pypa.package_index.insert(package, vec![0]);

        let merged = VulnerabilityDatabase::merge(vec![db_osv, db_pypa]);

        assert_eq!(
            merged.len(),
            1,
            "Three-way alias merge should produce one entry"
        );
        let merged_vuln = &merged.advisories[0];
        assert_eq!(merged_vuln.cvss_score, Some(8.1));
        assert_eq!(merged_vuln.severity, Severity::High);
        let source = merged_vuln.source.as_deref().unwrap();
        assert!(source.contains("osv"));
        assert!(source.contains("pypa"));
    }

    #[test]
    fn test_merge_aliases_are_collected() {
        let mut db = VulnerabilityDatabase::new();

        let mut vuln1 = create_test_vulnerability("PYSEC-1", Some("osv"));
        vuln1.aliases = vec!["CVE-2024-1".to_string()];
        db.advisories.push(vuln1);

        let mut vuln2 = create_test_vulnerability("GHSA-a", Some("osv"));
        vuln2.aliases = vec!["CVE-2024-1".to_string(), "PYSEC-1".to_string()];
        vuln2.cvss_score = Some(5.0);
        vuln2.cvss_version = Some(3);
        db.advisories.push(vuln2);

        let package = PackageName::from("pkg");
        db.package_index.insert(package, vec![0, 1]);

        let merged = VulnerabilityDatabase::merge(vec![db]);

        assert_eq!(merged.len(), 1);
        let merged_vuln = &merged.advisories[0];
        // The merged entry should list the other ID and CVE as aliases
        assert!(
            merged_vuln.aliases.contains(&"GHSA-a".to_string())
                || merged_vuln.aliases.contains(&"PYSEC-1".to_string()),
            "Merged aliases should include the other ID"
        );
        assert!(
            merged_vuln.aliases.contains(&"CVE-2024-1".to_string()),
            "Merged aliases should include shared CVE"
        );
    }
}
