Skip to main content

jugar_probar/pixel_coverage/
wasm_demo.rs

1//! WASM Pixel GUI Demo - GPU-Accelerated Random Fill
2//!
3//! Demonstrates probar's pixel coverage capabilities with a TUI that
4//! randomly fills a 1080p grid using GPU-accelerated computation.
5//!
6//! Implements PROBAR-SPEC-009 with:
7//! - PCG-XSH-RR deterministic RNG
8//! - Wilson score confidence intervals
9//! - FalsifiabilityGate integration
10//! - **Actual GPU acceleration via trueno** (when `gpu` feature enabled)
11//!
12//! ## References
13//!
14//! - Nickolls et al. (2008): GPU parallel computing model
15//! - O'Neill (2014): PCG random number generation
16//! - Wilson (1927): Wilson score interval
17//! - W3C (2021): WebGPU specification
18//! - Mahajan et al. (2021): Pixel-based visual testing
19
20use super::ConfidenceInterval;
21use std::time::Duration;
22
23#[cfg(feature = "gpu")]
24use trueno::backends::gpu::GpuDevice;
25
26/// PCG-XSH-RR constants from O'Neill (2014)
27const PCG_MULTIPLIER: u32 = 747796405;
28const PCG_INCREMENT: u32 = 2891336453;
29const PCG_OUTPUT_MUL: u32 = 277803737;
30
31/// Configuration for the WASM pixel demo
32#[derive(Debug, Clone)]
33pub struct WasmDemoConfig {
34    /// Screen width in pixels (default: 1920)
35    pub width: u32,
36    /// Screen height in pixels (default: 1080)
37    pub height: u32,
38    /// Probability of filling a pixel per frame (default: 0.01)
39    pub fill_probability: f32,
40    /// Target coverage percentage (default: 0.99)
41    pub target_coverage: f32,
42    /// RNG seed for determinism
43    pub seed: u64,
44    /// Color palette for rendering
45    pub palette: DemoPalette,
46}
47
48impl Default for WasmDemoConfig {
49    fn default() -> Self {
50        Self {
51            width: 1920,
52            height: 1080,
53            fill_probability: 0.01,
54            target_coverage: 0.99,
55            seed: 42,
56            palette: DemoPalette::Viridis,
57        }
58    }
59}
60
61impl WasmDemoConfig {
62    /// Create 1080p configuration
63    #[must_use]
64    pub fn hd_1080p() -> Self {
65        Self::default()
66    }
67
68    /// Create 720p configuration
69    #[must_use]
70    pub fn hd_720p() -> Self {
71        Self {
72            width: 1280,
73            height: 720,
74            ..Self::default()
75        }
76    }
77
78    /// Create small test configuration
79    #[must_use]
80    pub fn test_small() -> Self {
81        Self {
82            width: 100,
83            height: 100,
84            fill_probability: 0.1,
85            seed: 42,
86            ..Self::default()
87        }
88    }
89
90    /// Set custom seed
91    #[must_use]
92    pub fn with_seed(mut self, seed: u64) -> Self {
93        self.seed = seed;
94        self
95    }
96
97    /// Set fill probability
98    #[must_use]
99    pub fn with_fill_probability(mut self, prob: f32) -> Self {
100        self.fill_probability = prob.clamp(0.0, 1.0);
101        self
102    }
103
104    /// Validate configuration
105    pub fn validate(&self) -> Result<(), ConfigError> {
106        if self.width == 0 || self.height == 0 {
107            return Err(ConfigError::InvalidDimensions {
108                width: self.width,
109                height: self.height,
110            });
111        }
112        if !(0.0..=1.0).contains(&self.fill_probability) {
113            return Err(ConfigError::InvalidProbability(self.fill_probability));
114        }
115        if !(0.0..=1.0).contains(&self.target_coverage) {
116            return Err(ConfigError::InvalidTargetCoverage(self.target_coverage));
117        }
118        Ok(())
119    }
120}
121
122/// Color palette options
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
124pub enum DemoPalette {
125    /// Viridis - colorblind-safe (default)
126    #[default]
127    Viridis,
128    /// Magma - dark to bright
129    Magma,
130    /// Heat - traditional heat map
131    Heat,
132    /// Grayscale
133    Grayscale,
134}
135
136/// Severity level for gap regions
137#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub enum GapSeverity {
139    /// Informational (small gap, <25 pixels)
140    Info,
141    /// Warning (medium gap, 25-100 pixels)
142    Warning,
143    /// Critical (large gap, >100 pixels)
144    Critical,
145}
146
147/// A gap region in the pixel buffer
148#[derive(Debug, Clone)]
149pub struct DemoGapRegion {
150    /// X position
151    pub x: usize,
152    /// Y position
153    pub y: usize,
154    /// Width
155    pub width: usize,
156    /// Height
157    pub height: usize,
158    /// Total pixel count
159    pub size: usize,
160    /// Severity level
161    pub severity: GapSeverity,
162}
163
164/// Configuration errors
165#[derive(Debug, Clone, PartialEq)]
166pub enum ConfigError {
167    /// Invalid screen dimensions (width and height must be > 0)
168    InvalidDimensions {
169        /// Width value
170        width: u32,
171        /// Height value
172        height: u32,
173    },
174    /// Invalid fill probability
175    InvalidProbability(f32),
176    /// Invalid target coverage
177    InvalidTargetCoverage(f32),
178}
179
180impl std::fmt::Display for ConfigError {
181    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
182        match self {
183            Self::InvalidDimensions { width, height } => {
184                write!(f, "Invalid dimensions: {}x{} (must be > 0)", width, height)
185            }
186            Self::InvalidProbability(p) => {
187                write!(f, "Invalid probability: {} (must be 0.0..=1.0)", p)
188            }
189            Self::InvalidTargetCoverage(c) => {
190                write!(f, "Invalid target coverage: {} (must be 0.0..=1.0)", c)
191            }
192        }
193    }
194}
195
196impl std::error::Error for ConfigError {}
197
198/// PCG-XSH-RR random number generator (O'Neill, 2014)
199///
200/// Provides deterministic, high-quality random numbers with minimal state.
201#[derive(Debug, Clone)]
202pub struct PcgRng {
203    state: u64,
204    increment: u64,
205}
206
207impl PcgRng {
208    /// Create new RNG with seed
209    #[must_use]
210    pub fn new(seed: u64) -> Self {
211        let mut rng = Self {
212            state: 0,
213            increment: (seed << 1) | 1, // Must be odd
214        };
215        // Warm up state
216        let _ = rng.next_u32();
217        rng.state = rng.state.wrapping_add(seed);
218        let _ = rng.next_u32();
219        rng
220    }
221
222    /// Generate next 32-bit random value
223    #[must_use]
224    pub fn next_u32(&mut self) -> u32 {
225        let old_state = self.state;
226
227        // Advance state using LCG
228        self.state = old_state
229            .wrapping_mul(u64::from(PCG_MULTIPLIER))
230            .wrapping_add(self.increment);
231
232        // XSH-RR output function
233        let xorshifted = ((old_state >> 18) ^ old_state) >> 27;
234        let rot = (old_state >> 59) as u32;
235
236        #[allow(clippy::cast_possible_truncation)]
237        let result = xorshifted as u32;
238        result.rotate_right(rot)
239    }
240
241    /// Generate random float in [0, 1)
242    #[must_use]
243    pub fn next_f32(&mut self) -> f32 {
244        (self.next_u32() as f64 / u32::MAX as f64) as f32
245    }
246
247    /// Generate hash for pixel index (pure function for GPU)
248    #[must_use]
249    pub fn hash_pixel(seed: u32, index: u32, frame: u32) -> u32 {
250        let input = seed ^ index ^ frame.wrapping_mul(12345);
251        let state = input
252            .wrapping_mul(PCG_MULTIPLIER)
253            .wrapping_add(PCG_INCREMENT);
254        let word =
255            ((state >> ((state >> 28).wrapping_add(4))) ^ state).wrapping_mul(PCG_OUTPUT_MUL);
256        (word >> 22) ^ word
257    }
258
259    /// Check if pixel should be filled based on probability
260    #[must_use]
261    pub fn should_fill(seed: u32, index: u32, frame: u32, probability: f32) -> bool {
262        let hash = Self::hash_pixel(seed, index, frame);
263        let random_value = hash as f32 / u32::MAX as f32;
264        random_value < probability
265    }
266}
267
268/// GPU pixel buffer for demo rendering
269///
270/// When `gpu` feature is enabled, this uses actual GPU compute via trueno/wgpu.
271/// Otherwise falls back to CPU implementation.
272#[derive(Debug, Clone)]
273pub struct GpuPixelBuffer {
274    /// Pixel data (0.0 = uncovered, 1.0 = fully covered)
275    pub pixels: Vec<f32>,
276    /// Buffer width
277    pub width: u32,
278    /// Buffer height
279    pub height: u32,
280    /// Current frame number
281    pub frame: u32,
282    /// RNG seed
283    pub seed: u32,
284    /// Whether GPU is being used
285    pub using_gpu: bool,
286}
287
288/// GPU backend for actual hardware acceleration
289#[cfg(feature = "gpu")]
290pub struct GpuAccelerator {
291    device: GpuDevice,
292}
293
294#[cfg(feature = "gpu")]
295impl GpuAccelerator {
296    /// Create new GPU accelerator
297    pub fn new() -> Result<Self, String> {
298        let device = GpuDevice::new()?;
299        Ok(Self { device })
300    }
301
302    /// Check if GPU is available
303    #[allow(dead_code)]
304    pub fn is_available() -> bool {
305        GpuDevice::is_available()
306    }
307
308    /// Execute parallel fill on GPU using wgpu compute
309    ///
310    /// Uses trueno's GPU backend (wgpu/Vulkan) for actual hardware acceleration.
311    /// The RNG is computed on CPU but the comparison and assignment can be
312    /// parallelized via GPU vector operations.
313    pub fn parallel_fill(
314        &self,
315        pixels: &mut [f32],
316        width: u32,
317        height: u32,
318        frame: u32,
319        seed: u32,
320        probability: f32,
321    ) -> Result<(), String> {
322        let total = pixels.len();
323
324        // Generate random values (this computation is embarrassingly parallel)
325        let random_values: Vec<f32> = (0..total)
326            .map(|idx| {
327                let hash = PcgRng::hash_pixel(seed, idx as u32, frame);
328                hash as f32 / u32::MAX as f32
329            })
330            .collect();
331
332        // Generate gradient values for positions
333        let gradient_values: Vec<f32> = (0..total)
334            .map(|idx| {
335                let x = idx as u32 % width;
336                let y = idx as u32 / width;
337                ((x + y) as f32 / (width + height) as f32).max(0.001)
338            })
339            .collect();
340
341        // Use GPU for vector operations when buffer is large enough
342        // trueno's GPU backend works best for operations on >100K elements
343        if total > 100_000 {
344            // GPU-accelerated threshold comparison using trueno's sigmoid
345            // (sigmoid of large negative = ~0, sigmoid of large positive = ~1)
346            // This demonstrates actual GPU compute dispatch
347            let scaled: Vec<f32> = random_values
348                .iter()
349                .map(|&r| (probability - r) * 100.0) // Scale difference for sigmoid
350                .collect();
351
352            let mut mask = vec![0.0f32; total];
353            // Use GPU sigmoid as a soft threshold function
354            self.device.sigmoid(&scaled, &mut mask)?;
355
356            // Apply mask to update pixels
357            for (idx, pixel) in pixels.iter_mut().enumerate() {
358                if *pixel == 0.0 && mask[idx] > 0.5 {
359                    *pixel = gradient_values[idx];
360                }
361            }
362        } else {
363            // For smaller buffers, CPU is faster due to transfer overhead
364            for (idx, pixel) in pixels.iter_mut().enumerate() {
365                if *pixel == 0.0 && random_values[idx] < probability {
366                    *pixel = gradient_values[idx];
367                }
368            }
369        }
370
371        Ok(())
372    }
373}
374
375impl GpuPixelBuffer {
376    /// Create new pixel buffer (tries GPU first, falls back to CPU)
377    #[must_use]
378    pub fn new(width: u32, height: u32, seed: u64) -> Self {
379        let total_pixels = (width as usize) * (height as usize);
380        let using_gpu = Self::gpu_available();
381
382        Self {
383            pixels: vec![0.0; total_pixels],
384            width,
385            height,
386            frame: 0,
387            seed: (seed & 0xFFFF_FFFF) as u32,
388            using_gpu,
389        }
390    }
391
392    /// Check if GPU acceleration is available
393    #[must_use]
394    pub fn gpu_available() -> bool {
395        #[cfg(feature = "gpu")]
396        {
397            GpuDevice::is_available()
398        }
399        #[cfg(not(feature = "gpu"))]
400        {
401            false
402        }
403    }
404
405    /// Get GPU device name if available
406    #[must_use]
407    pub fn gpu_device_name() -> Option<String> {
408        #[cfg(feature = "gpu")]
409        {
410            if GpuDevice::is_available() {
411                // trueno uses wgpu which auto-detects best GPU
412                // Return generic name since adapter_info is internal
413                Some("wgpu (Vulkan/Metal/DX12)".to_string())
414            } else {
415                None
416            }
417        }
418        #[cfg(not(feature = "gpu"))]
419        {
420            None
421        }
422    }
423
424    /// Create 1080p buffer
425    #[must_use]
426    pub fn new_1080p() -> Self {
427        Self::new(1920, 1080, 42)
428    }
429
430    /// Create 720p buffer
431    #[must_use]
432    pub fn new_720p() -> Self {
433        Self::new(1280, 720, 42)
434    }
435
436    /// Total number of pixels
437    #[must_use]
438    pub fn total_pixels(&self) -> usize {
439        self.pixels.len()
440    }
441
442    /// Execute random fill pass (GPU-accelerated when available)
443    pub fn random_fill_pass(&mut self, probability: f32) {
444        self.frame += 1;
445
446        #[cfg(feature = "gpu")]
447        {
448            if self.using_gpu {
449                if let Ok(accelerator) = GpuAccelerator::new() {
450                    if accelerator
451                        .parallel_fill(
452                            &mut self.pixels,
453                            self.width,
454                            self.height,
455                            self.frame,
456                            self.seed,
457                            probability,
458                        )
459                        .is_ok()
460                    {
461                        return;
462                    }
463                }
464            }
465        }
466
467        // CPU fallback
468        self.random_fill_pass_cpu(probability);
469    }
470
471    /// CPU-only random fill pass (fallback)
472    fn random_fill_pass_cpu(&mut self, probability: f32) {
473        let frame = self.frame;
474        let seed = self.seed;
475
476        for (idx, pixel) in self.pixels.iter_mut().enumerate() {
477            if *pixel == 0.0 && PcgRng::should_fill(seed, idx as u32, frame, probability) {
478                // Calculate color based on position (viridis-like gradient)
479                let x = idx as u32 % self.width;
480                let y = idx as u32 / self.width;
481                let normalized = (x + y) as f32 / (self.width + self.height) as f32;
482                *pixel = normalized.max(0.001); // Minimum non-zero for "covered"
483            }
484        }
485    }
486
487    /// Run multiple fill passes until target coverage
488    pub fn fill_to_coverage(&mut self, target: f32, probability: f32, max_frames: u32) {
489        for _ in 0..max_frames {
490            self.random_fill_pass(probability);
491            if self.coverage_percentage() >= target {
492                break;
493            }
494        }
495    }
496
497    /// Calculate coverage statistics
498    #[must_use]
499    pub fn coverage_stats(&self) -> CoverageStats {
500        let covered = self.pixels.iter().filter(|&&v| v > 0.0).count();
501        let total = self.pixels.len();
502        let percentage = covered as f32 / total as f32;
503
504        CoverageStats {
505            covered,
506            total,
507            percentage,
508            wilson_ci: wilson_confidence_interval(covered, total, 0.95),
509            gaps: self.find_gaps(),
510        }
511    }
512
513    /// Get coverage percentage
514    #[must_use]
515    pub fn coverage_percentage(&self) -> f32 {
516        let covered = self.pixels.iter().filter(|&&v| v > 0.0).count();
517        covered as f32 / self.pixels.len() as f32
518    }
519
520    /// Find gap regions (contiguous uncovered areas)
521    #[must_use]
522    pub fn find_gaps(&self) -> Vec<DemoGapRegion> {
523        let mut gaps = Vec::new();
524        let mut visited = vec![false; self.pixels.len()];
525
526        for y in 0..self.height {
527            for x in 0..self.width {
528                let idx = (y * self.width + x) as usize;
529                if self.pixels[idx] == 0.0 && !visited[idx] {
530                    // Found start of gap - flood fill to find extent
531                    let gap = self.flood_fill_gap(x, y, &mut visited);
532                    if gap.size > 0 {
533                        gaps.push(gap);
534                    }
535                }
536            }
537        }
538
539        gaps
540    }
541
542    /// Flood fill to find connected gap region
543    fn flood_fill_gap(&self, start_x: u32, start_y: u32, visited: &mut [bool]) -> DemoGapRegion {
544        let mut stack = vec![(start_x, start_y)];
545        let mut min_x = start_x;
546        let mut max_x = start_x;
547        let mut min_y = start_y;
548        let mut max_y = start_y;
549        let mut size = 0;
550
551        while let Some((x, y)) = stack.pop() {
552            if x >= self.width || y >= self.height {
553                continue;
554            }
555            let idx = (y * self.width + x) as usize;
556            if visited[idx] || self.pixels[idx] > 0.0 {
557                continue;
558            }
559
560            visited[idx] = true;
561            size += 1;
562            min_x = min_x.min(x);
563            max_x = max_x.max(x);
564            min_y = min_y.min(y);
565            max_y = max_y.max(y);
566
567            // Add neighbors (4-connected)
568            if x > 0 {
569                stack.push((x - 1, y));
570            }
571            if x < self.width - 1 {
572                stack.push((x + 1, y));
573            }
574            if y > 0 {
575                stack.push((x, y - 1));
576            }
577            if y < self.height - 1 {
578                stack.push((x, y + 1));
579            }
580        }
581
582        DemoGapRegion {
583            x: min_x as usize,
584            y: min_y as usize,
585            width: (max_x - min_x + 1) as usize,
586            height: (max_y - min_y + 1) as usize,
587            size,
588            severity: if size > 100 {
589                GapSeverity::Critical
590            } else if size > 25 {
591                GapSeverity::Warning
592            } else {
593                GapSeverity::Info
594            },
595        }
596    }
597
598    /// Downsample to terminal resolution
599    #[must_use]
600    pub fn downsample(&self, term_width: usize, term_height: usize) -> Vec<f32> {
601        let scale_x = self.width as usize / term_width.max(1);
602        let scale_y = self.height as usize / term_height.max(1);
603        let mut result = vec![0.0; term_width * term_height];
604
605        for ty in 0..term_height {
606            for tx in 0..term_width {
607                // Average pixels in this cell
608                let mut sum = 0.0;
609                let mut count = 0;
610
611                for py in 0..scale_y {
612                    for px in 0..scale_x {
613                        let src_x = tx * scale_x + px;
614                        let src_y = ty * scale_y + py;
615                        if src_x < self.width as usize && src_y < self.height as usize {
616                            let idx = src_y * self.width as usize + src_x;
617                            sum += self.pixels[idx];
618                            count += 1;
619                        }
620                    }
621                }
622
623                if count > 0 {
624                    result[ty * term_width + tx] = sum / count as f32;
625                }
626            }
627        }
628
629        result
630    }
631
632    /// Reset buffer
633    pub fn reset(&mut self) {
634        self.pixels.fill(0.0);
635        self.frame = 0;
636    }
637
638    /// Check if this buffer is using GPU acceleration
639    #[must_use]
640    pub fn is_using_gpu(&self) -> bool {
641        self.using_gpu
642    }
643}
644
645/// Coverage statistics with Wilson CI
646#[derive(Debug, Clone)]
647pub struct CoverageStats {
648    /// Number of covered pixels
649    pub covered: usize,
650    /// Total number of pixels
651    pub total: usize,
652    /// Coverage percentage (0.0 - 1.0)
653    pub percentage: f32,
654    /// Wilson confidence interval
655    pub wilson_ci: ConfidenceInterval,
656    /// Gap regions
657    pub gaps: Vec<DemoGapRegion>,
658}
659
660impl CoverageStats {
661    /// Check if coverage meets threshold
662    #[must_use]
663    pub fn meets_threshold(&self, threshold: f32) -> bool {
664        self.percentage >= threshold
665    }
666
667    /// Get largest gap size
668    #[must_use]
669    pub fn max_gap_size(&self) -> usize {
670        self.gaps.iter().map(|g| g.size).max().unwrap_or(0)
671    }
672}
673
674/// Calculate Wilson score confidence interval (Wilson, 1927)
675///
676/// Provides better coverage for small samples than normal approximation.
677#[must_use]
678pub fn wilson_confidence_interval(
679    successes: usize,
680    total: usize,
681    confidence: f32,
682) -> ConfidenceInterval {
683    if total == 0 {
684        return ConfidenceInterval {
685            lower: 0.0,
686            upper: 0.0,
687            level: confidence,
688        };
689    }
690
691    let n = total as f32;
692    let p = successes as f32 / n;
693
694    // Z-score for confidence level (95% = 1.96)
695    let z: f32 = if (confidence - 0.90).abs() < 0.01 {
696        1.645
697    } else if (confidence - 0.95).abs() < 0.01 {
698        1.96
699    } else if (confidence - 0.99).abs() < 0.01 {
700        2.576
701    } else {
702        1.96
703    };
704
705    let z2 = z * z;
706    let denominator = 1.0 + z2 / n;
707    let center = (p + z2 / (2.0 * n)) / denominator;
708    let margin = (z / denominator) * ((p * (1.0 - p) / n + z2 / (4.0 * n * n)).sqrt());
709
710    ConfidenceInterval {
711        lower: (center - margin).max(0.0),
712        upper: (center + margin).min(1.0),
713        level: confidence,
714    }
715}
716
717/// Demo state for TUI rendering
718#[derive(Debug)]
719pub struct WasmPixelDemo {
720    /// GPU pixel buffer
721    pub buffer: GpuPixelBuffer,
722    /// Configuration
723    pub config: WasmDemoConfig,
724    /// Start time for measuring convergence
725    pub start_time: std::time::Instant,
726    /// Whether demo is complete
727    pub complete: bool,
728}
729
730impl WasmPixelDemo {
731    /// Create new demo with configuration
732    #[must_use]
733    pub fn new(config: WasmDemoConfig) -> Self {
734        Self {
735            buffer: GpuPixelBuffer::new(config.width, config.height, config.seed),
736            config,
737            start_time: std::time::Instant::now(),
738            complete: false,
739        }
740    }
741
742    /// Create 1080p demo
743    #[must_use]
744    pub fn hd_1080p() -> Self {
745        Self::new(WasmDemoConfig::hd_1080p())
746    }
747
748    /// Execute one frame
749    pub fn tick(&mut self) {
750        if self.complete {
751            return;
752        }
753
754        self.buffer.random_fill_pass(self.config.fill_probability);
755
756        if self.buffer.coverage_percentage() >= self.config.target_coverage {
757            self.complete = true;
758        }
759    }
760
761    /// Get current stats
762    #[must_use]
763    pub fn stats(&self) -> CoverageStats {
764        self.buffer.coverage_stats()
765    }
766
767    /// Get elapsed time
768    #[must_use]
769    pub fn elapsed(&self) -> Duration {
770        self.start_time.elapsed()
771    }
772
773    /// Check if demo is complete
774    #[must_use]
775    pub fn is_complete(&self) -> bool {
776        self.complete
777    }
778
779    /// Get frame count
780    #[must_use]
781    pub fn frame_count(&self) -> u32 {
782        self.buffer.frame
783    }
784
785    /// Reset demo
786    pub fn reset(&mut self) {
787        self.buffer.reset();
788        self.start_time = std::time::Instant::now();
789        self.complete = false;
790    }
791}
792
793#[cfg(test)]
794#[allow(clippy::unwrap_used, clippy::expect_used, clippy::float_cmp)]
795mod tests {
796    use super::*;
797
798    // =========================================================================
799    // Section 1: Configuration Tests (QA 6-10)
800    // =========================================================================
801
802    #[test]
803    fn h0_demo_01_config_default() {
804        let config = WasmDemoConfig::default();
805        assert_eq!(config.width, 1920);
806        assert_eq!(config.height, 1080);
807        assert!((config.fill_probability - 0.01).abs() < f32::EPSILON);
808    }
809
810    #[test]
811    fn h0_demo_02_config_validation_valid() {
812        let config = WasmDemoConfig::default();
813        assert!(config.validate().is_ok());
814    }
815
816    #[test]
817    fn h0_demo_03_config_validation_zero_width() {
818        let config = WasmDemoConfig {
819            width: 0,
820            ..Default::default()
821        };
822        assert!(matches!(
823            config.validate(),
824            Err(ConfigError::InvalidDimensions { .. })
825        ));
826    }
827
828    #[test]
829    fn h0_demo_04_config_validation_invalid_probability() {
830        let config = WasmDemoConfig {
831            fill_probability: -0.5,
832            ..Default::default()
833        };
834        assert!(matches!(
835            config.validate(),
836            Err(ConfigError::InvalidProbability(_))
837        ));
838    }
839
840    #[test]
841    fn h0_demo_05_config_validation_probability_over_1() {
842        let config = WasmDemoConfig {
843            fill_probability: 1.5,
844            ..Default::default()
845        };
846        assert!(matches!(
847            config.validate(),
848            Err(ConfigError::InvalidProbability(_))
849        ));
850    }
851
852    // =========================================================================
853    // Section 2: PCG RNG Tests (QA 21-30)
854    // =========================================================================
855
856    #[test]
857    fn h0_rng_01_determinism_same_seed() {
858        let mut rng1 = PcgRng::new(42);
859        let mut rng2 = PcgRng::new(42);
860
861        for _ in 0..100 {
862            assert_eq!(rng1.next_u32(), rng2.next_u32());
863        }
864    }
865
866    #[test]
867    fn h0_rng_02_determinism_different_seeds() {
868        let mut rng1 = PcgRng::new(42);
869        let mut rng2 = PcgRng::new(123);
870
871        // At least some values should differ
872        let mut any_different = false;
873        for _ in 0..100 {
874            if rng1.next_u32() != rng2.next_u32() {
875                any_different = true;
876                break;
877            }
878        }
879        assert!(
880            any_different,
881            "Different seeds should produce different sequences"
882        );
883    }
884
885    #[test]
886    fn h0_rng_03_pixel_hash_determinism() {
887        let hash1 = PcgRng::hash_pixel(42, 1000, 5);
888        let hash2 = PcgRng::hash_pixel(42, 1000, 5);
889        assert_eq!(hash1, hash2);
890    }
891
892    #[test]
893    fn h0_rng_04_pixel_hash_frame_dependency() {
894        let hash_frame_0 = PcgRng::hash_pixel(42, 1000, 0);
895        let hash_frame_1 = PcgRng::hash_pixel(42, 1000, 1);
896        assert_ne!(hash_frame_0, hash_frame_1);
897    }
898
899    #[test]
900    fn h0_rng_05_should_fill_zero_probability() {
901        // With 0.0 probability, should never fill
902        for idx in 0..1000 {
903            assert!(!PcgRng::should_fill(42, idx, 1, 0.0));
904        }
905    }
906
907    #[test]
908    fn h0_rng_06_should_fill_full_probability() {
909        // With 1.0 probability, should always fill
910        for idx in 0..1000 {
911            assert!(PcgRng::should_fill(42, idx, 1, 1.0));
912        }
913    }
914
915    #[test]
916    fn h0_rng_07_float_range() {
917        let mut rng = PcgRng::new(42);
918        for _ in 0..1000 {
919            let f = rng.next_f32();
920            assert!((0.0..1.0).contains(&f));
921        }
922    }
923
924    #[test]
925    fn h0_rng_08_zero_seed_works() {
926        // PCG should handle zero seed correctly (unlike some simple LCGs)
927        let mut rng = PcgRng::new(0);
928        let val1 = rng.next_u32();
929        let val2 = rng.next_u32();
930        assert_ne!(val1, val2, "Zero seed should still produce varying output");
931    }
932
933    // =========================================================================
934    // Section 3: GPU Buffer Tests (QA 11-20)
935    // =========================================================================
936
937    #[test]
938    fn h0_buffer_01_creation_1080p() {
939        let buffer = GpuPixelBuffer::new_1080p();
940        assert_eq!(buffer.width, 1920);
941        assert_eq!(buffer.height, 1080);
942        assert_eq!(buffer.total_pixels(), 1920 * 1080);
943    }
944
945    #[test]
946    fn h0_buffer_02_creation_720p() {
947        let buffer = GpuPixelBuffer::new_720p();
948        assert_eq!(buffer.width, 1280);
949        assert_eq!(buffer.height, 720);
950    }
951
952    #[test]
953    fn h0_buffer_03_initial_zero_coverage() {
954        let buffer = GpuPixelBuffer::new(100, 100, 42);
955        let stats = buffer.coverage_stats();
956        assert_eq!(stats.covered, 0);
957        assert_eq!(stats.percentage, 0.0);
958    }
959
960    #[test]
961    fn h0_buffer_04_random_fill_increases_coverage() {
962        let mut buffer = GpuPixelBuffer::new(100, 100, 42);
963        buffer.random_fill_pass(0.1);
964        assert!(buffer.coverage_percentage() > 0.0);
965    }
966
967    #[test]
968    fn h0_buffer_05_fill_convergence() {
969        let mut buffer = GpuPixelBuffer::new(50, 50, 42);
970        buffer.fill_to_coverage(0.99, 0.1, 1000);
971        assert!(
972            buffer.coverage_percentage() >= 0.99,
973            "Should converge to 99%+ coverage"
974        );
975    }
976
977    #[test]
978    fn h0_buffer_06_deterministic_fill() {
979        let mut buffer1 = GpuPixelBuffer::new(50, 50, 42);
980        let mut buffer2 = GpuPixelBuffer::new(50, 50, 42);
981
982        for _ in 0..10 {
983            buffer1.random_fill_pass(0.1);
984            buffer2.random_fill_pass(0.1);
985        }
986
987        assert_eq!(
988            buffer1.pixels, buffer2.pixels,
989            "Same seed should produce same pattern"
990        );
991    }
992
993    #[test]
994    fn h0_buffer_07_reset_clears() {
995        let mut buffer = GpuPixelBuffer::new(50, 50, 42);
996        buffer.random_fill_pass(0.5);
997        assert!(buffer.coverage_percentage() > 0.0);
998
999        buffer.reset();
1000        assert_eq!(buffer.coverage_percentage(), 0.0);
1001        assert_eq!(buffer.frame, 0);
1002    }
1003
1004    // =========================================================================
1005    // Section 4: Coverage Statistics Tests (QA 41-50)
1006    // =========================================================================
1007
1008    #[test]
1009    fn h0_stats_01_wilson_ci_bounds() {
1010        let ci = wilson_confidence_interval(50, 100, 0.95);
1011        assert!(ci.lower <= 0.50);
1012        assert!(ci.upper >= 0.50);
1013        assert!(ci.lower >= 0.0);
1014        assert!(ci.upper <= 1.0);
1015    }
1016
1017    #[test]
1018    fn h0_stats_02_wilson_ci_empty() {
1019        let ci = wilson_confidence_interval(0, 0, 0.95);
1020        assert_eq!(ci.lower, 0.0);
1021        assert_eq!(ci.upper, 0.0);
1022    }
1023
1024    #[test]
1025    fn h0_stats_03_wilson_ci_zero_coverage() {
1026        let ci = wilson_confidence_interval(0, 100, 0.95);
1027        assert!(ci.lower == 0.0);
1028        assert!(ci.upper > 0.0);
1029    }
1030
1031    #[test]
1032    fn h0_stats_04_wilson_ci_full_coverage() {
1033        let ci = wilson_confidence_interval(100, 100, 0.95);
1034        assert!(ci.lower < 1.0);
1035        // Upper bound should be very close to 1.0 (clamped)
1036        assert!((ci.upper - 1.0).abs() < 0.001);
1037    }
1038
1039    #[test]
1040    fn h0_stats_05_wilson_ci_narrows_with_samples() {
1041        let ci_small = wilson_confidence_interval(5, 10, 0.95);
1042        let ci_large = wilson_confidence_interval(500, 1000, 0.95);
1043
1044        let width_small = ci_small.upper - ci_small.lower;
1045        let width_large = ci_large.upper - ci_large.lower;
1046
1047        assert!(
1048            width_large < width_small,
1049            "CI should narrow with more samples"
1050        );
1051    }
1052
1053    #[test]
1054    fn h0_stats_06_coverage_meets_threshold() {
1055        let mut buffer = GpuPixelBuffer::new(50, 50, 42);
1056        buffer.fill_to_coverage(0.8, 0.1, 500);
1057        let stats = buffer.coverage_stats();
1058        assert!(stats.meets_threshold(0.8));
1059    }
1060
1061    // =========================================================================
1062    // Section 5: Gap Detection Tests (QA 51-60)
1063    // =========================================================================
1064
1065    #[test]
1066    fn h0_gap_01_empty_buffer_is_one_gap() {
1067        let buffer = GpuPixelBuffer::new(10, 10, 42);
1068        let gaps = buffer.find_gaps();
1069        assert_eq!(gaps.len(), 1);
1070        assert_eq!(gaps[0].size, 100);
1071    }
1072
1073    #[test]
1074    fn h0_gap_02_full_buffer_no_gaps() {
1075        let mut buffer = GpuPixelBuffer::new(10, 10, 42);
1076        // Fill all pixels
1077        for pixel in &mut buffer.pixels {
1078            *pixel = 1.0;
1079        }
1080        let gaps = buffer.find_gaps();
1081        assert!(gaps.is_empty());
1082    }
1083
1084    #[test]
1085    fn h0_gap_03_max_gap_size() {
1086        let buffer = GpuPixelBuffer::new(10, 10, 42);
1087        let stats = buffer.coverage_stats();
1088        assert_eq!(stats.max_gap_size(), 100);
1089    }
1090
1091    // =========================================================================
1092    // Section 6: Demo Lifecycle Tests (QA 61-70)
1093    // =========================================================================
1094
1095    #[test]
1096    fn h0_demo_lifecycle_01_creation() {
1097        let demo = WasmPixelDemo::new(WasmDemoConfig::test_small());
1098        assert!(!demo.is_complete());
1099        assert_eq!(demo.frame_count(), 0);
1100    }
1101
1102    #[test]
1103    fn h0_demo_lifecycle_02_tick_advances() {
1104        let mut demo = WasmPixelDemo::new(WasmDemoConfig::test_small());
1105        demo.tick();
1106        assert_eq!(demo.frame_count(), 1);
1107    }
1108
1109    #[test]
1110    fn h0_demo_lifecycle_03_completes_on_target() {
1111        let config = WasmDemoConfig {
1112            width: 10,
1113            height: 10,
1114            fill_probability: 0.5,
1115            target_coverage: 0.5,
1116            ..Default::default()
1117        };
1118        let mut demo = WasmPixelDemo::new(config);
1119
1120        // Run until complete or max frames
1121        for _ in 0..1000 {
1122            demo.tick();
1123            if demo.is_complete() {
1124                break;
1125            }
1126        }
1127
1128        assert!(demo.is_complete());
1129    }
1130
1131    #[test]
1132    fn h0_demo_lifecycle_04_reset() {
1133        let mut demo = WasmPixelDemo::new(WasmDemoConfig::test_small());
1134        demo.tick();
1135        demo.tick();
1136
1137        demo.reset();
1138        assert!(!demo.is_complete());
1139        assert_eq!(demo.frame_count(), 0);
1140        assert_eq!(demo.buffer.coverage_percentage(), 0.0);
1141    }
1142
1143    // =========================================================================
1144    // Section 7: Downsampling Tests (QA 31-40)
1145    // =========================================================================
1146
1147    #[test]
1148    fn h0_downsample_01_correct_size() {
1149        let buffer = GpuPixelBuffer::new(100, 100, 42);
1150        let downsampled = buffer.downsample(10, 10);
1151        assert_eq!(downsampled.len(), 100);
1152    }
1153
1154    #[test]
1155    fn h0_downsample_02_preserves_coverage_ratio() {
1156        let mut buffer = GpuPixelBuffer::new(100, 100, 42);
1157        buffer.random_fill_pass(1.0); // Fill all
1158
1159        let downsampled = buffer.downsample(10, 10);
1160        let ds_covered = downsampled.iter().filter(|&&v| v > 0.0).count();
1161
1162        // All cells should be covered
1163        assert_eq!(ds_covered, 100);
1164    }
1165
1166    #[test]
1167    fn h0_downsample_03_handles_zero_terminal() {
1168        let buffer = GpuPixelBuffer::new(100, 100, 42);
1169        let downsampled = buffer.downsample(0, 0);
1170        assert!(downsampled.is_empty());
1171    }
1172
1173    // =========================================================================
1174    // Section 8: Palette Tests
1175    // =========================================================================
1176
1177    #[test]
1178    fn h0_palette_01_default_is_viridis() {
1179        let config = WasmDemoConfig::default();
1180        assert_eq!(config.palette, DemoPalette::Viridis);
1181    }
1182
1183    // =========================================================================
1184    // Section 9: Work-Bound Structural Tests (QA 61-70)
1185    // =========================================================================
1186    //
1187    // These assert the *structural* work bound, never wall-clock time. A
1188    // `elapsed.as_secs() < N` assertion cancels nothing about machine speed and
1189    // flakes under CI load; it is banned in this repo. State the bound as a
1190    // value instead: creation is a single zero-fill, and one `random_fill_pass`
1191    // call applies exactly one frame of fill - nothing skipped, deferred into a
1192    // later batch, or repeated.
1193
1194    #[test]
1195    fn h0_perf_01_1080p_creation_allocates_zeroed_buffer() {
1196        let buffer = GpuPixelBuffer::new_1080p();
1197
1198        // Creation is a single O(total_pixels) zero-fill: exact allocation,
1199        // every pixel uncovered, no frames consumed.
1200        assert_eq!(buffer.pixels.len(), 1920 * 1080);
1201        assert_eq!(buffer.frame, 0);
1202        assert!(
1203            buffer.pixels.iter().all(|&p| p == 0.0),
1204            "1080p buffer must be fully uncovered at creation"
1205        );
1206    }
1207
1208    #[test]
1209    fn h0_perf_02_fill_pass_does_exactly_one_frame_of_work() {
1210        const W: u32 = 64;
1211        const H: u32 = 64;
1212        const SEED: u64 = 42;
1213        const PROB: f32 = 0.05;
1214        const PASSES: u32 = 20;
1215
1216        let seed32 = (SEED & 0xFFFF_FFFF) as u32;
1217        let mut buffer = GpuPixelBuffer::new(W, H, SEED);
1218        let mut previous = buffer.pixels.clone();
1219        let mut newly_covered_total = 0usize;
1220
1221        for pass in 1..=PASSES {
1222            buffer.random_fill_pass(PROB);
1223
1224            // One call advances exactly one frame - no hidden extra sweeps.
1225            assert_eq!(
1226                buffer.frame, pass,
1227                "call {pass} did not advance the frame counter by exactly 1"
1228            );
1229
1230            for idx in 0..(W * H) {
1231                let before = previous[idx as usize];
1232                let after = buffer.pixels[idx as usize];
1233
1234                // This frame's draw is a pure function of (seed, idx, frame),
1235                // so exactly which pixels the pass owes work to is known ahead
1236                // of the call.
1237                let owed = before == 0.0 && PcgRng::should_fill(seed32, idx, pass, PROB);
1238
1239                if owed {
1240                    // No work skipped: every pixel this frame selected is now
1241                    // covered, holding its position gradient.
1242                    let x = idx % W;
1243                    let y = idx / W;
1244                    let gradient = ((x + y) as f32 / (W + H) as f32).max(0.001);
1245                    assert_eq!(
1246                        after, gradient,
1247                        "pass {pass} skipped pixel {idx} that frame {pass} selected"
1248                    );
1249                    newly_covered_total += 1;
1250                } else {
1251                    // No work deferred, repeated, or invented: everything else
1252                    // is byte-identical to before the call. Covers monotonicity
1253                    // (no pixel un-covers) and rules out a pass that batches
1254                    // several frames of fill into one sweep.
1255                    assert_eq!(
1256                        after, before,
1257                        "pass {pass} changed pixel {idx} that frame {pass} did not select"
1258                    );
1259                }
1260            }
1261
1262            previous.clone_from(&buffer.pixels);
1263        }
1264
1265        // Non-vacuity: the checks above must discriminate, not be satisfied by
1266        // a buffer that stayed all-zero or saturated on the first pass.
1267        assert!(
1268            newly_covered_total > 0,
1269            "no pixel was ever covered - the per-pass check is vacuous"
1270        );
1271        assert!(
1272            newly_covered_total < (W * H) as usize,
1273            "every pixel covered - PROB/PASSES too high to exercise both branches"
1274        );
1275    }
1276
1277    // =========================================================================
1278    // Section 10: Error Handling Tests (QA 81-90)
1279    // =========================================================================
1280
1281    #[test]
1282    fn h0_error_01_config_error_display() {
1283        let err = ConfigError::InvalidDimensions {
1284            width: 0,
1285            height: 100,
1286        };
1287        let msg = format!("{}", err);
1288        assert!(msg.contains("Invalid dimensions"));
1289    }
1290
1291    #[test]
1292    fn h0_error_02_probability_clamping() {
1293        let config = WasmDemoConfig::default().with_fill_probability(1.5);
1294        assert_eq!(config.fill_probability, 1.0);
1295
1296        let config = WasmDemoConfig::default().with_fill_probability(-0.5);
1297        assert_eq!(config.fill_probability, 0.0);
1298    }
1299
1300    // =========================================================================
1301    // Section 11: Additional Configuration Tests (Coverage Boost)
1302    // =========================================================================
1303
1304    #[test]
1305    fn h0_config_06_hd_720p() {
1306        let config = WasmDemoConfig::hd_720p();
1307        assert_eq!(config.width, 1280);
1308        assert_eq!(config.height, 720);
1309        // Should inherit defaults for other fields
1310        assert!((config.fill_probability - 0.01).abs() < f32::EPSILON);
1311        assert_eq!(config.palette, DemoPalette::Viridis);
1312    }
1313
1314    #[test]
1315    fn h0_config_07_test_small() {
1316        let config = WasmDemoConfig::test_small();
1317        assert_eq!(config.width, 100);
1318        assert_eq!(config.height, 100);
1319        assert!((config.fill_probability - 0.1).abs() < f32::EPSILON);
1320        assert_eq!(config.seed, 42);
1321    }
1322
1323    #[test]
1324    fn h0_config_08_with_seed() {
1325        let config = WasmDemoConfig::default().with_seed(12345);
1326        assert_eq!(config.seed, 12345);
1327        // Should preserve other defaults
1328        assert_eq!(config.width, 1920);
1329        assert_eq!(config.height, 1080);
1330    }
1331
1332    #[test]
1333    fn h0_config_09_validation_zero_height() {
1334        let config = WasmDemoConfig {
1335            height: 0,
1336            ..Default::default()
1337        };
1338        let err = config.validate().unwrap_err();
1339        assert!(matches!(
1340            err,
1341            ConfigError::InvalidDimensions {
1342                width: 1920,
1343                height: 0
1344            }
1345        ));
1346    }
1347
1348    #[test]
1349    fn h0_config_10_validation_invalid_target_coverage_low() {
1350        let config = WasmDemoConfig {
1351            target_coverage: -0.5,
1352            ..Default::default()
1353        };
1354        assert!(matches!(
1355            config.validate(),
1356            Err(ConfigError::InvalidTargetCoverage(_))
1357        ));
1358    }
1359
1360    #[test]
1361    fn h0_config_11_validation_invalid_target_coverage_high() {
1362        let config = WasmDemoConfig {
1363            target_coverage: 1.5,
1364            ..Default::default()
1365        };
1366        assert!(matches!(
1367            config.validate(),
1368            Err(ConfigError::InvalidTargetCoverage(_))
1369        ));
1370    }
1371
1372    // =========================================================================
1373    // Section 12: ConfigError Display Tests (Coverage Boost)
1374    // =========================================================================
1375
1376    #[test]
1377    fn h0_error_03_probability_display() {
1378        let err = ConfigError::InvalidProbability(1.5);
1379        let msg = format!("{}", err);
1380        assert!(msg.contains("Invalid probability"));
1381        assert!(msg.contains("1.5"));
1382        assert!(msg.contains("0.0..=1.0"));
1383    }
1384
1385    #[test]
1386    fn h0_error_04_target_coverage_display() {
1387        let err = ConfigError::InvalidTargetCoverage(-0.5);
1388        let msg = format!("{}", err);
1389        assert!(msg.contains("Invalid target coverage"));
1390        assert!(msg.contains("-0.5"));
1391    }
1392
1393    #[test]
1394    fn h0_error_05_config_error_is_error_trait() {
1395        let err: Box<dyn std::error::Error> = Box::new(ConfigError::InvalidProbability(1.5));
1396        // Verify it implements std::error::Error
1397        let _ = err.source(); // Should return None (default impl)
1398        let _ = format!("{}", err);
1399    }
1400
1401    #[test]
1402    fn h0_error_06_config_error_clone_eq() {
1403        let err1 = ConfigError::InvalidProbability(1.5);
1404        let err2 = err1.clone();
1405        assert_eq!(err1, err2);
1406
1407        let err3 = ConfigError::InvalidDimensions {
1408            width: 0,
1409            height: 100,
1410        };
1411        let err4 = err3.clone();
1412        assert_eq!(err3, err4);
1413    }
1414
1415    // =========================================================================
1416    // Section 13: DemoPalette Tests (Coverage Boost)
1417    // =========================================================================
1418
1419    #[test]
1420    fn h0_palette_02_all_variants() {
1421        let palettes = [
1422            DemoPalette::Viridis,
1423            DemoPalette::Magma,
1424            DemoPalette::Heat,
1425            DemoPalette::Grayscale,
1426        ];
1427
1428        for palette in &palettes {
1429            // Test Debug
1430            let debug = format!("{:?}", palette);
1431            assert!(!debug.is_empty());
1432
1433            // Test Clone
1434            let cloned = *palette;
1435            assert_eq!(*palette, cloned);
1436        }
1437    }
1438
1439    #[test]
1440    fn h0_palette_03_equality() {
1441        assert_eq!(DemoPalette::Magma, DemoPalette::Magma);
1442        assert_ne!(DemoPalette::Magma, DemoPalette::Heat);
1443        assert_ne!(DemoPalette::Viridis, DemoPalette::Grayscale);
1444    }
1445
1446    // =========================================================================
1447    // Section 14: GapSeverity Tests (Coverage Boost)
1448    // =========================================================================
1449
1450    #[test]
1451    fn h0_gap_04_severity_info() {
1452        // Gaps < 25 pixels should be Info
1453        let mut buffer = GpuPixelBuffer::new(10, 10, 42);
1454        // Fill most pixels, leaving a small gap of ~10 pixels
1455        for (idx, pixel) in buffer.pixels.iter_mut().enumerate() {
1456            if idx >= 10 {
1457                *pixel = 1.0;
1458            }
1459        }
1460
1461        let gaps = buffer.find_gaps();
1462        assert!(!gaps.is_empty());
1463        let small_gap = &gaps[0];
1464        assert!(small_gap.size < 25);
1465        assert_eq!(small_gap.severity, GapSeverity::Info);
1466    }
1467
1468    #[test]
1469    fn h0_gap_05_severity_warning() {
1470        // Gaps 25-100 pixels should be Warning
1471        let mut buffer = GpuPixelBuffer::new(20, 20, 42);
1472        // Fill all but 50 pixels (5x10 region)
1473        for y in 0..20 {
1474            for x in 0..20 {
1475                let idx = y * 20 + x;
1476                if x >= 10 || y >= 5 {
1477                    buffer.pixels[idx] = 1.0;
1478                }
1479            }
1480        }
1481
1482        let gaps = buffer.find_gaps();
1483        assert!(!gaps.is_empty());
1484        let medium_gap = &gaps[0];
1485        assert!(medium_gap.size >= 25 && medium_gap.size <= 100);
1486        assert_eq!(medium_gap.severity, GapSeverity::Warning);
1487    }
1488
1489    #[test]
1490    fn h0_gap_06_severity_critical() {
1491        // Gaps > 100 pixels should be Critical
1492        let buffer = GpuPixelBuffer::new(20, 20, 42);
1493        // All 400 pixels are uncovered
1494        let gaps = buffer.find_gaps();
1495        assert_eq!(gaps.len(), 1);
1496        assert!(gaps[0].size > 100);
1497        assert_eq!(gaps[0].severity, GapSeverity::Critical);
1498    }
1499
1500    #[test]
1501    fn h0_gap_07_severity_variants() {
1502        // Test all severity variants for Debug, Clone, Eq
1503        let severities = [
1504            GapSeverity::Info,
1505            GapSeverity::Warning,
1506            GapSeverity::Critical,
1507        ];
1508
1509        for sev in &severities {
1510            let debug = format!("{:?}", sev);
1511            assert!(!debug.is_empty());
1512
1513            let cloned = *sev;
1514            assert_eq!(*sev, cloned);
1515        }
1516    }
1517
1518    // =========================================================================
1519    // Section 15: DemoGapRegion Tests (Coverage Boost)
1520    // =========================================================================
1521
1522    #[test]
1523    fn h0_gap_08_region_fields() {
1524        let region = DemoGapRegion {
1525            x: 10,
1526            y: 20,
1527            width: 30,
1528            height: 40,
1529            size: 150,
1530            severity: GapSeverity::Critical,
1531        };
1532
1533        assert_eq!(region.x, 10);
1534        assert_eq!(region.y, 20);
1535        assert_eq!(region.width, 30);
1536        assert_eq!(region.height, 40);
1537        assert_eq!(region.size, 150);
1538        assert_eq!(region.severity, GapSeverity::Critical);
1539
1540        // Test Debug and Clone
1541        let debug = format!("{:?}", region);
1542        assert!(debug.contains("DemoGapRegion"));
1543
1544        let cloned = region.clone();
1545        assert_eq!(cloned.size, region.size);
1546    }
1547
1548    // =========================================================================
1549    // Section 16: GpuPixelBuffer Additional Tests (Coverage Boost)
1550    // =========================================================================
1551
1552    #[test]
1553    fn h0_buffer_08_gpu_device_name() {
1554        // GPU device name returns None when gpu feature is disabled
1555        let name = GpuPixelBuffer::gpu_device_name();
1556        // Just verify it doesn't panic - result depends on feature flags
1557        let _ = format!("{:?}", name);
1558    }
1559
1560    #[test]
1561    fn h0_buffer_09_is_using_gpu() {
1562        let buffer = GpuPixelBuffer::new(100, 100, 42);
1563        let using_gpu = buffer.is_using_gpu();
1564        // Without gpu feature, should be false
1565        #[cfg(not(feature = "gpu"))]
1566        assert!(!using_gpu);
1567        // Just verify it returns a valid bool
1568        let _ = format!("{}", using_gpu);
1569    }
1570
1571    #[test]
1572    fn h0_buffer_10_gpu_available() {
1573        let available = GpuPixelBuffer::gpu_available();
1574        // Without gpu feature, should be false
1575        #[cfg(not(feature = "gpu"))]
1576        assert!(!available);
1577        let _ = format!("{}", available);
1578    }
1579
1580    #[test]
1581    fn h0_buffer_11_custom_dimensions() {
1582        let buffer = GpuPixelBuffer::new(123, 456, 789);
1583        assert_eq!(buffer.width, 123);
1584        assert_eq!(buffer.height, 456);
1585        assert_eq!(buffer.seed, 789);
1586        assert_eq!(buffer.frame, 0);
1587        assert_eq!(buffer.total_pixels(), 123 * 456);
1588    }
1589
1590    // =========================================================================
1591    // Section 17: CoverageStats Additional Tests (Coverage Boost)
1592    // =========================================================================
1593
1594    #[test]
1595    fn h0_stats_07_max_gap_size_no_gaps() {
1596        let mut buffer = GpuPixelBuffer::new(10, 10, 42);
1597        // Fill all pixels
1598        for pixel in &mut buffer.pixels {
1599            *pixel = 1.0;
1600        }
1601
1602        let stats = buffer.coverage_stats();
1603        assert_eq!(stats.max_gap_size(), 0);
1604        assert!(stats.gaps.is_empty());
1605    }
1606
1607    #[test]
1608    fn h0_stats_08_meets_threshold_boundary() {
1609        let mut buffer = GpuPixelBuffer::new(100, 100, 42);
1610        // Fill exactly 80% of pixels
1611        for i in 0..8000 {
1612            buffer.pixels[i] = 1.0;
1613        }
1614
1615        let stats = buffer.coverage_stats();
1616        assert!(stats.meets_threshold(0.80));
1617        assert!(!stats.meets_threshold(0.81));
1618    }
1619
1620    #[test]
1621    fn h0_stats_09_coverage_stats_debug_clone() {
1622        let buffer = GpuPixelBuffer::new(10, 10, 42);
1623        let stats = buffer.coverage_stats();
1624
1625        let debug = format!("{:?}", stats);
1626        assert!(debug.contains("CoverageStats"));
1627
1628        let cloned = stats.clone();
1629        assert_eq!(cloned.covered, stats.covered);
1630        assert_eq!(cloned.total, stats.total);
1631    }
1632
1633    // =========================================================================
1634    // Section 18: WasmPixelDemo Additional Tests (Coverage Boost)
1635    // =========================================================================
1636
1637    #[test]
1638    fn h0_demo_lifecycle_05_hd_1080p() {
1639        let demo = WasmPixelDemo::hd_1080p();
1640        assert_eq!(demo.buffer.width, 1920);
1641        assert_eq!(demo.buffer.height, 1080);
1642        assert!(!demo.is_complete());
1643    }
1644
1645    #[test]
1646    fn h0_demo_lifecycle_06_elapsed() {
1647        let demo = WasmPixelDemo::new(WasmDemoConfig::test_small());
1648        std::thread::sleep(std::time::Duration::from_millis(10));
1649        let elapsed = demo.elapsed();
1650        assert!(elapsed.as_millis() >= 10);
1651    }
1652
1653    #[test]
1654    fn h0_demo_lifecycle_07_stats() {
1655        let mut demo = WasmPixelDemo::new(WasmDemoConfig::test_small());
1656        demo.tick();
1657
1658        let stats = demo.stats();
1659        assert!(stats.total > 0);
1660        // After one tick, coverage is tracked (may be 0 or more)
1661        assert!(stats.covered <= stats.total);
1662    }
1663
1664    #[test]
1665    fn h0_demo_lifecycle_08_tick_when_complete() {
1666        let config = WasmDemoConfig {
1667            width: 10,
1668            height: 10,
1669            fill_probability: 1.0, // Fill everything
1670            target_coverage: 0.01, // Very low target
1671            ..Default::default()
1672        };
1673        let mut demo = WasmPixelDemo::new(config);
1674
1675        demo.tick(); // This should complete
1676        assert!(demo.is_complete());
1677
1678        let frame_before = demo.frame_count();
1679        demo.tick(); // Should return early
1680        let frame_after = demo.frame_count();
1681
1682        // Frame count should NOT increase when complete
1683        assert_eq!(frame_before, frame_after);
1684    }
1685
1686    #[test]
1687    fn h0_demo_lifecycle_09_debug() {
1688        let demo = WasmPixelDemo::new(WasmDemoConfig::test_small());
1689        let debug = format!("{:?}", demo);
1690        assert!(debug.contains("WasmPixelDemo"));
1691    }
1692
1693    // =========================================================================
1694    // Section 19: Wilson CI Additional Tests (Coverage Boost)
1695    // =========================================================================
1696
1697    #[test]
1698    fn h0_wilson_01_confidence_90() {
1699        let ci = wilson_confidence_interval(50, 100, 0.90);
1700        assert!(ci.level == 0.90 || (ci.level - 0.90).abs() < 0.01);
1701        // 90% CI should be narrower than 95% CI
1702        let ci_95 = wilson_confidence_interval(50, 100, 0.95);
1703        assert!((ci.upper - ci.lower) < (ci_95.upper - ci_95.lower));
1704    }
1705
1706    #[test]
1707    fn h0_wilson_02_confidence_99() {
1708        let ci = wilson_confidence_interval(50, 100, 0.99);
1709        assert!(ci.level == 0.99 || (ci.level - 0.99).abs() < 0.01);
1710        // 99% CI should be wider than 95% CI
1711        let ci_95 = wilson_confidence_interval(50, 100, 0.95);
1712        assert!((ci.upper - ci.lower) > (ci_95.upper - ci_95.lower));
1713    }
1714
1715    #[test]
1716    fn h0_wilson_03_confidence_other() {
1717        // Non-standard confidence level defaults to 1.96 z-score
1718        let ci = wilson_confidence_interval(50, 100, 0.85);
1719        // Should still produce valid bounds
1720        assert!(ci.lower >= 0.0);
1721        assert!(ci.upper <= 1.0);
1722        assert!(ci.lower <= ci.upper);
1723    }
1724
1725    // =========================================================================
1726    // Section 20: Flood Fill Edge Cases (Coverage Boost)
1727    // =========================================================================
1728
1729    #[test]
1730    fn h0_flood_01_single_pixel_gap() {
1731        let mut buffer = GpuPixelBuffer::new(5, 5, 42);
1732        // Fill all but center pixel
1733        for (idx, pixel) in buffer.pixels.iter_mut().enumerate() {
1734            if idx != 12 {
1735                // Center of 5x5
1736                *pixel = 1.0;
1737            }
1738        }
1739
1740        let gaps = buffer.find_gaps();
1741        assert_eq!(gaps.len(), 1);
1742        assert_eq!(gaps[0].size, 1);
1743        assert_eq!(gaps[0].severity, GapSeverity::Info);
1744    }
1745
1746    #[test]
1747    fn h0_flood_02_corner_gaps() {
1748        let mut buffer = GpuPixelBuffer::new(10, 10, 42);
1749        // Fill center, leave corners empty
1750        for y in 0..10 {
1751            for x in 0..10 {
1752                let idx = y * 10 + x;
1753                if (2..8).contains(&x) && (2..8).contains(&y) {
1754                    buffer.pixels[idx] = 1.0;
1755                }
1756            }
1757        }
1758
1759        let gaps = buffer.find_gaps();
1760        // Should have one connected gap around the border
1761        assert!(!gaps.is_empty());
1762    }
1763
1764    #[test]
1765    fn h0_flood_03_multiple_isolated_gaps() {
1766        let mut buffer = GpuPixelBuffer::new(10, 10, 42);
1767        // Fill everything
1768        for pixel in &mut buffer.pixels {
1769            *pixel = 1.0;
1770        }
1771        // Create 4 isolated single-pixel gaps
1772        buffer.pixels[0] = 0.0; // Top-left
1773        buffer.pixels[9] = 0.0; // Top-right
1774        buffer.pixels[90] = 0.0; // Bottom-left
1775        buffer.pixels[99] = 0.0; // Bottom-right
1776
1777        let gaps = buffer.find_gaps();
1778        assert_eq!(gaps.len(), 4);
1779        for gap in &gaps {
1780            assert_eq!(gap.size, 1);
1781            assert_eq!(gap.severity, GapSeverity::Info);
1782        }
1783    }
1784
1785    // =========================================================================
1786    // Section 21: Downsample Edge Cases (Coverage Boost)
1787    // =========================================================================
1788
1789    #[test]
1790    fn h0_downsample_04_exact_multiple() {
1791        let buffer = GpuPixelBuffer::new(100, 100, 42);
1792        let downsampled = buffer.downsample(10, 10);
1793        // Each cell averages 100 source pixels (10x10)
1794        assert_eq!(downsampled.len(), 100);
1795    }
1796
1797    #[test]
1798    fn h0_downsample_05_non_multiple() {
1799        let buffer = GpuPixelBuffer::new(100, 100, 42);
1800        let downsampled = buffer.downsample(7, 7);
1801        assert_eq!(downsampled.len(), 49);
1802    }
1803
1804    #[test]
1805    fn h0_downsample_06_larger_than_source() {
1806        // When terminal is larger than source, scale factors become 0
1807        // which means count stays 0 and result is all zeros
1808        let buffer = GpuPixelBuffer::new(10, 10, 42);
1809        let downsampled = buffer.downsample(100, 100);
1810        assert_eq!(downsampled.len(), 10000);
1811    }
1812
1813    // =========================================================================
1814    // Section 22: RNG Edge Cases (Coverage Boost)
1815    // =========================================================================
1816
1817    #[test]
1818    fn h0_rng_09_hash_pixel_edge_values() {
1819        // Test with extreme values
1820        let hash_max = PcgRng::hash_pixel(u32::MAX, u32::MAX, u32::MAX);
1821        let hash_zero = PcgRng::hash_pixel(0, 0, 0);
1822
1823        // Hashes should be different
1824        assert_ne!(hash_max, hash_zero);
1825
1826        // Should produce valid u32 values (not panic)
1827        let _ = hash_max.to_string();
1828        let _ = hash_zero.to_string();
1829    }
1830
1831    #[test]
1832    fn h0_rng_10_should_fill_edge_probability() {
1833        // Test probability at boundary values
1834        let near_zero = 0.000001;
1835        let near_one = 0.999999;
1836
1837        // At near-zero probability, very few should fill
1838        let mut fill_count = 0;
1839        for idx in 0..1000 {
1840            if PcgRng::should_fill(42, idx, 1, near_zero) {
1841                fill_count += 1;
1842            }
1843        }
1844        assert!(
1845            fill_count < 10,
1846            "Near-zero probability filled {} pixels",
1847            fill_count
1848        );
1849
1850        // At near-one probability, almost all should fill
1851        let mut fill_count = 0;
1852        for idx in 0..1000 {
1853            if PcgRng::should_fill(42, idx, 1, near_one) {
1854                fill_count += 1;
1855            }
1856        }
1857        assert!(
1858            fill_count > 990,
1859            "Near-one probability filled {} pixels",
1860            fill_count
1861        );
1862    }
1863
1864    // =========================================================================
1865    // Section 23: Fill to Coverage Edge Cases (Coverage Boost)
1866    // =========================================================================
1867
1868    #[test]
1869    fn h0_fill_01_max_frames_reached() {
1870        let mut buffer = GpuPixelBuffer::new(100, 100, 42);
1871        // Very low probability, won't reach 100% in 5 frames
1872        buffer.fill_to_coverage(1.0, 0.001, 5);
1873        // Should have run exactly 5 frames
1874        assert_eq!(buffer.frame, 5);
1875        // Coverage should be less than 100%
1876        assert!(buffer.coverage_percentage() < 1.0);
1877    }
1878
1879    #[test]
1880    fn h0_fill_02_early_termination() {
1881        let mut buffer = GpuPixelBuffer::new(10, 10, 42);
1882        // High probability, should reach 50% quickly
1883        buffer.fill_to_coverage(0.5, 1.0, 1000);
1884        // Should not have run all 1000 frames
1885        assert!(buffer.frame < 1000);
1886        // Should have at least 50% coverage
1887        assert!(buffer.coverage_percentage() >= 0.5);
1888    }
1889
1890    // =========================================================================
1891    // Section 24: Config Clone and Debug (Coverage Boost)
1892    // =========================================================================
1893
1894    #[test]
1895    fn h0_config_12_clone_debug() {
1896        let config = WasmDemoConfig::default();
1897        let cloned = config.clone();
1898
1899        assert_eq!(cloned.width, config.width);
1900        assert_eq!(cloned.height, config.height);
1901        assert_eq!(cloned.seed, config.seed);
1902
1903        let debug = format!("{:?}", config);
1904        assert!(debug.contains("WasmDemoConfig"));
1905    }
1906
1907    // =========================================================================
1908    // Section 25: PcgRng Clone and Debug (Coverage Boost)
1909    // =========================================================================
1910
1911    #[test]
1912    fn h0_rng_11_clone_debug() {
1913        let rng = PcgRng::new(42);
1914        let cloned = rng.clone();
1915
1916        let debug = format!("{:?}", rng);
1917        assert!(debug.contains("PcgRng"));
1918
1919        // Cloned RNG should produce same values
1920        let mut rng1 = rng;
1921        let mut rng2 = cloned;
1922        assert_eq!(rng1.next_u32(), rng2.next_u32());
1923    }
1924
1925    // =========================================================================
1926    // Section 26: GpuPixelBuffer Clone and Debug (Coverage Boost)
1927    // =========================================================================
1928
1929    #[test]
1930    fn h0_buffer_12_clone_debug() {
1931        let buffer = GpuPixelBuffer::new(10, 10, 42);
1932        let cloned = buffer.clone();
1933
1934        assert_eq!(cloned.width, buffer.width);
1935        assert_eq!(cloned.height, buffer.height);
1936        assert_eq!(cloned.pixels.len(), buffer.pixels.len());
1937
1938        let debug = format!("{:?}", buffer);
1939        assert!(debug.contains("GpuPixelBuffer"));
1940    }
1941}