use crate::error::ConformResult;
use crate::types::{ClipMatch, ClipReference};
use serde::{Deserialize, Serialize};
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MatchReport {
pub matched: Vec<ClipMatch>,
pub missing: Vec<ClipReference>,
pub ambiguous: Vec<AmbiguousMatch>,
pub stats: MatchStatistics,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AmbiguousMatch {
pub clip: ClipReference,
pub candidates: Vec<ClipMatch>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MatchStatistics {
pub total_clips: usize,
pub matched_count: usize,
pub missing_count: usize,
pub ambiguous_count: usize,
pub conform_rate: f64,
}
impl MatchReport {
#[must_use]
pub fn new(
matched: Vec<ClipMatch>,
missing: Vec<ClipReference>,
ambiguous: Vec<AmbiguousMatch>,
) -> Self {
let total_clips = matched.len() + missing.len() + ambiguous.len();
let conform_rate = if total_clips == 0 {
0.0
} else {
matched.len() as f64 / total_clips as f64
};
Self {
stats: MatchStatistics {
total_clips,
matched_count: matched.len(),
missing_count: missing.len(),
ambiguous_count: ambiguous.len(),
conform_rate,
},
matched,
missing,
ambiguous,
}
}
pub fn to_json(&self) -> ConformResult<String> {
Ok(serde_json::to_string_pretty(self)?)
}
#[must_use]
pub fn to_html(&self) -> String {
let mut html = String::new();
html.push_str("<!DOCTYPE html>\n<html>\n<head>\n");
html.push_str("<title>Conform Report</title>\n");
html.push_str("<style>\n");
html.push_str("body { font-family: Arial, sans-serif; margin: 20px; }\n");
html.push_str("table { border-collapse: collapse; width: 100%; }\n");
html.push_str("th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }\n");
html.push_str("th { background-color: #4CAF50; color: white; }\n");
html.push_str(".matched { color: green; }\n");
html.push_str(".missing { color: red; }\n");
html.push_str(".ambiguous { color: orange; }\n");
html.push_str("</style>\n</head>\n<body>\n");
html.push_str("<h1>Conform Report</h1>\n");
html.push_str("<h2>Statistics</h2>\n");
html.push_str("<ul>\n");
html.push_str(&format!(
"<li>Total Clips: {}</li>\n",
self.stats.total_clips
));
html.push_str(&format!(
"<li class='matched'>Matched: {}</li>\n",
self.stats.matched_count
));
html.push_str(&format!(
"<li class='missing'>Missing: {}</li>\n",
self.stats.missing_count
));
html.push_str(&format!(
"<li class='ambiguous'>Ambiguous: {}</li>\n",
self.stats.ambiguous_count
));
html.push_str(&format!(
"<li>Conform Rate: {:.1}%</li>\n",
self.stats.conform_rate * 100.0
));
html.push_str("</ul>\n");
if !self.matched.is_empty() {
html.push_str("<h2>Matched Clips</h2>\n");
html.push_str("<table>\n");
html.push_str("<tr><th>Clip ID</th><th>Source File</th><th>Match Score</th><th>Method</th></tr>\n");
for m in &self.matched {
html.push_str(&format!(
"<tr><td>{}</td><td>{}</td><td>{:.2}</td><td>{}</td></tr>\n",
m.clip.id,
m.clip.source_file.as_ref().unwrap_or(&"N/A".to_string()),
m.score,
m.method
));
}
html.push_str("</table>\n");
}
if !self.missing.is_empty() {
html.push_str("<h2 class='missing'>Missing Clips</h2>\n");
html.push_str("<table>\n");
html.push_str("<tr><th>Clip ID</th><th>Source File</th></tr>\n");
for clip in &self.missing {
html.push_str(&format!(
"<tr><td>{}</td><td>{}</td></tr>\n",
clip.id,
clip.source_file.as_ref().unwrap_or(&"N/A".to_string())
));
}
html.push_str("</table>\n");
}
if !self.ambiguous.is_empty() {
html.push_str("<h2 class='ambiguous'>Ambiguous Matches</h2>\n");
for amb in &self.ambiguous {
html.push_str(&format!("<h3>Clip: {}</h3>\n", amb.clip.id));
html.push_str("<table>\n");
html.push_str("<tr><th>Candidate</th><th>Score</th><th>Method</th></tr>\n");
for candidate in &amb.candidates {
html.push_str(&format!(
"<tr><td>{}</td><td>{:.2}</td><td>{}</td></tr>\n",
candidate.media.filename, candidate.score, candidate.method
));
}
html.push_str("</table>\n");
}
}
html.push_str("</body>\n</html>\n");
html
}
pub fn save_json<P: AsRef<Path>>(&self, path: P) -> ConformResult<()> {
let json = self.to_json()?;
std::fs::write(path, json)?;
Ok(())
}
pub fn save_html<P: AsRef<Path>>(&self, path: P) -> ConformResult<()> {
let html = self.to_html();
std::fs::write(path, html)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_report() {
let report = MatchReport::new(Vec::new(), Vec::new(), Vec::new());
assert_eq!(report.stats.total_clips, 0);
assert!((report.stats.conform_rate - 0.0).abs() < f64::EPSILON);
}
#[test]
fn test_report_statistics() {
let report = MatchReport::new(vec![], vec![], vec![]);
let json = report.to_json().expect("json should be valid");
assert!(json.contains("total_clips"));
}
#[test]
fn test_html_generation() {
let report = MatchReport::new(Vec::new(), Vec::new(), Vec::new());
let html = report.to_html();
assert!(html.contains("<html>"));
assert!(html.contains("Conform Report"));
}
}