tldr-cli 0.1.2

CLI binary for TLDR code analysis tool
Documentation
//! Coverage command - Parse and report code coverage from existing reports
//!
//! Parses coverage reports in multiple formats:
//! - Cobertura XML (GitLab/Jenkins standard)
//! - LCOV (llvm-cov, gcov)
//! - coverage.py JSON
//!
//! **Note:** This command parses existing coverage reports. It does NOT run tests
//! or generate coverage data. Use your test framework with coverage enabled first.
//!
//! # Example
//! ```bash
//! # Parse a Cobertura XML report
//! tldr coverage coverage.xml
//!
//! # Parse LCOV with explicit format
//! tldr coverage coverage.lcov --format lcov
//!
//! # Show per-file breakdown and uncovered code
//! tldr coverage coverage.xml --by-file --uncovered
//!
//! # Check against 80% threshold
//! tldr coverage coverage.xml --threshold 80
//! ```

use std::path::PathBuf;

use anyhow::Result;
use clap::{Args, ValueEnum};

use tldr_core::quality::coverage::{
    parse_coverage, CoverageFormat as CoreCoverageFormat, CoverageOptions, CoverageReport,
};

use crate::output::{OutputFormat, OutputWriter};

/// Coverage report format options
#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)]
pub enum CoverageFormat {
    /// Cobertura XML format (GitLab/Jenkins standard)
    Cobertura,
    /// LCOV format (llvm-cov, gcov)
    Lcov,
    /// coverage.py JSON format
    #[value(name = "coveragepy")]
    CoveragePy,
    /// Auto-detect from file content
    Auto,
}

impl From<CoverageFormat> for Option<CoreCoverageFormat> {
    fn from(format: CoverageFormat) -> Self {
        match format {
            CoverageFormat::Cobertura => Some(CoreCoverageFormat::Cobertura),
            CoverageFormat::Lcov => Some(CoreCoverageFormat::Lcov),
            CoverageFormat::CoveragePy => Some(CoreCoverageFormat::CoveragePy),
            CoverageFormat::Auto => None,
        }
    }
}

/// Sort order for files
#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)]
pub enum SortOrder {
    /// Ascending order (lowest coverage first)
    Asc,
    /// Descending order (highest coverage first)
    Desc,
}

/// Parse and report code coverage from existing reports
///
/// This command parses coverage reports generated by test frameworks.
/// It does NOT run tests or generate coverage data.
///
/// Supported formats:
/// - Cobertura XML (GitLab CI, Jenkins, pytest-cov)
/// - LCOV (llvm-cov, gcov, c8)
/// - coverage.py JSON (Python coverage.py)
#[derive(Debug, Args)]
pub struct CoverageArgs {
    /// Path to coverage report file
    pub report: PathBuf,

    /// Coverage report format (auto-detect if not specified)
    #[arg(
        long = "report-format",
        short = 'R',
        value_enum,
        default_value = "auto"
    )]
    pub report_format: CoverageFormat,

    /// Minimum coverage threshold (default: 80%)
    #[arg(long, default_value = "80.0")]
    pub threshold: f64,

    /// Show per-file coverage breakdown
    #[arg(long)]
    pub by_file: bool,

    /// List uncovered lines and functions
    #[arg(long)]
    pub uncovered: bool,

    /// Filter to files matching pattern (can be repeated)
    #[arg(long)]
    pub filter: Vec<String>,

    /// Sort files by coverage
    #[arg(long, value_enum)]
    pub sort: Option<SortOrder>,

    /// Base path for resolving file paths (for existence checking)
    #[arg(long)]
    pub base_path: Option<PathBuf>,

    /// Show only files below threshold
    #[arg(long)]
    pub uncovered_only: bool,
}

impl CoverageArgs {
    /// Run the coverage command
    pub fn run(&self, format: OutputFormat, quiet: bool) -> Result<()> {
        let writer = OutputWriter::new(format, quiet);

        writer.progress(&format!(
            "Parsing coverage report: {}...",
            self.report.display()
        ));

        // Build options
        let options = CoverageOptions {
            threshold: self.threshold,
            by_file: self.by_file || self.uncovered_only,
            include_uncovered: self.uncovered,
            filter: self.filter.clone(),
            base_path: self.base_path.clone(),
        };

        // Parse the report
        let mut report = parse_coverage(&self.report, self.report_format.into(), &options)?;

        // Apply sorting if requested
        if let Some(sort_order) = self.sort {
            report.files.sort_by(|a, b| {
                let cmp = a.line_coverage.partial_cmp(&b.line_coverage).unwrap();
                match sort_order {
                    SortOrder::Asc => cmp,
                    SortOrder::Desc => cmp.reverse(),
                }
            });
        }

        // Filter to uncovered only if requested
        if self.uncovered_only {
            report.files.retain(|f| f.line_coverage < self.threshold);
        }

        // Output based on format
        if writer.is_text() {
            let text = format_coverage_text(&report, self.threshold);
            writer.write_text(&text)?;
        } else {
            writer.write(&report)?;
        }

        Ok(())
    }
}

