cargo-autodd 0.1.2

Automatically update dependencies in Cargo.toml
Documentation
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;

use anyhow::{Context, Result};
use semver::Version;
use toml_edit::{DocumentMut, Item};

use crate::dependency_manager::updater::DependencyUpdater;
use crate::models::CrateReference;

pub struct DependencyReporter {
    project_root: PathBuf,
    cargo_toml: PathBuf,
    updater: DependencyUpdater,
}

impl DependencyReporter {
    pub fn new(project_root: PathBuf) -> Self {
        let cargo_toml = project_root.join("Cargo.toml");
        let updater = DependencyUpdater::new(project_root.clone());
        Self {
            project_root,
            cargo_toml,
            updater,
        }
    }

    pub fn generate_dependency_report(
        &self,
        crate_refs: &HashMap<String, CrateReference>,
    ) -> Result<()> {
        let content = fs::read_to_string(&self.cargo_toml)
            .context("Failed to read Cargo.toml")?;
        let doc = content.parse::<DocumentMut>()
            .context("Failed to parse Cargo.toml")?;

        println!("\nDependency Usage Report");
        println!("=====================\n");

        if let Some(Item::Table(deps)) = doc.get("dependencies") {
            for (name, dep) in deps.iter() {
                println!("📦 {}", name);

                if let Some(version) = self.updater.get_dependency_version(dep) {
                    println!("  Version: {}", version);

                    if let Ok(latest) = self.updater.get_latest_version(name) {
                        if let (Ok(current), Ok(latest_ver)) = (
                            Version::parse(version.trim_start_matches('^')),
                            Version::parse(latest.trim_start_matches('^')),
                        ) {
                            if latest_ver > current {
                                println!("  Update available: {} -> {}", version, latest);
                            }
                        }
                    }
                }

                if let Some(crate_ref) = crate_refs.get(name) {
                    println!("  Used in {} file(s)", crate_ref.usage_count());
                    println!("  Usage locations:");
                    for path in &crate_ref.used_in {
                        if let Ok(relative) = path.strip_prefix(&self.project_root) {
                            println!("    - {}", relative.display());
                        }
                    }
                } else {
                    println!("  ⚠️ Warning: No usage detected in the project");
                }
                println!();
            }
        }

        Ok(())
    }

    pub fn generate_security_report(&self) -> Result<()> {
        println!("\nDependency Security Report");
        println!("========================\n");

        let outdated = self.check_security()?;

        if outdated.is_empty() {
            println!("✅ All dependencies are up to date.");
            return Ok(());
        }

        println!("⚠️ The following dependencies have updates available:\n");

        for (name, version_info) in outdated {
            println!("📦 {}", name);
            println!("  Version update available: {}", version_info);
            println!("  Please check the changelog for security fixes.");
            println!();
        }

        println!("Note: For a complete security audit, please use:");
        println!("  cargo audit");
        println!("  https://github.com/rustsec/rustsec\n");

        Ok(())
    }

    fn check_security(&self) -> Result<Vec<(String, String)>> {
        let content = fs::read_to_string(&self.cargo_toml)
            .context("Failed to read Cargo.toml")?;
        let doc = content.parse::<DocumentMut>()
            .context("Failed to parse Cargo.toml")?;
        let mut outdated = Vec::new();

        if let Some(Item::Table(deps)) = doc.get("dependencies") {
            for (name, dep) in deps.iter() {
                if let Some(version) = self.updater.get_dependency_version(dep) {
                    if let Ok(latest) = self.updater.get_latest_version(name) {
                        let current = Version::parse(version.trim_start_matches('^'))
                            .unwrap_or_else(|_| Version::new(0, 0, 0));
                        let latest = Version::parse(latest.trim_start_matches('^'))
                            .unwrap_or_else(|_| Version::new(0, 0, 0));

                        if latest > current {
                            outdated.push((name.to_string(), format!("{} -> {}", version, latest)));
                        }
                    }
                }
            }
        }

        Ok(outdated)
    }

    pub fn check_version(&self, version: &str, latest: &str) -> Result<bool> {
        Ok(
            match (
                Version::parse(version.trim_start_matches('^')),
                Version::parse(latest.trim_start_matches('^')),
            ) {
                (Ok(current), Ok(latest_ver)) => latest_ver > current,
                _ => false,
            },
        )
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs::File;
    use std::io::Write;
    use tempfile::TempDir;

    fn create_test_environment() -> Result<(TempDir, PathBuf)> {
        let temp_dir = TempDir::new()?;
        let cargo_toml = temp_dir.path().join("Cargo.toml");

        let content = r#"
[package]
name = "test-package"
version = "0.1.0"
edition = "2021"

[dependencies]
serde = "1.0"
tokio = "1.0"
"#;
        let mut file = File::create(&cargo_toml)?;
        writeln!(file, "{}", content)?;

        Ok((temp_dir, cargo_toml))
    }

    #[test]
    fn test_generate_dependency_report() -> Result<()> {
        let (temp_dir, _) = create_test_environment()?;
        let reporter = DependencyReporter::new(temp_dir.path().to_path_buf());

        let mut crate_refs = HashMap::new();
        let mut serde_ref = CrateReference::new("serde".to_string());
        serde_ref.add_usage(temp_dir.path().join("src/main.rs"));
        crate_refs.insert("serde".to_string(), serde_ref);

        reporter.generate_dependency_report(&crate_refs)?;
        Ok(())
    }

    #[test]
    fn test_generate_security_report() -> Result<()> {
        let (temp_dir, _) = create_test_environment()?;
        let reporter = DependencyReporter::new(temp_dir.path().to_path_buf());
        reporter.generate_security_report()?;
        Ok(())
    }
}