dotmax/
grid.rs

1//! Core [`BrailleGrid`] data structure for terminal braille rendering.
2//!
3//! This module provides the central [`BrailleGrid`] type and [`Color`] struct
4//! for high-resolution terminal graphics using Unicode braille characters.
5//!
6//! # Overview
7//!
8//! Each terminal cell displays a braille character (U+2800-U+28FF) representing
9//! a 2×4 dot matrix. This gives 4× the resolution of ASCII art.
10//!
11//! # Examples
12//!
13//! ```
14//! use dotmax::BrailleGrid;
15//!
16//! // Create 80×24 grid (= 160×96 dot resolution)
17//! let mut grid = BrailleGrid::new(80, 24).unwrap();
18//!
19//! // Set individual dots
20//! grid.set_dot(0, 0).unwrap();  // Top-left
21//! grid.set_dot(159, 95).unwrap(); // Bottom-right
22//!
23//! // Get the grid dimensions
24//! let (width, height) = grid.dimensions();
25//! assert_eq!(width, 80);
26//! assert_eq!(height, 24);
27//! ```
28//!
29//! # Extraction Note
30//!
31//! This module was extracted from the [crabmusic](https://github.com/newjordan/crabmusic) project.
32//! See ADR 0005 (Copy-Refactor-Test strategy) for details.
33
34// Import error types from error module
35use crate::error::DotmaxError;
36
37// Tracing for structured logging (Story 2.7)
38use tracing::{debug, error, info, instrument};
39
40/// Maximum grid dimensions to prevent OOM attacks (NFR-S2)
41const MAX_GRID_WIDTH: usize = 10_000;
42const MAX_GRID_HEIGHT: usize = 10_000;
43
44// ============================================================================
45// Color struct - Extracted from crabmusic/src/visualization/mod.rs
46// ============================================================================
47
48/// RGB color representation for braille cells
49///
50/// Extracted from crabmusic. Story 2.6 will implement full color rendering.
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
52pub struct Color {
53    /// Red component (0-255)
54    pub r: u8,
55    /// Green component (0-255)
56    pub g: u8,
57    /// Blue component (0-255)
58    pub b: u8,
59}
60
61impl Color {
62    /// Create a new RGB color
63    ///
64    /// Extracted from `crabmusic::Color::new()`
65    #[must_use]
66    pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
67        Self { r, g, b }
68    }
69
70    /// Create black color (0, 0, 0)
71    #[must_use]
72    pub const fn black() -> Self {
73        Self { r: 0, g: 0, b: 0 }
74    }
75
76    /// Create white color (255, 255, 255)
77    #[must_use]
78    pub const fn white() -> Self {
79        Self {
80            r: 255,
81            g: 255,
82            b: 255,
83        }
84    }
85}
86
87// ============================================================================
88// BrailleDot enum - Extracted from crabmusic/src/visualization/braille.rs:16-28
89// ============================================================================
90
91/// Braille dot positions
92///
93/// Extracted from crabmusic. Maps dot positions to bit patterns for Unicode braille.
94///
95/// Dot positions in a Braille character:
96///   1 4
97///   2 5
98///   3 6
99///   7 8
100#[repr(u8)]
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub enum BrailleDot {
103    /// Top-left dot (position 1)
104    Dot1 = 0b0000_0001,
105    /// Middle-left dot (position 2)
106    Dot2 = 0b0000_0010,
107    /// Lower-left dot (position 3)
108    Dot3 = 0b0000_0100,
109    /// Top-right dot (position 4)
110    Dot4 = 0b0000_1000,
111    /// Middle-right dot (position 5)
112    Dot5 = 0b0001_0000,
113    /// Lower-right dot (position 6)
114    Dot6 = 0b0010_0000,
115    /// Bottom-left dot (position 7)
116    Dot7 = 0b0100_0000,
117    /// Bottom-right dot (position 8)
118    Dot8 = 0b1000_0000,
119}
120
121// ============================================================================
122// dots_to_char - Extracted from crabmusic/src/visualization/braille.rs:52-56
123// ============================================================================
124
125/// Convert dot pattern to Braille Unicode character
126///
127/// Extracted from crabmusic. Story 2.2 will integrate this into the rendering pipeline.
128///
129/// # Arguments
130/// * `dots` - Bit pattern where each bit represents a dot (1 = filled)
131///
132/// # Returns
133/// Unicode Braille character
134///
135/// # Examples
136///
137/// ```
138/// use dotmax::grid::dots_to_char;
139///
140/// // Empty pattern
141/// assert_eq!(dots_to_char(0b00000000), '⠀');
142///
143/// // All dots filled
144/// assert_eq!(dots_to_char(0b11111111), '⣿');
145/// ```
146#[inline]
147#[must_use]
148pub fn dots_to_char(dots: u8) -> char {
149    // Braille patterns start at U+2800
150    // SAFETY: crabmusic uses unwrap_or here; we keep the same logic
151    // since 0x2800 + (0..=255) is always valid Unicode
152    char::from_u32(0x2800 + u32::from(dots)).unwrap_or('⠀')
153}
154
155// ============================================================================
156// BrailleGrid - Extracted from crabmusic/src/visualization/braille.rs:73-369
157// ============================================================================
158
159/// High-resolution grid using Braille characters
160///
161/// **Extracted from crabmusic** - Battle-tested rendering engine.
162///
163/// Each terminal cell contains a 2×4 dot pattern (8 dots total), giving us
164/// high-resolution graphics in any terminal that supports Unicode braille.
165///
166/// ## Architecture (Preserved from crabmusic)
167///
168/// - **`patterns: Vec<u8>`** - Flat array, each u8 is a bitfield (8 bits = 8 dots)
169/// - **Dot coordinates**: (`dot_x`, `dot_y`) in pixel space (width*2 × height*4)
170/// - **Cell coordinates**: (`cell_x`, `cell_y`) in terminal space (width × height)
171///
172/// ## Dot Indexing (Unicode Braille Standard)
173///
174/// ```text
175/// Braille cell (8 dots):
176/// 1 4    (Dot1=0x01, Dot4=0x08)
177/// 2 5    (Dot2=0x02, Dot5=0x10)
178/// 3 6    (Dot3=0x04, Dot6=0x20)
179/// 7 8    (Dot7=0x40, Dot8=0x80)
180/// ```
181///
182/// # Example
183///
184/// ```
185/// use dotmax::BrailleGrid;
186///
187/// let mut grid = BrailleGrid::new(40, 20).unwrap();
188/// // Grid is 40×20 cells = 80×80 dot resolution
189/// grid.set_dot(0, 0); // Top-left dot
190/// grid.set_dot(1, 0); // Top-right dot of first cell
191/// ```
192#[derive(Debug, Clone)]
193pub struct BrailleGrid {
194    /// Width in terminal cells
195    width: usize,
196    /// Height in terminal cells
197    height: usize,
198    /// Dot patterns for each cell (binary on/off)
199    ///
200    /// **Preserved from crabmusic**: `Vec<u8>` bitfield representation
201    /// Each `u8` represents one terminal cell with 8 dots
202    patterns: Vec<u8>,
203    /// Optional colors for each cell
204    ///
205    /// **Preserved from crabmusic**: `Vec<Option<Color>>`
206    /// Story 2.6 will implement color rendering
207    colors: Vec<Option<Color>>,
208    /// Optional text characters for density rendering (Story 4.4)
209    ///
210    /// When set, these characters override braille dot patterns for rendering.
211    /// This enables character-density based rendering (ASCII art style) as an
212    /// alternative to binary braille dot rendering.
213    ///
214    /// `None` = use braille dots (default), `Some(char)` = render this character
215    characters: Vec<Option<char>>,
216}
217
218impl BrailleGrid {
219    /// Create a new Braille grid
220    ///
221    /// **Extracted from `crabmusic::BrailleGrid::new()`** with added validation.
222    ///
223    /// # Arguments
224    /// * `width` - Width in terminal cells (must be > 0 and <= `MAX_GRID_WIDTH`)
225    /// * `height` - Height in terminal cells (must be > 0 and <= `MAX_GRID_HEIGHT`)
226    ///
227    /// # Returns
228    /// * `Ok(BrailleGrid)` if dimensions are valid
229    /// * `Err(DotmaxError::InvalidDimensions)` if width/height is 0 or exceeds max
230    ///
231    /// # Errors
232    /// Returns `InvalidDimensions` if width or height is 0 or exceeds max allowed dimensions.
233    ///
234    /// # Crabmusic Change
235    /// Original crabmusic code never validated dimensions.
236    /// Dotmax adds validation for security (NFR-S2).
237    #[instrument]
238    pub fn new(width: usize, height: usize) -> Result<Self, DotmaxError> {
239        // Validate dimensions (NEW - not in crabmusic)
240        if width == 0 || height == 0 {
241            error!(
242                width = width,
243                height = height,
244                "Invalid grid dimensions: width or height is zero"
245            );
246            return Err(DotmaxError::InvalidDimensions { width, height });
247        }
248
249        if width > MAX_GRID_WIDTH || height > MAX_GRID_HEIGHT {
250            error!(
251                width = width,
252                height = height,
253                max_width = MAX_GRID_WIDTH,
254                max_height = MAX_GRID_HEIGHT,
255                "Invalid grid dimensions: exceeds maximum allowed size"
256            );
257            return Err(DotmaxError::InvalidDimensions { width, height });
258        }
259
260        // Allocate grid (PRESERVED from crabmusic)
261        let size = width * height;
262        info!(
263            width = width,
264            height = height,
265            total_cells = size,
266            "Creating BrailleGrid"
267        );
268        Ok(Self {
269            width,
270            height,
271            patterns: vec![0; size],
272            colors: vec![None; size],
273            characters: vec![None; size], // Story 4.4: character buffer for density rendering
274        })
275    }
276
277    /// Get width in terminal cells
278    ///
279    /// **Extracted from crabmusic** (lines 104-106)
280    #[must_use]
281    pub const fn width(&self) -> usize {
282        self.width
283    }
284
285    /// Get height in terminal cells
286    ///
287    /// **Extracted from crabmusic** (lines 108-111)
288    #[must_use]
289    pub const fn height(&self) -> usize {
290        self.height
291    }
292
293    /// Get width in dots (2× terminal width)
294    ///
295    /// **Extracted from crabmusic** (lines 113-116)
296    #[must_use]
297    pub const fn dot_width(&self) -> usize {
298        self.width * 2
299    }
300
301    /// Get height in dots (4× terminal height)
302    ///
303    /// **Extracted from crabmusic** (lines 118-121)
304    #[must_use]
305    pub const fn dot_height(&self) -> usize {
306        self.height * 4
307    }
308
309    /// Get the dimensions of the grid (dotmax addition for AC #7)
310    ///
311    /// **NEW** - Not in crabmusic. Added to satisfy AC #7 requirement.
312    ///
313    /// # Returns
314    /// A tuple of (width, height) in terminal cells
315    #[must_use]
316    pub const fn dimensions(&self) -> (usize, usize) {
317        (self.width, self.height)
318    }
319
320    /// Clear all dots
321    ///
322    /// **Extracted from crabmusic** (lines 124-127) with minor adaptation
323    #[instrument(skip(self))]
324    pub fn clear(&mut self) {
325        debug!(
326            width = self.width,
327            height = self.height,
328            "Clearing all dots in grid"
329        );
330        self.patterns.fill(0);
331        self.colors.fill(None);
332    }
333
334    /// Set a single dot at the specified position
335    ///
336    /// **Extracted from crabmusic** (lines 144-172) with added error handling.
337    ///
338    /// **CRITICAL**: This uses PIXEL coordinates (`dot_x`, `dot_y`), not cell coordinates.
339    /// The grid is width*2 × height*4 dots.
340    ///
341    /// # Arguments
342    /// * `dot_x` - X position in dots (0 to width*2-1)
343    /// * `dot_y` - Y position in dots (0 to height*4-1)
344    ///
345    /// # Crabmusic Change
346    /// Original crabmusic silently ignored out-of-bounds coordinates.
347    /// Dotmax returns an error for explicit bounds checking (zero panics policy).
348    ///
349    /// # Errors
350    /// Returns `OutOfBounds` if dot coordinates exceed grid dimensions.
351    pub fn set_dot(&mut self, dot_x: usize, dot_y: usize) -> Result<(), DotmaxError> {
352        // Bounds check (MODIFIED from crabmusic - return error instead of silent ignore)
353        if dot_x >= self.dot_width() || dot_y >= self.dot_height() {
354            error!(
355                dot_x = dot_x,
356                dot_y = dot_y,
357                dot_width = self.dot_width(),
358                dot_height = self.dot_height(),
359                "Out of bounds dot access: ({}, {}) in grid of size ({}, {})",
360                dot_x,
361                dot_y,
362                self.dot_width(),
363                self.dot_height()
364            );
365            return Err(DotmaxError::OutOfBounds {
366                x: dot_x,
367                y: dot_y,
368                width: self.dot_width(),
369                height: self.dot_height(),
370            });
371        }
372
373        // Convert dot coordinates to cell coordinates (PRESERVED from crabmusic)
374        let cell_x = dot_x / 2;
375        let cell_y = dot_y / 4;
376        let cell_index = cell_y * self.width + cell_x;
377
378        // Determine which dot within the cell (0-7) (PRESERVED from crabmusic)
379        let local_x = dot_x % 2;
380        let local_y = dot_y % 4;
381
382        // Map to Braille dot position (PRESERVED from crabmusic, lines 159-169)
383        let dot_bit = match (local_x, local_y) {
384            (0, 0) => BrailleDot::Dot1 as u8,
385            (0, 1) => BrailleDot::Dot2 as u8,
386            (0, 2) => BrailleDot::Dot3 as u8,
387            (0, 3) => BrailleDot::Dot7 as u8,
388            (1, 0) => BrailleDot::Dot4 as u8,
389            (1, 1) => BrailleDot::Dot5 as u8,
390            (1, 2) => BrailleDot::Dot6 as u8,
391            (1, 3) => BrailleDot::Dot8 as u8,
392            _ => unreachable!(),
393        };
394
395        // Set the dot (PRESERVED from crabmusic, line 171)
396        self.patterns[cell_index] |= dot_bit;
397        Ok(())
398    }
399
400    /// Get an individual dot value
401    ///
402    /// **NEW** - Not in crabmusic. Added to match AC #4 requirement.
403    ///
404    /// # Arguments
405    /// * `dot_x` - X position in dots (0 to width*2-1)
406    /// * `dot_y` - Y position in dots (0 to height*4-1)
407    /// * `dot_index` - Dot position 0-7 in the cell
408    ///
409    /// # Returns
410    /// * `Ok(bool)` - The dot value (true = enabled, false = disabled)
411    /// * `Err(DotmaxError::OutOfBounds)` if coordinates exceed grid dimensions
412    /// * `Err(DotmaxError::InvalidDotIndex)` if `dot_index` > 7
413    ///
414    /// # Errors
415    /// Returns `OutOfBounds` if dot coordinates exceed grid dimensions, or `InvalidDotIndex` if dot index > 7.
416    pub fn get_dot(&self, x: usize, y: usize, dot_index: u8) -> Result<bool, DotmaxError> {
417        // Validate cell bounds
418        if x >= self.width || y >= self.height {
419            return Err(DotmaxError::OutOfBounds {
420                x,
421                y,
422                width: self.width,
423                height: self.height,
424            });
425        }
426
427        // Validate dot index
428        if dot_index > 7 {
429            return Err(DotmaxError::InvalidDotIndex { index: dot_index });
430        }
431
432        // Calculate cell index
433        let cell_index = y * self.width + x;
434        let pattern = self.patterns[cell_index];
435
436        // Check if dot is set using bit mask
437        let dot_bit = 1u8 << dot_index;
438        Ok((pattern & dot_bit) != 0)
439    }
440
441    /// Clear a rectangular region of the grid
442    ///
443    /// **NEW** - Not in crabmusic. Added to satisfy AC #6 requirement.
444    ///
445    /// # Arguments
446    /// * `x` - Starting column in cells (0-indexed)
447    /// * `y` - Starting row in cells (0-indexed)
448    /// * `width` - Width of region to clear in cells
449    /// * `height` - Height of region to clear in cells
450    ///
451    /// # Returns
452    /// * `Ok(())` if region was cleared successfully
453    /// * `Err(DotmaxError::OutOfBounds)` if region extends beyond grid bounds
454    ///
455    /// # Errors
456    /// Returns `OutOfBounds` if the specified region extends beyond grid dimensions.
457    pub fn clear_region(
458        &mut self,
459        x: usize,
460        y: usize,
461        width: usize,
462        height: usize,
463    ) -> Result<(), DotmaxError> {
464        // Validate bounds - check if region fits within grid
465        let end_x = x.saturating_add(width);
466        let end_y = y.saturating_add(height);
467
468        if end_x > self.width || end_y > self.height {
469            return Err(DotmaxError::OutOfBounds {
470                x: end_x.saturating_sub(1),
471                y: end_y.saturating_sub(1),
472                width: self.width,
473                height: self.height,
474            });
475        }
476
477        // Clear the specified region
478        for row_idx in y..end_y {
479            for col_idx in x..end_x {
480                let cell_index = row_idx * self.width + col_idx;
481                self.patterns[cell_index] = 0;
482                self.colors[cell_index] = None;
483            }
484        }
485
486        Ok(())
487    }
488
489    /// Get the Braille character at a cell position
490    ///
491    /// **Extracted from crabmusic** (lines 338-347)
492    ///
493    /// # Arguments
494    /// * `cell_x` - X position in cells
495    /// * `cell_y` - Y position in cells
496    ///
497    /// # Returns
498    /// Character at the cell position:
499    /// - If a text character is set (Story 4.4 density rendering), returns that character
500    /// - Otherwise, returns braille character representing the dot pattern
501    #[must_use]
502    pub fn get_char(&self, cell_x: usize, cell_y: usize) -> char {
503        if cell_x >= self.width || cell_y >= self.height {
504            return '⠀';
505        }
506
507        let index = cell_y * self.width + cell_x;
508
509        // Story 4.4: Prioritize text characters for density rendering
510        if let Some(ch) = self.characters[index] {
511            return ch;
512        }
513
514        // Default: Convert dot pattern to braille character
515        dots_to_char(self.patterns[index])
516    }
517
518    /// Get the color at a cell position
519    ///
520    /// **Extracted from crabmusic** (lines 350-357)
521    #[must_use]
522    pub fn get_color(&self, cell_x: usize, cell_y: usize) -> Option<Color> {
523        if cell_x >= self.width || cell_y >= self.height {
524            return None;
525        }
526
527        let index = cell_y * self.width + cell_x;
528        self.colors[index]
529    }
530
531    /// Check if a cell has any dots set
532    ///
533    /// **Extracted from crabmusic** (lines 360-368)
534    #[must_use]
535    pub fn is_empty(&self, cell_x: usize, cell_y: usize) -> bool {
536        if cell_x >= self.width || cell_y >= self.height {
537            return true;
538        }
539
540        let index = cell_y * self.width + cell_x;
541        self.patterns[index] == 0
542    }
543
544    // ========================================================================
545    // Story 6.4: Raw Pattern Access for Animation Serialization
546    // ========================================================================
547
548    /// Get raw access to the pattern buffer for serialization.
549    ///
550    /// **Story 6.4** - Enables efficient animation frame serialization.
551    ///
552    /// Returns a slice of the internal pattern buffer, where each byte
553    /// represents one braille cell's dot pattern (8 bits = 8 dots).
554    ///
555    /// # Returns
556    /// A byte slice of length `width * height` containing dot patterns.
557    ///
558    /// # Examples
559    /// ```
560    /// use dotmax::BrailleGrid;
561    ///
562    /// let mut grid = BrailleGrid::new(10, 5).unwrap();
563    /// grid.set_dot(0, 0).unwrap(); // Set top-left dot
564    ///
565    /// let patterns = grid.get_raw_patterns();
566    /// assert_eq!(patterns.len(), 50); // 10 * 5 cells
567    /// assert_eq!(patterns[0], 0b0000_0001); // First cell has dot 1 set
568    /// ```
569    #[must_use]
570    pub fn get_raw_patterns(&self) -> &[u8] {
571        &self.patterns
572    }
573
574    /// Set raw pattern buffer from serialized data.
575    ///
576    /// **Story 6.4** - Enables efficient animation frame deserialization.
577    ///
578    /// Copies data into the internal pattern buffer. The data slice length
579    /// must match `width * height`. If the length doesn't match, excess data
580    /// is truncated or missing data is left unchanged.
581    ///
582    /// # Arguments
583    /// * `data` - Raw pattern bytes to copy into the grid
584    ///
585    /// # Examples
586    /// ```
587    /// use dotmax::BrailleGrid;
588    ///
589    /// let mut grid = BrailleGrid::new(10, 5).unwrap();
590    /// let mut patterns = vec![0u8; 50];
591    /// patterns[0] = 0b0000_0001; // Set dot 1 in first cell
592    ///
593    /// grid.set_raw_patterns(&patterns);
594    /// assert!(grid.get_char(0, 0) == '⠁'); // First cell shows dot 1
595    /// ```
596    pub fn set_raw_patterns(&mut self, data: &[u8]) {
597        let copy_len = data.len().min(self.patterns.len());
598        self.patterns[..copy_len].copy_from_slice(&data[..copy_len]);
599    }
600
601    // ========================================================================
602    // Story 2.2: Unicode Braille Character Conversion
603    // ========================================================================
604
605    /// Convert entire grid to 2D array of Unicode braille characters
606    ///
607    /// **Story 2.2** - Batch conversion for rendering pipeline.
608    ///
609    /// This method converts the entire grid from dot patterns to Unicode braille
610    /// characters, producing a 2D array that matches the grid dimensions.
611    ///
612    /// Uses the proven `dots_to_char()` function extracted from crabmusic
613    /// (lines 53-56) which applies the Unicode Braille standard formula:
614    /// `U+2800 + bitfield`
615    ///
616    /// # Returns
617    /// A 2D vector of Unicode braille characters, where `result[y][x]` corresponds
618    /// to cell `(x, y)` in the grid.
619    ///
620    /// # Examples
621    ///
622    /// ```
623    /// use dotmax::BrailleGrid;
624    ///
625    /// let mut grid = BrailleGrid::new(5, 5).unwrap();
626    /// grid.set_dot(0, 0).unwrap(); // Top-left dot of cell (0,0)
627    ///
628    /// let chars = grid.to_unicode_grid();
629    /// assert_eq!(chars.len(), 5); // 5 rows
630    /// assert_eq!(chars[0].len(), 5); // 5 columns
631    /// assert_eq!(chars[0][0], '⠁'); // Cell (0,0) has dot 1 set
632    /// ```
633    ///
634    /// # Performance
635    /// Time complexity: O(width × height) - processes each cell once
636    /// Allocates: `Vec<Vec<char>>` with dimensions matching grid size
637    #[must_use]
638    pub fn to_unicode_grid(&self) -> Vec<Vec<char>> {
639        let mut result = Vec::with_capacity(self.height);
640
641        for y in 0..self.height {
642            let mut row = Vec::with_capacity(self.width);
643            for x in 0..self.width {
644                let index = y * self.width + x;
645                // Use extracted crabmusic conversion function
646                row.push(dots_to_char(self.patterns[index]));
647            }
648            result.push(row);
649        }
650
651        result
652    }
653
654    /// Convert single cell at (x, y) to Unicode braille character
655    ///
656    /// **Story 2.2** - Single-cell conversion with bounds validation.
657    ///
658    /// Returns the Unicode braille character for a specific cell, or an error
659    /// if coordinates are out of bounds.
660    ///
661    /// # Arguments
662    /// * `x` - X position in cells (0 to width-1)
663    /// * `y` - Y position in cells (0 to height-1)
664    ///
665    /// # Returns
666    /// * `Ok(char)` - Unicode braille character (U+2800 to U+28FF)
667    /// * `Err(DotmaxError::OutOfBounds)` - If coordinates exceed grid dimensions
668    ///
669    /// # Examples
670    ///
671    /// ```
672    /// use dotmax::BrailleGrid;
673    ///
674    /// let mut grid = BrailleGrid::new(10, 10).unwrap();
675    /// grid.set_dot(2, 4).unwrap(); // Set a dot in cell (1,1)
676    ///
677    /// let ch = grid.cell_to_braille_char(1, 1).unwrap();
678    /// assert!(ch >= '\u{2800}' && ch <= '\u{28FF}'); // Valid braille range
679    /// ```
680    ///
681    /// # Errors
682    /// Returns `OutOfBounds` if x >= width or y >= height.
683    pub fn cell_to_braille_char(&self, x: usize, y: usize) -> Result<char, DotmaxError> {
684        // Validate bounds
685        if x >= self.width || y >= self.height {
686            return Err(DotmaxError::OutOfBounds {
687                x,
688                y,
689                width: self.width,
690                height: self.height,
691            });
692        }
693
694        // Convert cell pattern to Unicode
695        let index = y * self.width + x;
696        Ok(dots_to_char(self.patterns[index]))
697    }
698
699    /// Resize the grid to new dimensions
700    ///
701    /// **NEW for Story 2.5** - Not in crabmusic. Enables terminal resize handling.
702    ///
703    /// # Arguments
704    /// * `new_width` - New width in braille cells
705    /// * `new_height` - New height in braille cells
706    ///
707    /// # Behavior
708    /// - **Grow**: New cells initialized to empty (pattern=0, color=None)
709    /// - **Shrink**: Existing dots outside new bounds are truncated
710    /// - **Preserve**: Dots within overlap region are preserved
711    /// - **Colors**: Color buffer resizes in sync with patterns
712    ///
713    /// # Errors
714    /// Returns `DotmaxError::InvalidDimensions` if:
715    /// - `new_width` or `new_height` is 0
716    /// - `new_width` or `new_height` exceeds `MAX_GRID_WIDTH`/`MAX_GRID_HEIGHT` (10,000)
717    ///
718    /// # Examples
719    /// ```
720    /// use dotmax::BrailleGrid;
721    ///
722    /// let mut grid = BrailleGrid::new(10, 10)?;
723    /// grid.set_dot(0, 0)?; // Set top-left dot
724    ///
725    /// // Resize to larger dimensions
726    /// grid.resize(20, 20)?;
727    /// assert_eq!(grid.dimensions(), (20, 20));
728    ///
729    /// // Resize to smaller dimensions
730    /// grid.resize(5, 5)?;
731    /// assert_eq!(grid.dimensions(), (5, 5));
732    /// # Ok::<(), dotmax::DotmaxError>(())
733    /// ```
734    #[instrument(skip(self))]
735    pub fn resize(&mut self, new_width: usize, new_height: usize) -> Result<(), DotmaxError> {
736        debug!(
737            old_width = self.width,
738            old_height = self.height,
739            new_width = new_width,
740            new_height = new_height,
741            "Resizing BrailleGrid"
742        );
743
744        // Validation (same logic as new())
745        if new_width == 0 || new_height == 0 {
746            error!(
747                new_width = new_width,
748                new_height = new_height,
749                "Invalid resize dimensions: width or height is zero"
750            );
751            return Err(DotmaxError::InvalidDimensions {
752                width: new_width,
753                height: new_height,
754            });
755        }
756        if new_width > MAX_GRID_WIDTH || new_height > MAX_GRID_HEIGHT {
757            error!(
758                new_width = new_width,
759                new_height = new_height,
760                max_width = MAX_GRID_WIDTH,
761                max_height = MAX_GRID_HEIGHT,
762                "Invalid resize dimensions: exceeds maximum allowed size"
763            );
764            return Err(DotmaxError::InvalidDimensions {
765                width: new_width,
766                height: new_height,
767            });
768        }
769
770        // Create new storage
771        let new_size = new_width * new_height;
772        let mut new_patterns = vec![0; new_size];
773        let mut new_colors = vec![None; new_size];
774
775        // Copy existing data (preserve overlap region)
776        let copy_width = self.width.min(new_width);
777        let copy_height = self.height.min(new_height);
778
779        for y in 0..copy_height {
780            for x in 0..copy_width {
781                let old_index = y * self.width + x;
782                let new_index = y * new_width + x;
783                new_patterns[new_index] = self.patterns[old_index];
784                new_colors[new_index] = self.colors[old_index];
785            }
786        }
787
788        // Update grid state
789        self.width = new_width;
790        self.height = new_height;
791        self.patterns = new_patterns;
792        self.colors = new_colors;
793
794        Ok(())
795    }
796
797    // ========================================================================
798    // Story 2.6: Color Support for Braille Cells
799    // ========================================================================
800
801    /// Enable color support by allocating color buffer
802    ///
803    /// **Story 2.6** - Allocates per-cell color storage.
804    ///
805    /// Note: In the current implementation, the color buffer is always allocated
806    /// during `BrailleGrid::new()`, so this method is a no-op for compatibility
807    /// with the AC specification. It ensures the color buffer exists.
808    ///
809    /// # Examples
810    /// ```
811    /// use dotmax::BrailleGrid;
812    ///
813    /// let mut grid = BrailleGrid::new(10, 10).unwrap();
814    /// grid.enable_color_support(); // Ensures color support is enabled
815    /// ```
816    #[instrument(skip(self))]
817    pub fn enable_color_support(&mut self) {
818        debug!(
819            width = self.width,
820            height = self.height,
821            "Enabling color support (already enabled in current implementation)"
822        );
823        // Color buffer is already allocated in new(), so this is a no-op
824        // This method exists for API compatibility with AC 2.6.3
825        //
826        // If we change to Option<Vec<Option<Color>>> in future, this would be:
827        // if self.colors.is_none() {
828        //     self.colors = Some(vec![None; self.width * self.height]);
829        // }
830    }
831
832    /// Assign RGB color to cell at (x, y)
833    ///
834    /// **Story 2.6** - Per-cell color assignment with bounds validation.
835    ///
836    /// Sets the color for a specific cell. The color will be applied when
837    /// rendering via `TerminalRenderer`.
838    ///
839    /// # Arguments
840    /// * `x` - X position in cells (0 to width-1)
841    /// * `y` - Y position in cells (0 to height-1)
842    /// * `color` - RGB color to assign
843    ///
844    /// # Returns
845    /// * `Ok(())` if color was assigned successfully
846    /// * `Err(DotmaxError::OutOfBounds)` if coordinates exceed grid dimensions
847    ///
848    /// # Examples
849    /// ```
850    /// use dotmax::{BrailleGrid, Color};
851    ///
852    /// let mut grid = BrailleGrid::new(10, 10).unwrap();
853    /// grid.enable_color_support();
854    ///
855    /// // Set cell (5, 5) to red
856    /// grid.set_cell_color(5, 5, Color::rgb(255, 0, 0)).unwrap();
857    ///
858    /// // Verify color was set
859    /// assert_eq!(grid.get_color(5, 5), Some(Color::rgb(255, 0, 0)));
860    /// ```
861    ///
862    /// # Errors
863    /// Returns `OutOfBounds` if x >= width or y >= height.
864    pub fn set_cell_color(&mut self, x: usize, y: usize, color: Color) -> Result<(), DotmaxError> {
865        // Validate bounds
866        if x >= self.width || y >= self.height {
867            error!(
868                x = x,
869                y = y,
870                width = self.width,
871                height = self.height,
872                "Out of bounds color assignment: ({}, {}) in grid of size ({}, {})",
873                x,
874                y,
875                self.width,
876                self.height
877            );
878            return Err(DotmaxError::OutOfBounds {
879                x,
880                y,
881                width: self.width,
882                height: self.height,
883            });
884        }
885
886        // Set color
887        let index = y * self.width + x;
888        self.colors[index] = Some(color);
889        Ok(())
890    }
891
892    /// Reset all colors to None (monochrome)
893    ///
894    /// **Story 2.6** - Clear color buffer without deallocating.
895    ///
896    /// Resets all cell colors to `None` while keeping the color buffer
897    /// allocated. This is useful for switching back to monochrome rendering
898    /// without disabling color support entirely.
899    ///
900    /// # Examples
901    /// ```
902    /// use dotmax::{BrailleGrid, Color};
903    ///
904    /// let mut grid = BrailleGrid::new(10, 10).unwrap();
905    /// grid.enable_color_support();
906    ///
907    /// // Set some colors
908    /// grid.set_cell_color(5, 5, Color::rgb(255, 0, 0)).unwrap();
909    /// grid.set_cell_color(7, 7, Color::rgb(0, 255, 0)).unwrap();
910    ///
911    /// // Clear all colors
912    /// grid.clear_colors();
913    ///
914    /// // All colors are now None
915    /// assert_eq!(grid.get_color(5, 5), None);
916    /// assert_eq!(grid.get_color(7, 7), None);
917    /// ```
918    pub fn clear_colors(&mut self) {
919        self.colors.fill(None);
920    }
921
922    /// Set a text character at a cell position (Story 4.4)
923    ///
924    /// **Story 4.4** - Character density-based rendering support.
925    ///
926    /// Sets a text character at the specified cell position. When a character is set,
927    /// it overrides the braille dot pattern for that cell during rendering. This enables
928    /// ASCII-art style density rendering as an alternative to binary braille dots.
929    ///
930    /// # Arguments
931    ///
932    /// * `x` - X position in cells (0-indexed)
933    /// * `y` - Y position in cells (0-indexed)
934    /// * `character` - The character to set at this position
935    ///
936    /// # Returns
937    ///
938    /// * `Ok(())` if character was set successfully
939    /// * `Err(DotmaxError::OutOfBounds)` if coordinates exceed grid dimensions
940    ///
941    /// # Examples
942    ///
943    /// ```
944    /// use dotmax::BrailleGrid;
945    ///
946    /// let mut grid = BrailleGrid::new(10, 10).unwrap();
947    ///
948    /// // Set character at cell (5, 5)
949    /// grid.set_char(5, 5, '@').unwrap();
950    ///
951    /// // Verify character was set
952    /// assert_eq!(grid.get_char(5, 5), '@');
953    /// ```
954    ///
955    /// # Errors
956    ///
957    /// Returns `OutOfBounds` if x >= width or y >= height.
958    pub fn set_char(&mut self, x: usize, y: usize, character: char) -> Result<(), DotmaxError> {
959        // Validate bounds
960        if x >= self.width || y >= self.height {
961            error!(
962                x = x,
963                y = y,
964                width = self.width,
965                height = self.height,
966                "Out of bounds character assignment: ({}, {}) in grid of size ({}, {})",
967                x,
968                y,
969                self.width,
970                self.height
971            );
972            return Err(DotmaxError::OutOfBounds {
973                x,
974                y,
975                width: self.width,
976                height: self.height,
977            });
978        }
979
980        // Set character
981        let index = y * self.width + x;
982        self.characters[index] = Some(character);
983        Ok(())
984    }
985
986    /// Clear all text characters (Story 4.4)
987    ///
988    /// **Story 4.4** - Reset character buffer for density rendering.
989    ///
990    /// Resets all cell characters to `None`, restoring default braille dot rendering
991    /// mode. This is useful for switching back from density rendering to braille dots
992    /// without creating a new grid.
993    ///
994    /// # Examples
995    ///
996    /// ```
997    /// use dotmax::BrailleGrid;
998    ///
999    /// let mut grid = BrailleGrid::new(10, 10).unwrap();
1000    ///
1001    /// // Set some characters
1002    /// grid.set_char(5, 5, '@').unwrap();
1003    /// grid.set_char(7, 7, '#').unwrap();
1004    ///
1005    /// // Clear all characters
1006    /// grid.clear_characters();
1007    ///
1008    /// // All characters are now None (braille mode restored)
1009    /// assert_eq!(grid.get_char(5, 5), '⠀'); // Empty braille pattern
1010    /// ```
1011    pub fn clear_characters(&mut self) {
1012        self.characters.fill(None);
1013    }
1014
1015    // ========================================================================
1016    // Story 5.5: Apply Color Scheme to Intensity Buffer
1017    // ========================================================================
1018
1019    /// Apply a color scheme to a flattened intensity buffer.
1020    ///
1021    /// **Story 5.5** - Convenience method for colorizing intensity data.
1022    ///
1023    /// Maps a 1D intensity buffer to colors using the provided color scheme,
1024    /// populating the grid's color buffer. The intensity buffer must be in
1025    /// row-major order and match the grid dimensions (`width × height`).
1026    ///
1027    /// This method directly modifies the grid's internal color buffer for
1028    /// maximum performance, avoiding intermediate allocations.
1029    ///
1030    /// # Arguments
1031    ///
1032    /// * `intensities` - Flattened 1D intensity buffer (row-major order)
1033    /// * `scheme` - Color scheme for intensity-to-color mapping
1034    ///
1035    /// # Returns
1036    ///
1037    /// * `Ok(())` if colors were applied successfully
1038    /// * `Err(DotmaxError::BufferSizeMismatch)` if buffer length doesn't match grid size
1039    ///
1040    /// # Intensity Handling
1041    ///
1042    /// - Values in range 0.0-1.0 are mapped normally
1043    /// - Values outside range are clamped (consistent with `ColorScheme::sample`)
1044    /// - NaN values are treated as 0.0
1045    /// - Infinity values are clamped to 0.0 or 1.0
1046    ///
1047    /// # Examples
1048    ///
1049    /// ```
1050    /// use dotmax::{BrailleGrid, ColorScheme};
1051    ///
1052    /// let mut grid = BrailleGrid::new(4, 3).unwrap();
1053    ///
1054    /// // Create intensity buffer (4×3 = 12 cells)
1055    /// let intensities: Vec<f32> = (0..12)
1056    ///     .map(|i| i as f32 / 11.0)
1057    ///     .collect();
1058    ///
1059    /// // Apply heat map scheme
1060    /// let scheme = ColorScheme::heat_map();
1061    /// grid.apply_color_scheme(&intensities, &scheme).unwrap();
1062    ///
1063    /// // First cell is black (intensity 0.0)
1064    /// let c0 = grid.get_color(0, 0).unwrap();
1065    /// assert_eq!(c0.r, 0);
1066    ///
1067    /// // Last cell is white (intensity 1.0)
1068    /// let c11 = grid.get_color(3, 2).unwrap();
1069    /// assert_eq!(c11.r, 255);
1070    /// ```
1071    ///
1072    /// # Integration with Epic 3 Image Pipeline
1073    ///
1074    /// ```no_run
1075    /// use dotmax::{BrailleGrid, ColorScheme};
1076    /// # // Note: This requires the image feature
1077    /// # // Load image -> Grayscale -> Intensity buffer -> Color scheme
1078    /// ```
1079    ///
1080    /// # Performance
1081    ///
1082    /// Target: <10ms for 80×24 grid (1,920 cells)
1083    ///
1084    /// # Errors
1085    ///
1086    /// Returns [`DotmaxError::BufferSizeMismatch`] if `intensities.len() != width × height`.
1087    pub fn apply_color_scheme(
1088        &mut self,
1089        intensities: &[f32],
1090        scheme: &crate::color::schemes::ColorScheme,
1091    ) -> Result<(), DotmaxError> {
1092        let expected_len = self.width * self.height;
1093
1094        // Validate buffer size
1095        if intensities.len() != expected_len {
1096            return Err(DotmaxError::BufferSizeMismatch {
1097                expected: expected_len,
1098                actual: intensities.len(),
1099            });
1100        }
1101
1102        // Apply colors directly to internal buffer
1103        for (index, &intensity) in intensities.iter().enumerate() {
1104            // Handle special float values and clamp
1105            let normalized = if intensity.is_nan() {
1106                0.0
1107            } else if intensity.is_infinite() {
1108                if intensity.is_sign_positive() {
1109                    1.0
1110                } else {
1111                    0.0
1112                }
1113            } else {
1114                intensity.clamp(0.0, 1.0)
1115            };
1116
1117            self.colors[index] = Some(scheme.sample(normalized));
1118        }
1119
1120        Ok(())
1121    }
1122}
1123
1124// ============================================================================
1125// STRIPPED from crabmusic - Not in Story 2.1 scope:
1126// ============================================================================
1127// - set_dot_with_color() → Story 2.6 (Color Support)
1128// - draw_line() / draw_line_with_color() → Epic 4 (Drawing Primitives)
1129// - draw_circle() → Epic 4 (Drawing Primitives)
1130// - Anti-aliasing logic → Out of scope (audio-reactive feature)
1131// - FFT/spectrum integration → Audio dependencies (discarded)
1132// ============================================================================
1133
1134#[cfg(test)]
1135mod tests {
1136    use super::*;
1137
1138    // ========================================================================
1139    // Grid Creation Tests (AC #2) - Adapted from crabmusic tests
1140    // ========================================================================
1141
1142    #[test]
1143    fn test_new_valid_dimensions() {
1144        let grid = BrailleGrid::new(80, 24);
1145        assert!(grid.is_ok());
1146        let grid = grid.unwrap();
1147        assert_eq!(grid.dimensions(), (80, 24));
1148        assert_eq!(grid.width(), 80);
1149        assert_eq!(grid.height(), 24);
1150    }
1151
1152    #[test]
1153    fn test_braille_grid_creation() {
1154        // Ported from crabmusic test_braille_grid_creation (line 389)
1155        let grid = BrailleGrid::new(40, 20).unwrap();
1156        assert_eq!(grid.width(), 40);
1157        assert_eq!(grid.height(), 20);
1158        assert_eq!(grid.dot_width(), 80);
1159        assert_eq!(grid.dot_height(), 80);
1160    }
1161
1162    #[test]
1163    fn test_new_minimal_dimensions() {
1164        let grid = BrailleGrid::new(1, 1);
1165        assert!(grid.is_ok());
1166        assert_eq!(grid.unwrap().dimensions(), (1, 1));
1167    }
1168
1169    #[test]
1170    fn test_new_large_dimensions() {
1171        let grid = BrailleGrid::new(200, 50);
1172        assert!(grid.is_ok());
1173        assert_eq!(grid.unwrap().dimensions(), (200, 50));
1174    }
1175
1176    #[test]
1177    fn test_new_max_dimensions() {
1178        let grid = BrailleGrid::new(10_000, 10_000);
1179        assert!(grid.is_ok());
1180    }
1181
1182    #[test]
1183    fn test_new_zero_width_returns_error() {
1184        let result = BrailleGrid::new(0, 24);
1185        assert!(matches!(
1186            result,
1187            Err(DotmaxError::InvalidDimensions {
1188                width: 0,
1189                height: 24
1190            })
1191        ));
1192    }
1193
1194    #[test]
1195    fn test_new_zero_height_returns_error() {
1196        let result = BrailleGrid::new(80, 0);
1197        assert!(matches!(
1198            result,
1199            Err(DotmaxError::InvalidDimensions {
1200                width: 80,
1201                height: 0
1202            })
1203        ));
1204    }
1205
1206    #[test]
1207    fn test_new_both_zero_returns_error() {
1208        let result = BrailleGrid::new(0, 0);
1209        assert!(matches!(
1210            result,
1211            Err(DotmaxError::InvalidDimensions {
1212                width: 0,
1213                height: 0
1214            })
1215        ));
1216    }
1217
1218    #[test]
1219    fn test_new_exceeds_max_width() {
1220        let result = BrailleGrid::new(10_001, 100);
1221        assert!(matches!(result, Err(DotmaxError::InvalidDimensions { .. })));
1222    }
1223
1224    #[test]
1225    fn test_new_exceeds_max_height() {
1226        let result = BrailleGrid::new(100, 10_001);
1227        assert!(matches!(result, Err(DotmaxError::InvalidDimensions { .. })));
1228    }
1229
1230    // ========================================================================
1231    // Dot Manipulation Tests (AC #3, #4, #8) - Adapted from crabmusic
1232    // ========================================================================
1233
1234    #[test]
1235    fn test_set_dot() {
1236        // Ported from crabmusic test_set_dot (line 398)
1237        let mut grid = BrailleGrid::new(10, 10).unwrap();
1238
1239        // Set top-left dot of first cell (dot coordinate 0,0)
1240        grid.set_dot(0, 0).unwrap();
1241        assert_eq!(grid.get_char(0, 0), '⠁');
1242
1243        // Set top-right dot of first cell (dot coordinate 1,0)
1244        grid.set_dot(1, 0).unwrap();
1245        assert_eq!(grid.get_char(0, 0), '⠉');
1246    }
1247
1248    #[test]
1249    fn test_dot_positions() {
1250        // Ported from crabmusic test_dot_positions (line 476)
1251        let mut grid = BrailleGrid::new(10, 10).unwrap();
1252
1253        // Test all 8 dot positions in first cell
1254        grid.clear();
1255        grid.set_dot(0, 0).unwrap(); // Dot 1
1256        assert_eq!(grid.patterns[0], 0b0000_0001);
1257
1258        grid.clear();
1259        grid.set_dot(0, 1).unwrap(); // Dot 2
1260        assert_eq!(grid.patterns[0], 0b0000_0010);
1261
1262        grid.clear();
1263        grid.set_dot(0, 2).unwrap(); // Dot 3
1264        assert_eq!(grid.patterns[0], 0b0000_0100);
1265
1266        grid.clear();
1267        grid.set_dot(0, 3).unwrap(); // Dot 7
1268        assert_eq!(grid.patterns[0], 0b0100_0000);
1269
1270        grid.clear();
1271        grid.set_dot(1, 0).unwrap(); // Dot 4
1272        assert_eq!(grid.patterns[0], 0b0000_1000);
1273
1274        grid.clear();
1275        grid.set_dot(1, 1).unwrap(); // Dot 5
1276        assert_eq!(grid.patterns[0], 0b0001_0000);
1277
1278        grid.clear();
1279        grid.set_dot(1, 2).unwrap(); // Dot 6
1280        assert_eq!(grid.patterns[0], 0b0010_0000);
1281
1282        grid.clear();
1283        grid.set_dot(1, 3).unwrap(); // Dot 8
1284        assert_eq!(grid.patterns[0], 0b1000_0000);
1285    }
1286
1287    #[test]
1288    fn test_get_dot_all_positions() {
1289        let mut grid = BrailleGrid::new(10, 10).unwrap();
1290
1291        // Set all 8 dots in cell (5,5) using set_dot pixel API
1292        for dot_y in 0..4 {
1293            for dot_x in 0..2 {
1294                // Cell (5,5) corresponds to dot range (10-11, 20-23)
1295                grid.set_dot(5 * 2 + dot_x, 5 * 4 + dot_y).unwrap();
1296            }
1297        }
1298
1299        // Verify all 8 dots are set using get_dot
1300        for dot_index in 0..8 {
1301            assert!(
1302                grid.get_dot(5, 5, dot_index).unwrap(),
1303                "Dot {dot_index} should be set"
1304            );
1305        }
1306    }
1307
1308    #[test]
1309    fn test_set_dot_out_of_bounds() {
1310        let mut grid = BrailleGrid::new(10, 10).unwrap();
1311        // Grid is 10×10 cells = 20×40 dots
1312        let result = grid.set_dot(100, 5);
1313        assert!(matches!(result, Err(DotmaxError::OutOfBounds { .. })));
1314    }
1315
1316    #[test]
1317    fn test_get_dot_out_of_bounds() {
1318        let grid = BrailleGrid::new(10, 10).unwrap();
1319        let result = grid.get_dot(100, 100, 0);
1320        assert!(matches!(result, Err(DotmaxError::OutOfBounds { .. })));
1321    }
1322
1323    #[test]
1324    fn test_get_dot_invalid_dot_index() {
1325        let grid = BrailleGrid::new(10, 10).unwrap();
1326        let result = grid.get_dot(5, 5, 8);
1327        assert!(matches!(result, Err(DotmaxError::InvalidDotIndex { .. })));
1328    }
1329
1330    #[test]
1331    fn test_bounds_checking() {
1332        // Ported from crabmusic test_bounds_checking (line 514)
1333        let mut grid = BrailleGrid::new(10, 10).unwrap();
1334
1335        // Should return error, not panic
1336        let result = grid.set_dot(1000, 1000);
1337        assert!(result.is_err());
1338
1339        assert_eq!(grid.get_char(1000, 1000), '⠀');
1340        assert!(grid.is_empty(1000, 1000));
1341    }
1342
1343    // ========================================================================
1344    // Clear Operations Tests (AC #5, #6) - Adapted from crabmusic
1345    // ========================================================================
1346
1347    #[test]
1348    fn test_clear() {
1349        // Ported from crabmusic test_clear (line 411)
1350        let mut grid = BrailleGrid::new(10, 10).unwrap();
1351
1352        grid.set_dot(0, 0).unwrap();
1353        grid.set_dot(5, 5).unwrap();
1354
1355        grid.clear();
1356
1357        assert_eq!(grid.get_char(0, 0), '⠀');
1358        assert!(grid.is_empty(0, 0));
1359    }
1360
1361    #[test]
1362    fn test_clear_empty_grid() {
1363        let mut grid = BrailleGrid::new(5, 5).unwrap();
1364        grid.clear(); // Should not panic on empty grid
1365        assert!(grid.is_empty(0, 0));
1366    }
1367
1368    #[test]
1369    fn test_clear_region_basic() {
1370        let mut grid = BrailleGrid::new(20, 20).unwrap();
1371
1372        // Set dots in various cells
1373        grid.set_dot(5 * 2, 5 * 4).unwrap(); // Cell (5,5)
1374        grid.set_dot(6 * 2, 6 * 4).unwrap(); // Cell (6,6)
1375        grid.set_dot(10 * 2, 10 * 4).unwrap(); // Cell (10,10)
1376
1377        // Clear region (5, 5, 2, 2) - clears cells (5,5), (6,5), (5,6), (6,6)
1378        grid.clear_region(5, 5, 2, 2).unwrap();
1379
1380        // Verify region cleared
1381        assert!(grid.is_empty(5, 5));
1382        assert!(grid.is_empty(6, 6));
1383
1384        // Verify outside region unchanged
1385        assert!(!grid.is_empty(10, 10));
1386    }
1387
1388    #[test]
1389    fn test_clear_region_out_of_bounds() {
1390        let mut grid = BrailleGrid::new(10, 10).unwrap();
1391
1392        // Region extends beyond grid
1393        let result = grid.clear_region(5, 5, 10, 10);
1394        assert!(matches!(result, Err(DotmaxError::OutOfBounds { .. })));
1395    }
1396
1397    #[test]
1398    fn test_clear_region_zero_size() {
1399        let mut grid = BrailleGrid::new(10, 10).unwrap();
1400
1401        // Zero-size region should succeed (clears nothing)
1402        let result = grid.clear_region(5, 5, 0, 0);
1403        assert!(result.is_ok());
1404    }
1405
1406    // ========================================================================
1407    // Dimensions Test (AC #7)
1408    // ========================================================================
1409
1410    #[test]
1411    fn test_dimensions_returns_correct_size() {
1412        let grid1 = BrailleGrid::new(80, 24).unwrap();
1413        assert_eq!(grid1.dimensions(), (80, 24));
1414
1415        let grid2 = BrailleGrid::new(100, 50).unwrap();
1416        assert_eq!(grid2.dimensions(), (100, 50));
1417
1418        let grid3 = BrailleGrid::new(1, 1).unwrap();
1419        assert_eq!(grid3.dimensions(), (1, 1));
1420    }
1421
1422    // ========================================================================
1423    // Unicode Conversion Tests - Ported from crabmusic
1424    // ========================================================================
1425
1426    #[test]
1427    fn test_dots_to_char() {
1428        // Ported from crabmusic test_dots_to_char (line 376)
1429        // Empty pattern
1430        assert_eq!(dots_to_char(0b0000_0000), '⠀');
1431
1432        // All dots
1433        assert_eq!(dots_to_char(0b1111_1111), '⣿');
1434
1435        // Single dots
1436        assert_eq!(dots_to_char(0b0000_0001), '⠁'); // Dot 1
1437        assert_eq!(dots_to_char(0b0000_1000), '⠈'); // Dot 4
1438    }
1439
1440    // ========================================================================
1441    // Color Tests (AC #1 - Color struct)
1442    // ========================================================================
1443
1444    #[test]
1445    fn test_color_rgb() {
1446        let color = Color::rgb(255, 128, 64);
1447        assert_eq!(color.r, 255);
1448        assert_eq!(color.g, 128);
1449        assert_eq!(color.b, 64);
1450    }
1451
1452    #[test]
1453    fn test_color_black() {
1454        let color = Color::black();
1455        assert_eq!(color.r, 0);
1456        assert_eq!(color.g, 0);
1457        assert_eq!(color.b, 0);
1458    }
1459
1460    #[test]
1461    fn test_color_white() {
1462        let color = Color::white();
1463        assert_eq!(color.r, 255);
1464        assert_eq!(color.g, 255);
1465        assert_eq!(color.b, 255);
1466    }
1467
1468    #[test]
1469    fn test_color_equality() {
1470        let color1 = Color::rgb(100, 150, 200);
1471        let color2 = Color::rgb(100, 150, 200);
1472        let color3 = Color::rgb(100, 150, 201);
1473
1474        assert_eq!(color1, color2);
1475        assert_ne!(color1, color3);
1476    }
1477
1478    // ========================================================================
1479    // Story 2.2: Unicode Braille Character Conversion Tests (AC #4, #5)
1480    // ========================================================================
1481
1482    /// Test all 256 braille patterns (exhaustive coverage for AC #4)
1483    ///
1484    /// This test verifies correctness of the Unicode Braille conversion
1485    /// for ALL possible 8-dot patterns (2^8 = 256 combinations).
1486    ///
1487    /// Tests the bitfield formula: U+2800 + (dots[0]<<0 | dots[1]<<1 | ... | dots[7]<<7)
1488    #[test]
1489    fn test_all_256_braille_patterns() {
1490        for bitfield in 0u8..=255 {
1491            let ch = dots_to_char(bitfield);
1492            let expected = char::from_u32(0x2800 + u32::from(bitfield)).unwrap();
1493            assert_eq!(
1494                ch, expected,
1495                "Failed for bitfield {bitfield:08b} (decimal {bitfield})"
1496            );
1497        }
1498    }
1499
1500    /// Test empty cell → U+2800 (AC #5)
1501    #[test]
1502    fn test_empty_cell_is_u2800() {
1503        let ch = dots_to_char(0b0000_0000);
1504        assert_eq!(ch, '\u{2800}', "Empty cell should be blank braille U+2800");
1505    }
1506
1507    /// Test full cell → U+28FF (AC #5)
1508    #[test]
1509    fn test_full_cell_is_u28ff() {
1510        let ch = dots_to_char(0b1111_1111);
1511        assert_eq!(ch, '\u{28FF}', "Full cell should be U+28FF (all dots)");
1512    }
1513
1514    /// Test specific patterns match Unicode standard (AC #5)
1515    #[test]
1516    fn test_specific_braille_patterns() {
1517        // Pattern: dots [true, false, true, false, false, false, false, false]
1518        // Bitfield: 0b00000101 = 5
1519        // Expected: U+2805 = '⠅'
1520        assert_eq!(dots_to_char(0b0000_0101), '\u{2805}');
1521
1522        // Pattern: dots [true, true, true, true, false, false, false, false]
1523        // Bitfield: 0b00001111 = 15
1524        // Expected: U+280F = '⠏'
1525        assert_eq!(dots_to_char(0b0000_1111), '\u{280F}');
1526
1527        // Single dot patterns
1528        assert_eq!(dots_to_char(0b0000_0001), '\u{2801}'); // Dot 1 only
1529        assert_eq!(dots_to_char(0b0000_1000), '\u{2808}'); // Dot 4 only
1530        assert_eq!(dots_to_char(0b0100_0000), '\u{2840}'); // Dot 7 only
1531        assert_eq!(dots_to_char(0b1000_0000), '\u{2880}'); // Dot 8 only
1532    }
1533
1534    /// Test `to_unicode_grid()` dimensions (AC #2)
1535    #[test]
1536    fn test_to_unicode_grid_dimensions() {
1537        // 5×5 grid → verify result is 5×5 Vec<Vec<char>>
1538        let grid = BrailleGrid::new(5, 5).unwrap();
1539        let chars = grid.to_unicode_grid();
1540
1541        assert_eq!(chars.len(), 5, "Grid should have 5 rows");
1542        for row in &chars {
1543            assert_eq!(row.len(), 5, "Each row should have 5 columns");
1544        }
1545    }
1546
1547    /// Test `to_unicode_grid()` with various grid sizes
1548    #[test]
1549    fn test_to_unicode_grid_various_sizes() {
1550        // 80×24 (standard terminal)
1551        let grid1 = BrailleGrid::new(80, 24).unwrap();
1552        let chars1 = grid1.to_unicode_grid();
1553        assert_eq!(chars1.len(), 24);
1554        assert_eq!(chars1[0].len(), 80);
1555
1556        // 1×1 (minimal)
1557        let grid2 = BrailleGrid::new(1, 1).unwrap();
1558        let chars2 = grid2.to_unicode_grid();
1559        assert_eq!(chars2.len(), 1);
1560        assert_eq!(chars2[0].len(), 1);
1561
1562        // 100×50 (large terminal)
1563        let grid3 = BrailleGrid::new(100, 50).unwrap();
1564        let chars3 = grid3.to_unicode_grid();
1565        assert_eq!(chars3.len(), 50);
1566        assert_eq!(chars3[0].len(), 100);
1567    }
1568
1569    /// Test `to_unicode_grid()` with empty grid (all blank braille)
1570    #[test]
1571    fn test_to_unicode_grid_empty() {
1572        let grid = BrailleGrid::new(3, 3).unwrap();
1573        let chars = grid.to_unicode_grid();
1574
1575        // All cells should be blank braille U+2800
1576        for row in chars {
1577            for ch in row {
1578                assert_eq!(ch, '\u{2800}', "Empty grid should have blank braille");
1579            }
1580        }
1581    }
1582
1583    /// Test `to_unicode_grid()` with dots set
1584    #[test]
1585    fn test_to_unicode_grid_with_dots() {
1586        let mut grid = BrailleGrid::new(5, 5).unwrap();
1587
1588        // Set top-left dot of cell (0,0)
1589        grid.set_dot(0, 0).unwrap();
1590
1591        // Set all dots of cell (2,2)
1592        for dot_y in 0..4 {
1593            for dot_x in 0..2 {
1594                grid.set_dot(2 * 2 + dot_x, 2 * 4 + dot_y).unwrap();
1595            }
1596        }
1597
1598        let chars = grid.to_unicode_grid();
1599
1600        // Cell (0,0) should have dot 1 set → '⠁'
1601        assert_eq!(chars[0][0], '\u{2801}');
1602
1603        // Cell (2,2) should have all dots → '⣿'
1604        assert_eq!(chars[2][2], '\u{28FF}');
1605
1606        // Other cells should be blank
1607        assert_eq!(chars[1][1], '\u{2800}');
1608        assert_eq!(chars[4][4], '\u{2800}');
1609    }
1610
1611    /// Test `cell_to_braille_char()` bounds validation (AC #3)
1612    #[test]
1613    fn test_cell_to_braille_char_out_of_bounds() {
1614        let grid = BrailleGrid::new(10, 10).unwrap();
1615
1616        // Out of bounds → Err(OutOfBounds)
1617        let result1 = grid.cell_to_braille_char(100, 5);
1618        assert!(matches!(result1, Err(DotmaxError::OutOfBounds { .. })));
1619
1620        let result2 = grid.cell_to_braille_char(5, 100);
1621        assert!(matches!(result2, Err(DotmaxError::OutOfBounds { .. })));
1622
1623        let result3 = grid.cell_to_braille_char(10, 10); // Exactly at boundary
1624        assert!(matches!(result3, Err(DotmaxError::OutOfBounds { .. })));
1625    }
1626
1627    /// Test `cell_to_braille_char()` returns correct character
1628    #[test]
1629    fn test_cell_to_braille_char_correct_conversion() {
1630        let mut grid = BrailleGrid::new(10, 10).unwrap();
1631
1632        // Set specific pattern in cell (5,5)
1633        // Set dots to create pattern 0b00001111 (bitfield 15) → '⠏'
1634        grid.set_dot(5 * 2, 5 * 4).unwrap(); // Dot 1
1635        grid.set_dot(5 * 2, 5 * 4 + 1).unwrap(); // Dot 2
1636        grid.set_dot(5 * 2, 5 * 4 + 2).unwrap(); // Dot 3
1637        grid.set_dot(5 * 2 + 1, 5 * 4).unwrap(); // Dot 4
1638
1639        let ch = grid.cell_to_braille_char(5, 5).unwrap();
1640        assert_eq!(ch, '\u{280F}');
1641    }
1642
1643    /// Test `cell_to_braille_char()` for empty cells
1644    #[test]
1645    fn test_cell_to_braille_char_empty_cells() {
1646        let grid = BrailleGrid::new(10, 10).unwrap();
1647
1648        // All cells should start as blank braille
1649        for y in 0..10 {
1650            for x in 0..10 {
1651                let ch = grid.cell_to_braille_char(x, y).unwrap();
1652                assert_eq!(ch, '\u{2800}', "Empty cell ({x}, {y}) should be blank");
1653            }
1654        }
1655    }
1656
1657    /// Test that conversion is correct after clearing
1658    #[test]
1659    fn test_unicode_conversion_after_clear() {
1660        let mut grid = BrailleGrid::new(5, 5).unwrap();
1661
1662        // Set some dots
1663        grid.set_dot(0, 0).unwrap();
1664        grid.set_dot(5, 5).unwrap();
1665
1666        // Verify they're set
1667        assert_ne!(grid.cell_to_braille_char(0, 0).unwrap(), '\u{2800}');
1668
1669        // Clear grid
1670        grid.clear();
1671
1672        // Verify all cells are blank braille
1673        let chars = grid.to_unicode_grid();
1674        for row in chars {
1675            for ch in row {
1676                assert_eq!(ch, '\u{2800}');
1677            }
1678        }
1679    }
1680
1681    /// Test Unicode range validity (all conversions produce valid braille)
1682    #[test]
1683    fn test_unicode_range_validity() {
1684        let grid = BrailleGrid::new(5, 5).unwrap();
1685        let chars = grid.to_unicode_grid();
1686
1687        for row in chars {
1688            for ch in row {
1689                assert!(
1690                    ('\u{2800}'..='\u{28FF}').contains(&ch),
1691                    "Character {ch} is outside braille range U+2800-U+28FF"
1692                );
1693            }
1694        }
1695    }
1696
1697    // ========================================================================
1698    // Story 2.4: Error Context Verification Tests (AC #3)
1699    // ========================================================================
1700
1701    /// Test `InvalidDimensions` error message includes context (AC #3)
1702    #[test]
1703    fn test_invalid_dimensions_error_message_includes_context() {
1704        let result = BrailleGrid::new(0, 10);
1705        match result {
1706            Err(DotmaxError::InvalidDimensions { width, height }) => {
1707                let msg = format!("{}", DotmaxError::InvalidDimensions { width, height });
1708                assert!(msg.contains('0'), "Error message should include width=0");
1709                assert!(msg.contains("10"), "Error message should include height=10");
1710                assert!(
1711                    msg.contains("width") && msg.contains("height"),
1712                    "Error message should label dimensions"
1713                );
1714            }
1715            _ => panic!("Expected InvalidDimensions error"),
1716        }
1717    }
1718
1719    /// Test `OutOfBounds` error message includes all context (AC #3)
1720    #[test]
1721    fn test_out_of_bounds_error_message_includes_all_context() {
1722        let mut grid = BrailleGrid::new(10, 10).unwrap();
1723        let result = grid.set_dot(100, 50);
1724        match result {
1725            Err(DotmaxError::OutOfBounds {
1726                x,
1727                y,
1728                width,
1729                height,
1730            }) => {
1731                let msg = format!(
1732                    "{}",
1733                    DotmaxError::OutOfBounds {
1734                        x,
1735                        y,
1736                        width,
1737                        height
1738                    }
1739                );
1740                assert!(msg.contains("100"), "Error message should include x=100");
1741                assert!(msg.contains("50"), "Error message should include y=50");
1742                // width*2=20 and height*4=40 for dot coordinates
1743                assert!(
1744                    msg.contains("20"),
1745                    "Error message should include dot_width=20"
1746                );
1747                assert!(
1748                    msg.contains("40"),
1749                    "Error message should include dot_height=40"
1750                );
1751            }
1752            _ => panic!("Expected OutOfBounds error"),
1753        }
1754    }
1755
1756    /// Test `InvalidDotIndex` error message includes index (AC #3)
1757    #[test]
1758    fn test_invalid_dot_index_error_message_includes_index() {
1759        let grid = BrailleGrid::new(10, 10).unwrap();
1760        let result = grid.get_dot(5, 5, 10);
1761        match result {
1762            Err(DotmaxError::InvalidDotIndex { index }) => {
1763                let msg = format!("{}", DotmaxError::InvalidDotIndex { index });
1764                assert!(msg.contains("10"), "Error message should include index=10");
1765                assert!(
1766                    msg.contains("0-7"),
1767                    "Error message should specify valid range"
1768                );
1769            }
1770            _ => panic!("Expected InvalidDotIndex error"),
1771        }
1772    }
1773
1774    /// Test exceeding maximum dimensions returns proper error (AC #1, #3)
1775    #[test]
1776    fn test_new_exceeds_both_max_dimensions() {
1777        let result = BrailleGrid::new(20_000, 20_000);
1778        assert!(
1779            matches!(result, Err(DotmaxError::InvalidDimensions { .. })),
1780            "Grid exceeding MAX_GRID_WIDTH and MAX_GRID_HEIGHT should return InvalidDimensions"
1781        );
1782    }
1783
1784    /// Test `set_dot` with invalid dot index returns `InvalidDotIndex` (AC #1)
1785    #[test]
1786    fn test_set_dot_invalid_dot_index_high() {
1787        let grid = BrailleGrid::new(10, 10).unwrap();
1788        // set_dot uses dot coordinates, not dot_index, so we test via get_dot
1789        let result = grid.get_dot(5, 5, 255);
1790        assert!(
1791            matches!(result, Err(DotmaxError::InvalidDotIndex { index: 255 })),
1792            "Dot index 255 should return InvalidDotIndex error"
1793        );
1794    }
1795
1796    // ========================================================================
1797    // Story 2.5: Terminal Resize Event Handling Tests
1798    // ========================================================================
1799
1800    /// Test resize grow updates dimensions (AC #2, #3)
1801    #[test]
1802    fn test_resize_grow_updates_dimensions() {
1803        let mut grid = BrailleGrid::new(10, 10).unwrap();
1804        grid.resize(20, 20).unwrap();
1805        assert_eq!(grid.dimensions(), (20, 20));
1806        assert_eq!(grid.width(), 20);
1807        assert_eq!(grid.height(), 20);
1808    }
1809
1810    /// Test resize grow preserves existing dots (AC #3)
1811    #[test]
1812    fn test_resize_grow_preserves_existing_dots() {
1813        let mut grid = BrailleGrid::new(10, 10).unwrap();
1814        grid.set_dot(5, 5).unwrap(); // Sets bit in cell (2, 1)
1815        grid.set_dot(18, 38).unwrap(); // Sets bit in cell (9, 9)
1816
1817        // Check initial state
1818        let cell_2_1 = 10 + 2; // cell (2, 1) = index 12
1819        let cell_9_9 = 9 * 10 + 9; // cell (9, 9) = index 99
1820        assert_ne!(grid.patterns[cell_2_1], 0, "Cell (2,1) should have dots");
1821        assert_ne!(grid.patterns[cell_9_9], 0, "Cell (9,9) should have dots");
1822
1823        grid.resize(20, 20).unwrap();
1824
1825        // After resize, cells should be at same logical positions
1826        let new_cell_2_1 = 20 + 2; // cell (2, 1) in new grid
1827        let new_cell_9_9 = 9 * 20 + 9; // cell (9, 9) in new grid
1828
1829        // Existing dots should be preserved
1830        assert_ne!(
1831            grid.patterns[new_cell_2_1], 0,
1832            "Cell (2,1) dots should be preserved"
1833        );
1834        assert_ne!(
1835            grid.patterns[new_cell_9_9], 0,
1836            "Cell (9,9) dots should be preserved"
1837        );
1838
1839        // New cells should be empty
1840        let new_cell_15_15 = 15 * 20 + 15;
1841        assert_eq!(
1842            grid.patterns[new_cell_15_15], 0,
1843            "New cells should be empty"
1844        );
1845    }
1846
1847    /// Test resize shrink truncates cleanly (AC #4)
1848    #[test]
1849    fn test_resize_shrink_truncates_cleanly() {
1850        let mut grid = BrailleGrid::new(20, 20).unwrap();
1851        grid.set_dot(5, 5).unwrap(); // Sets bit in cell (2, 1)
1852        grid.set_dot(30, 60).unwrap(); // Sets bit in cell (15, 15) - will be truncated
1853
1854        // Check initial state
1855        let cell_2_1 = 20 + 2;
1856        let cell_15_15 = 15 * 20 + 15;
1857        assert_ne!(grid.patterns[cell_2_1], 0, "Cell (2,1) should have dots");
1858        assert_ne!(
1859            grid.patterns[cell_15_15], 0,
1860            "Cell (15,15) should have dots"
1861        );
1862
1863        grid.resize(10, 10).unwrap();
1864
1865        assert_eq!(grid.dimensions(), (10, 10));
1866
1867        // Preserved dot should still exist
1868        let new_cell_2_1 = 10 + 2;
1869        assert_ne!(
1870            grid.patterns[new_cell_2_1], 0,
1871            "Cell (2,1) dots should be preserved"
1872        );
1873
1874        // Grid is now only 10×10 = 100 cells, so cell (15,15) is truncated
1875        assert_eq!(
1876            grid.patterns.len(),
1877            100,
1878            "Grid should have only 100 cells after resize to 10×10"
1879        );
1880    }
1881
1882    /// Test resize to same dimensions (no-op case)
1883    #[test]
1884    fn test_resize_same_dimensions() {
1885        let mut grid = BrailleGrid::new(10, 10).unwrap();
1886        grid.set_dot(5, 5).unwrap(); // Sets bit in cell (2, 1)
1887
1888        let cell_2_1 = 10 + 2;
1889        let original_pattern = grid.patterns[cell_2_1];
1890        assert_ne!(original_pattern, 0);
1891
1892        grid.resize(10, 10).unwrap();
1893
1894        assert_eq!(grid.dimensions(), (10, 10));
1895        // Pattern should be unchanged
1896        assert_eq!(
1897            grid.patterns[cell_2_1], original_pattern,
1898            "Existing dot pattern should be preserved"
1899        );
1900    }
1901
1902    /// Test resize with colors syncs color buffer (AC #5)
1903    #[test]
1904    fn test_resize_with_colors_syncs_color_buffer() {
1905        let mut grid = BrailleGrid::new(10, 10).unwrap();
1906        // Note: Color support is implicit in BrailleGrid (colors vec always allocated)
1907        // We can test via internal state
1908
1909        grid.resize(20, 20).unwrap();
1910
1911        // Color buffer should have resized to match new dimensions
1912        assert_eq!(
1913            grid.colors.len(),
1914            400,
1915            "Color buffer should have 400 cells for 20×20 grid"
1916        );
1917    }
1918
1919    /// Test resize zero width dimension error (AC #2)
1920    #[test]
1921    fn test_resize_zero_width_error() {
1922        let mut grid = BrailleGrid::new(10, 10).unwrap();
1923        let result = grid.resize(0, 10);
1924        assert!(
1925            matches!(
1926                result,
1927                Err(DotmaxError::InvalidDimensions {
1928                    width: 0,
1929                    height: 10
1930                })
1931            ),
1932            "Resize to width=0 should return InvalidDimensions error"
1933        );
1934        // Grid dimensions should remain unchanged after failed resize
1935        assert_eq!(grid.dimensions(), (10, 10));
1936    }
1937
1938    /// Test resize zero height dimension error (AC #2)
1939    #[test]
1940    fn test_resize_zero_height_error() {
1941        let mut grid = BrailleGrid::new(10, 10).unwrap();
1942        let result = grid.resize(10, 0);
1943        assert!(
1944            matches!(
1945                result,
1946                Err(DotmaxError::InvalidDimensions {
1947                    width: 10,
1948                    height: 0
1949                })
1950            ),
1951            "Resize to height=0 should return InvalidDimensions error"
1952        );
1953        assert_eq!(grid.dimensions(), (10, 10));
1954    }
1955
1956    /// Test resize exceeds max width (AC #2)
1957    #[test]
1958    fn test_resize_exceeds_max_width_error() {
1959        let mut grid = BrailleGrid::new(10, 10).unwrap();
1960        let result = grid.resize(20000, 10);
1961        assert!(
1962            matches!(result, Err(DotmaxError::InvalidDimensions { .. })),
1963            "Resize to width=20000 should return InvalidDimensions error"
1964        );
1965        assert_eq!(grid.dimensions(), (10, 10));
1966    }
1967
1968    /// Test resize exceeds max height (AC #2)
1969    #[test]
1970    fn test_resize_exceeds_max_height_error() {
1971        let mut grid = BrailleGrid::new(10, 10).unwrap();
1972        let result = grid.resize(10, 20000);
1973        assert!(
1974            matches!(result, Err(DotmaxError::InvalidDimensions { .. })),
1975            "Resize to height=20000 should return InvalidDimensions error"
1976        );
1977        assert_eq!(grid.dimensions(), (10, 10));
1978    }
1979
1980    /// Test resize maintains grid invariants (AC #6)
1981    #[test]
1982    fn test_resize_maintains_invariants() {
1983        let mut grid = BrailleGrid::new(10, 10).unwrap();
1984        grid.resize(20, 15).unwrap();
1985
1986        // Verify invariants
1987        assert_eq!(
1988            grid.patterns.len(),
1989            300,
1990            "Patterns buffer should have 300 cells for 20×15 grid"
1991        );
1992        assert_eq!(
1993            grid.colors.len(),
1994            300,
1995            "Colors buffer should have 300 cells for 20×15 grid"
1996        );
1997        assert_eq!(grid.width(), 20);
1998        assert_eq!(grid.height(), 15);
1999    }
2000
2001    /// Test resize from 1×1 to large grid (edge case)
2002    #[test]
2003    fn test_resize_from_tiny_to_large() {
2004        let mut grid = BrailleGrid::new(1, 1).unwrap();
2005        grid.set_dot(0, 0).unwrap();
2006
2007        grid.resize(50, 30).unwrap();
2008
2009        assert_eq!(grid.dimensions(), (50, 30));
2010        assert!(
2011            grid.get_dot(0, 0, 0).unwrap(),
2012            "Single dot should be preserved at (0,0)"
2013        );
2014        assert!(
2015            !grid.get_dot(10, 10, 0).unwrap(),
2016            "New cells should be empty"
2017        );
2018    }
2019
2020    /// Test resize shrink to 1×1 (edge case)
2021    #[test]
2022    fn test_resize_shrink_to_tiny() {
2023        let mut grid = BrailleGrid::new(50, 30).unwrap();
2024        grid.set_dot(0, 0).unwrap();
2025        grid.set_dot(10, 10).unwrap();
2026
2027        grid.resize(1, 1).unwrap();
2028
2029        assert_eq!(grid.dimensions(), (1, 1));
2030        assert!(
2031            grid.get_dot(0, 0, 0).unwrap(),
2032            "Top-left dot should be preserved"
2033        );
2034        // Other dots are now out of bounds
2035        assert!(grid.get_dot(10, 10, 0).is_err());
2036    }
2037
2038    // ========================================================================
2039    // Story 2.6: Color Support Tests (AC #1-#7)
2040    // ========================================================================
2041
2042    /// Test `enable_color_support()` allocates buffer (AC #3)
2043    #[test]
2044    fn test_enable_color_support_allocates_buffer() {
2045        let mut grid = BrailleGrid::new(10, 10).unwrap();
2046        // Color buffer is already allocated in new(), so this is a no-op
2047        // But we verify it exists and has correct size
2048        grid.enable_color_support();
2049
2050        // Verify buffer size matches grid dimensions
2051        assert_eq!(grid.colors.len(), 100); // 10×10 = 100 cells
2052    }
2053
2054    /// Test `set_cell_color()` assigns color (AC #4)
2055    #[test]
2056    fn test_set_cell_color_assigns_color() {
2057        let mut grid = BrailleGrid::new(10, 10).unwrap();
2058        grid.enable_color_support();
2059
2060        let red = Color::rgb(255, 0, 0);
2061        grid.set_cell_color(5, 5, red).unwrap();
2062
2063        assert_eq!(grid.get_color(5, 5), Some(red));
2064    }
2065
2066    /// Test `set_cell_color()` with valid coordinates (AC #4)
2067    #[test]
2068    fn test_set_cell_color_valid_coordinates() {
2069        let mut grid = BrailleGrid::new(10, 10).unwrap();
2070        grid.enable_color_support();
2071
2072        // Test corners and center
2073        let blue = Color::rgb(0, 0, 255);
2074        grid.set_cell_color(0, 0, blue).unwrap();
2075        assert_eq!(grid.get_color(0, 0), Some(blue));
2076
2077        let green = Color::rgb(0, 255, 0);
2078        grid.set_cell_color(9, 9, green).unwrap();
2079        assert_eq!(grid.get_color(9, 9), Some(green));
2080
2081        let yellow = Color::rgb(255, 255, 0);
2082        grid.set_cell_color(5, 5, yellow).unwrap();
2083        assert_eq!(grid.get_color(5, 5), Some(yellow));
2084    }
2085
2086    /// Test `set_cell_color()` out of bounds returns error (AC #4)
2087    #[test]
2088    fn test_set_cell_color_out_of_bounds_error() {
2089        let mut grid = BrailleGrid::new(10, 10).unwrap();
2090        grid.enable_color_support();
2091
2092        let result = grid.set_cell_color(100, 100, Color::black());
2093        assert!(matches!(result, Err(DotmaxError::OutOfBounds { .. })));
2094    }
2095
2096    /// Test `set_cell_color()` out of bounds X (AC #4)
2097    #[test]
2098    fn test_set_cell_color_out_of_bounds_x() {
2099        let mut grid = BrailleGrid::new(10, 10).unwrap();
2100        grid.enable_color_support();
2101
2102        let result = grid.set_cell_color(10, 5, Color::white());
2103        assert!(matches!(result, Err(DotmaxError::OutOfBounds { .. })));
2104    }
2105
2106    /// Test `set_cell_color()` out of bounds Y (AC #4)
2107    #[test]
2108    fn test_set_cell_color_out_of_bounds_y() {
2109        let mut grid = BrailleGrid::new(10, 10).unwrap();
2110        grid.enable_color_support();
2111
2112        let result = grid.set_cell_color(5, 10, Color::white());
2113        assert!(matches!(result, Err(DotmaxError::OutOfBounds { .. })));
2114    }
2115
2116    /// Test `get_color()` returns None when no color set (AC #5)
2117    #[test]
2118    fn test_get_color_none_when_not_set() {
2119        let mut grid = BrailleGrid::new(10, 10).unwrap();
2120        grid.enable_color_support();
2121
2122        // No color set on cell (5, 5)
2123        assert_eq!(grid.get_color(5, 5), None);
2124    }
2125
2126    /// Test `get_color()` returns None for out of bounds (AC #5)
2127    #[test]
2128    fn test_get_color_none_when_out_of_bounds() {
2129        let grid = BrailleGrid::new(10, 10).unwrap();
2130
2131        // Out of bounds returns None (not error)
2132        assert_eq!(grid.get_color(100, 100), None);
2133    }
2134
2135    /// Test `get_color()` returns color after set (AC #5)
2136    #[test]
2137    fn test_get_color_returns_color_after_set() {
2138        let mut grid = BrailleGrid::new(10, 10).unwrap();
2139        grid.enable_color_support();
2140
2141        let magenta = Color::rgb(255, 0, 255);
2142        grid.set_cell_color(7, 7, magenta).unwrap();
2143
2144        assert_eq!(grid.get_color(7, 7), Some(magenta));
2145    }
2146
2147    /// Test `clear_colors()` resets all colors to None (AC #7)
2148    #[test]
2149    fn test_clear_colors_resets_all() {
2150        let mut grid = BrailleGrid::new(10, 10).unwrap();
2151        grid.enable_color_support();
2152
2153        // Set colors on multiple cells
2154        grid.set_cell_color(5, 5, Color::rgb(255, 0, 0)).unwrap();
2155        grid.set_cell_color(7, 7, Color::rgb(0, 255, 0)).unwrap();
2156        grid.set_cell_color(2, 2, Color::rgb(0, 0, 255)).unwrap();
2157
2158        // Verify colors are set
2159        assert!(grid.get_color(5, 5).is_some());
2160        assert!(grid.get_color(7, 7).is_some());
2161        assert!(grid.get_color(2, 2).is_some());
2162
2163        // Clear all colors
2164        grid.clear_colors();
2165
2166        // All colors should be None
2167        assert_eq!(grid.get_color(5, 5), None);
2168        assert_eq!(grid.get_color(7, 7), None);
2169        assert_eq!(grid.get_color(2, 2), None);
2170    }
2171
2172    /// Test `clear_colors()` doesn't affect dots (AC #7)
2173    #[test]
2174    fn test_clear_colors_preserves_dots() {
2175        let mut grid = BrailleGrid::new(10, 10).unwrap();
2176        grid.enable_color_support();
2177
2178        // Set dots and colors
2179        grid.set_dot(10, 20).unwrap();
2180        grid.set_cell_color(5, 5, Color::rgb(255, 0, 0)).unwrap();
2181
2182        // Clear colors
2183        grid.clear_colors();
2184
2185        // Dots should still exist
2186        assert!(!grid.is_empty(5, 5));
2187        // But colors should be None
2188        assert_eq!(grid.get_color(5, 5), None);
2189    }
2190
2191    /// Test Color `PartialEq` works correctly (AC #7)
2192    #[test]
2193    fn test_color_partial_eq() {
2194        let red1 = Color::rgb(255, 0, 0);
2195        let red2 = Color::rgb(255, 0, 0);
2196        let blue = Color::rgb(0, 0, 255);
2197
2198        assert_eq!(red1, red2);
2199        assert_ne!(red1, blue);
2200    }
2201
2202    /// Test colors persist after resize (AC #5, Story 2.5 AC #5)
2203    #[test]
2204    fn test_colors_persist_after_resize_grow() {
2205        let mut grid = BrailleGrid::new(10, 10).unwrap();
2206        grid.enable_color_support();
2207
2208        let purple = Color::rgb(128, 0, 128);
2209        grid.set_cell_color(5, 5, purple).unwrap();
2210
2211        // Resize to larger
2212        grid.resize(20, 20).unwrap();
2213
2214        // Color should be preserved at same logical position
2215        assert_eq!(grid.get_color(5, 5), Some(purple));
2216
2217        // New cells should have no color
2218        assert_eq!(grid.get_color(15, 15), None);
2219    }
2220
2221    /// Test colors truncated correctly after resize shrink (AC #5, Story 2.5 AC #4)
2222    #[test]
2223    fn test_colors_truncated_after_resize_shrink() {
2224        let mut grid = BrailleGrid::new(20, 20).unwrap();
2225        grid.enable_color_support();
2226
2227        let orange = Color::rgb(255, 165, 0);
2228        grid.set_cell_color(5, 5, orange).unwrap();
2229        grid.set_cell_color(15, 15, Color::rgb(0, 255, 255))
2230            .unwrap();
2231
2232        // Resize to smaller
2233        grid.resize(10, 10).unwrap();
2234
2235        // Color within bounds should be preserved
2236        assert_eq!(grid.get_color(5, 5), Some(orange));
2237
2238        // Cell (15, 15) is now out of bounds
2239        assert_eq!(grid.get_color(15, 15), None);
2240    }
2241
2242    /// Test `enable_color_support()` is idempotent (AC #3)
2243    #[test]
2244    fn test_enable_color_support_idempotent() {
2245        let mut grid = BrailleGrid::new(10, 10).unwrap();
2246
2247        grid.enable_color_support();
2248        grid.enable_color_support();
2249        grid.enable_color_support();
2250
2251        // Should not panic or cause issues
2252        assert_eq!(grid.colors.len(), 100);
2253    }
2254
2255    /// Test `set_cell_color()` with all predefined colors (AC #2, #4)
2256    #[test]
2257    fn test_set_cell_color_with_predefined_colors() {
2258        let mut grid = BrailleGrid::new(10, 10).unwrap();
2259        grid.enable_color_support();
2260
2261        // Test black() constructor
2262        grid.set_cell_color(0, 0, Color::black()).unwrap();
2263        assert_eq!(grid.get_color(0, 0), Some(Color::rgb(0, 0, 0)));
2264
2265        // Test white() constructor
2266        grid.set_cell_color(1, 1, Color::white()).unwrap();
2267        assert_eq!(grid.get_color(1, 1), Some(Color::rgb(255, 255, 255)));
2268
2269        // Test rgb() constructor
2270        grid.set_cell_color(2, 2, Color::rgb(128, 64, 32)).unwrap();
2271        assert_eq!(grid.get_color(2, 2), Some(Color::rgb(128, 64, 32)));
2272    }
2273
2274    /// Test `clear()` also clears colors (not just `clear_colors()`)
2275    #[test]
2276    fn test_clear_also_clears_colors() {
2277        let mut grid = BrailleGrid::new(10, 10).unwrap();
2278        grid.enable_color_support();
2279
2280        grid.set_dot(10, 20).unwrap();
2281        grid.set_cell_color(5, 5, Color::rgb(255, 0, 0)).unwrap();
2282
2283        grid.clear();
2284
2285        // Both dots and colors should be cleared
2286        assert!(grid.is_empty(5, 5));
2287        assert_eq!(grid.get_color(5, 5), None);
2288    }
2289
2290    /// Test resize preserves multiple dots in complex pattern (AC #3, #4)
2291    #[test]
2292    fn test_resize_preserves_complex_pattern() {
2293        let mut grid = BrailleGrid::new(15, 15).unwrap();
2294        // Set dots in a diagonal pattern
2295        // dot (0,0) → cell (0,0), dot (2,4) → cell (1,1), dot (4,8) → cell (2,2), etc.
2296        for i in 0..10 {
2297            grid.set_dot(i * 2, i * 4).unwrap();
2298        }
2299
2300        // Store original patterns for cells we'll verify
2301        let mut original_patterns = Vec::new();
2302        for i in 0..10 {
2303            let cell_x = (i * 2) / 2;
2304            let cell_y = (i * 4) / 4;
2305            let cell_index = cell_y * 15 + cell_x;
2306            original_patterns.push((cell_x, cell_y, grid.patterns[cell_index]));
2307        }
2308
2309        // Resize to larger
2310        grid.resize(20, 20).unwrap();
2311
2312        // All original diagonal dots should be preserved
2313        for (cell_x, cell_y, pattern) in &original_patterns {
2314            let new_index = cell_y * 20 + cell_x;
2315            assert_eq!(
2316                grid.patterns[new_index], *pattern,
2317                "Cell ({cell_x}, {cell_y}) pattern should be preserved after grow"
2318            );
2319        }
2320
2321        // Resize to smaller (truncate some dots)
2322        grid.resize(8, 8).unwrap();
2323
2324        // Dots within new bounds should be preserved (cells 0-7 in both dimensions)
2325        for (cell_x, cell_y, pattern) in &original_patterns {
2326            if *cell_x < 8 && *cell_y < 8 {
2327                let new_index = cell_y * 8 + cell_x;
2328                assert_eq!(
2329                    grid.patterns[new_index], *pattern,
2330                    "Cell ({cell_x}, {cell_y}) pattern should be preserved after shrink"
2331                );
2332            }
2333        }
2334    }
2335
2336    // ========================================================================
2337    // Story 2.7: Debug Logging and Tracing Tests (AC #1-#6)
2338    // ========================================================================
2339
2340    /// Test that tracing instrumentation compiles (#[instrument] attributes work)
2341    /// AC 2.7.2: Verify #[instrument] on key functions compiles without type errors
2342    #[test]
2343    fn test_instrumentation_compiles() {
2344        // This test verifies that #[instrument] attributes don't cause compilation errors
2345        // If this test compiles and runs, the instrumentation is correct
2346        let grid = BrailleGrid::new(10, 10);
2347        assert!(grid.is_ok());
2348    }
2349
2350    /// Test logging works when subscriber initialized
2351    /// AC 2.7.6: Tests can enable logging via tracing-subscriber
2352    #[test]
2353    fn test_logging_with_subscriber_initialized() {
2354        // Initialize subscriber for this test
2355        // Using try_init() to handle case where subscriber already initialized by other tests
2356        let _ = tracing_subscriber::fmt()
2357            .with_max_level(tracing::Level::DEBUG)
2358            .with_test_writer()
2359            .try_init();
2360
2361        // Operations should now log (logs will appear in test output with --nocapture)
2362        let mut grid = BrailleGrid::new(10, 10).unwrap();
2363        grid.clear();
2364        grid.resize(20, 20).unwrap();
2365        grid.enable_color_support();
2366
2367        // If this completes without panic, logging infrastructure works
2368        assert_eq!(grid.dimensions(), (20, 20));
2369    }
2370
2371    /// Test logging is silent when subscriber NOT initialized (zero-cost)
2372    /// AC 2.7.5: Library does not initialize subscriber (user controls logging)
2373    #[test]
2374    fn test_logging_silent_by_default() {
2375        // No subscriber initialized
2376        // Operations should complete without logging (zero-cost)
2377        let mut grid = BrailleGrid::new(10, 10).unwrap();
2378        grid.clear();
2379
2380        // This should work silently - no logs appear because no subscriber
2381        assert_eq!(grid.dimensions(), (10, 10));
2382    }
2383
2384    /// Test that hot paths have NO debug logs
2385    /// AC 2.7.4: `set_dot` and `get_dot` do NOT log at debug level
2386    #[test]
2387    fn test_hot_paths_no_debug_logs() {
2388        // set_dot and get_dot should NOT have debug! calls (only trace! if needed)
2389        // This is a code review check - the test verifies they work without performance impact
2390
2391        let mut grid = BrailleGrid::new(100, 100).unwrap();
2392
2393        // Call hot paths many times - should complete quickly
2394        for y in 0..200 {
2395            for x in 0..200 {
2396                // This should be fast - no debug logging overhead
2397                let _ = grid.set_dot(x, y);
2398            }
2399        }
2400
2401        // Verify operations completed
2402        assert_eq!(grid.dimensions(), (100, 100));
2403    }
2404
2405    /// Test error logging includes context
2406    /// AC 2.7.3: error! logs include actionable context (coordinates, dimensions)
2407    #[test]
2408    fn test_error_logging_includes_context() {
2409        let mut grid = BrailleGrid::new(10, 10).unwrap();
2410
2411        // These operations will emit error! logs with context
2412        let result1 = BrailleGrid::new(0, 0);
2413        assert!(result1.is_err());
2414
2415        let result2 = grid.set_dot(1000, 1000);
2416        assert!(result2.is_err());
2417
2418        let result3 = grid.set_cell_color(1000, 1000, Color::black());
2419        assert!(result3.is_err());
2420
2421        // If we reach here, error paths executed correctly
2422        // (Logs will show context if subscriber is initialized)
2423    }
2424
2425    /// Test instrumented functions return correct types
2426    /// AC 2.7.2: Verify #[instrument] doesn't break function signatures
2427    #[test]
2428    fn test_instrumented_functions_correct_types() {
2429        // Test that #[instrument] doesn't change function return types
2430        let result1: Result<BrailleGrid, DotmaxError> = BrailleGrid::new(10, 10);
2431        assert!(result1.is_ok());
2432
2433        let mut grid = result1.unwrap();
2434
2435        let result2: () = grid.clear();
2436        assert_eq!(result2, ());
2437
2438        let result3: Result<(), DotmaxError> = grid.resize(20, 20);
2439        assert!(result3.is_ok());
2440
2441        let result4: () = grid.enable_color_support();
2442        assert_eq!(result4, ());
2443    }
2444
2445    /// Test logging works in complex workflow
2446    /// AC 2.7.6: Verify logging throughout full workflow
2447    #[test]
2448    fn test_logging_in_full_workflow() {
2449        // Initialize subscriber
2450        let _ = tracing_subscriber::fmt()
2451            .with_max_level(tracing::Level::DEBUG)
2452            .with_test_writer()
2453            .try_init();
2454
2455        // Complex workflow that exercises all instrumented paths
2456        let mut grid = BrailleGrid::new(20, 20).unwrap();
2457        grid.enable_color_support();
2458
2459        // Set dots
2460        grid.set_dot(10, 20).unwrap();
2461        grid.set_dot(30, 60).unwrap();
2462
2463        // Set colors
2464        grid.set_cell_color(5, 5, Color::rgb(255, 0, 0)).unwrap();
2465
2466        // Resize
2467        grid.resize(30, 30).unwrap();
2468
2469        // Clear
2470        grid.clear();
2471
2472        // Verify final state
2473        assert_eq!(grid.dimensions(), (30, 30));
2474        assert!(grid.is_empty(0, 0));
2475
2476        // If this completes, logging worked throughout workflow
2477    }
2478
2479    // ========================================================================
2480    // Story 5.5: Apply Color Scheme to Intensity Buffer Tests (AC #3, #6)
2481    // ========================================================================
2482
2483    /// Test `apply_color_scheme()` basic functionality (AC #3)
2484    #[test]
2485    fn test_apply_color_scheme_basic() {
2486        use crate::color::schemes::grayscale;
2487
2488        let mut grid = BrailleGrid::new(4, 3).unwrap();
2489        let intensities: Vec<f32> = (0..12).map(|i| i as f32 / 11.0).collect();
2490        let scheme = grayscale();
2491
2492        let result = grid.apply_color_scheme(&intensities, &scheme);
2493        assert!(result.is_ok());
2494
2495        // First cell (intensity 0.0) -> black
2496        let c0 = grid.get_color(0, 0).unwrap();
2497        assert_eq!(c0, Color::black());
2498
2499        // Last cell (intensity 1.0) -> white
2500        let c_last = grid.get_color(3, 2).unwrap();
2501        assert_eq!(c_last, Color::white());
2502    }
2503
2504    /// Test `apply_color_scheme()` buffer size mismatch (AC #3)
2505    #[test]
2506    fn test_apply_color_scheme_buffer_mismatch() {
2507        use crate::color::schemes::grayscale;
2508
2509        let mut grid = BrailleGrid::new(4, 3).unwrap();
2510        let too_short: Vec<f32> = vec![0.5; 10]; // 10 < 12
2511        let too_long: Vec<f32> = vec![0.5; 15]; // 15 > 12
2512        let scheme = grayscale();
2513
2514        let result = grid.apply_color_scheme(&too_short, &scheme);
2515        assert!(matches!(result, Err(DotmaxError::BufferSizeMismatch { .. })));
2516
2517        let result = grid.apply_color_scheme(&too_long, &scheme);
2518        assert!(matches!(result, Err(DotmaxError::BufferSizeMismatch { .. })));
2519    }
2520
2521    /// Test `apply_color_scheme()` with special float values (AC #4)
2522    #[test]
2523    fn test_apply_color_scheme_special_floats() {
2524        use crate::color::schemes::grayscale;
2525
2526        let mut grid = BrailleGrid::new(5, 1).unwrap();
2527        let intensities = vec![
2528            f32::NAN,
2529            f32::INFINITY,
2530            f32::NEG_INFINITY,
2531            -0.5,
2532            1.5,
2533        ];
2534        let scheme = grayscale();
2535
2536        let result = grid.apply_color_scheme(&intensities, &scheme);
2537        assert!(result.is_ok());
2538
2539        // NaN -> 0.0 -> black
2540        assert_eq!(grid.get_color(0, 0), Some(Color::black()));
2541        // +Infinity -> 1.0 -> white
2542        assert_eq!(grid.get_color(1, 0), Some(Color::white()));
2543        // -Infinity -> 0.0 -> black
2544        assert_eq!(grid.get_color(2, 0), Some(Color::black()));
2545        // -0.5 clamped to 0.0 -> black
2546        assert_eq!(grid.get_color(3, 0), Some(Color::black()));
2547        // 1.5 clamped to 1.0 -> white
2548        assert_eq!(grid.get_color(4, 0), Some(Color::white()));
2549    }
2550
2551    /// Test `apply_color_scheme()` with heat_map scheme (AC #6, #7)
2552    #[test]
2553    fn test_apply_color_scheme_heat_map() {
2554        use crate::color::schemes::heat_map;
2555
2556        let mut grid = BrailleGrid::new(5, 1).unwrap();
2557        let intensities = vec![0.0, 0.25, 0.5, 0.75, 1.0];
2558        let scheme = heat_map();
2559
2560        grid.apply_color_scheme(&intensities, &scheme).unwrap();
2561
2562        // 0.0 -> black (cold)
2563        assert_eq!(grid.get_color(0, 0), Some(Color::black()));
2564        // 1.0 -> white (hot)
2565        assert_eq!(grid.get_color(4, 0), Some(Color::white()));
2566
2567        // Middle values should have color
2568        let mid = grid.get_color(2, 0).unwrap();
2569        assert!(mid.r > 0 || mid.g > 0 || mid.b > 0);
2570    }
2571
2572    /// Test `apply_color_scheme()` with rainbow scheme (AC #6, #7)
2573    #[test]
2574    fn test_apply_color_scheme_rainbow() {
2575        use crate::color::schemes::rainbow;
2576
2577        let mut grid = BrailleGrid::new(3, 1).unwrap();
2578        let intensities = vec![0.0, 0.5, 1.0];
2579        let scheme = rainbow();
2580
2581        grid.apply_color_scheme(&intensities, &scheme).unwrap();
2582
2583        // 0.0 -> red
2584        let red = grid.get_color(0, 0).unwrap();
2585        assert_eq!(red.r, 255);
2586        assert_eq!(red.g, 0);
2587        assert_eq!(red.b, 0);
2588
2589        // 1.0 -> purple-ish
2590        let purple = grid.get_color(2, 0).unwrap();
2591        assert!(purple.r > 200);
2592        assert!(purple.b > 200);
2593    }
2594
2595    /// Test `apply_color_scheme()` result is renderable (AC #5)
2596    #[test]
2597    fn test_apply_color_scheme_colors_renderable() {
2598        use crate::color::schemes::heat_map;
2599
2600        let mut grid = BrailleGrid::new(10, 10).unwrap();
2601
2602        // Set some dots
2603        grid.set_dot(5, 5).unwrap();
2604        grid.set_dot(10, 10).unwrap();
2605
2606        // Apply color scheme
2607        let intensities: Vec<f32> = (0..100)
2608            .map(|i| (i as f32) / 99.0)
2609            .collect();
2610        let scheme = heat_map();
2611        grid.apply_color_scheme(&intensities, &scheme).unwrap();
2612
2613        // All cells should have colors
2614        for y in 0..10 {
2615            for x in 0..10 {
2616                assert!(grid.get_color(x, y).is_some());
2617            }
2618        }
2619
2620        // Dots should still exist
2621        assert!(!grid.is_empty(2, 1)); // dot (5,5) -> cell (2,1)
2622        assert!(!grid.is_empty(5, 2)); // dot (10,10) -> cell (5,2)
2623    }
2624
2625    /// Test `apply_color_scheme()` empty grid (edge case)
2626    #[test]
2627    fn test_apply_color_scheme_single_cell() {
2628        use crate::color::schemes::grayscale;
2629
2630        let mut grid = BrailleGrid::new(1, 1).unwrap();
2631        let intensities = vec![0.5];
2632        let scheme = grayscale();
2633
2634        grid.apply_color_scheme(&intensities, &scheme).unwrap();
2635
2636        let color = grid.get_color(0, 0).unwrap();
2637        // 0.5 gray should be around 127-128
2638        assert!(color.r >= 127 && color.r <= 128);
2639    }
2640}