pmat 3.16.0

PMAT - Zero-config AI context generation and code quality toolkit (CLI, MCP, HTTP)
// Defect report formatting - extracted for file health (CB-040)
fn create_defect_report_from_predictions(
    predictions: Vec<(String, crate::services::defect_probability::DefectScore)>,
) -> Result<DefectPredictionReport> {
    use crate::services::defect_probability::RiskLevel;
    let mut high_risk_files = 0;
    let mut medium_risk_files = 0;
    let mut low_risk_files = 0;

    let file_predictions: Vec<FilePrediction> = predictions
        .iter()
        .map(|(file_path, score)| {
            match score.risk_level {
                RiskLevel::High => high_risk_files += 1,
                RiskLevel::Medium => medium_risk_files += 1,
                RiskLevel::Low => low_risk_files += 1,
            }

            let factors: Vec<String> = score
                .contributing_factors
                .iter()
                .map(|(factor, contribution)| format!("{}: {:.1}%", factor, contribution * 100.0))
                .collect();

            FilePrediction {
                file_path: file_path.clone(),
                risk_score: score.probability,
                risk_level: format!("{:?}", score.risk_level),
                factors,
            }
        })
        .collect();

    Ok(DefectPredictionReport {
        total_files: predictions.len(),
        high_risk_files,
        medium_risk_files,
        low_risk_files,
        file_predictions,
    })
}

#[derive(Debug, Serialize)]
/// Report containing defect prediction data.
pub struct DefectPredictionReport {
    pub total_files: usize,
    pub high_risk_files: usize,
    pub medium_risk_files: usize,
    pub low_risk_files: usize,
    pub file_predictions: Vec<FilePrediction>,
}

#[derive(Debug, Serialize)]
/// File prediction.
pub struct FilePrediction {
    pub file_path: String,
    pub risk_score: f32,
    pub risk_level: String,
    pub factors: Vec<String>,
}

/// Format defect prediction summary with top files
///
/// # Example
///
/// ```no_run
/// use pmat::cli::analysis_utilities::{format_defect_summary, DefectPredictionReport, FilePrediction};
///
/// let report = DefectPredictionReport {
///     total_files: 100,
///     high_risk_files: 5,
///     medium_risk_files: 20,
///     low_risk_files: 75,
///     file_predictions: vec![
///         FilePrediction {
///             file_path: "src/main.rs".to_string(),
///             risk_score: 0.9,
///             risk_level: "high".to_string(),
///             factors: vec!["High complexity".to_string()],
///         },
///         FilePrediction {
///             file_path: "src/lib.rs".to_string(),
///             risk_score: 0.6,
///             risk_level: "medium".to_string(),
///             factors: vec!["Recent churn".to_string()],
///         },
///     ],
/// };
///
/// let output = format_defect_summary(&report, 5).unwrap();
///
/// assert!(output.contains("# Defect Prediction Analysis"));
/// assert!(output.contains("Total files analyzed: 100"));
/// assert!(output.contains("## Top Files by Defect Risk"));
/// assert!(output.contains("1. `main.rs` - 90.0% risk (high)"));
/// ```
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn format_defect_summary(report: &DefectPredictionReport, top_files: usize) -> Result<String> {
    use std::fmt::Write;
    let mut output = String::new();

    writeln!(&mut output, "# Defect Prediction Analysis\n")?;
    format_defect_summary_stats(&mut output, report)?;

    if !report.file_predictions.is_empty() {
        format_defect_top_files(&mut output, report, top_files)?;
    }

    Ok(output)
}

/// Format the defect prediction summary statistics
fn format_defect_summary_stats(output: &mut String, report: &DefectPredictionReport) -> Result<()> {
    use std::fmt::Write;

    writeln!(output, "## Summary")?;
    writeln!(output, "- Total files analyzed: {}", report.total_files)?;
    writeln!(output, "- High risk files: {}", report.high_risk_files)?;
    writeln!(output, "- Medium risk files: {}", report.medium_risk_files)?;
    writeln!(output, "- Low risk files: {}\n", report.low_risk_files)?;

    Ok(())
}

/// Format the top files by defect risk section
fn format_defect_top_files(
    output: &mut String,
    report: &DefectPredictionReport,
    top_files: usize,
) -> Result<()> {
    use std::fmt::Write;

    writeln!(output, "## Top Files by Defect Risk\n")?;

    let files_to_show = if top_files == 0 { 10 } else { top_files };
    for (i, prediction) in report
        .file_predictions
        .iter()
        .take(files_to_show)
        .enumerate()
    {
        format_defect_prediction_entry(output, i + 1, prediction)?;
    }

    Ok(())
}

/// Format a single defect prediction entry
fn format_defect_prediction_entry(
    output: &mut String,
    index: usize,
    prediction: &FilePrediction,
) -> Result<()> {
    use std::fmt::Write;

    let filename = extract_filename_from_prediction(prediction);
    writeln!(
        output,
        "{}. `{}` - {:.1}% risk ({})",
        index,
        filename,
        prediction.risk_score * 100.0,
        prediction.risk_level
    )?;

    Ok(())
}

