repolens 2.0.0

A CLI tool to audit and prepare repositories for open source or enterprise standards
Documentation
//! Markdown report output

use crate::error::RepoLensError;
use chrono::Utc;

use super::ReportRenderer;
use crate::rules::results::{AuditResults, Severity};

pub struct MarkdownReport {
    detailed: bool,
}

impl MarkdownReport {
    pub fn new(detailed: bool) -> Self {
        Self { detailed }
    }
}

impl ReportRenderer for MarkdownReport {
    fn render_report(&self, results: &AuditResults) -> Result<String, RepoLensError> {
        let mut output = String::new();

        // Header
        output.push_str(&format!(
            "# RepoLens Audit Report\n\n\
             **Repository:** {}\n\
             **Preset:** {}\n\
             **Generated:** {}\n\
             **RepoLens Version:** {}\n\n",
            results.repository_name,
            results.preset,
            Utc::now().format("%Y-%m-%d %H:%M:%S UTC"),
            env!("CARGO_PKG_VERSION")
        ));

        // Summary
        output.push_str("## Summary\n\n");
        output.push_str("| Severity | Count |\n");
        output.push_str("|----------|-------|\n");
        output.push_str(&format!(
            "| Critical | {} |\n",
            results.count_by_severity(Severity::Critical)
        ));
        output.push_str(&format!(
            "| Warning | {} |\n",
            results.count_by_severity(Severity::Warning)
        ));
        output.push_str(&format!(
            "| Info | {} |\n\n",
            results.count_by_severity(Severity::Info)
        ));

        // Critical findings
        let critical: Vec<_> = results.findings_by_severity(Severity::Critical).collect();
        if !critical.is_empty() {
            output.push_str("## Critical Issues\n\n");
            output.push_str("These issues must be resolved before proceeding.\n\n");
            for finding in critical {
                output.push_str(&format!(
                    "### {} - {}\n\n",
                    finding.rule_id, finding.message
                ));
                if let Some(location) = &finding.location {
                    output.push_str(&format!("**Location:** `{}`\n\n", location));
                }
                if self.detailed {
                    if let Some(description) = &finding.description {
                        output.push_str(&format!("{}\n\n", description));
                    }
                    if let Some(remediation) = &finding.remediation {
                        output.push_str(&format!("**Remediation:** {}\n\n", remediation));
                    }
                }
            }
        }

        // Warning findings
        let warnings: Vec<_> = results.findings_by_severity(Severity::Warning).collect();
        if !warnings.is_empty() {
            output.push_str("## Warnings\n\n");
            output.push_str("These issues should be addressed.\n\n");
            for finding in warnings {
                output.push_str(&format!(
                    "### {} - {}\n\n",
                    finding.rule_id, finding.message
                ));
                if let Some(location) = &finding.location {
                    output.push_str(&format!("**Location:** `{}`\n\n", location));
                }
                if self.detailed {
                    if let Some(description) = &finding.description {
                        output.push_str(&format!("{}\n\n", description));
                    }
                    if let Some(remediation) = &finding.remediation {
                        output.push_str(&format!("**Remediation:** {}\n\n", remediation));
                    }
                }
            }
        }

        // Info findings
        let info: Vec<_> = results.findings_by_severity(Severity::Info).collect();
        if !info.is_empty() && self.detailed {
            output.push_str("## Informational\n\n");
            output.push_str("These are suggestions for improvement.\n\n");
            for finding in info {
                output.push_str(&format!("- **{}**: {}\n", finding.rule_id, finding.message));
            }
            output.push('\n');
        }

        // Footer
        output.push_str("---\n\n");
        output.push_str("*Report generated by [RepoLens](https://github.com/systm-d/repolens)*\n");

        Ok(output)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::rules::results::Finding;

    fn create_test_results() -> AuditResults {
        let mut results = AuditResults::new("test-repo", "opensource");
        results.add_finding(
            Finding::new("SEC001", "secrets", Severity::Critical, "Secret exposed")
                .with_location("src/config.rs:42")
                .with_description("A secret was found in the code")
                .with_remediation("Move the secret to environment variables"),
        );
        results.add_finding(
            Finding::new("DOC001", "docs", Severity::Warning, "README missing")
                .with_location("README.md")
                .with_description("The README file is missing")
                .with_remediation("Create a README.md file"),
        );
        results.add_finding(Finding::new(
            "INFO001",
            "info",
            Severity::Info,
            "Consider adding tests",
        ));
        results
    }

    #[test]
    fn test_markdown_report_new() {
        let report = MarkdownReport::new(false);
        assert!(!report.detailed);
    }

    #[test]
    fn test_render_report_simple() {
        let report = MarkdownReport::new(false);
        let results = create_test_results();
        let rendered = report.render_report(&results).unwrap();

        assert!(rendered.contains("# RepoLens Audit Report"));
        assert!(rendered.contains("**Repository:** test-repo"));
        assert!(rendered.contains("**Preset:** opensource"));
        assert!(rendered.contains("## Summary"));
        assert!(rendered.contains("| Critical | 1 |"));
        assert!(rendered.contains("| Warning | 1 |"));
        assert!(rendered.contains("## Critical Issues"));
        assert!(rendered.contains("SEC001"));
        assert!(rendered.contains("## Warnings"));
        assert!(rendered.contains("DOC001"));
    }

    #[test]
    fn test_render_report_detailed() {
        let report = MarkdownReport::new(true);
        let results = create_test_results();
        let rendered = report.render_report(&results).unwrap();

        // Should include descriptions and remediations
        assert!(rendered.contains("A secret was found in the code"));
        assert!(rendered.contains("**Remediation:**"));
        assert!(rendered.contains("Move the secret to environment variables"));

        // Should include info section
        assert!(rendered.contains("## Informational"));
        assert!(rendered.contains("Consider adding tests"));
    }

    #[test]
    fn test_render_report_no_critical() {
        let report = MarkdownReport::new(false);
        let mut results = AuditResults::new("clean-repo", "opensource");
        results.add_finding(Finding::new(
            "DOC001",
            "docs",
            Severity::Warning,
            "Minor issue",
        ));

        let rendered = report.render_report(&results).unwrap();
        assert!(!rendered.contains("## Critical Issues"));
        assert!(rendered.contains("## Warnings"));
    }

    #[test]
    fn test_render_report_no_warnings() {
        let report = MarkdownReport::new(false);
        let mut results = AuditResults::new("clean-repo", "opensource");
        results.add_finding(Finding::new(
            "SEC001",
            "secrets",
            Severity::Critical,
            "Critical issue",
        ));

        let rendered = report.render_report(&results).unwrap();
        assert!(rendered.contains("## Critical Issues"));
        assert!(!rendered.contains("## Warnings"));
    }

    #[test]
    fn test_render_report_empty() {
        let report = MarkdownReport::new(false);
        let results = AuditResults::new("empty-repo", "opensource");
        let rendered = report.render_report(&results).unwrap();

        assert!(rendered.contains("# RepoLens Audit Report"));
        assert!(rendered.contains("| Critical | 0 |"));
        assert!(rendered.contains("| Warning | 0 |"));
        assert!(!rendered.contains("## Critical Issues"));
        assert!(!rendered.contains("## Warnings"));
    }

    #[test]
    fn test_render_report_contains_footer() {
        let report = MarkdownReport::new(false);
        let results = AuditResults::new("test-repo", "opensource");
        let rendered = report.render_report(&results).unwrap();

        assert!(rendered.contains("---"));
        assert!(rendered.contains("*Report generated by [RepoLens]"));
    }
}