use std::fmt::Write;
use std::path::Path;
use super::backend::ProofBackend;
use crate::diff::FrameDiff;
use crate::frame::TerminalFrame;
#[derive(Debug, Clone)]
pub struct ProofCapture {
pub label: String,
pub description: String,
pub frame_text: String,
pub frame_bytes: Vec<u8>,
pub cols: u16,
pub rows: u16,
pub assertions: Vec<AssertionResult>,
}
#[derive(Debug, Clone)]
pub struct AssertionResult {
pub passed: bool,
pub description: String,
}
#[derive(Debug, thiserror::Error)]
pub enum ProofError {
#[error("proof I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("proof format error: {0}")]
Format(String),
}
#[derive(Debug, Clone)]
pub struct ProofReport {
pub scenario_name: String,
pub captures: Vec<ProofCapture>,
pub diffs: Vec<FrameDiff>,
}
impl ProofReport {
pub fn new(scenario_name: impl Into<String>) -> Self {
Self {
scenario_name: scenario_name.into(),
captures: Vec::new(),
diffs: Vec::new(),
}
}
pub fn add_capture(
&mut self,
label: impl Into<String>,
description: impl Into<String>,
frame: &TerminalFrame,
) {
if let Some(previous) = self.captures.last() {
let previous_frame =
TerminalFrame::new(previous.cols, previous.rows, &previous.frame_bytes);
self.diffs.push(FrameDiff::compute(&previous_frame, frame));
}
self.captures.push(ProofCapture {
label: label.into(),
description: description.into(),
frame_text: frame.all_text(),
frame_bytes: frame.contents_formatted(),
cols: frame.cols(),
rows: frame.rows(),
assertions: Vec::new(),
});
}
pub fn add_assertion(
&mut self,
label: &str,
passed: bool,
description: impl Into<String>,
) -> bool {
if let Some(capture) = self
.captures
.iter_mut()
.find(|capture| capture.label == label)
{
capture.assertions.push(AssertionResult {
passed,
description: description.into(),
});
return true;
}
false
}
pub fn save(&self, backend: &dyn ProofBackend, path: &Path) -> Result<(), ProofError> {
backend.render(self, path)
}
pub fn to_annotated_text(&self) -> String {
let mut output = String::new();
write_header(&mut output, &self.scenario_name);
for (index, capture) in self.captures.iter().enumerate() {
let step_number = index + 1;
write_capture_section(&mut output, step_number, capture);
}
write_footer(&mut output, self.captures.len());
output
}
}
fn write_header(output: &mut String, scenario_name: &str) {
let title = format!("Proof Report: {scenario_name}");
let border = "=".repeat(title.len().max(60));
let _ = writeln!(output, "{border}");
let _ = writeln!(output, "{title}");
let _ = writeln!(output, "{border}");
let _ = writeln!(output);
}
fn write_capture_section(output: &mut String, step_number: usize, capture: &ProofCapture) {
let heading = format!(
"Step {step_number}: [{}] {}",
capture.label, capture.description
);
let separator = "-".repeat(heading.len().max(60));
let _ = writeln!(output, "{separator}");
let _ = writeln!(output, "{heading}");
let _ = writeln!(output, " Terminal: {}x{}", capture.cols, capture.rows);
let _ = writeln!(output, "{separator}");
let _ = writeln!(output);
let frame_border = format!("+{}+", "-".repeat(usize::from(capture.cols) + 2));
let _ = writeln!(output, "{frame_border}");
for line in capture.frame_text.lines() {
let padded = format!("{line:<width$}", width = usize::from(capture.cols));
let _ = writeln!(output, "| {padded} |");
}
let _ = writeln!(output, "{frame_border}");
let _ = writeln!(output);
if !capture.assertions.is_empty() {
let _ = writeln!(output, " Assertions:");
for assertion in &capture.assertions {
let marker = if assertion.passed { "PASS" } else { "FAIL" };
let _ = writeln!(output, " [{marker}] {}", assertion.description);
}
let _ = writeln!(output);
}
}
fn write_footer(output: &mut String, capture_count: usize) {
let _ = writeln!(output, "Total captures: {capture_count}");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn proof_report_collects_captures() {
let frame = TerminalFrame::new(80, 24, b"Hello, World!");
let mut report = ProofReport::new("test_scenario");
report.add_capture("startup", "Application launched", &frame);
report.add_capture("after_input", "User typed text", &frame);
assert_eq!(report.captures.len(), 2);
assert_eq!(report.captures[0].label, "startup");
assert_eq!(report.captures[1].label, "after_input");
assert!(report.captures[0].frame_text.contains("Hello, World!"));
}
#[test]
fn add_assertion_attaches_to_labeled_capture() {
let frame = TerminalFrame::new(80, 24, b"Test");
let mut report = ProofReport::new("assert_test");
report.add_capture("check", "Checking state", &frame);
report.add_assertion("check", true, "Text 'Test' is visible");
report.add_assertion("check", false, "Color is blue");
let capture = &report.captures[0];
assert_eq!(capture.assertions.len(), 2);
assert!(capture.assertions[0].passed);
assert!(!capture.assertions[1].passed);
}
#[test]
fn add_assertion_targets_specific_capture() {
let frame = TerminalFrame::new(80, 24, b"Test");
let mut report = ProofReport::new("targeted_test");
report.add_capture("first", "First", &frame);
report.add_capture("second", "Second", &frame);
let found = report.add_assertion("first", true, "targeted assertion");
assert!(found);
assert_eq!(report.captures[0].assertions.len(), 1);
assert_eq!(
report.captures[0].assertions[0].description,
"targeted assertion"
);
assert!(report.captures[1].assertions.is_empty());
}
#[test]
fn add_assertion_returns_false_for_unknown_label() {
let frame = TerminalFrame::new(80, 24, b"Test");
let mut report = ProofReport::new("miss_test");
report.add_capture("existing", "Existing", &frame);
let found = report.add_assertion("nonexistent", true, "orphan");
assert!(!found);
assert!(report.captures[0].assertions.is_empty());
}
#[test]
fn annotated_text_contains_scenario_name() {
let report = ProofReport::new("my_scenario");
let text = report.to_annotated_text();
assert!(text.contains("Proof Report: my_scenario"));
}
#[test]
fn annotated_text_contains_step_labels_and_descriptions() {
let frame = TerminalFrame::new(40, 5, b"content");
let mut report = ProofReport::new("labeled_test");
report.add_capture("init", "Initial state", &frame);
report.add_capture("done", "Final state", &frame);
let text = report.to_annotated_text();
assert!(text.contains("Step 1: [init] Initial state"));
assert!(text.contains("Step 2: [done] Final state"));
assert!(text.contains("Total captures: 2"));
}
#[test]
fn annotated_text_contains_frame_content() {
let frame = TerminalFrame::new(40, 5, b"Hello proof");
let mut report = ProofReport::new("frame_test");
report.add_capture("snap", "Snapshot", &frame);
let text = report.to_annotated_text();
assert!(text.contains("Hello proof"));
assert!(text.contains("Terminal: 40x5"));
}
#[test]
fn annotated_text_contains_assertion_markers() {
let frame = TerminalFrame::new(40, 5, b"Test");
let mut report = ProofReport::new("assert_output");
report.add_capture("check", "Verify state", &frame);
report.add_assertion("check", true, "text visible");
report.add_assertion("check", false, "color match");
let text = report.to_annotated_text();
assert!(text.contains("[PASS] text visible"));
assert!(text.contains("[FAIL] color match"));
}
#[test]
fn proof_capture_stores_dimensions() {
let frame = TerminalFrame::new(120, 40, b"wide");
let mut report = ProofReport::new("dims");
report.add_capture("wide_term", "Wide terminal", &frame);
assert_eq!(report.captures[0].cols, 120);
assert_eq!(report.captures[0].rows, 40);
}
#[test]
fn auto_diff_computed_between_consecutive_captures() {
let frame_a = TerminalFrame::new(80, 24, b"Hello");
let frame_b = TerminalFrame::new(80, 24, b"World");
let mut report = ProofReport::new("diff_test");
report.add_capture("before", "Before change", &frame_a);
report.add_capture("after", "After change", &frame_b);
assert_eq!(report.diffs.len(), 1);
assert!(!report.diffs[0].is_identical());
}
#[test]
fn no_diff_for_single_capture() {
let frame = TerminalFrame::new(80, 24, b"Solo");
let mut report = ProofReport::new("single");
report.add_capture("only", "Only capture", &frame);
assert!(report.diffs.is_empty());
}
#[test]
fn identical_captures_produce_identical_diff() {
let frame = TerminalFrame::new(80, 24, b"Same");
let mut report = ProofReport::new("same");
report.add_capture("first", "First", &frame);
report.add_capture("second", "Second", &frame);
assert_eq!(report.diffs.len(), 1);
assert!(report.diffs[0].is_identical());
}
#[test]
fn frame_bytes_preserves_style_for_diff() {
let frame_a = TerminalFrame::new(80, 24, b"Hello");
let frame_b = TerminalFrame::new(80, 24, b"\x1b[1mHello\x1b[0m");
let mut report = ProofReport::new("style_diff");
report.add_capture("plain", "Plain text", &frame_a);
report.add_capture("bold", "Bold text", &frame_b);
assert_eq!(report.diffs.len(), 1);
assert!(!report.diffs[0].is_identical());
}
#[test]
fn frame_bytes_stores_formatted_output() {
let frame = TerminalFrame::new(80, 24, b"\x1b[31mRed\x1b[0m");
let mut report = ProofReport::new("bytes_test");
report.add_capture("colored", "Red text", &frame);
let capture = &report.captures[0];
assert!(!capture.frame_bytes.is_empty());
assert!(capture.frame_bytes.len() > capture.frame_text.len());
}
}