use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use super::scenarios::{ScenarioMatch, TestScenario};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScenarioReport {
pub total_groups: usize,
pub coverage: HashMap<String, Vec<ScenarioMatch>>,
pub uncovered: Vec<String>,
pub unexpected: Vec<String>,
}
impl ScenarioReport {
pub fn from_matches(matches: Vec<ScenarioMatch>, total_groups: usize) -> Self {
let mut coverage: HashMap<String, Vec<ScenarioMatch>> = HashMap::new();
for m in matches {
let key = m.scenario.to_string();
coverage.entry(key).or_default().push(m);
}
let all_scenarios = TestScenario::all();
let uncovered: Vec<String> = all_scenarios
.iter()
.filter(|s| !coverage.contains_key(&s.to_string()))
.map(|s| s.to_string())
.collect();
Self {
total_groups,
coverage,
uncovered,
unexpected: Vec::new(),
}
}
pub fn add_unexpected(&mut self, pattern: String) {
self.unexpected.push(pattern);
}
}
pub fn format_report(report: &ScenarioReport) -> String {
let mut output = String::new();
output.push_str("=== Test Scenario Coverage Report ===\n\n");
let total_scenarios = TestScenario::all().len();
let covered_count = report.coverage.len();
let coverage_pct = (covered_count as f64 / total_scenarios as f64) * 100.0;
output.push_str(&format!(
"COVERED ({}/{} scenarios, {:.0}%):\n",
covered_count, total_scenarios, coverage_pct
));
let categories = ["Winner Selection", "Consolidation", "Conflicts", "Edge Cases"];
for category in categories {
let category_scenarios: Vec<(&String, &Vec<ScenarioMatch>)> = report
.coverage
.iter()
.filter(|(k, _)| {
let prefix = k.chars().next().unwrap_or('?');
match category {
"Winner Selection" => prefix == 'W',
"Consolidation" => prefix == 'C',
"Conflicts" => prefix == 'F',
"Edge Cases" => prefix == 'X',
_ => false,
}
})
.collect();
if !category_scenarios.is_empty() {
output.push_str(&format!("\n {}:\n", category));
for (scenario, matches) in category_scenarios {
output.push_str(&format!(" {}: {} groups\n", scenario, matches.len()));
if let Some(first) = matches.first() {
output.push_str(&format!(
" Example: {} ({})\n",
first.duplicate_id, first.details
));
}
}
}
}
if !report.uncovered.is_empty() {
output.push_str(&format!(
"\nNOT COVERED ({} scenarios):\n",
report.uncovered.len()
));
for scenario in &report.uncovered {
output.push_str(&format!(" {}: 0 groups\n", scenario));
}
}
if !report.unexpected.is_empty() {
output.push_str("\nUNEXPECTED PATTERNS:\n");
for pattern in &report.unexpected {
output.push_str(&format!(" - {}\n", pattern));
}
}
output.push_str("\n=== Summary ===\n");
output.push_str(&format!("Total groups analyzed: {}\n", report.total_groups));
output.push_str(&format!(
"Scenarios covered: {}/{} ({:.0}%)\n",
covered_count, total_scenarios, coverage_pct
));
output.push_str(&format!(
"Synthetic images needed for: {} scenarios\n",
report.uncovered.len()
));
output
}