//! Cargo-audit vulnerability scanning and integration with SBOM

use crate::error::Result;
use crate::sbom::SbomComponent;
use crate::LifecycleError;
use serde::{Deserialize, Serialize};
use std::path::Path;
use std::process::Command;

/// Vulnerability severity levels
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd)]
#[serde(rename_all = "lowercase")]
pub enum VulnerabilitySeverity {
    /// Low severity
    Low,
    /// Medium severity
    Medium,
    /// High severity
    High,
    /// Critical severity
    Critical,
}

impl std::fmt::Display for VulnerabilitySeverity {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Low => write!(f, "Low"),
            Self::Medium => write!(f, "Medium"),
            Self::High => write!(f, "High"),
            Self::Critical => write!(f, "Critical"),
        }
    }
}

/// Single vulnerability finding
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Vulnerability {
    /// Package name affected
    pub package: String,
    /// Vulnerable version(s)
    pub version: String,
    /// Vulnerability ID (CVE or advisory ID)
    pub id: String,
    /// Severity level
    pub severity: VulnerabilitySeverity,
    /// Title/summary
    pub title: String,
    /// Description
    pub description: String,
}

/// Cargo-audit report structure (from JSON output)
#[derive(Debug, Deserialize)]
struct CargoAuditReport {
    vulnerabilities: CargoAuditVulnerabilities,
}

#[derive(Debug, Deserialize)]
struct CargoAuditVulnerabilities {
    list: Vec<CargoAuditVulnerability>,
}

#[derive(Debug, Deserialize)]
struct CargoAuditVulnerability {
    advisory: CargoAuditAdvisory,
    versions: CargoAuditVersions,
}

#[derive(Debug, Deserialize)]
struct CargoAuditAdvisory {
    id: String,
    title: String,
    description: String,
    severity: String,
    #[serde(default)]
    package: String,
}

#[derive(Debug, Deserialize)]
struct CargoAuditVersions {
    vulnerable: Vec<String>,
}

/// Cargo audit scanner
#[derive(Debug)]
pub struct CargoAuditScanner;

impl CargoAuditScanner {
    /// Run cargo-audit and get vulnerabilities
    ///
    /// Returns list of vulnerabilities found in the project
    /// Returns empty list if cargo-audit is not installed
    pub fn scan(project_path: &Path) -> Result<Vec<Vulnerability>> {
        // Check if cargo-audit is available
        let check = Command::new("cargo").args(["audit", "--version"]).output();

        if check.is_err() || !check.unwrap().status.success() {
            // cargo-audit not installed, return empty list
            return Ok(Vec::new());
        }

        // Run cargo-audit with JSON output
        let output = Command::new("cargo")
            .args(["audit", "--json"])
            .current_dir(project_path)
            .output()
            .map_err(|e| LifecycleError::custom(format!("Failed to run cargo-audit: {}", e)))?;

        if !output.status.success() {
            // cargo-audit found vulnerabilities (exit code 1) or other error
            // Still try to parse the JSON output
            let stderr = String::from_utf8_lossy(&output.stderr);
            if stderr.contains("no audit data found") {
                // No vulnerability database available, but that's ok
                return Ok(Vec::new());
            }
        }

        // Parse JSON output
        let audit_output = String::from_utf8_lossy(&output.stdout);
        let mut vulnerabilities = Vec::new();

        // Try to parse the audit report
        if let Ok(report) = serde_json::from_str::<CargoAuditReport>(&audit_output) {
            for vuln in report.vulnerabilities.list {
                let severity = match vuln.advisory.severity.to_lowercase().as_str() {
                    "critical" => VulnerabilitySeverity::Critical,
                    "high" => VulnerabilitySeverity::High,
                    "medium" => VulnerabilitySeverity::Medium,
                    _ => VulnerabilitySeverity::Low,
                };

                for version in &vuln.versions.vulnerable {
                    vulnerabilities.push(Vulnerability {
                        package: vuln.advisory.package.clone(),
                        version: version.clone(),
                        id: vuln.advisory.id.clone(),
                        severity,
                        title: vuln.advisory.title.clone(),
                        description: vuln.advisory.description.clone(),
                    });
                }
            }
        }

        Ok(vulnerabilities)
    }

    /// Check if cargo-audit is installed
    pub fn is_available() -> bool {
        Command::new("cargo")
            .args(["audit", "--version"])
            .output()
            .map(|output| output.status.success())
            .unwrap_or(false)
    }

    /// Filter vulnerabilities that match SBOM components
    pub fn filter_by_sbom(
        vulnerabilities: &[Vulnerability],
        sbom_components: &[SbomComponent],
    ) -> Vec<Vulnerability> {
        let component_map: std::collections::HashMap<_, _> = sbom_components
            .iter()
            .map(|c| (c.name.clone(), c.version.clone()))
            .collect();

        vulnerabilities
            .iter()
            .filter(|v| {
                // Check if vulnerable package is in SBOM
                if let Some(version) = component_map.get(&v.package) {
                    // Simple version comparison - check if vulnerable version matches
                    version == &v.version || v.version.contains('*')
                } else {
                    false
                }
            })
            .cloned()
            .collect()
    }
}

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

    #[test]
    fn test_vulnerability_severity_ordering() {
        assert!(VulnerabilitySeverity::Low < VulnerabilitySeverity::Medium);
        assert!(VulnerabilitySeverity::Medium < VulnerabilitySeverity::High);
        assert!(VulnerabilitySeverity::High < VulnerabilitySeverity::Critical);
    }

    #[test]
    fn test_vulnerability_severity_display() {
        assert_eq!(VulnerabilitySeverity::Low.to_string(), "Low");
        assert_eq!(VulnerabilitySeverity::Critical.to_string(), "Critical");
    }

    #[test]
    fn test_filter_by_sbom() {
        let vulns = vec![
            Vulnerability {
                package: "serde".to_string(),
                version: "1.0.0".to_string(),
                id: "CVE-2021-1234".to_string(),
                severity: VulnerabilitySeverity::High,
                title: "Test vuln".to_string(),
                description: "Test description".to_string(),
            },
            Vulnerability {
                package: "tokio".to_string(),
                version: "1.5.0".to_string(),
                id: "CVE-2021-5678".to_string(),
                severity: VulnerabilitySeverity::Medium,
                title: "Test vuln 2".to_string(),
                description: "Test description 2".to_string(),
            },
        ];

        let components = vec![SbomComponent {
            name: "serde".to_string(),
            version: "1.0.0".to_string(),
            dependency_type: "direct".to_string(),
        }];

        let filtered = CargoAuditScanner::filter_by_sbom(&vulns, &components);
        assert_eq!(filtered.len(), 1);
        assert_eq!(filtered[0].package, "serde");
    }
}
