Skip to main content

jugar_probar/pixel_coverage/
terminal.rs

1//! First-Class Terminal Output for Pixel Coverage (PIXEL-001 v2.1 Phase 7)
2//!
3//! Rich terminal heatmap with score bars, gap analysis, and hypothesis status.
4//! Implements Popperian falsification display for coverage claims.
5
6use super::heatmap::ColorPalette;
7use super::tracker::{CombinedCoverageReport, CoverageCell};
8
9/// Output mode for terminal rendering
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
11pub enum OutputMode {
12    /// Rich ANSI true-color (24-bit) output
13    #[default]
14    RichAnsi,
15    /// No color ASCII output (NO_COLOR env or --no-color flag)
16    NoColorAscii,
17    /// JSON output for CI tools
18    Json,
19}
20
21impl OutputMode {
22    /// Detect output mode from environment
23    #[must_use]
24    pub fn from_env() -> Self {
25        if std::env::var("NO_COLOR").is_ok() {
26            Self::NoColorAscii
27        } else if std::env::var("CI").is_ok() {
28            Self::Json
29        } else {
30            Self::RichAnsi
31        }
32    }
33}
34
35/// ANSI escape codes for terminal output
36pub mod ansi {
37    /// Reset all attributes
38    pub const RESET: &str = "\x1b[0m";
39    /// Bold text
40    pub const BOLD: &str = "\x1b[1m";
41    /// Dim text
42    pub const DIM: &str = "\x1b[2m";
43
44    /// RGB foreground color
45    #[must_use]
46    pub fn rgb_fg(r: u8, g: u8, b: u8) -> String {
47        format!("\x1b[38;2;{r};{g};{b}m")
48    }
49
50    /// RGB background color
51    #[must_use]
52    pub fn rgb_bg(r: u8, g: u8, b: u8) -> String {
53        format!("\x1b[48;2;{r};{g};{b}m")
54    }
55
56    /// Green color for passing tests
57    pub const PASS: &str = "\x1b[32m";
58    /// Red color for failing tests
59    pub const FAIL: &str = "\x1b[31m";
60    /// Yellow color for warnings
61    pub const WARN: &str = "\x1b[33m";
62    /// Cyan color for info messages
63    pub const INFO: &str = "\x1b[36m";
64}
65
66/// A falsifiable coverage hypothesis
67#[derive(Debug, Clone)]
68pub struct CoverageHypothesis {
69    /// Hypothesis ID (e.g., "H0-COV-01")
70    pub id: String,
71    /// Description of the claim
72    pub description: String,
73    /// Threshold value for falsification
74    pub threshold: f32,
75    /// Actual measured value
76    pub actual: f32,
77    /// Whether the hypothesis was falsified
78    pub falsified: bool,
79}
80
81impl CoverageHypothesis {
82    /// Create a new hypothesis
83    #[must_use]
84    pub fn new(id: &str, description: &str, threshold: f32, actual: f32) -> Self {
85        let falsified = actual < threshold;
86        Self {
87            id: id.to_string(),
88            description: description.to_string(),
89            threshold,
90            actual,
91            falsified,
92        }
93    }
94
95    /// Create coverage threshold hypothesis
96    #[must_use]
97    pub fn coverage_threshold(threshold: f32, actual: f32) -> Self {
98        Self::new(
99            "H0-COV-01",
100            &format!("Coverage >= {:.0}%", threshold * 100.0),
101            threshold,
102            actual,
103        )
104    }
105
106    /// Create gap size hypothesis
107    #[must_use]
108    pub fn max_gap_size(max_gap_percent: f32, actual_gap_percent: f32) -> Self {
109        // Falsified if actual gap > max allowed
110        let falsified = actual_gap_percent > max_gap_percent;
111        Self {
112            id: "H0-COV-02".to_string(),
113            description: format!("No gap > {:.0}% area", max_gap_percent * 100.0),
114            threshold: max_gap_percent,
115            actual: actual_gap_percent,
116            falsified,
117        }
118    }
119}
120
121/// Detected gap region in coverage
122#[derive(Debug, Clone)]
123pub struct GapRegion {
124    /// Row range (start, end)
125    pub rows: (usize, usize),
126    /// Column range (start, end)
127    pub cols: (usize, usize),
128    /// Percentage of total screen
129    pub percent: f32,
130    /// Suggested component name (if identifiable)
131    pub suggestion: Option<String>,
132}
133
134/// Visual score bar for terminal output
135#[derive(Debug, Clone)]
136pub struct ScoreBar {
137    /// Score value (0.0 - 1.0)
138    pub score: f32,
139    /// Width in characters
140    pub width: usize,
141    /// Threshold for pass/fail coloring
142    pub threshold: f32,
143    /// Label text
144    pub label: String,
145}
146
147impl ScoreBar {
148    /// Create a new score bar
149    #[must_use]
150    pub fn new(label: &str, score: f32, threshold: f32) -> Self {
151        Self {
152            score,
153            width: 25,
154            threshold,
155            label: label.to_string(),
156        }
157    }
158
159    /// Set bar width
160    #[must_use]
161    pub fn with_width(mut self, width: usize) -> Self {
162        self.width = width;
163        self
164    }
165
166    /// Render the score bar
167    #[must_use]
168    pub fn render(&self, mode: OutputMode) -> String {
169        let filled = ((self.score * self.width as f32) as usize).min(self.width);
170        let empty = self.width - filled;
171
172        let bar = format!(
173            "{:>16}: {:5.1}%  {}{}",
174            self.label,
175            self.score * 100.0,
176            "\u{2588}".repeat(filled),
177            "\u{2591}".repeat(empty)
178        );
179
180        match mode {
181            OutputMode::RichAnsi => {
182                if self.score >= self.threshold {
183                    format!("{}{}{}", ansi::PASS, bar, ansi::RESET)
184                } else {
185                    format!("{}{}{}", ansi::FAIL, bar, ansi::RESET)
186                }
187            }
188            OutputMode::NoColorAscii => {
189                let status = if self.score >= self.threshold {
190                    "[PASS]"
191                } else {
192                    "[FAIL]"
193                };
194                format!(
195                    "{} {}",
196                    bar.replace('\u{2588}', "#").replace('\u{2591}', "-"),
197                    status
198                )
199            }
200            OutputMode::Json => bar,
201        }
202    }
203}
204
205/// Confidence interval for statistical rigor
206#[derive(Debug, Clone, Copy)]
207pub struct ConfidenceInterval {
208    /// Lower bound
209    pub lower: f32,
210    /// Upper bound
211    pub upper: f32,
212    /// Confidence level (e.g., 0.95 for 95%)
213    pub level: f32,
214}
215
216impl ConfidenceInterval {
217    /// Create a new confidence interval
218    #[must_use]
219    pub fn new(lower: f32, upper: f32, level: f32) -> Self {
220        Self {
221            lower,
222            upper,
223            level,
224        }
225    }
226
227    /// Calculate Wilson score interval for proportion
228    /// More accurate than normal approximation for small samples
229    #[must_use]
230    pub fn wilson_score(successes: u32, total: u32, confidence: f32) -> Self {
231        if total == 0 {
232            return Self::new(0.0, 0.0, confidence);
233        }
234
235        let n = total as f64;
236        let p = successes as f64 / n;
237
238        // Z-score for confidence level (approximation)
239        let z = match confidence {
240            c if c >= 0.99 => 2.576,
241            c if c >= 0.95 => 1.96,
242            c if c >= 0.90 => 1.645,
243            _ => 1.96,
244        };
245
246        let z2 = z * z;
247        let denominator = 1.0 + z2 / n;
248        let center = (p + z2 / (2.0 * n)) / denominator;
249        let margin = (z / denominator) * ((p * (1.0 - p) / n) + (z2 / (4.0 * n * n))).sqrt();
250
251        Self::new(
252            (center - margin).max(0.0) as f32,
253            (center + margin).min(1.0) as f32,
254            confidence,
255        )
256    }
257
258    /// Format as string
259    #[must_use]
260    pub fn format(&self) -> String {
261        format!(
262            "{:.0}% CI [{:.1}%, {:.1}%]",
263            self.level * 100.0,
264            self.lower * 100.0,
265            self.upper * 100.0
266        )
267    }
268}
269
270/// Rich terminal heatmap with full visualization
271#[derive(Debug, Clone)]
272pub struct RichTerminalHeatmap {
273    /// Coverage cells
274    cells: Vec<Vec<CoverageCell>>,
275    /// Color palette
276    palette: ColorPalette,
277    /// Output mode
278    mode: OutputMode,
279    /// Title text
280    title: Option<String>,
281    /// Show score panel
282    show_scores: bool,
283    /// Show gap analysis
284    show_gaps: bool,
285    /// Show hypothesis status
286    show_hypotheses: bool,
287    /// Coverage threshold
288    threshold: f32,
289    /// Confidence level for intervals
290    confidence_level: f32,
291}
292
293impl RichTerminalHeatmap {
294    /// Create from coverage cells
295    #[must_use]
296    pub fn new(cells: Vec<Vec<CoverageCell>>) -> Self {
297        Self {
298            cells,
299            palette: ColorPalette::viridis(),
300            mode: OutputMode::from_env(),
301            title: None,
302            show_scores: true,
303            show_gaps: true,
304            show_hypotheses: true,
305            threshold: 0.85,
306            confidence_level: 0.95,
307        }
308    }
309
310    /// Set title
311    #[must_use]
312    pub fn with_title(mut self, title: &str) -> Self {
313        self.title = Some(title.to_string());
314        self
315    }
316
317    /// Set output mode
318    #[must_use]
319    pub fn with_mode(mut self, mode: OutputMode) -> Self {
320        self.mode = mode;
321        self
322    }
323
324    /// Set color palette
325    #[must_use]
326    pub fn with_palette(mut self, palette: ColorPalette) -> Self {
327        self.palette = palette;
328        self
329    }
330
331    /// Set coverage threshold
332    #[must_use]
333    pub fn with_threshold(mut self, threshold: f32) -> Self {
334        self.threshold = threshold;
335        self
336    }
337
338    /// Enable/disable score panel
339    #[must_use]
340    pub fn with_scores(mut self, show: bool) -> Self {
341        self.show_scores = show;
342        self
343    }
344
345    /// Enable/disable gap analysis
346    #[must_use]
347    pub fn with_gaps(mut self, show: bool) -> Self {
348        self.show_gaps = show;
349        self
350    }
351
352    /// Enable/disable hypothesis status
353    #[must_use]
354    pub fn with_hypotheses(mut self, show: bool) -> Self {
355        self.show_hypotheses = show;
356        self
357    }
358
359    /// Calculate coverage statistics
360    fn calculate_stats(&self) -> (f32, u32, u32) {
361        let mut covered = 0u32;
362        let mut total = 0u32;
363
364        for row in &self.cells {
365            for cell in row {
366                total += 1;
367                if cell.coverage > 0.0 {
368                    covered += 1;
369                }
370            }
371        }
372
373        let coverage = if total > 0 {
374            covered as f32 / total as f32
375        } else {
376            0.0
377        };
378
379        (coverage, covered, total)
380    }
381
382    /// Find gap regions
383    fn find_gaps(&self) -> Vec<GapRegion> {
384        let mut gaps = Vec::new();
385        let rows = self.cells.len();
386        let cols = self.cells.first().map_or(0, Vec::len);
387        let total_cells = (rows * cols) as f32;
388
389        if total_cells == 0.0 {
390            return gaps;
391        }
392
393        // Simple gap detection: find contiguous regions of 0 coverage
394        let mut visited = vec![vec![false; cols]; rows];
395
396        for r in 0..rows {
397            for c in 0..cols {
398                if !visited[r][c] && self.cells[r][c].coverage <= 0.0 {
399                    // BFS to find contiguous gap region
400                    let mut min_row = r;
401                    let mut max_row = r;
402                    let mut min_col = c;
403                    let mut max_col = c;
404                    let mut gap_cells = 0;
405
406                    let mut queue = vec![(r, c)];
407                    visited[r][c] = true;
408
409                    while let Some((row, col)) = queue.pop() {
410                        gap_cells += 1;
411                        min_row = min_row.min(row);
412                        max_row = max_row.max(row);
413                        min_col = min_col.min(col);
414                        max_col = max_col.max(col);
415
416                        // Check neighbors
417                        for (dr, dc) in &[(0, 1), (1, 0), (0, -1), (-1, 0)] {
418                            let nr = row as i32 + dr;
419                            let nc = col as i32 + dc;
420                            if nr >= 0 && nr < rows as i32 && nc >= 0 && nc < cols as i32 {
421                                let nr = nr as usize;
422                                let nc = nc as usize;
423                                if !visited[nr][nc] && self.cells[nr][nc].coverage <= 0.0 {
424                                    visited[nr][nc] = true;
425                                    queue.push((nr, nc));
426                                }
427                            }
428                        }
429                    }
430
431                    let percent = gap_cells as f32 / total_cells;
432                    if percent >= 0.01 {
433                        // Only report gaps >= 1%
434                        gaps.push(GapRegion {
435                            rows: (min_row, max_row),
436                            cols: (min_col, max_col),
437                            percent,
438                            suggestion: None,
439                        });
440                    }
441                }
442            }
443        }
444
445        // Sort by size (largest first)
446        gaps.sort_by(|a, b| {
447            b.percent
448                .partial_cmp(&a.percent)
449                .unwrap_or(std::cmp::Ordering::Equal)
450        });
451        gaps
452    }
453
454    /// Render the heatmap grid
455    #[must_use]
456    pub fn render_grid(&self) -> String {
457        let mut output = String::new();
458
459        for row in &self.cells {
460            output.push_str("  ");
461            for cell in row {
462                let ch = Self::coverage_char(cell.coverage);
463                match self.mode {
464                    OutputMode::RichAnsi => {
465                        let color = self.palette.interpolate(cell.coverage);
466                        output.push_str(&ansi::rgb_fg(color.r, color.g, color.b));
467                        output.push(ch);
468                        output.push_str(ansi::RESET);
469                    }
470                    OutputMode::NoColorAscii => {
471                        output.push(Self::ascii_coverage_char(cell.coverage));
472                    }
473                    OutputMode::Json => {
474                        output.push(ch);
475                    }
476                }
477            }
478            output.push('\n');
479        }
480
481        output
482    }
483
484    /// Render score panel
485    #[must_use]
486    pub fn render_scores(&self, pixel_coverage: f32, line_coverage: Option<f32>) -> String {
487        let mut output = String::new();
488        let combined = line_coverage.map_or(pixel_coverage, |l| (pixel_coverage + l) / 2.0);
489
490        output.push_str("  \u{250C}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2510}\n");
491        output.push_str(
492            "  \u{2502}  COVERAGE SCORE                                        \u{2502}\n",
493        );
494        output.push_str("  \u{2502}  \u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}  \u{2502}\n");
495        output.push_str(
496            "  \u{2502}                                                        \u{2502}\n",
497        );
498
499        // Pixel coverage bar
500        let pixel_bar = ScoreBar::new("Pixel Coverage", pixel_coverage, self.threshold);
501        output.push_str(&format!(
502            "  \u{2502}    {}  \u{2502}\n",
503            pixel_bar.render(self.mode)
504        ));
505
506        // Line coverage bar (if available)
507        if let Some(line) = line_coverage {
508            let line_bar = ScoreBar::new("Line Coverage", line, self.threshold);
509            output.push_str(&format!(
510                "  \u{2502}    {}  \u{2502}\n",
511                line_bar.render(self.mode)
512            ));
513        }
514
515        // Combined score bar
516        let combined_bar = ScoreBar::new("Combined Score", combined, self.threshold);
517        output.push_str(&format!(
518            "  \u{2502}    {}  \u{2502}\n",
519            combined_bar.render(self.mode)
520        ));
521
522        output.push_str(
523            "  \u{2502}                                                        \u{2502}\n",
524        );
525
526        // Status and confidence interval
527        let (_, covered, total) = self.calculate_stats();
528        let ci = ConfidenceInterval::wilson_score(covered, total, self.confidence_level);
529        let status = if combined >= self.threshold {
530            match self.mode {
531                OutputMode::RichAnsi => format!("{}\u{2705} PASS{}", ansi::PASS, ansi::RESET),
532                _ => "PASS".to_string(),
533            }
534        } else {
535            match self.mode {
536                OutputMode::RichAnsi => format!("{}\u{274C} FAIL{}", ansi::FAIL, ansi::RESET),
537                _ => "FAIL".to_string(),
538            }
539        };
540
541        output.push_str(&format!(
542            "  \u{2502}    Threshold: {:.1}%    Status: {}                  \u{2502}\n",
543            self.threshold * 100.0,
544            status
545        ));
546        output.push_str(&format!(
547            "  \u{2502}    Confidence: {}                          \u{2502}\n",
548            ci.format()
549        ));
550        output.push_str(
551            "  \u{2502}                                                        \u{2502}\n",
552        );
553        output.push_str("  \u{2514}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2518}\n");
554
555        output
556    }
557
558    /// Render gap analysis
559    #[must_use]
560    pub fn render_gap_analysis(&self) -> String {
561        let gaps = self.find_gaps();
562        let mut output = String::new();
563
564        if gaps.is_empty() {
565            output.push_str(&format!(
566                "  {}\u{2705} No coverage gaps detected{}\n",
567                if self.mode == OutputMode::RichAnsi {
568                    ansi::PASS
569                } else {
570                    ""
571                },
572                if self.mode == OutputMode::RichAnsi {
573                    ansi::RESET
574                } else {
575                    ""
576                }
577            ));
578            return output;
579        }
580
581        let total_gap_percent: f32 = gaps.iter().map(|g| g.percent).sum();
582        output.push_str(&format!(
583            "  {}\u{26A0} GAPS DETECTED ({} region{}, {:.1}% of screen){}\n",
584            if self.mode == OutputMode::RichAnsi {
585                ansi::WARN
586            } else {
587                ""
588            },
589            gaps.len(),
590            if gaps.len() == 1 { "" } else { "s" },
591            total_gap_percent * 100.0,
592            if self.mode == OutputMode::RichAnsi {
593                ansi::RESET
594            } else {
595                ""
596            }
597        ));
598
599        for (i, gap) in gaps.iter().take(5).enumerate() {
600            let connector = if i == gaps.len().min(5) - 1 {
601                "\u{2514}"
602            } else {
603                "\u{251C}"
604            };
605            output.push_str(&format!(
606                "  {}\u{2500} Gap #{}: rows {}-{}, cols {}-{} ({:.1}%)\n",
607                connector,
608                i + 1,
609                gap.rows.0,
610                gap.rows.1,
611                gap.cols.0,
612                gap.cols.1,
613                gap.percent * 100.0
614            ));
615        }
616
617        if gaps.len() > 5 {
618            output.push_str(&format!("     ... and {} more gaps\n", gaps.len() - 5));
619        }
620
621        output
622    }
623
624    /// Render hypothesis falsification status
625    #[must_use]
626    pub fn render_hypotheses(&self, hypotheses: &[CoverageHypothesis]) -> String {
627        let mut output = String::new();
628
629        output.push_str("  FALSIFICATION STATUS\n");
630
631        for (i, h) in hypotheses.iter().enumerate() {
632            let connector = if i == hypotheses.len() - 1 {
633                "\u{2514}"
634            } else {
635                "\u{251C}"
636            };
637            let status = if h.falsified {
638                match self.mode {
639                    OutputMode::RichAnsi => {
640                        format!("{}\u{274C} FALSIFIED{}", ansi::FAIL, ansi::RESET)
641                    }
642                    _ => "FALSIFIED".to_string(),
643                }
644            } else {
645                match self.mode {
646                    OutputMode::RichAnsi => {
647                        format!("{}\u{2705} NOT FALSIFIED{}", ansi::PASS, ansi::RESET)
648                    }
649                    _ => "NOT FALSIFIED".to_string(),
650                }
651            };
652
653            output.push_str(&format!(
654                "  {}\u{2500} {}: {} \u{2192} {} ({:.1}%)\n",
655                connector,
656                h.id,
657                h.description,
658                status,
659                h.actual * 100.0
660            ));
661        }
662
663        output
664    }
665
666    /// Render complete terminal display
667    #[must_use]
668    pub fn render(&self) -> String {
669        self.render_with_report(None)
670    }
671
672    /// Render with combined report
673    #[must_use]
674    pub fn render_with_report(&self, report: Option<&CombinedCoverageReport>) -> String {
675        let mut output = String::new();
676        let (pixel_coverage, _, _) = self.calculate_stats();
677
678        // Header
679        let border = "\u{2550}".repeat(70);
680        output.push_str(&format!("\u{2554}{}\u{2557}\n", border));
681
682        if let Some(title) = &self.title {
683            let padding = (68 - title.len()) / 2;
684            output.push_str(&format!(
685                "\u{2551}{:^70}\u{2551}\n",
686                format!("{}{}", " ".repeat(padding.max(0)), title)
687            ));
688        } else {
689            output.push_str(&format!(
690                "\u{2551}{:^70}\u{2551}\n",
691                "PIXEL COVERAGE HEATMAP"
692            ));
693        }
694
695        output.push_str(&format!("\u{2560}{}\u{2563}\n", border));
696
697        // Grid
698        output.push_str(&format!("\u{2551}{:70}\u{2551}\n", ""));
699        let grid = self.render_grid();
700        for line in grid.lines() {
701            output.push_str(&format!("\u{2551}{:70}\u{2551}\n", line));
702        }
703        output.push_str(&format!("\u{2551}{:70}\u{2551}\n", ""));
704
705        // Legend
706        output.push_str(&format!("\u{2560}{}\u{2563}\n", border));
707        output.push_str(
708            "\u{2551}  LEGEND: \u{2588} 76-100%  \u{2593} 51-75%  \u{2592} 26-50%  \u{2591} 1-25%  \u{00B7} 0% (GAP)   \u{2551}\n"
709        );
710
711        // Score panel
712        if self.show_scores {
713            output.push_str(&format!("\u{2560}{}\u{2563}\n", border));
714            let line_coverage = report.map(|r| r.line_coverage.element_coverage);
715            let scores = self.render_scores(pixel_coverage, line_coverage);
716            for line in scores.lines() {
717                output.push_str(&format!("\u{2551}{:70}\u{2551}\n", line));
718            }
719        }
720
721        // Gap analysis
722        if self.show_gaps {
723            output.push_str(&format!("\u{2560}{}\u{2563}\n", border));
724            let gaps = self.render_gap_analysis();
725            for line in gaps.lines() {
726                output.push_str(&format!("\u{2551}{:70}\u{2551}\n", line));
727            }
728        }
729
730        // Hypothesis status
731        if self.show_hypotheses {
732            let gaps = self.find_gaps();
733            let max_gap = gaps.first().map_or(0.0, |g| g.percent);
734
735            let hypotheses = vec![
736                CoverageHypothesis::coverage_threshold(self.threshold, pixel_coverage),
737                CoverageHypothesis::max_gap_size(0.15, max_gap),
738            ];
739
740            output.push_str(&format!("\u{2560}{}\u{2563}\n", border));
741            let hyp_output = self.render_hypotheses(&hypotheses);
742            for line in hyp_output.lines() {
743                output.push_str(&format!("\u{2551}{:70}\u{2551}\n", line));
744            }
745        }
746
747        // Footer
748        output.push_str(&format!("\u{255A}{}\u{255D}\n", border));
749
750        output
751    }
752
753    /// Get coverage character for value
754    fn coverage_char(coverage: f32) -> char {
755        match coverage {
756            c if c <= 0.0 => '\u{00B7}',  // Middle dot for gaps
757            c if c <= 0.25 => '\u{2591}', // Light shade
758            c if c <= 0.50 => '\u{2592}', // Medium shade
759            c if c <= 0.75 => '\u{2593}', // Dark shade
760            _ => '\u{2588}',              // Full block
761        }
762    }
763
764    /// Get ASCII coverage character (no-color mode)
765    fn ascii_coverage_char(coverage: f32) -> char {
766        match coverage {
767            c if c <= 0.0 => '.',
768            c if c <= 0.25 => '-',
769            c if c <= 0.50 => '+',
770            c if c <= 0.75 => '#',
771            _ => '@',
772        }
773    }
774}
775
776#[cfg(test)]
777#[allow(clippy::unwrap_used, clippy::float_cmp, clippy::needless_range_loop)]
778mod tests {
779    use super::*;
780
781    // =========================================================================
782    // Score Bar Tests (H0-TERM-01-XX)
783    // =========================================================================
784
785    #[test]
786    fn h0_term_01_score_bar_render() {
787        let bar = ScoreBar::new("Test", 0.85, 0.80);
788        let output = bar.render(OutputMode::NoColorAscii);
789        assert!(output.contains("85.0%"));
790        assert!(output.contains("[PASS]"));
791    }
792
793    #[test]
794    fn h0_term_02_score_bar_fail() {
795        let bar = ScoreBar::new("Test", 0.50, 0.80);
796        let output = bar.render(OutputMode::NoColorAscii);
797        assert!(output.contains("50.0%"));
798        assert!(output.contains("[FAIL]"));
799    }
800
801    #[test]
802    fn h0_term_03_score_bar_width() {
803        let bar = ScoreBar::new("Test", 1.0, 0.80).with_width(10);
804        let output = bar.render(OutputMode::NoColorAscii);
805        assert!(output.contains("##########")); // 10 filled chars
806    }
807
808    // =========================================================================
809    // Confidence Interval Tests (H0-TERM-04-XX)
810    // =========================================================================
811
812    #[test]
813    fn h0_term_04_wilson_score_full() {
814        let ci = ConfidenceInterval::wilson_score(100, 100, 0.95);
815        assert!(ci.lower > 0.95);
816        assert!((ci.upper - 1.0).abs() < 0.01);
817    }
818
819    #[test]
820    fn h0_term_05_wilson_score_empty() {
821        let ci = ConfidenceInterval::wilson_score(0, 100, 0.95);
822        assert!(ci.lower < 0.05);
823        assert!(ci.upper < 0.10);
824    }
825
826    #[test]
827    fn h0_term_06_wilson_score_half() {
828        let ci = ConfidenceInterval::wilson_score(50, 100, 0.95);
829        assert!(ci.lower > 0.35);
830        assert!(ci.upper < 0.65);
831    }
832
833    #[test]
834    fn h0_term_07_wilson_zero_total() {
835        let ci = ConfidenceInterval::wilson_score(0, 0, 0.95);
836        assert_eq!(ci.lower, 0.0);
837        assert_eq!(ci.upper, 0.0);
838    }
839
840    // =========================================================================
841    // Hypothesis Tests (H0-TERM-08-XX)
842    // =========================================================================
843
844    #[test]
845    fn h0_term_08_hypothesis_pass() {
846        let h = CoverageHypothesis::coverage_threshold(0.80, 0.85);
847        assert!(!h.falsified);
848    }
849
850    #[test]
851    fn h0_term_09_hypothesis_fail() {
852        let h = CoverageHypothesis::coverage_threshold(0.80, 0.75);
853        assert!(h.falsified);
854    }
855
856    #[test]
857    fn h0_term_10_gap_hypothesis() {
858        let h = CoverageHypothesis::max_gap_size(0.15, 0.10);
859        assert!(!h.falsified);
860
861        let h2 = CoverageHypothesis::max_gap_size(0.15, 0.20);
862        assert!(h2.falsified);
863    }
864
865    // =========================================================================
866    // Rich Terminal Heatmap Tests (H0-TERM-11-XX)
867    // =========================================================================
868
869    #[test]
870    fn h0_term_11_render_empty() {
871        let cells = vec![
872            vec![
873                CoverageCell {
874                    coverage: 0.0,
875                    hit_count: 0
876                };
877                5
878            ];
879            5
880        ];
881        let heatmap = RichTerminalHeatmap::new(cells).with_mode(OutputMode::NoColorAscii);
882        let output = heatmap.render();
883        assert!(!output.is_empty());
884    }
885
886    #[test]
887    fn h0_term_12_render_full() {
888        let cells = vec![
889            vec![
890                CoverageCell {
891                    coverage: 1.0,
892                    hit_count: 10
893                };
894                5
895            ];
896            5
897        ];
898        let heatmap = RichTerminalHeatmap::new(cells).with_mode(OutputMode::NoColorAscii);
899        let output = heatmap.render();
900        assert!(output.contains("PASS") || output.contains("NOT FALSIFIED"));
901    }
902
903    #[test]
904    fn h0_term_13_render_with_gaps() {
905        let mut cells = vec![
906            vec![
907                CoverageCell {
908                    coverage: 1.0,
909                    hit_count: 10
910                };
911                10
912            ];
913            10
914        ];
915        // Create a gap
916        for r in 3..7 {
917            for c in 3..7 {
918                cells[r][c] = CoverageCell {
919                    coverage: 0.0,
920                    hit_count: 0,
921                };
922            }
923        }
924        let heatmap = RichTerminalHeatmap::new(cells).with_mode(OutputMode::NoColorAscii);
925        let output = heatmap.render();
926        assert!(output.contains("GAP"));
927    }
928
929    #[test]
930    fn h0_term_14_output_mode_env() {
931        // Default should be RichAnsi or based on env
932        let mode = OutputMode::from_env();
933        // Just verify it doesn't panic
934        assert!(matches!(
935            mode,
936            OutputMode::RichAnsi | OutputMode::NoColorAscii | OutputMode::Json
937        ));
938    }
939
940    #[test]
941    fn h0_term_15_coverage_chars() {
942        assert_eq!(RichTerminalHeatmap::coverage_char(0.0), '\u{00B7}');
943        assert_eq!(RichTerminalHeatmap::coverage_char(0.1), '\u{2591}');
944        assert_eq!(RichTerminalHeatmap::coverage_char(0.4), '\u{2592}');
945        assert_eq!(RichTerminalHeatmap::coverage_char(0.6), '\u{2593}');
946        assert_eq!(RichTerminalHeatmap::coverage_char(1.0), '\u{2588}');
947    }
948
949    #[test]
950    fn h0_term_16_ascii_coverage_chars() {
951        assert_eq!(RichTerminalHeatmap::ascii_coverage_char(0.0), '.');
952        assert_eq!(RichTerminalHeatmap::ascii_coverage_char(0.1), '-');
953        assert_eq!(RichTerminalHeatmap::ascii_coverage_char(0.4), '+');
954        assert_eq!(RichTerminalHeatmap::ascii_coverage_char(0.6), '#');
955        assert_eq!(RichTerminalHeatmap::ascii_coverage_char(1.0), '@');
956    }
957
958    #[test]
959    fn h0_term_17_find_gaps() {
960        let mut cells = vec![
961            vec![
962                CoverageCell {
963                    coverage: 1.0,
964                    hit_count: 10
965                };
966                10
967            ];
968            10
969        ];
970        // Create a 4x4 gap (16% of 100 cells)
971        for r in 3..7 {
972            for c in 3..7 {
973                cells[r][c] = CoverageCell {
974                    coverage: 0.0,
975                    hit_count: 0,
976                };
977            }
978        }
979        let heatmap = RichTerminalHeatmap::new(cells);
980        let gaps = heatmap.find_gaps();
981        assert!(!gaps.is_empty());
982        assert!((gaps[0].percent - 0.16).abs() < 0.01);
983    }
984
985    #[test]
986    fn h0_term_18_confidence_interval_format() {
987        let ci = ConfidenceInterval::new(0.80, 0.90, 0.95);
988        let formatted = ci.format();
989        assert!(formatted.contains("95%"));
990        assert!(formatted.contains("80.0%"));
991        assert!(formatted.contains("90.0%"));
992    }
993
994    // =========================================================================
995    // Additional Tests for 95%+ Coverage (H0-TERM-19-XX)
996    // =========================================================================
997
998    // --- ANSI Module Tests ---
999
1000    #[test]
1001    fn h0_term_19_ansi_rgb_fg() {
1002        let color = ansi::rgb_fg(255, 128, 64);
1003        assert!(color.contains("38;2;255;128;64"));
1004        assert!(color.starts_with("\x1b["));
1005        assert!(color.ends_with('m'));
1006    }
1007
1008    #[test]
1009    fn h0_term_20_ansi_rgb_bg() {
1010        let color = ansi::rgb_bg(100, 200, 50);
1011        assert!(color.contains("48;2;100;200;50"));
1012        assert!(color.starts_with("\x1b["));
1013        assert!(color.ends_with('m'));
1014    }
1015
1016    #[test]
1017    fn h0_term_21_ansi_constants() {
1018        // Verify ANSI constants are correctly defined
1019        assert_eq!(ansi::RESET, "\x1b[0m");
1020        assert_eq!(ansi::BOLD, "\x1b[1m");
1021        assert_eq!(ansi::DIM, "\x1b[2m");
1022        assert_eq!(ansi::PASS, "\x1b[32m");
1023        assert_eq!(ansi::FAIL, "\x1b[31m");
1024        assert_eq!(ansi::WARN, "\x1b[33m");
1025        assert_eq!(ansi::INFO, "\x1b[36m");
1026    }
1027
1028    // --- OutputMode Tests ---
1029
1030    #[test]
1031    fn h0_term_22_output_mode_default() {
1032        let mode = OutputMode::default();
1033        assert_eq!(mode, OutputMode::RichAnsi);
1034    }
1035
1036    #[test]
1037    fn h0_term_23_output_mode_debug() {
1038        let mode = OutputMode::RichAnsi;
1039        let debug_str = format!("{:?}", mode);
1040        assert!(debug_str.contains("RichAnsi"));
1041    }
1042
1043    #[test]
1044    fn h0_term_24_output_mode_clone_eq() {
1045        let mode1 = OutputMode::Json;
1046        let mode2 = mode1;
1047        assert_eq!(mode1, mode2);
1048    }
1049
1050    // --- ScoreBar RichAnsi and Json Mode Tests ---
1051
1052    #[test]
1053    fn h0_term_25_score_bar_rich_ansi_pass() {
1054        let bar = ScoreBar::new("Test", 0.90, 0.80);
1055        let output = bar.render(OutputMode::RichAnsi);
1056        // Should contain ANSI color codes for pass (green)
1057        assert!(output.contains(ansi::PASS));
1058        assert!(output.contains(ansi::RESET));
1059        assert!(output.contains("90.0%"));
1060    }
1061
1062    #[test]
1063    fn h0_term_26_score_bar_rich_ansi_fail() {
1064        let bar = ScoreBar::new("Test", 0.50, 0.80);
1065        let output = bar.render(OutputMode::RichAnsi);
1066        // Should contain ANSI color codes for fail (red)
1067        assert!(output.contains(ansi::FAIL));
1068        assert!(output.contains(ansi::RESET));
1069        assert!(output.contains("50.0%"));
1070    }
1071
1072    #[test]
1073    fn h0_term_27_score_bar_json_mode() {
1074        let bar = ScoreBar::new("Test", 0.75, 0.80);
1075        let output = bar.render(OutputMode::Json);
1076        // JSON mode should have the bar without status markers
1077        assert!(output.contains("75.0%"));
1078        // Should NOT contain [PASS] or [FAIL] or ANSI codes
1079        assert!(!output.contains("[PASS]"));
1080        assert!(!output.contains("[FAIL]"));
1081        assert!(!output.contains("\x1b["));
1082    }
1083
1084    #[test]
1085    fn h0_term_28_score_bar_zero_score() {
1086        let bar = ScoreBar::new("Empty", 0.0, 0.80);
1087        let output = bar.render(OutputMode::NoColorAscii);
1088        assert!(output.contains("0.0%"));
1089        assert!(output.contains("[FAIL]"));
1090    }
1091
1092    #[test]
1093    fn h0_term_29_score_bar_exact_threshold() {
1094        let bar = ScoreBar::new("Exact", 0.80, 0.80);
1095        let output = bar.render(OutputMode::NoColorAscii);
1096        assert!(output.contains("80.0%"));
1097        assert!(output.contains("[PASS]")); // At threshold is pass
1098    }
1099
1100    // --- Wilson Score Confidence Level Tests ---
1101
1102    #[test]
1103    fn h0_term_30_wilson_score_99_confidence() {
1104        let ci = ConfidenceInterval::wilson_score(50, 100, 0.99);
1105        // 99% CI should be wider than 95%
1106        assert!(ci.level >= 0.99);
1107        // CI should be valid
1108        assert!(ci.lower < ci.upper);
1109        assert!(ci.lower >= 0.0);
1110        assert!(ci.upper <= 1.0);
1111    }
1112
1113    #[test]
1114    fn h0_term_31_wilson_score_90_confidence() {
1115        let ci = ConfidenceInterval::wilson_score(50, 100, 0.90);
1116        // 90% CI should be narrower than 95%
1117        assert!((ci.level - 0.90).abs() < 0.01);
1118        assert!(ci.lower < ci.upper);
1119    }
1120
1121    #[test]
1122    fn h0_term_32_wilson_score_low_confidence() {
1123        // Confidence below 0.90 should use default z=1.96
1124        let ci = ConfidenceInterval::wilson_score(50, 100, 0.80);
1125        assert!(ci.lower < ci.upper);
1126        assert!((ci.level - 0.80).abs() < 0.01);
1127    }
1128
1129    // --- CoverageHypothesis Tests ---
1130
1131    #[test]
1132    fn h0_term_33_hypothesis_new_direct() {
1133        let h = CoverageHypothesis::new("H0-TEST", "Test description", 0.70, 0.80);
1134        assert_eq!(h.id, "H0-TEST");
1135        assert_eq!(h.description, "Test description");
1136        assert_eq!(h.threshold, 0.70);
1137        assert_eq!(h.actual, 0.80);
1138        assert!(!h.falsified); // actual >= threshold
1139    }
1140
1141    #[test]
1142    fn h0_term_34_hypothesis_clone_debug() {
1143        let h = CoverageHypothesis::coverage_threshold(0.80, 0.85);
1144        let h2 = h.clone();
1145        assert_eq!(h.id, h2.id);
1146        let debug_str = format!("{:?}", h);
1147        assert!(debug_str.contains("H0-COV-01"));
1148    }
1149
1150    #[test]
1151    fn h0_term_35_gap_hypothesis_exact() {
1152        // Test exact threshold case
1153        let h = CoverageHypothesis::max_gap_size(0.15, 0.15);
1154        assert!(!h.falsified); // Not falsified when actual == threshold
1155    }
1156
1157    // --- GapRegion Tests ---
1158
1159    #[test]
1160    fn h0_term_36_gap_region_debug_clone() {
1161        let gap = GapRegion {
1162            rows: (0, 5),
1163            cols: (2, 8),
1164            percent: 0.25,
1165            suggestion: Some("Check button component".to_string()),
1166        };
1167        let gap2 = gap.clone();
1168        assert_eq!(gap.rows, gap2.rows);
1169        assert_eq!(gap.cols, gap2.cols);
1170        let debug_str = format!("{:?}", gap);
1171        assert!(debug_str.contains("GapRegion"));
1172    }
1173
1174    // --- RichTerminalHeatmap Builder Tests ---
1175
1176    #[test]
1177    fn h0_term_37_heatmap_with_title() {
1178        let cells = vec![
1179            vec![
1180                CoverageCell {
1181                    coverage: 0.5,
1182                    hit_count: 5
1183                };
1184                3
1185            ];
1186            3
1187        ];
1188        let heatmap = RichTerminalHeatmap::new(cells)
1189            .with_title("Test Coverage Report")
1190            .with_mode(OutputMode::NoColorAscii);
1191        let output = heatmap.render();
1192        assert!(output.contains("Test Coverage Report"));
1193    }
1194
1195    #[test]
1196    fn h0_term_38_heatmap_with_palette() {
1197        let cells = vec![
1198            vec![
1199                CoverageCell {
1200                    coverage: 0.5,
1201                    hit_count: 5
1202                };
1203                3
1204            ];
1205            3
1206        ];
1207        let heatmap = RichTerminalHeatmap::new(cells)
1208            .with_palette(ColorPalette::magma())
1209            .with_mode(OutputMode::RichAnsi);
1210        let output = heatmap.render_grid();
1211        // Should contain ANSI color codes (from palette)
1212        assert!(output.contains("\x1b["));
1213    }
1214
1215    #[test]
1216    fn h0_term_39_heatmap_with_threshold() {
1217        let cells = vec![
1218            vec![
1219                CoverageCell {
1220                    coverage: 0.7,
1221                    hit_count: 7
1222                };
1223                3
1224            ];
1225            3
1226        ];
1227        let heatmap = RichTerminalHeatmap::new(cells)
1228            .with_threshold(0.60)
1229            .with_mode(OutputMode::NoColorAscii);
1230        let output = heatmap.render();
1231        assert!(output.contains("60.0%")); // threshold displayed
1232    }
1233
1234    #[test]
1235    fn h0_term_40_heatmap_disable_scores() {
1236        let cells = vec![
1237            vec![
1238                CoverageCell {
1239                    coverage: 1.0,
1240                    hit_count: 10
1241                };
1242                3
1243            ];
1244            3
1245        ];
1246        let heatmap = RichTerminalHeatmap::new(cells)
1247            .with_scores(false)
1248            .with_mode(OutputMode::NoColorAscii);
1249        let output = heatmap.render();
1250        // Should NOT contain score panel elements
1251        assert!(!output.contains("COVERAGE SCORE"));
1252    }
1253
1254    #[test]
1255    fn h0_term_41_heatmap_disable_gaps() {
1256        let cells = vec![
1257            vec![
1258                CoverageCell {
1259                    coverage: 0.0,
1260                    hit_count: 0
1261                };
1262                3
1263            ];
1264            3
1265        ];
1266        let heatmap = RichTerminalHeatmap::new(cells)
1267            .with_gaps(false)
1268            .with_mode(OutputMode::NoColorAscii);
1269        let _output = heatmap.render();
1270        // Gaps section should be disabled, but might still show falsification
1271        // Just verify it doesn't panic
1272    }
1273
1274    #[test]
1275    fn h0_term_42_heatmap_disable_hypotheses() {
1276        let cells = vec![
1277            vec![
1278                CoverageCell {
1279                    coverage: 1.0,
1280                    hit_count: 10
1281                };
1282                3
1283            ];
1284            3
1285        ];
1286        let heatmap = RichTerminalHeatmap::new(cells)
1287            .with_hypotheses(false)
1288            .with_mode(OutputMode::NoColorAscii);
1289        let output = heatmap.render();
1290        // Should NOT contain FALSIFICATION STATUS
1291        assert!(!output.contains("FALSIFICATION STATUS"));
1292    }
1293
1294    // --- render_grid Tests for All OutputModes ---
1295
1296    #[test]
1297    fn h0_term_43_render_grid_rich_ansi() {
1298        let cells = vec![vec![
1299            CoverageCell {
1300                coverage: 0.0,
1301                hit_count: 0,
1302            },
1303            CoverageCell {
1304                coverage: 0.5,
1305                hit_count: 5,
1306            },
1307            CoverageCell {
1308                coverage: 1.0,
1309                hit_count: 10,
1310            },
1311        ]];
1312        let heatmap = RichTerminalHeatmap::new(cells).with_mode(OutputMode::RichAnsi);
1313        let output = heatmap.render_grid();
1314        // Should contain ANSI color codes
1315        assert!(output.contains("\x1b[38;2;")); // RGB foreground
1316        assert!(output.contains(ansi::RESET));
1317    }
1318
1319    #[test]
1320    fn h0_term_44_render_grid_json() {
1321        let cells = vec![vec![
1322            CoverageCell {
1323                coverage: 0.0,
1324                hit_count: 0,
1325            },
1326            CoverageCell {
1327                coverage: 0.5,
1328                hit_count: 5,
1329            },
1330            CoverageCell {
1331                coverage: 1.0,
1332                hit_count: 10,
1333            },
1334        ]];
1335        let heatmap = RichTerminalHeatmap::new(cells).with_mode(OutputMode::Json);
1336        let output = heatmap.render_grid();
1337        // Should contain unicode chars but no ANSI codes
1338        assert!(!output.contains("\x1b["));
1339        assert!(output.contains('\u{00B7}')); // gap char
1340        assert!(output.contains('\u{2588}')); // full block
1341    }
1342
1343    // --- render_scores Tests ---
1344
1345    #[test]
1346    fn h0_term_45_render_scores_with_line_coverage() {
1347        let cells = vec![
1348            vec![
1349                CoverageCell {
1350                    coverage: 0.9,
1351                    hit_count: 9
1352                };
1353                5
1354            ];
1355            5
1356        ];
1357        let heatmap = RichTerminalHeatmap::new(cells).with_mode(OutputMode::NoColorAscii);
1358        let output = heatmap.render_scores(0.85, Some(0.90));
1359        assert!(output.contains("Pixel Coverage"));
1360        assert!(output.contains("Line Coverage"));
1361        assert!(output.contains("Combined Score"));
1362    }
1363
1364    #[test]
1365    fn h0_term_46_render_scores_without_line_coverage() {
1366        let cells = vec![
1367            vec![
1368                CoverageCell {
1369                    coverage: 0.9,
1370                    hit_count: 9
1371                };
1372                5
1373            ];
1374            5
1375        ];
1376        let heatmap = RichTerminalHeatmap::new(cells).with_mode(OutputMode::NoColorAscii);
1377        let output = heatmap.render_scores(0.85, None);
1378        assert!(output.contains("Pixel Coverage"));
1379        // Line coverage should not appear
1380        assert!(!output.contains("Line Coverage"));
1381        assert!(output.contains("Combined Score"));
1382    }
1383
1384    #[test]
1385    fn h0_term_47_render_scores_fail_status_rich_ansi() {
1386        let cells = vec![
1387            vec![
1388                CoverageCell {
1389                    coverage: 0.5,
1390                    hit_count: 5
1391                };
1392                5
1393            ];
1394            5
1395        ];
1396        let heatmap = RichTerminalHeatmap::new(cells)
1397            .with_threshold(0.90)
1398            .with_mode(OutputMode::RichAnsi);
1399        let output = heatmap.render_scores(0.50, None);
1400        // Should contain fail color codes
1401        assert!(output.contains(ansi::FAIL));
1402    }
1403
1404    #[test]
1405    fn h0_term_48_render_scores_pass_status_rich_ansi() {
1406        let cells = vec![
1407            vec![
1408                CoverageCell {
1409                    coverage: 0.95,
1410                    hit_count: 10
1411                };
1412                5
1413            ];
1414            5
1415        ];
1416        let heatmap = RichTerminalHeatmap::new(cells)
1417            .with_threshold(0.80)
1418            .with_mode(OutputMode::RichAnsi);
1419        let output = heatmap.render_scores(0.95, None);
1420        // Should contain pass color codes
1421        assert!(output.contains(ansi::PASS));
1422    }
1423
1424    // --- render_gap_analysis Tests ---
1425
1426    #[test]
1427    fn h0_term_49_render_gap_analysis_no_gaps_rich_ansi() {
1428        let cells = vec![
1429            vec![
1430                CoverageCell {
1431                    coverage: 1.0,
1432                    hit_count: 10
1433                };
1434                5
1435            ];
1436            5
1437        ];
1438        let heatmap = RichTerminalHeatmap::new(cells).with_mode(OutputMode::RichAnsi);
1439        let output = heatmap.render_gap_analysis();
1440        assert!(output.contains("No coverage gaps detected"));
1441        assert!(output.contains(ansi::PASS));
1442    }
1443
1444    #[test]
1445    fn h0_term_50_render_gap_analysis_with_gaps_rich_ansi() {
1446        let mut cells = vec![
1447            vec![
1448                CoverageCell {
1449                    coverage: 1.0,
1450                    hit_count: 10
1451                };
1452                10
1453            ];
1454            10
1455        ];
1456        // Create a large gap
1457        for r in 2..6 {
1458            for c in 2..6 {
1459                cells[r][c] = CoverageCell {
1460                    coverage: 0.0,
1461                    hit_count: 0,
1462                };
1463            }
1464        }
1465        let heatmap = RichTerminalHeatmap::new(cells).with_mode(OutputMode::RichAnsi);
1466        let output = heatmap.render_gap_analysis();
1467        assert!(output.contains("GAPS DETECTED"));
1468        assert!(output.contains(ansi::WARN));
1469    }
1470
1471    #[test]
1472    fn h0_term_51_render_gap_analysis_single_gap() {
1473        let mut cells = vec![
1474            vec![
1475                CoverageCell {
1476                    coverage: 1.0,
1477                    hit_count: 10
1478                };
1479                10
1480            ];
1481            10
1482        ];
1483        // Create one gap region
1484        for r in 0..5 {
1485            for c in 0..5 {
1486                cells[r][c] = CoverageCell {
1487                    coverage: 0.0,
1488                    hit_count: 0,
1489                };
1490            }
1491        }
1492        let heatmap = RichTerminalHeatmap::new(cells).with_mode(OutputMode::NoColorAscii);
1493        let output = heatmap.render_gap_analysis();
1494        // Should say "1 region" not "1 regions"
1495        assert!(output.contains("1 region,") || output.contains("1 region "));
1496    }
1497
1498    #[test]
1499    fn h0_term_52_render_gap_analysis_multiple_gaps() {
1500        let mut cells = vec![
1501            vec![
1502                CoverageCell {
1503                    coverage: 1.0,
1504                    hit_count: 10
1505                };
1506                20
1507            ];
1508            20
1509        ];
1510        // Create multiple disconnected gap regions
1511        for r in 0..4 {
1512            for c in 0..4 {
1513                cells[r][c] = CoverageCell {
1514                    coverage: 0.0,
1515                    hit_count: 0,
1516                };
1517            }
1518        }
1519        for r in 10..14 {
1520            for c in 10..14 {
1521                cells[r][c] = CoverageCell {
1522                    coverage: 0.0,
1523                    hit_count: 0,
1524                };
1525            }
1526        }
1527        let heatmap = RichTerminalHeatmap::new(cells).with_mode(OutputMode::NoColorAscii);
1528        let output = heatmap.render_gap_analysis();
1529        assert!(output.contains("regions")); // plural
1530    }
1531
1532    #[test]
1533    fn h0_term_53_render_gap_analysis_more_than_5_gaps() {
1534        let mut cells = vec![
1535            vec![
1536                CoverageCell {
1537                    coverage: 1.0,
1538                    hit_count: 10
1539                };
1540                30
1541            ];
1542            30
1543        ];
1544        // Create 7 separate gap regions (each 4x4 = 16 cells, >1% of 900)
1545        let gap_positions = [
1546            (0, 0),
1547            (0, 10),
1548            (0, 20),
1549            (10, 0),
1550            (10, 10),
1551            (10, 20),
1552            (20, 0),
1553        ];
1554        for (start_r, start_c) in gap_positions {
1555            for r in start_r..start_r + 4 {
1556                for c in start_c..start_c + 4 {
1557                    cells[r][c] = CoverageCell {
1558                        coverage: 0.0,
1559                        hit_count: 0,
1560                    };
1561                }
1562            }
1563        }
1564        let heatmap = RichTerminalHeatmap::new(cells).with_mode(OutputMode::NoColorAscii);
1565        let output = heatmap.render_gap_analysis();
1566        // Should show "... and X more gaps"
1567        assert!(output.contains("more gaps"));
1568    }
1569
1570    // --- render_hypotheses Tests ---
1571
1572    #[test]
1573    fn h0_term_54_render_hypotheses_rich_ansi_falsified() {
1574        let cells = vec![
1575            vec![
1576                CoverageCell {
1577                    coverage: 0.5,
1578                    hit_count: 5
1579                };
1580                5
1581            ];
1582            5
1583        ];
1584        let heatmap = RichTerminalHeatmap::new(cells).with_mode(OutputMode::RichAnsi);
1585        let hypotheses = vec![
1586            CoverageHypothesis::coverage_threshold(0.80, 0.50), // falsified
1587        ];
1588        let output = heatmap.render_hypotheses(&hypotheses);
1589        assert!(output.contains("FALSIFIED"));
1590        assert!(output.contains(ansi::FAIL));
1591    }
1592
1593    #[test]
1594    fn h0_term_55_render_hypotheses_rich_ansi_not_falsified() {
1595        let cells = vec![
1596            vec![
1597                CoverageCell {
1598                    coverage: 0.9,
1599                    hit_count: 9
1600                };
1601                5
1602            ];
1603            5
1604        ];
1605        let heatmap = RichTerminalHeatmap::new(cells).with_mode(OutputMode::RichAnsi);
1606        let hypotheses = vec![
1607            CoverageHypothesis::coverage_threshold(0.80, 0.90), // not falsified
1608        ];
1609        let output = heatmap.render_hypotheses(&hypotheses);
1610        assert!(output.contains("NOT FALSIFIED"));
1611        assert!(output.contains(ansi::PASS));
1612    }
1613
1614    #[test]
1615    fn h0_term_56_render_hypotheses_no_color() {
1616        let cells = vec![
1617            vec![
1618                CoverageCell {
1619                    coverage: 0.9,
1620                    hit_count: 9
1621                };
1622                5
1623            ];
1624            5
1625        ];
1626        let heatmap = RichTerminalHeatmap::new(cells).with_mode(OutputMode::NoColorAscii);
1627        let hypotheses = vec![
1628            CoverageHypothesis::coverage_threshold(0.80, 0.90),
1629            CoverageHypothesis::max_gap_size(0.15, 0.10),
1630        ];
1631        let output = heatmap.render_hypotheses(&hypotheses);
1632        assert!(output.contains("NOT FALSIFIED"));
1633        // No ANSI codes
1634        assert!(!output.contains("\x1b["));
1635    }
1636
1637    #[test]
1638    fn h0_term_57_render_hypotheses_json_mode() {
1639        let cells = vec![
1640            vec![
1641                CoverageCell {
1642                    coverage: 0.5,
1643                    hit_count: 5
1644                };
1645                5
1646            ];
1647            5
1648        ];
1649        let heatmap = RichTerminalHeatmap::new(cells).with_mode(OutputMode::Json);
1650        let hypotheses = vec![CoverageHypothesis::coverage_threshold(0.80, 0.50)];
1651        let output = heatmap.render_hypotheses(&hypotheses);
1652        assert!(output.contains("FALSIFIED"));
1653        // No ANSI codes in JSON mode
1654        assert!(!output.contains("\x1b["));
1655    }
1656
1657    // --- render_with_report Tests ---
1658
1659    #[test]
1660    fn h0_term_58_render_with_report() {
1661        use super::super::tracker::{
1662            CombinedCoverageReport, LineCoverageReport, PixelCoverageReport,
1663        };
1664
1665        let cells = vec![
1666            vec![
1667                CoverageCell {
1668                    coverage: 0.9,
1669                    hit_count: 9
1670                };
1671                5
1672            ];
1673            5
1674        ];
1675        let heatmap = RichTerminalHeatmap::new(cells).with_mode(OutputMode::NoColorAscii);
1676
1677        let line_report = LineCoverageReport::new(0.85, 1.0, 0.80, 20, 17);
1678        let pixel_report = PixelCoverageReport {
1679            overall_coverage: 0.90,
1680            ..Default::default()
1681        };
1682        let report = CombinedCoverageReport::from_parts(line_report, pixel_report);
1683
1684        let output = heatmap.render_with_report(Some(&report));
1685        assert!(output.contains("Line Coverage"));
1686        assert!(output.contains("Pixel Coverage"));
1687    }
1688
1689    // --- find_gaps Edge Cases ---
1690
1691    #[test]
1692    fn h0_term_59_find_gaps_empty_grid() {
1693        let cells: Vec<Vec<CoverageCell>> = vec![];
1694        let heatmap = RichTerminalHeatmap::new(cells);
1695        let gaps = heatmap.find_gaps();
1696        assert!(gaps.is_empty());
1697    }
1698
1699    #[test]
1700    fn h0_term_60_find_gaps_single_cell_gap() {
1701        // Single cell gap shouldn't be reported (< 1%)
1702        let mut cells = vec![
1703            vec![
1704                CoverageCell {
1705                    coverage: 1.0,
1706                    hit_count: 10
1707                };
1708                10
1709            ];
1710            10
1711        ];
1712        cells[5][5] = CoverageCell {
1713            coverage: 0.0,
1714            hit_count: 0,
1715        };
1716        let heatmap = RichTerminalHeatmap::new(cells);
1717        let gaps = heatmap.find_gaps();
1718        // 1 cell out of 100 = 1%, borderline
1719        assert!(gaps.is_empty() || gaps[0].percent < 0.02);
1720    }
1721
1722    #[test]
1723    fn h0_term_61_find_gaps_all_zero() {
1724        let cells = vec![
1725            vec![
1726                CoverageCell {
1727                    coverage: 0.0,
1728                    hit_count: 0
1729                };
1730                5
1731            ];
1732            5
1733        ];
1734        let heatmap = RichTerminalHeatmap::new(cells);
1735        let gaps = heatmap.find_gaps();
1736        // Should find one large gap covering everything
1737        assert!(!gaps.is_empty());
1738        assert!((gaps[0].percent - 1.0).abs() < 0.01); // 100% gap
1739    }
1740
1741    #[test]
1742    fn h0_term_62_find_gaps_sorted_by_size() {
1743        let mut cells = vec![
1744            vec![
1745                CoverageCell {
1746                    coverage: 1.0,
1747                    hit_count: 10
1748                };
1749                20
1750            ];
1751            20
1752        ];
1753        // Small gap (4 cells)
1754        for r in 0..2 {
1755            for c in 0..2 {
1756                cells[r][c] = CoverageCell {
1757                    coverage: 0.0,
1758                    hit_count: 0,
1759                };
1760            }
1761        }
1762        // Large gap (16 cells)
1763        for r in 10..14 {
1764            for c in 10..14 {
1765                cells[r][c] = CoverageCell {
1766                    coverage: 0.0,
1767                    hit_count: 0,
1768                };
1769            }
1770        }
1771        let heatmap = RichTerminalHeatmap::new(cells);
1772        let gaps = heatmap.find_gaps();
1773        // Largest gap should be first
1774        if gaps.len() >= 2 {
1775            assert!(gaps[0].percent >= gaps[1].percent);
1776        }
1777    }
1778
1779    // --- calculate_stats Tests ---
1780
1781    #[test]
1782    fn h0_term_63_calculate_stats_mixed() {
1783        let cells = vec![vec![
1784            CoverageCell {
1785                coverage: 1.0,
1786                hit_count: 10,
1787            },
1788            CoverageCell {
1789                coverage: 0.0,
1790                hit_count: 0,
1791            },
1792            CoverageCell {
1793                coverage: 0.5,
1794                hit_count: 5,
1795            },
1796            CoverageCell {
1797                coverage: 0.0,
1798                hit_count: 0,
1799            },
1800        ]];
1801        let heatmap = RichTerminalHeatmap::new(cells);
1802        let (coverage, covered, total) = heatmap.calculate_stats();
1803        assert_eq!(total, 4);
1804        assert_eq!(covered, 2); // Only cells with coverage > 0
1805        assert!((coverage - 0.5).abs() < 0.01);
1806    }
1807
1808    #[test]
1809    fn h0_term_64_calculate_stats_empty() {
1810        let cells: Vec<Vec<CoverageCell>> = vec![];
1811        let heatmap = RichTerminalHeatmap::new(cells);
1812        let (coverage, covered, total) = heatmap.calculate_stats();
1813        assert_eq!(total, 0);
1814        assert_eq!(covered, 0);
1815        assert_eq!(coverage, 0.0);
1816    }
1817
1818    // --- Coverage Char Edge Cases ---
1819
1820    #[test]
1821    fn h0_term_65_coverage_char_boundaries() {
1822        // Test exact boundary values
1823        assert_eq!(RichTerminalHeatmap::coverage_char(-0.1), '\u{00B7}');
1824        assert_eq!(RichTerminalHeatmap::coverage_char(0.25), '\u{2591}');
1825        assert_eq!(RichTerminalHeatmap::coverage_char(0.50), '\u{2592}');
1826        assert_eq!(RichTerminalHeatmap::coverage_char(0.75), '\u{2593}');
1827        assert_eq!(RichTerminalHeatmap::coverage_char(0.76), '\u{2588}');
1828    }
1829
1830    #[test]
1831    fn h0_term_66_ascii_coverage_char_boundaries() {
1832        // Test exact boundary values
1833        assert_eq!(RichTerminalHeatmap::ascii_coverage_char(-0.1), '.');
1834        assert_eq!(RichTerminalHeatmap::ascii_coverage_char(0.25), '-');
1835        assert_eq!(RichTerminalHeatmap::ascii_coverage_char(0.50), '+');
1836        assert_eq!(RichTerminalHeatmap::ascii_coverage_char(0.75), '#');
1837        assert_eq!(RichTerminalHeatmap::ascii_coverage_char(0.76), '@');
1838    }
1839
1840    // --- Full Render Tests ---
1841
1842    #[test]
1843    fn h0_term_67_render_full_output_rich_ansi() {
1844        let cells = vec![
1845            vec![
1846                CoverageCell {
1847                    coverage: 0.9,
1848                    hit_count: 9
1849                };
1850                5
1851            ];
1852            5
1853        ];
1854        let heatmap = RichTerminalHeatmap::new(cells)
1855            .with_title("Full Test")
1856            .with_mode(OutputMode::RichAnsi);
1857        let output = heatmap.render();
1858        // Should have all sections
1859        assert!(output.contains("Full Test"));
1860        assert!(output.contains("LEGEND"));
1861        assert!(output.contains("COVERAGE SCORE"));
1862        assert!(output.contains("FALSIFICATION STATUS"));
1863    }
1864
1865    #[test]
1866    fn h0_term_68_render_full_output_json() {
1867        let cells = vec![
1868            vec![
1869                CoverageCell {
1870                    coverage: 0.5,
1871                    hit_count: 5
1872                };
1873                3
1874            ];
1875            3
1876        ];
1877        let heatmap = RichTerminalHeatmap::new(cells).with_mode(OutputMode::Json);
1878        let output = heatmap.render();
1879        // Should have content without ANSI codes
1880        assert!(!output.is_empty());
1881    }
1882
1883    // --- ScoreBar Debug and Clone ---
1884
1885    #[test]
1886    fn h0_term_69_score_bar_debug_clone() {
1887        let bar = ScoreBar::new("Debug Test", 0.75, 0.80);
1888        let bar2 = bar.clone();
1889        assert_eq!(bar.score, bar2.score);
1890        assert_eq!(bar.label, bar2.label);
1891        let debug_str = format!("{:?}", bar);
1892        assert!(debug_str.contains("ScoreBar"));
1893    }
1894
1895    // --- ConfidenceInterval Debug Copy Clone ---
1896
1897    #[test]
1898    fn h0_term_70_confidence_interval_debug_copy() {
1899        let ci = ConfidenceInterval::new(0.70, 0.90, 0.95);
1900        let ci2 = ci; // Copy
1901        assert_eq!(ci.lower, ci2.lower);
1902        assert_eq!(ci.upper, ci2.upper);
1903        let debug_str = format!("{:?}", ci);
1904        assert!(debug_str.contains("ConfidenceInterval"));
1905    }
1906
1907    // --- RichTerminalHeatmap Debug Clone ---
1908
1909    #[test]
1910    fn h0_term_71_rich_terminal_heatmap_debug_clone() {
1911        let cells = vec![
1912            vec![
1913                CoverageCell {
1914                    coverage: 0.5,
1915                    hit_count: 5
1916                };
1917                2
1918            ];
1919            2
1920        ];
1921        let heatmap = RichTerminalHeatmap::new(cells);
1922        let heatmap2 = heatmap;
1923        let debug_str = format!("{:?}", heatmap2);
1924        assert!(debug_str.contains("RichTerminalHeatmap"));
1925    }
1926
1927    // --- Render Grid with Various Coverage Levels ---
1928
1929    #[test]
1930    fn h0_term_72_render_grid_all_coverage_levels() {
1931        let cells = vec![vec![
1932            CoverageCell {
1933                coverage: 0.0,
1934                hit_count: 0,
1935            }, // gap
1936            CoverageCell {
1937                coverage: 0.10,
1938                hit_count: 1,
1939            }, // light
1940            CoverageCell {
1941                coverage: 0.30,
1942                hit_count: 3,
1943            }, // medium
1944            CoverageCell {
1945                coverage: 0.60,
1946                hit_count: 6,
1947            }, // dark
1948            CoverageCell {
1949                coverage: 0.90,
1950                hit_count: 9,
1951            }, // full
1952        ]];
1953        let heatmap = RichTerminalHeatmap::new(cells).with_mode(OutputMode::NoColorAscii);
1954        let output = heatmap.render_grid();
1955        // All ASCII coverage chars should be present
1956        assert!(output.contains('.'));
1957        assert!(output.contains('-'));
1958        assert!(output.contains('+'));
1959        assert!(output.contains('#'));
1960        assert!(output.contains('@'));
1961    }
1962}