sorting_race/lib/
interactive.rs

1//! Interactive configuration menu components
2
3use crate::models::{
4    configuration::{ConfigurationState, DistributionType},
5    config::FairnessMode,
6    interactive_mode::{ApplicationMode, ConfigurationField, InteractiveMode},
7};
8use anyhow::Result;
9use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
10use ratatui::{
11    buffer::Buffer,
12    layout::{Alignment, Constraint, Direction, Layout, Rect},
13    style::{Color, Modifier, Style},
14    text::{Line, Span},
15    widgets::{Block, Borders, Clear, List, ListItem, Paragraph, Widget},
16};
17
18// Constants for configuration limits
19const TOTAL_ALGORITHMS: usize = 7;
20const MAX_ARRAY_SIZE: u32 = 10000;
21const MAX_BUDGET: u32 = 1000000;
22const MAX_FLOAT_PARAM: f32 = 100.0;
23const MAX_INPUT_LENGTH: usize = 10;
24
25/// Interactive configuration menu system
26#[derive(Debug, Clone)]
27pub struct InteractiveConfigMenu {
28    /// Current interactive mode state
29    pub interactive_mode: InteractiveMode,
30    /// Current configuration state
31    pub config_state: ConfigurationState,
32    /// Selected array size index for navigation
33    array_size_index: usize,
34    /// Selected distribution index for navigation
35    distribution_index: usize,
36    /// Selected fairness mode index for navigation
37    fairness_mode_index: usize,
38    /// Current parameter value being edited (for numeric inputs)
39    current_parameter_value: Option<String>,
40}
41
42impl InteractiveConfigMenu {
43    /// Create new interactive configuration menu
44    pub fn new() -> Self {
45        Self {
46            interactive_mode: InteractiveMode::new(),
47            config_state: ConfigurationState::new(),
48            array_size_index: 3, // Default to 100 (index 3 in [10, 25, 50, 100, 200, 500, 1000])
49            distribution_index: 0, // Default to Shuffled
50            fairness_mode_index: 2, // Default to WallTime
51            current_parameter_value: None,
52        }
53    }
54
55    /// Get current interactive mode
56    pub fn get_interactive_mode(&self) -> &InteractiveMode {
57        &self.interactive_mode
58    }
59
60    /// Get mutable interactive mode
61    pub fn get_interactive_mode_mut(&mut self) -> &mut InteractiveMode {
62        &mut self.interactive_mode
63    }
64
65    /// Get run configuration from current state if configuration is complete and transitioning to racing
66    pub fn get_run_config(&self) -> Option<crate::models::config::RunConfiguration> {
67        // Only return a configuration when the user has completed setup and wants to start racing
68        if self.config_state.is_valid() && self.interactive_mode.current_mode == ApplicationMode::Racing {
69            Some(crate::models::config::RunConfiguration {
70                array_size: self.config_state.array_size as usize,
71                distribution: self.config_state.to_legacy_distribution(),
72                seed: 12345, // Default seed
73                fairness_mode: self.config_state.fairness_mode.clone(),
74                target_fps: 30,
75            })
76        } else {
77            None
78        }
79    }
80
81    /// Check if configuration has changed and is ready for racing
82    pub fn should_start_new_race(&self) -> bool {
83        self.config_state.is_valid() &&
84        self.interactive_mode.current_mode == ApplicationMode::Racing &&
85        self.interactive_mode.config_focus.is_none()
86    }
87
88    /// Check if currently in racing mode
89    pub fn is_racing(&self) -> bool {
90        self.interactive_mode.current_mode == ApplicationMode::Racing
91    }
92
93    /// Handle key events for the interactive menu
94    pub fn handle_key_event(&mut self, key_event: KeyEvent) -> Result<bool> {
95        match key_event {
96            // Configuration focus keys (only work when not racing)
97            KeyEvent {
98                code: KeyCode::Char('k'),
99                modifiers: KeyModifiers::NONE,
100                ..
101            } => {
102                if self.interactive_mode.current_mode != ApplicationMode::Racing {
103                    self.interactive_mode.set_config_focus(ConfigurationField::ArraySize)?;
104                    self.update_array_size_index_from_config();
105                    Ok(true)
106                } else {
107                    Ok(false)
108                }
109            }
110            KeyEvent {
111                code: KeyCode::Char('b'),
112                modifiers: KeyModifiers::NONE,
113                ..
114            } => {
115                if self.interactive_mode.current_mode != ApplicationMode::Racing {
116                    self.interactive_mode.set_config_focus(ConfigurationField::Distribution)?;
117                    self.update_distribution_index_from_config();
118                    Ok(true)
119                } else {
120                    Ok(false)
121                }
122            }
123            KeyEvent {
124                code: KeyCode::Char('f'),
125                modifiers: KeyModifiers::NONE,
126                ..
127            } => {
128                if self.interactive_mode.current_mode != ApplicationMode::Racing {
129                    self.interactive_mode.set_config_focus(ConfigurationField::FairnessMode)?;
130                    self.update_fairness_mode_index_from_config();
131                    Ok(true)
132                } else {
133                    Ok(false)
134                }
135            }
136            // Help toggle (works in any mode)
137            KeyEvent {
138                code: KeyCode::Char('?'),
139                modifiers: KeyModifiers::NONE,
140                ..
141            } => {
142                self.interactive_mode.toggle_help();
143                Ok(true)
144            }
145            // Visualization switching (works in any mode if algorithms exist)
146            KeyEvent {
147                code: KeyCode::Char('v'),
148                modifiers: KeyModifiers::NONE,
149                ..
150            } => {
151                self.interactive_mode.cycle_array_view_algorithm(TOTAL_ALGORITHMS);
152                Ok(true)
153            }
154            // Race control (Space key)
155            KeyEvent {
156                code: KeyCode::Char(' '),
157                modifiers: KeyModifiers::NONE,
158                ..
159            } => {
160                match self.interactive_mode.current_mode {
161                    ApplicationMode::Configuration => {
162                        self.interactive_mode.transition_to_racing()?;
163                        Ok(true)
164                    }
165                    ApplicationMode::Racing => {
166                        self.interactive_mode.transition_to_paused()?;
167                        Ok(true)
168                    }
169                    ApplicationMode::Paused => {
170                        self.interactive_mode.transition_to_racing()?;
171                        Ok(true)
172                    }
173                    ApplicationMode::Complete => {
174                        self.interactive_mode.transition_to_configuration()?;
175                        Ok(true)
176                    }
177                }
178            }
179            // Navigation keys (when in configuration focus)
180            KeyEvent {
181                code: KeyCode::Up | KeyCode::Down | KeyCode::Left | KeyCode::Right,
182                modifiers: KeyModifiers::NONE,
183                ..
184            } => {
185                if let Some(field) = self.interactive_mode.config_focus {
186                    self.handle_navigation_key(field, key_event.code)?;
187                    Ok(true)
188                } else {
189                    Ok(false)
190                }
191            }
192            // Confirmation key (Enter)
193            KeyEvent {
194                code: KeyCode::Enter,
195                modifiers: KeyModifiers::NONE,
196                ..
197            } => {
198                if self.interactive_mode.config_focus.is_some() {
199                    self.handle_confirmation()?;
200                    Ok(true)
201                } else {
202                    Ok(false)
203                }
204            }
205            // Escape key to cancel focus
206            KeyEvent {
207                code: KeyCode::Esc,
208                modifiers: KeyModifiers::NONE,
209                ..
210            } => {
211                if self.interactive_mode.config_focus.is_some() {
212                    self.interactive_mode.clear_config_focus();
213                    Ok(true)
214                } else {
215                    Ok(false)
216                }
217            }
218            // Numeric input for parameters (digits and decimal point)
219            KeyEvent {
220                code: KeyCode::Char(c),
221                modifiers: KeyModifiers::NONE,
222                ..
223            } if c.is_ascii_digit() || c == '.' => {
224                if let Some(field) = self.interactive_mode.config_focus {
225                    if matches!(field, ConfigurationField::BudgetParam | ConfigurationField::AlphaParam | ConfigurationField::BetaParam | ConfigurationField::LearningRateParam) {
226                        self.handle_numeric_input(c)?;
227                        Ok(true)
228                    } else {
229                        Ok(false)
230                    }
231                } else {
232                    Ok(false)
233                }
234            }
235            // Backspace for parameter editing
236            KeyEvent {
237                code: KeyCode::Backspace,
238                modifiers: KeyModifiers::NONE,
239                ..
240            } => {
241                if let Some(field) = self.interactive_mode.config_focus {
242                    if matches!(field, ConfigurationField::BudgetParam | ConfigurationField::AlphaParam | ConfigurationField::BetaParam | ConfigurationField::LearningRateParam) {
243                        self.handle_backspace_input()?;
244                        Ok(true)
245                    } else {
246                        Ok(false)
247                    }
248                } else {
249                    Ok(false)
250                }
251            }
252            _ => Ok(false), // Event not handled
253        }
254    }
255
256    /// Handle navigation keys within configuration menus
257    fn handle_navigation_key(&mut self, field: ConfigurationField, key_code: KeyCode) -> Result<()> {
258        match field {
259            ConfigurationField::ArraySize => {
260                let sizes = ConfigurationState::get_available_array_sizes();
261                match key_code {
262                    KeyCode::Up => {
263                        if self.array_size_index > 0 {
264                            self.array_size_index -= 1;
265                        } else {
266                            self.array_size_index = sizes.len() - 1; // Wrap to end
267                        }
268                    }
269                    KeyCode::Down => {
270                        if self.array_size_index < sizes.len() - 1 {
271                            self.array_size_index += 1;
272                        } else {
273                            self.array_size_index = 0; // Wrap to beginning
274                        }
275                    }
276                    _ => {}
277                }
278            }
279            ConfigurationField::Distribution => {
280                let distributions = ConfigurationState::get_available_distributions();
281                match key_code {
282                    KeyCode::Up => {
283                        if self.distribution_index > 0 {
284                            self.distribution_index -= 1;
285                        } else {
286                            self.distribution_index = distributions.len() - 1; // Wrap to end
287                        }
288                    }
289                    KeyCode::Down => {
290                        if self.distribution_index < distributions.len() - 1 {
291                            self.distribution_index += 1;
292                        } else {
293                            self.distribution_index = 0; // Wrap to beginning
294                        }
295                    }
296                    _ => {}
297                }
298            }
299            ConfigurationField::FairnessMode => {
300                let fairness_modes = ConfigurationState::get_available_fairness_modes();
301                match key_code {
302                    KeyCode::Up => {
303                        if self.fairness_mode_index > 0 {
304                            self.fairness_mode_index -= 1;
305                        } else {
306                            self.fairness_mode_index = fairness_modes.len() - 1; // Wrap to end
307                        }
308                    }
309                    KeyCode::Down => {
310                        if self.fairness_mode_index < fairness_modes.len() - 1 {
311                            self.fairness_mode_index += 1;
312                        } else {
313                            self.fairness_mode_index = 0; // Wrap to beginning
314                        }
315                    }
316                    _ => {}
317                }
318            }
319            _ => {
320                // Handle parameter fields if needed
321            }
322        }
323        Ok(())
324    }
325
326    /// Handle confirmation (Enter key) in configuration menus
327    fn handle_confirmation(&mut self) -> Result<()> {
328        if let Some(field) = self.interactive_mode.config_focus {
329            match field {
330                ConfigurationField::ArraySize => {
331                    let sizes = ConfigurationState::get_available_array_sizes();
332                    if let Some(size) = sizes.get(self.array_size_index) {
333                        self.interactive_mode.set_array_size_interactive(*size)?;
334                        self.config_state.set_array_size(*size)?; // Sync config_state
335                        self.interactive_mode.clear_config_focus();
336                    }
337                }
338                ConfigurationField::Distribution => {
339                    let distributions = ConfigurationState::get_available_distributions();
340                    if let Some(distribution) = distributions.get(self.distribution_index) {
341                        self.interactive_mode.set_distribution_interactive(*distribution);
342                        self.config_state.distribution = *distribution; // Sync config_state
343                        self.interactive_mode.clear_config_focus();
344                    }
345                }
346                ConfigurationField::FairnessMode => {
347                    let fairness_modes = ConfigurationState::get_available_fairness_modes();
348                    if let Some(fairness_mode) = fairness_modes.get(self.fairness_mode_index) {
349                        self.interactive_mode.set_fairness_mode_interactive(fairness_mode.clone());
350                        self.config_state.set_fairness_mode(fairness_mode.clone()); // Sync config_state
351                        self.interactive_mode.clear_config_focus();
352                    }
353                }
354                ConfigurationField::BudgetParam => {
355                    if let Some(ref value_str) = self.current_parameter_value
356                        && let Ok(budget) = value_str.parse::<u32>() {
357                            self.interactive_mode.set_budget_parameter(budget)?;
358                            self.config_state.budget = Some(budget);
359                            self.current_parameter_value = None;
360                            self.interactive_mode.clear_config_focus();
361                        }
362                }
363                ConfigurationField::AlphaParam => {
364                    if let Some(ref value_str) = self.current_parameter_value
365                        && let Ok(alpha) = value_str.parse::<f32>()
366                            && alpha > 0.0 {
367                                self.config_state.alpha = Some(alpha);
368                                self.current_parameter_value = None;
369                                self.interactive_mode.clear_config_focus();
370                            }
371                }
372                ConfigurationField::BetaParam => {
373                    if let Some(ref value_str) = self.current_parameter_value
374                        && let Ok(beta) = value_str.parse::<f32>()
375                            && beta > 0.0 {
376                                self.config_state.beta = Some(beta);
377                                self.current_parameter_value = None;
378                                self.interactive_mode.clear_config_focus();
379                            }
380                }
381                ConfigurationField::LearningRateParam => {
382                    if let Some(ref value_str) = self.current_parameter_value
383                        && let Ok(learning_rate) = value_str.parse::<f32>()
384                            && learning_rate > 0.0 && learning_rate <= 1.0 {
385                                self.config_state.learning_rate = Some(learning_rate);
386                                self.current_parameter_value = None;
387                                self.interactive_mode.clear_config_focus();
388                            }
389                }
390            }
391        }
392        Ok(())
393    }
394
395    /// Update array size index from current configuration
396    fn update_array_size_index_from_config(&mut self) {
397        let sizes = ConfigurationState::get_available_array_sizes();
398        let current_size = self.interactive_mode.get_current_config().array_size;
399        
400        if let Some(index) = sizes.iter().position(|&size| size == current_size) {
401            self.array_size_index = index;
402        }
403    }
404
405    /// Update distribution index from current configuration
406    fn update_distribution_index_from_config(&mut self) {
407        let distributions = ConfigurationState::get_available_distributions();
408        let current_distribution = self.interactive_mode.get_current_config().distribution;
409        
410        if let Some(index) = distributions.iter().position(|&dist| dist == current_distribution) {
411            self.distribution_index = index;
412        }
413    }
414
415    /// Update fairness mode index from current configuration  
416    fn update_fairness_mode_index_from_config(&mut self) {
417        let fairness_modes = ConfigurationState::get_available_fairness_modes();
418        let current_mode = &self.interactive_mode.get_current_config().fairness_mode;
419        
420        // Find matching fairness mode by type
421        if let Some(index) = fairness_modes.iter().position(|mode| {
422            std::mem::discriminant(mode) == std::mem::discriminant(current_mode)
423        }) {
424            self.fairness_mode_index = index;
425        }
426    }
427
428    /// Render the interactive configuration UI
429    pub fn render(&self, area: Rect, buf: &mut Buffer) {
430        // Main configuration screen
431        self.render_main_config_screen(area, buf);
432
433        // Overlay menus based on current focus
434        if let Some(field) = self.interactive_mode.config_focus {
435            match field {
436                ConfigurationField::ArraySize => {
437                    self.render_array_size_menu(area, buf);
438                }
439                ConfigurationField::Distribution => {
440                    self.render_distribution_menu(area, buf);
441                }
442                ConfigurationField::FairnessMode => {
443                    self.render_fairness_mode_menu(area, buf);
444                }
445                _ => {
446                    // Render parameter input menus
447                }
448            }
449        }
450
451        // Help overlay
452        if self.interactive_mode.should_show_help_overlay() {
453            self.render_help_overlay(area, buf);
454        }
455    }
456
457    /// Render the main configuration screen
458    fn render_main_config_screen(&self, area: Rect, buf: &mut Buffer) {
459        let config = self.interactive_mode.get_current_config();
460        
461        // Create main layout
462        let chunks = Layout::default()
463            .direction(Direction::Vertical)
464            .constraints([
465                Constraint::Length(3), // Title
466                Constraint::Min(0),    // Content
467                Constraint::Length(3), // Status/Instructions
468            ])
469            .split(area);
470
471        // Title
472        let title = Paragraph::new(vec![
473            Line::from(vec![
474                Span::styled(
475                    "Sorting Race v0.2 - Interactive Configuration",
476                    Style::default()
477                        .fg(Color::Cyan)
478                        .add_modifier(Modifier::BOLD),
479                ),
480            ]),
481        ])
482        .block(Block::default().borders(Borders::ALL))
483        .alignment(Alignment::Center);
484        title.render(chunks[0], buf);
485
486        // Configuration content
487        let config_lines = vec![
488            Line::from(""),
489            Line::from(vec![
490                Span::styled("Current Configuration:", Style::default().add_modifier(Modifier::BOLD)),
491            ]),
492            Line::from(""),
493            Line::from(vec![
494                Span::styled("Array Size:     ", Style::default().fg(Color::Yellow)),
495                Span::styled(
496                    format!("{} elements", config.array_size),
497                    Style::default().fg(Color::White),
498                ),
499                Span::styled("  [Press 'k' to change]", Style::default().fg(Color::Gray)),
500            ]),
501            Line::from(vec![
502                Span::styled("Distribution:   ", Style::default().fg(Color::Yellow)),
503                Span::styled(
504                    format!("{:?}", config.distribution),
505                    Style::default().fg(Color::White),
506                ),
507                Span::styled("  [Press 'b' to change]", Style::default().fg(Color::Gray)),
508            ]),
509            Line::from(vec![
510                Span::styled("Fairness Mode:  ", Style::default().fg(Color::Yellow)),
511                Span::styled(
512                    self.format_fairness_mode(&config.fairness_mode),
513                    Style::default().fg(Color::White),
514                ),
515                Span::styled("  [Press 'f' to change]", Style::default().fg(Color::Gray)),
516            ]),
517            Line::from(""),
518            Line::from(vec![
519                Span::styled(
520                    match self.interactive_mode.current_mode {
521                        ApplicationMode::Configuration => "Ready to start race",
522                        ApplicationMode::Racing => "Race in progress...",
523                        ApplicationMode::Paused => "Race paused",
524                        ApplicationMode::Complete => "Race complete - configure for next race",
525                    },
526                    Style::default().fg(match self.interactive_mode.current_mode {
527                        ApplicationMode::Configuration => Color::Green,
528                        ApplicationMode::Racing => Color::Blue,
529                        ApplicationMode::Paused => Color::Yellow,
530                        ApplicationMode::Complete => Color::Magenta,
531                    }),
532                ),
533            ]),
534        ];
535
536        let config_content = Paragraph::new(config_lines)
537            .block(Block::default().borders(Borders::ALL))
538            .alignment(Alignment::Left);
539        config_content.render(chunks[1], buf);
540
541        // Instructions
542        let instruction_text = match self.interactive_mode.current_mode {
543            ApplicationMode::Configuration => "Press SPACE to start race | k/b/f to configure | v to switch array view | ? for help | q to quit",
544            ApplicationMode::Racing => "Press SPACE to pause | v to switch array view | ? for help | q to quit",
545            ApplicationMode::Paused => "Press SPACE to resume | k/b/f to configure | v to switch array view | ? for help | q to quit",
546            ApplicationMode::Complete => "Press SPACE or k/b/f to configure next race | v to switch array view | ? for help | q to quit",
547        };
548
549        let instructions = Paragraph::new(instruction_text)
550            .block(Block::default().borders(Borders::ALL))
551            .alignment(Alignment::Center)
552            .style(Style::default().fg(Color::Cyan));
553        instructions.render(chunks[2], buf);
554
555        // Error message overlay if present
556        if let Some(error) = self.interactive_mode.get_error_message() {
557            self.render_error_overlay(area, buf, error);
558        }
559    }
560
561    /// Format fairness mode for display
562    fn format_fairness_mode(&self, mode: &FairnessMode) -> String {
563        match mode {
564            FairnessMode::ComparisonBudget { k } => format!("Comparison (budget: {})", k),
565            FairnessMode::Weighted { alpha, beta } => format!("Weighted (α:{:.1}, β:{:.1})", alpha, beta),
566            FairnessMode::WallTime { slice_ms } => format!("Wall-time ({}ms)", slice_ms),
567            FairnessMode::Adaptive { learning_rate } => format!("Adaptive (rate:{:.1})", learning_rate),
568            FairnessMode::EqualSteps => "Equal Steps".to_string(),
569        }
570    }
571
572    /// Render array size selection menu
573    fn render_array_size_menu(&self, area: Rect, buf: &mut Buffer) {
574        let sizes = ConfigurationState::get_available_array_sizes();
575        
576        // Create popup area
577        let popup_area = self.centered_rect(40, 60, area);
578        
579        // Clear background
580        Clear.render(popup_area, buf);
581        
582        // Create menu items
583        let items: Vec<ListItem> = sizes
584            .iter()
585            .enumerate()
586            .map(|(i, &size)| {
587                let style = if i == self.array_size_index {
588                    Style::default().bg(Color::Blue).fg(Color::White)
589                } else {
590                    Style::default()
591                };
592                
593                ListItem::new(format!("{} elements", size)).style(style)
594            })
595            .collect();
596
597        let list = List::new(items)
598            .block(Block::default()
599                .borders(Borders::ALL)
600                .title("Select Array Size"))
601            .highlight_style(Style::default().bg(Color::Blue));
602
603        list.render(popup_area, buf);
604
605        // Instructions
606        let instruction_area = Rect {
607            x: popup_area.x,
608            y: popup_area.y + popup_area.height,
609            width: popup_area.width,
610            height: 1,
611        };
612
613        if instruction_area.y < area.height {
614            let instructions = Paragraph::new("↑↓ Navigate | Enter to select | Esc to cancel")
615                .style(Style::default().fg(Color::Gray));
616            instructions.render(instruction_area, buf);
617        }
618    }
619
620    /// Render distribution selection menu
621    fn render_distribution_menu(&self, area: Rect, buf: &mut Buffer) {
622        let distributions = ConfigurationState::get_available_distributions();
623        
624        // Create popup area
625        let popup_area = self.centered_rect(40, 50, area);
626        
627        // Clear background
628        Clear.render(popup_area, buf);
629        
630        // Create menu items
631        let items: Vec<ListItem> = distributions
632            .iter()
633            .enumerate()
634            .map(|(i, &dist)| {
635                let style = if i == self.distribution_index {
636                    Style::default().bg(Color::Blue).fg(Color::White)
637                } else {
638                    Style::default()
639                };
640                
641                let description = match dist {
642                    DistributionType::Shuffled => "Random order",
643                    DistributionType::Reversed => "Reverse sorted",
644                    DistributionType::NearlySorted => "Mostly sorted",
645                    DistributionType::FewUnique => "Few unique values",
646                };
647                
648                ListItem::new(format!("{:?} - {}", dist, description)).style(style)
649            })
650            .collect();
651
652        let list = List::new(items)
653            .block(Block::default()
654                .borders(Borders::ALL)
655                .title("Select Distribution"))
656            .highlight_style(Style::default().bg(Color::Blue));
657
658        list.render(popup_area, buf);
659
660        // Instructions
661        let instruction_area = Rect {
662            x: popup_area.x,
663            y: popup_area.y + popup_area.height,
664            width: popup_area.width,
665            height: 1,
666        };
667
668        if instruction_area.y < area.height {
669            let instructions = Paragraph::new("↑↓ Navigate | Enter to select | Esc to cancel")
670                .style(Style::default().fg(Color::Gray));
671            instructions.render(instruction_area, buf);
672        }
673    }
674
675    /// Render fairness mode selection menu
676    fn render_fairness_mode_menu(&self, area: Rect, buf: &mut Buffer) {
677        let fairness_modes = ConfigurationState::get_available_fairness_modes();
678        
679        // Create popup area
680        let popup_area = self.centered_rect(60, 70, area);
681        
682        // Clear background
683        Clear.render(popup_area, buf);
684        
685        // Create menu items
686        let items: Vec<ListItem> = fairness_modes
687            .iter()
688            .enumerate()
689            .map(|(i, mode)| {
690                let style = if i == self.fairness_mode_index {
691                    Style::default().bg(Color::Blue).fg(Color::White)
692                } else {
693                    Style::default()
694                };
695                
696                let description = match mode {
697                    FairnessMode::ComparisonBudget { .. } => "Fixed comparison budget per step",
698                    FairnessMode::Weighted { .. } => "Weighted by comparisons and moves",
699                    FairnessMode::WallTime { .. } => "Equal time slices for each algorithm",
700                    FairnessMode::Adaptive { .. } => "Adaptive allocation based on performance",
701                    _ => "Equal steps",
702                };
703                
704                ListItem::new(vec![
705                    Line::from(self.format_fairness_mode(mode)),
706                    Line::from(Span::styled(description, Style::default().fg(Color::Gray))),
707                ]).style(style)
708            })
709            .collect();
710
711        let list = List::new(items)
712            .block(Block::default()
713                .borders(Borders::ALL)
714                .title("Select Fairness Mode"))
715            .highlight_style(Style::default().bg(Color::Blue));
716
717        list.render(popup_area, buf);
718
719        // Instructions
720        let instruction_area = Rect {
721            x: popup_area.x,
722            y: popup_area.y + popup_area.height,
723            width: popup_area.width,
724            height: 1,
725        };
726
727        if instruction_area.y < area.height {
728            let instructions = Paragraph::new("↑↓ Navigate | Enter to select | Esc to cancel")
729                .style(Style::default().fg(Color::Gray));
730            instructions.render(instruction_area, buf);
731        }
732    }
733
734    /// Render help overlay
735    fn render_help_overlay(&self, area: Rect, buf: &mut Buffer) {
736        let popup_area = self.centered_rect(80, 80, area);
737        
738        // Clear background
739        Clear.render(popup_area, buf);
740        
741        let help_content = self.interactive_mode.get_help_overlay_content();
742        let help_widget = Paragraph::new(help_content)
743            .block(Block::default()
744                .borders(Borders::ALL)
745                .title("Help - Keyboard Shortcuts"))
746            .alignment(Alignment::Left);
747        
748        help_widget.render(popup_area, buf);
749    }
750
751    /// Render error message overlay
752    fn render_error_overlay(&self, area: Rect, buf: &mut Buffer, error: &str) {
753        let popup_area = self.centered_rect(60, 20, area);
754        
755        // Clear background
756        Clear.render(popup_area, buf);
757        
758        let error_widget = Paragraph::new(error)
759            .block(Block::default()
760                .borders(Borders::ALL)
761                .title("Error")
762                .border_style(Style::default().fg(Color::Red)))
763            .style(Style::default().fg(Color::Red))
764            .alignment(Alignment::Center);
765        
766        error_widget.render(popup_area, buf);
767    }
768
769    /// Helper function to create centered rectangle
770    fn centered_rect(&self, percent_x: u16, percent_y: u16, r: Rect) -> Rect {
771        let popup_layout = Layout::default()
772            .direction(Direction::Vertical)
773            .constraints([
774                Constraint::Percentage((100 - percent_y) / 2),
775                Constraint::Percentage(percent_y),
776                Constraint::Percentage((100 - percent_y) / 2),
777            ])
778            .split(r);
779
780        Layout::default()
781            .direction(Direction::Horizontal)
782            .constraints([
783                Constraint::Percentage((100 - percent_x) / 2),
784                Constraint::Percentage(percent_x),
785                Constraint::Percentage((100 - percent_x) / 2),
786            ])
787            .split(popup_layout[1])[1]
788    }
789
790    /// Handle numeric input for parameter fields
791    fn handle_numeric_input(&mut self, digit: char) -> Result<()> {
792        if self.current_parameter_value.is_none() {
793            self.current_parameter_value = Some(String::new());
794        }
795        let current_value = self.current_parameter_value.as_mut().unwrap();
796
797        // Only allow one decimal point
798        if digit == '.' && current_value.contains('.') {
799            return Ok(());
800        }
801
802        // Create test value to validate before adding
803        let test_value = format!("{}{}", current_value, digit);
804
805        // Validate based on the field type with proper bounds
806        let is_valid = match self.interactive_mode.config_focus {
807            Some(ConfigurationField::ArraySize) => {
808                test_value.parse::<u32>()
809                    .map(|v| v <= MAX_ARRAY_SIZE)
810                    .unwrap_or(true)  // Allow incomplete numbers
811            }
812            Some(ConfigurationField::BudgetParam) => {
813                test_value.parse::<u32>()
814                    .map(|v| v <= MAX_BUDGET)
815                    .unwrap_or(true)
816            }
817            Some(ConfigurationField::AlphaParam | ConfigurationField::BetaParam | ConfigurationField::LearningRateParam) => {
818                test_value.parse::<f32>()
819                    .map(|v| v <= MAX_FLOAT_PARAM)
820                    .unwrap_or(true)
821            }
822            _ => true,
823        };
824
825        if is_valid && current_value.len() < MAX_INPUT_LENGTH {
826            current_value.push(digit);
827        }
828        Ok(())
829    }
830
831    /// Handle backspace input for parameter fields
832    fn handle_backspace_input(&mut self) -> Result<()> {
833        if let Some(ref mut current_value) = self.current_parameter_value {
834            current_value.pop();
835            if current_value.is_empty() {
836                self.current_parameter_value = None;
837            }
838        }
839        Ok(())
840    }
841}
842
843impl Default for InteractiveConfigMenu {
844    fn default() -> Self {
845        Self::new()
846    }
847}
848
849impl Widget for InteractiveConfigMenu {
850    fn render(self, area: Rect, buf: &mut Buffer) {
851        InteractiveConfigMenu::render(&self, area, buf);
852    }
853}
854
855impl Widget for &InteractiveConfigMenu {
856    fn render(self, area: Rect, buf: &mut Buffer) {
857        InteractiveConfigMenu::render(self, area, buf);
858    }
859}
860
861#[cfg(test)]
862mod tests {
863    use super::*;
864    use crossterm::event::KeyEventKind;
865
866    #[test]
867    fn test_interactive_config_menu_creation() {
868        let menu = InteractiveConfigMenu::new();
869        assert_eq!(menu.interactive_mode.current_mode, ApplicationMode::Configuration);
870        assert_eq!(menu.array_size_index, 3); // 100 elements
871        assert_eq!(menu.distribution_index, 0); // Shuffled
872        assert_eq!(menu.fairness_mode_index, 2); // WallTime
873    }
874
875    #[test]
876    fn test_configuration_key_handling() {
877        let mut menu = InteractiveConfigMenu::new();
878        
879        // Test 'k' key for array size
880        let k_key = KeyEvent {
881            code: KeyCode::Char('k'),
882            modifiers: KeyModifiers::NONE,
883            kind: KeyEventKind::Press,
884            state: crossterm::event::KeyEventState::empty(),
885        };
886        
887        let handled = menu.handle_key_event(k_key).unwrap();
888        assert!(handled);
889        assert_eq!(menu.interactive_mode.config_focus, Some(ConfigurationField::ArraySize));
890    }
891
892    #[test]
893    fn test_navigation_in_array_size_menu() {
894        let mut menu = InteractiveConfigMenu::new();
895        menu.interactive_mode.set_config_focus(ConfigurationField::ArraySize).unwrap();
896        menu.array_size_index = 2; // Start at index 2 (50)
897        
898        // Test up navigation
899        let up_key = KeyEvent {
900            code: KeyCode::Up,
901            modifiers: KeyModifiers::NONE,
902            kind: KeyEventKind::Press,
903            state: crossterm::event::KeyEventState::empty(),
904        };
905        
906        let handled = menu.handle_key_event(up_key).unwrap();
907        assert!(handled);
908        assert_eq!(menu.array_size_index, 1); // Should move to index 1 (25)
909    }
910
911    #[test]
912    fn test_navigation_wrapping() {
913        let mut menu = InteractiveConfigMenu::new();
914        menu.interactive_mode.set_config_focus(ConfigurationField::ArraySize).unwrap();
915        menu.array_size_index = 0; // Start at first item
916        
917        // Test up navigation from first item (should wrap to last)
918        let up_key = KeyEvent {
919            code: KeyCode::Up,
920            modifiers: KeyModifiers::NONE,
921            kind: KeyEventKind::Press,
922            state: crossterm::event::KeyEventState::empty(),
923        };
924        
925        let handled = menu.handle_key_event(up_key).unwrap();
926        assert!(handled);
927        let sizes = ConfigurationState::get_available_array_sizes();
928        assert_eq!(menu.array_size_index, sizes.len() - 1); // Should wrap to last
929    }
930
931    #[test]
932    fn test_confirmation_handling() {
933        let mut menu = InteractiveConfigMenu::new();
934        menu.interactive_mode.set_config_focus(ConfigurationField::ArraySize).unwrap();
935        menu.array_size_index = 0; // Select first size (10)
936        
937        let enter_key = KeyEvent {
938            code: KeyCode::Enter,
939            modifiers: KeyModifiers::NONE,
940            kind: KeyEventKind::Press,
941            state: crossterm::event::KeyEventState::empty(),
942        };
943        
944        let handled = menu.handle_key_event(enter_key).unwrap();
945        assert!(handled);
946        assert_eq!(menu.interactive_mode.get_current_config().array_size, 10);
947        assert_eq!(menu.interactive_mode.config_focus, None); // Should clear focus
948    }
949
950    #[test]
951    fn test_help_toggle() {
952        let mut menu = InteractiveConfigMenu::new();
953        
954        let help_key = KeyEvent {
955            code: KeyCode::Char('?'),
956            modifiers: KeyModifiers::NONE,
957            kind: KeyEventKind::Press,
958            state: crossterm::event::KeyEventState::empty(),
959        };
960        
961        assert!(!menu.interactive_mode.should_show_help_overlay());
962        
963        let handled = menu.handle_key_event(help_key).unwrap();
964        assert!(handled);
965        assert!(menu.interactive_mode.should_show_help_overlay());
966    }
967
968    #[test]
969    fn test_race_control_transitions() {
970        let mut menu = InteractiveConfigMenu::new();
971        
972        let space_key = KeyEvent {
973            code: KeyCode::Char(' '),
974            modifiers: KeyModifiers::NONE,
975            kind: KeyEventKind::Press,
976            state: crossterm::event::KeyEventState::empty(),
977        };
978        
979        // Configuration -> Racing
980        assert_eq!(menu.interactive_mode.current_mode, ApplicationMode::Configuration);
981        let handled = menu.handle_key_event(space_key).unwrap();
982        assert!(handled);
983        assert_eq!(menu.interactive_mode.current_mode, ApplicationMode::Racing);
984        
985        // Racing -> Paused
986        let handled = menu.handle_key_event(space_key).unwrap();
987        assert!(handled);
988        assert_eq!(menu.interactive_mode.current_mode, ApplicationMode::Paused);
989    }
990}