/// Format coverage report for text output
fn format_coverage_text(report: &CoverageReport, threshold: f64) -> String {
    use colored::Colorize;
    use comfy_table::{presets::UTF8_FULL, Cell, Color, ContentArrangement, Table};

    let mut output = String::new();

    // Header
    output.push_str(&format!(
        "Coverage Report ({})\n",
        report.format.to_string().cyan()
    ));
    output.push_str("============================\n\n");

    // Summary section
    let summary = &report.summary;
    output.push_str(&"Summary:\n".bold().to_string());
    output.push_str(&format!(
        "  Line Coverage:     {:.1}% ({}/{})\n",
        summary.line_coverage,
        summary.covered_lines.to_string().as_str().green(),
        summary.total_lines
    ));

    if let Some(branch_cov) = summary.branch_coverage {
        output.push_str(&format!("  Branch Coverage:   {:.1}%", branch_cov));
        if let (Some(covered), Some(total)) = (summary.covered_branches, summary.total_branches) {
            output.push_str(&format!(" ({}/{})", covered, total));
        }
        output.push('\n');
    }

    if let Some(func_cov) = summary.function_coverage {
        output.push_str(&format!("  Function Coverage: {:.1}%", func_cov));
        if let (Some(covered), Some(total)) = (summary.covered_functions, summary.total_functions) {
            output.push_str(&format!(" ({}/{})", covered, total));
        }
        output.push('\n');
    }

    // Threshold status
    let threshold_status = if summary.threshold_met {
        format!("PASS (>= {:.0}%)", threshold).green().to_string()
    } else {
        format!("FAIL (< {:.0}%)", threshold).red().to_string()
    };
    output.push_str(&format!("  Threshold:         {}\n", threshold_status));

    output.push('\n');

    // Warnings
    for warning in &report.warnings {
        output.push_str(&format!("{} {}\n", "Warning:".yellow(), warning));
    }
    if !report.warnings.is_empty() {
        output.push('\n');
    }

    // Per-file breakdown
    if !report.files.is_empty() {
        output.push_str(&"Per-File Coverage:\n".bold().to_string());

        let mut table = Table::new();
        table
            .load_preset(UTF8_FULL)
            .set_content_arrangement(ContentArrangement::Dynamic)
            .set_header(vec![
                Cell::new("File").fg(Color::Cyan),
                Cell::new("Line %").fg(Color::Cyan),
                Cell::new("Lines").fg(Color::Cyan),
                Cell::new("Branch %").fg(Color::Cyan),
                Cell::new("Status").fg(Color::Cyan),
            ]);

        for file in &report.files {
            let cov_color = if file.line_coverage >= threshold {
                Color::Green
            } else if file.line_coverage >= threshold * 0.8 {
                Color::Yellow
            } else {
                Color::Red
            };

            let status = if file.line_coverage >= threshold {
                "OK".to_string()
            } else {
                "LOW".to_string()
            };

            let branch_str = file
                .branch_coverage
                .map(|b| format!("{:.1}%", b))
                .unwrap_or_else(|| "-".to_string());

            table.add_row(vec![
                Cell::new(&file.path),
                Cell::new(format!("{:.1}%", file.line_coverage)).fg(cov_color),
                Cell::new(format!("{}/{}", file.covered_lines, file.total_lines)),
                Cell::new(branch_str),
                Cell::new(status).fg(cov_color),
            ]);
        }

        output.push_str(&table.to_string());
        output.push_str("\n\n");
    }

    // Uncovered code section
    if let Some(uncovered) = &report.uncovered {
        if !uncovered.functions.is_empty() {
            output.push_str(&"Uncovered Functions:\n".bold().to_string());
            for func in &uncovered.functions {
                output.push_str(&format!(
                    "  {}:{} - {}\n",
                    func.file.dimmed(),
                    func.line.to_string().cyan(),
                    func.name.red()
                ));
            }
            output.push('\n');
        }

        if !uncovered.line_ranges.is_empty() {
            output.push_str(&"Uncovered Line Ranges:\n".bold().to_string());

            // Group by file
            let mut by_file: std::collections::HashMap<&str, Vec<(u32, u32)>> =
                std::collections::HashMap::new();
            for range in &uncovered.line_ranges {
                by_file
                    .entry(&range.file)
                    .or_default()
                    .push((range.start, range.end));
            }

            for (file, ranges) in by_file {
                let range_strs: Vec<String> = ranges
                    .iter()
                    .map(|(s, e)| {
                        if s == e {
                            format!("{}", s)
                        } else {
                            format!("{}-{}", s, e)
                        }
                    })
                    .collect();
                output.push_str(&format!(
                    "  {}: {}\n",
                    file.dimmed(),
                    range_strs.join(", ").red()
                ));
            }
        }
    }

    output
}