use crate::ui::constants::*;
use crate::ui::utils::{box_line, divider, empty_box_line, severity_bar, term_width};
use serde::Serialize;
#[derive(Debug, Clone, Serialize)]
pub struct AnalysisSummary {
pub target: String,
pub chain: String,
pub total_checks: usize,
pub violations: usize,
pub passed: usize,
pub suppressed: usize,
pub duration_secs: f64,
pub severity_breakdown: SeverityBreakdown,
}
#[derive(Debug, Clone, Serialize)]
pub struct SeverityBreakdown {
pub critical: usize,
pub high: usize,
pub medium: usize,
pub low: usize,
}
pub fn render_summary(summary: &AnalysisSummary) -> String {
let width = term_width();
let mut output = String::new();
output.push('\n');
output.push_str(&format!(
"{}{}{}\n",
color_border("╭"),
divider(width.saturating_sub(2)),
color_border("╮")
));
let header_line = "─ Analysis Summary ─".to_string();
output.push_str(&format!("{}\n", box_line(&header_line, width)));
output.push_str(&format!("{}\n", empty_box_line(width)));
let target_line = format!(
"{} {}",
color_label("Target"),
color_value(&summary.target)
);
output.push_str(&format!("{}\n", box_line(&target_line, width)));
let chain_line = format!("{} {}", color_label("Chain"), color_value(&summary.chain));
output.push_str(&format!("{}\n", box_line(&chain_line, width)));
let checks_line = format!(
"{} {} total {} {} violations {} {} passed {} {} suppressed",
color_label("Checks"),
color_value(&summary.total_checks.to_string()),
color_dim("·"),
color_value(&summary.violations.to_string()),
color_dim("·"),
color_value(&summary.passed.to_string()),
color_dim("·"),
color_value(&summary.suppressed.to_string()),
);
output.push_str(&format!("{}\n", box_line(&checks_line, width)));
let duration_line = format!("{} {:.2}s", color_label("Duration"), summary.duration_secs);
output.push_str(&format!("{}\n", box_line(&duration_line, width)));
output.push_str(&format!("{}\n", empty_box_line(width)));
output.push_str(&format!(
"{}{}{} \n",
color_border("├"),
divider(width.saturating_sub(4)),
color_border("┤")
));
output.push_str(&format!("{}\n", empty_box_line(width)));
let severity_header = "Severity Breakdown";
output.push_str(&format!("{}\n", box_line(severity_header, width)));
output.push_str(&format!("{}\n", empty_box_line(width)));
let max_severity = summary
.severity_breakdown
.critical
.max(summary.severity_breakdown.high)
.max(summary.severity_breakdown.medium)
.max(summary.severity_breakdown.low);
let critical_bar = severity_bar(summary.severity_breakdown.critical, max_severity, "█", "░");
let critical_count = summary.severity_breakdown.critical.to_string();
let critical_line = format!(
"{} {} {} {} {} {}",
color_critical("CRITICAL"),
critical_bar,
color_value(&critical_count),
color_high("HIGH"),
severity_bar(summary.severity_breakdown.high, max_severity, "█", "░"),
color_value(&summary.severity_breakdown.high.to_string()),
);
output.push_str(&format!("{}\n", box_line(&critical_line, width)));
let medium_bar = severity_bar(summary.severity_breakdown.medium, max_severity, "█", "░");
let medium_count = summary.severity_breakdown.medium.to_string();
let low_bar = severity_bar(summary.severity_breakdown.low, max_severity, "█", "░");
let low_count = summary.severity_breakdown.low.to_string();
let medium_line = format!(
"{} {} {} {} {} {}",
color_medium("MEDIUM"),
medium_bar,
color_value(&medium_count),
color_low("LOW"),
low_bar,
color_value(&low_count),
);
output.push_str(&format!("{}\n", box_line(&medium_line, width)));
output.push_str(&format!("{}\n", empty_box_line(width)));
output.push_str(&format!(
"{}{}{} \n",
color_border("├"),
divider(width.saturating_sub(4)),
color_border("┤")
));
output.push_str(&format!("{}\n", empty_box_line(width)));
let (status_icon, status_color_text) = if summary.violations == 0 {
(ICON_PASS, color_success("PASS — all checks passed"))
} else {
(
ICON_CRITICAL,
color_failure("FAIL — violations found at or above 'low' threshold"),
)
};
let status_line = format!(
"{} {} {}",
color_label("Status"),
status_icon,
status_color_text
);
output.push_str(&format!("{}\n", box_line(&status_line, width)));
output.push_str(&format!("{}\n", empty_box_line(width)));
output.push_str(&format!(
"{}{}{}\n",
color_border("╰"),
divider(width.saturating_sub(2)),
color_border("╯")
));
output
}
#[allow(dead_code)]
pub fn render_no_violations() -> String {
let _width = term_width();
let mut output = String::new();
output.push('\n');
output.push_str(&format!(
" {}\n",
color_success(&format!(
"{} No violations found. All checks passed.",
ICON_PASS
))
));
output
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_render_summary_with_violations() {
let summary = AnalysisSummary {
target: "./contracts/Token.sol".to_string(),
chain: "EVM".to_string(),
total_checks: 47,
violations: 3,
passed: 44,
suppressed: 0,
duration_secs: 1.24,
severity_breakdown: SeverityBreakdown {
critical: 1,
high: 1,
medium: 1,
low: 0,
},
};
let rendered = render_summary(&summary);
assert!(rendered.contains("Analysis Summary"));
assert!(rendered.contains("./contracts/Token.sol"));
assert!(rendered.contains("EVM"));
assert!(rendered.contains("47"));
assert!(rendered.contains("3"));
assert!(rendered.contains("44"));
assert!(rendered.contains("1.24"));
assert!(rendered.contains("FAIL"));
}
#[test]
fn test_render_summary_no_violations() {
let summary = AnalysisSummary {
target: "./contracts/Safe.sol".to_string(),
chain: "EVM".to_string(),
total_checks: 100,
violations: 0,
passed: 100,
suppressed: 0,
duration_secs: 2.5,
severity_breakdown: SeverityBreakdown {
critical: 0,
high: 0,
medium: 0,
low: 0,
},
};
let rendered = render_summary(&summary);
assert!(rendered.contains("Analysis Summary"));
assert!(rendered.contains("PASS"));
assert!(rendered.contains("100"));
}
#[test]
fn test_render_no_violations_message() {
let msg = render_no_violations();
assert!(msg.contains("No violations found"));
assert!(msg.contains("All checks passed"));
}
}