/// Extract display filename from prediction
fn extract_filename_from_prediction(prediction: &FilePrediction) -> &str {
    std::path::Path::new(&prediction.file_path)
        .file_name()
        .and_then(|n| n.to_str())
        .unwrap_or(&prediction.file_path)
}

fn format_defect_full(report: &DefectPredictionReport, top_files: usize) -> Result<String> {
    crate::cli::defect_formatter::format_defect_report(report, "full", top_files)
}

fn format_defect_sarif(report: &DefectPredictionReport) -> Result<String> {
    crate::cli::defect_formatter::format_defect_report(report, "sarif", 0)
}

fn format_defect_csv(report: &DefectPredictionReport) -> Result<String> {
    crate::cli::defect_formatter::format_defect_report(report, "csv", 0)
}

// Single file quality gate check functions

#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod defect_report_tests {
    use super::*;

    fn create_test_prediction(file: &str, score: f32, level: &str) -> FilePrediction {
        FilePrediction {
            file_path: file.to_string(),
            risk_score: score,
            risk_level: level.to_string(),
            factors: vec!["test factor".to_string()],
        }
    }

    fn create_test_report() -> DefectPredictionReport {
        DefectPredictionReport {
            total_files: 3,
            high_risk_files: 1,
            medium_risk_files: 1,
            low_risk_files: 1,
            file_predictions: vec![
                create_test_prediction("src/high.rs", 0.9, "High"),
                create_test_prediction("src/medium.rs", 0.5, "Medium"),
                create_test_prediction("src/low.rs", 0.2, "Low"),
            ],
        }
    }

    #[test]
    fn test_extract_filename_from_prediction() {
        let pred = create_test_prediction("src/services/context.rs", 0.5, "Medium");
        assert_eq!(extract_filename_from_prediction(&pred), "context.rs");
    }

    #[test]
    fn test_extract_filename_simple_path() {
        let pred = create_test_prediction("main.rs", 0.5, "Medium");
        assert_eq!(extract_filename_from_prediction(&pred), "main.rs");
    }

    #[test]
    fn test_format_defect_prediction_entry() {
        let pred = create_test_prediction("src/test.rs", 0.85, "High");
        let mut output = String::new();
        format_defect_prediction_entry(&mut output, 1, &pred).unwrap();
        assert!(output.contains("1. `test.rs`"));
        assert!(output.contains("85.0% risk"));
        assert!(output.contains("High"));
    }

    #[test]
    fn test_format_defect_summary_stats() {
        let report = create_test_report();
        let mut output = String::new();
        format_defect_summary_stats(&mut output, &report).unwrap();

        assert!(output.contains("## Summary"));
        assert!(output.contains("Total files analyzed: 3"));
        assert!(output.contains("High risk files: 1"));
        assert!(output.contains("Medium risk files: 1"));
        assert!(output.contains("Low risk files: 1"));
    }

    #[test]
    fn test_format_defect_top_files() {
        let report = create_test_report();
        let mut output = String::new();
        format_defect_top_files(&mut output, &report, 2).unwrap();

        assert!(output.contains("## Top Files by Defect Risk"));
        assert!(output.contains("high.rs"));
        assert!(output.contains("medium.rs"));
    }

    #[test]
    fn test_format_defect_top_files_default_count() {
        let report = create_test_report();
        let mut output = String::new();
        // When top_files is 0, it defaults to 10
        format_defect_top_files(&mut output, &report, 0).unwrap();

        // Should show all 3 files (we only have 3)
        assert!(output.contains("high.rs"));
        assert!(output.contains("medium.rs"));
        assert!(output.contains("low.rs"));
    }

    #[test]
    fn test_format_defect_summary() {
        let report = create_test_report();
        let output = format_defect_summary(&report, 2).unwrap();

        assert!(output.contains("# Defect Prediction Analysis"));
        assert!(output.contains("## Summary"));
        assert!(output.contains("## Top Files by Defect Risk"));
    }

    #[test]
    fn test_format_defect_summary_empty_predictions() {
        let report = DefectPredictionReport {
            total_files: 0,
            high_risk_files: 0,
            medium_risk_files: 0,
            low_risk_files: 0,
            file_predictions: vec![],
        };
        let output = format_defect_summary(&report, 5).unwrap();

        assert!(output.contains("# Defect Prediction Analysis"));
        assert!(output.contains("Total files analyzed: 0"));
        // Should not have top files section since predictions is empty
        assert!(!output.contains("## Top Files by Defect Risk"));
    }

    #[test]
    fn test_defect_prediction_report_struct() {
        let report = create_test_report();
        assert_eq!(report.total_files, 3);
        assert_eq!(report.high_risk_files, 1);
        assert_eq!(report.file_predictions.len(), 3);
    }

    #[test]
    fn test_file_prediction_struct() {
        let pred = create_test_prediction("path/to/file.rs", 0.75, "High");
        assert_eq!(pred.file_path, "path/to/file.rs");
        assert!((pred.risk_score - 0.75).abs() < 0.001);
        assert_eq!(pred.risk_level, "High");
        assert_eq!(pred.factors.len(), 1);
    }
}