use crate::reporting::ascii::{detect_trend, sparkline};
use crate::reporting::TranspileReport;
use super::ReportFormatter;
#[derive(Default)]
pub struct MarkdownFormatter;
impl ReportFormatter for MarkdownFormatter {
fn format(&self, report: &TranspileReport) -> String {
let mut sections = vec![
"# Ruchy Transpilation Report".to_string(),
String::new(),
format!("**Generated**: {}", chrono_lite_now()),
String::new(),
"## Executive Summary".to_string(),
String::new(),
format!(
"| Metric | Value |
|--------|-------|
| Total Files | {} |
| Passed | {} |
| Failed | {} |
| Success Rate | {:.1}% |
| Andon Status | {} |
| Grade | {} |",
report.total,
report.passed,
report.failed,
report.success_rate(),
report.andon(),
report.grade()
),
];
if !report.trend.is_empty() {
let line = sparkline(&report.trend);
let direction = detect_trend(&report.trend);
sections.push(String::new());
sections.push("## Trend Analysis".to_string());
sections.push(String::new());
sections.push(format!(
"**7-Day Trend**: `{}` ({})",
line,
direction.label()
));
}
if !report.errors.is_empty() {
sections.push(String::new());
sections.push("## Error Taxonomy".to_string());
sections.push(String::new());
sections.push("| Error Code | Count | % of Failures | Sample |".to_string());
sections.push("|------------|-------|---------------|--------|".to_string());
for entry in &report.errors {
let percentage = if report.failed > 0 {
(entry.count as f64 / report.failed as f64) * 100.0
} else {
0.0
};
let sample = entry
.samples
.first()
.map_or_else(|| "-".to_string(), |s| truncate_md(s, 40));
sections.push(format!(
"| {} | {} | {:.1}% | {} |",
entry.code, entry.count, percentage, sample
));
}
}
sections.push(String::new());
sections.push("---".to_string());
sections.push("*Report generated by Ruchy Transpile Oracle [VIS-001]*".to_string());
sections.join("\n")
}
}
fn chrono_lite_now() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let duration = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default();
let secs = duration.as_secs();
let days = secs / 86400;
let years_since_1970 = days / 365;
let year = 1970 + years_since_1970;
format!("{year}-XX-XX (Unix: {secs})")
}
fn truncate_md(s: &str, max_len: usize) -> String {
let escaped = s.replace('|', "\\|");
if escaped.len() <= max_len {
escaped
} else {
format!("{}...", &escaped[..max_len.saturating_sub(3)])
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::reporting::ErrorEntry;
#[test]
fn test_markdown_formatter_header() {
let report = TranspileReport::new(100, 85, 15);
let fmt = MarkdownFormatter;
let output = fmt.format(&report);
assert!(output.contains("# Ruchy Transpilation Report"));
}
#[test]
fn test_markdown_formatter_summary_table() {
let report = TranspileReport::new(100, 85, 15);
let fmt = MarkdownFormatter;
let output = fmt.format(&report);
assert!(output.contains("| Total Files | 100 |"));
assert!(output.contains("| Passed | 85 |"));
assert!(output.contains("| Failed | 15 |"));
assert!(output.contains("| Success Rate | 85.0% |"));
}
#[test]
fn test_markdown_formatter_with_errors() {
let mut report = TranspileReport::new(100, 85, 15);
report.add_error(ErrorEntry::new("E0308", 8).with_sample("mismatched types"));
let fmt = MarkdownFormatter;
let output = fmt.format(&report);
assert!(output.contains("## Error Taxonomy"));
assert!(output.contains("| E0308 | 8 |"));
assert!(output.contains("mismatched types"));
}
#[test]
fn test_markdown_formatter_with_trend() {
let report = TranspileReport::new(100, 85, 15).with_trend(vec![70.0, 75.0, 80.0, 85.0]);
let fmt = MarkdownFormatter;
let output = fmt.format(&report);
assert!(output.contains("## Trend Analysis"));
assert!(output.contains("7-Day Trend"));
assert!(output.contains("improving"));
}
#[test]
fn test_markdown_formatter_footer() {
let report = TranspileReport::new(100, 85, 15);
let fmt = MarkdownFormatter;
let output = fmt.format(&report);
assert!(output.contains("VIS-001"));
assert!(output.contains("---"));
}
#[test]
fn test_truncate_md_short() {
assert_eq!(truncate_md("hello", 10), "hello");
}
#[test]
fn test_truncate_md_long() {
assert_eq!(truncate_md("hello world this is long", 10), "hello w...");
}
#[test]
fn test_truncate_md_pipes() {
assert_eq!(truncate_md("a|b|c", 10), "a\\|b\\|c");
}
}