Skip to main content

ttop/
app.rs

1//! Application state and logic for ttop.
2
3use crossterm::event::{KeyCode, KeyModifiers};
4use std::time::{Duration, Instant};
5
6use trueno_viz::monitor::collectors::{
7    BatteryCollector, CpuCollector, DiskCollector, MemoryCollector, NetworkCollector,
8    ProcessCollector, SensorCollector,
9};
10use trueno_viz::monitor::types::Collector;
11
12#[cfg(feature = "nvidia")]
13use trueno_viz::monitor::collectors::NvidiaGpuCollector;
14
15#[cfg(target_os = "linux")]
16use trueno_viz::monitor::collectors::AmdGpuCollector;
17
18#[cfg(target_os = "macos")]
19use trueno_viz::monitor::collectors::AppleGpuCollector;
20
21use crate::analyzers::{ContainerAnalyzer, DiskEntropyAnalyzer, DiskIoAnalyzer, GpuProcessAnalyzer, NetworkStatsAnalyzer, PsiAnalyzer, SensorHealthAnalyzer, StorageAnalyzer, SwapAnalyzer, ThrashingSeverity};
22use crate::state::{PanelType, ProcessSortColumn, SignalType};
23
24/// Allocation-free case-insensitive substring search.
25///
26/// Checks if `haystack` contains `needle` (which must already be lowercase).
27/// This avoids the O(n) allocation of `haystack.to_lowercase()` on every call.
28#[inline]
29fn contains_ignore_case(haystack: &str, needle_lower: &str) -> bool {
30    if needle_lower.is_empty() {
31        return true;
32    }
33    if haystack.len() < needle_lower.len() {
34        return false;
35    }
36
37    // Fast path for ASCII-only strings (common for process names)
38    let needle_bytes = needle_lower.as_bytes();
39    let haystack_bytes = haystack.as_bytes();
40
41    'outer: for i in 0..=(haystack_bytes.len() - needle_bytes.len()) {
42        for (j, &nb) in needle_bytes.iter().enumerate() {
43            let hb = haystack_bytes[i + j];
44            // Compare lowercase ASCII bytes
45            let hb_lower = if hb.is_ascii_uppercase() { hb + 32 } else { hb };
46            if hb_lower != nb {
47                continue 'outer;
48            }
49        }
50        return true;
51    }
52    false
53}
54
55/// Mock GPU data for testing panel rendering
56#[derive(Debug, Clone)]
57pub struct MockGpuData {
58    pub name: String,
59    pub gpu_util: f64,
60    pub vram_used: u64,
61    pub vram_total: u64,
62    pub temperature: f64,
63    pub power_watts: u32,
64    pub power_limit_watts: u32,
65    pub clock_mhz: u32,
66    pub history: Vec<f64>,
67}
68
69/// Mock battery data for testing panel rendering
70#[derive(Debug, Clone)]
71pub struct MockBatteryData {
72    pub percent: f64,
73    pub charging: bool,
74    pub time_remaining_mins: Option<u32>,
75    pub power_watts: f64,
76    pub health_percent: f64,
77    pub cycle_count: u32,
78}
79
80/// Mock sensor data for testing panel rendering
81#[derive(Debug, Clone)]
82pub struct MockSensorData {
83    pub name: String,
84    pub label: String,
85    pub value: f64,
86    pub max: Option<f64>,
87    pub crit: Option<f64>,
88    pub sensor_type: MockSensorType,
89}
90
91#[derive(Debug, Clone, Copy, PartialEq)]
92pub enum MockSensorType {
93    Temperature,
94    Fan,
95    Voltage,
96    Power,
97}
98
99/// Mock container data for testing panel rendering
100#[derive(Debug, Clone)]
101pub struct MockContainerData {
102    pub name: String,
103    pub status: String,
104    pub cpu_percent: f64,
105    pub mem_used: u64,
106    pub mem_limit: u64,
107}
108
109/// Panel visibility state
110#[derive(Debug, Clone, Copy)]
111pub struct PanelVisibility {
112    pub cpu: bool,
113    pub memory: bool,
114    pub disk: bool,
115    pub network: bool,
116    pub process: bool,
117    pub gpu: bool,
118    pub battery: bool,
119    pub sensors: bool,
120    pub files: bool,
121}
122
123impl Default for PanelVisibility {
124    fn default() -> Self {
125        Self {
126            cpu: true,
127            memory: true,
128            disk: true,
129            network: true,
130            process: true,
131            gpu: true,
132            battery: true,
133            sensors: true,
134            files: false, // Off by default, toggle with '9'
135        }
136    }
137}
138
139/// CPU core state breakdown (user/system/iowait/idle percentages)
140#[derive(Debug, Clone, Default)]
141pub struct CpuCoreState {
142    pub user: f64,
143    pub system: f64,
144    pub iowait: f64,
145    pub idle: f64,
146}
147
148impl CpuCoreState {
149    /// Total busy percentage (user + system + iowait)
150    pub fn total_busy(&self) -> f64 {
151        self.user + self.system + self.iowait
152    }
153}
154
155/// Top process info for a core
156#[derive(Debug, Clone, Default)]
157pub struct TopProcessForCore {
158    pub pid: u32,
159    pub name: String,
160    pub cpu_percent: f64,
161}
162
163/// Memory breakdown categories
164#[derive(Debug, Clone, Default)]
165pub struct MemoryBreakdown {
166    pub used_bytes: u64,
167    pub cached_bytes: u64,
168    pub buffers_bytes: u64,
169    pub free_bytes: u64,
170}
171
172/// Swap usage trend indicator
173#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
174pub enum SwapTrend {
175    Rising,
176    #[default]
177    Stable,
178    Falling,
179}
180
181impl SwapTrend {
182    /// Symbol for the trend
183    pub fn symbol(&self) -> &'static str {
184        match self {
185            SwapTrend::Rising => "↑",
186            SwapTrend::Stable => "→",
187            SwapTrend::Falling => "↓",
188        }
189    }
190}
191
192/// Top memory consumer info
193#[derive(Debug, Clone, Default)]
194pub struct TopMemConsumer {
195    pub pid: u32,
196    pub name: String,
197    pub mem_bytes: u64,
198    pub mem_percent: f64,
199}
200
201/// Disk health status
202#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
203pub enum DiskHealth {
204    #[default]
205    Good,
206    Warning,
207    Critical,
208    Unknown,
209}
210
211impl DiskHealth {
212    /// Symbol for disk health
213    pub fn symbol(&self) -> &'static str {
214        match self {
215            DiskHealth::Good => "✓",
216            DiskHealth::Warning => "⚠",
217            DiskHealth::Critical => "✗",
218            DiskHealth::Unknown => "?",
219        }
220    }
221}
222
223/// Disk health status for a device
224#[derive(Debug, Clone, Default)]
225pub struct DiskHealthStatus {
226    pub device: String,
227    pub status: DiskHealth,
228    pub temperature: Option<f64>,
229    pub reallocated_sectors: u64,
230}
231
232/// Main application state
233pub struct App {
234    // Collectors
235    pub cpu: CpuCollector,
236    pub memory: MemoryCollector,
237    pub disk: DiskCollector,
238    pub network: NetworkCollector,
239    pub process: ProcessCollector,
240    pub sensors: SensorCollector,
241    pub battery: BatteryCollector,
242
243    #[cfg(feature = "nvidia")]
244    pub nvidia_gpu: NvidiaGpuCollector,
245
246    #[cfg(target_os = "linux")]
247    pub amd_gpu: AmdGpuCollector,
248
249    #[cfg(target_os = "macos")]
250    pub apple_gpu: AppleGpuCollector,
251
252    // Advanced analyzers (ttop-improve.md spec)
253    pub swap_analyzer: SwapAnalyzer,
254    pub disk_io_analyzer: DiskIoAnalyzer,
255    pub storage_analyzer: StorageAnalyzer,
256    pub connection_analyzer: crate::analyzers::ConnectionAnalyzer,
257    pub treemap_analyzer: crate::analyzers::TreemapAnalyzer,
258    pub gpu_process_analyzer: GpuProcessAnalyzer,
259    pub psi_analyzer: PsiAnalyzer,
260    pub container_analyzer: ContainerAnalyzer,
261    pub network_stats: NetworkStatsAnalyzer,
262    pub disk_entropy: DiskEntropyAnalyzer,
263    pub process_extra: crate::analyzers::ProcessExtraAnalyzer,
264    pub file_analyzer: crate::analyzers::FileAnalyzer,
265    pub sensor_health: SensorHealthAnalyzer,
266
267    // History buffers (normalized 0-1)
268    pub cpu_history: Vec<f64>,
269    pub mem_history: Vec<f64>,
270    pub mem_available_history: Vec<f64>,
271    pub mem_cached_history: Vec<f64>,
272    pub mem_free_history: Vec<f64>,
273    pub swap_history: Vec<f64>,
274    pub net_rx_history: Vec<f64>,
275    pub net_tx_history: Vec<f64>,
276    pub per_core_percent: Vec<f64>,
277
278    // CPU Exploded Mode Features (SPEC-TDD)
279    /// Per-core CPU history (60 samples per core for sparklines)
280    pub per_core_history: Vec<Vec<f64>>,
281    /// Per-core state breakdown (user/system/iowait/idle)
282    pub per_core_state: Vec<CpuCoreState>,
283    /// Frequency history (average MHz over time)
284    pub freq_history: Vec<f64>,
285    /// Top process consuming each core
286    pub top_process_per_core: Vec<TopProcessForCore>,
287    /// Thermal throttling active (None if unknown)
288    pub thermal_throttle_active: Option<bool>,
289
290    // Memory Exploded Mode Features (SPEC-TDD)
291    /// Memory pressure history (PSI avg10 values, 60 samples)
292    pub mem_pressure_history: Vec<f64>,
293    /// Memory reclaim rate (pages/sec)
294    pub mem_reclaim_rate: f64,
295    /// Top memory consumers
296    pub top_mem_consumers: Vec<TopMemConsumer>,
297    /// Swap usage trend
298    pub swap_trend: SwapTrend,
299
300    // Disk Exploded Mode Features (SPEC-TDD)
301    /// Disk latency history (60 samples)
302    pub disk_latency_history: Vec<f64>,
303    /// Read IOPS
304    pub disk_read_iops: f64,
305    /// Write IOPS
306    pub disk_write_iops: f64,
307    /// Queue depth
308    pub disk_queue_depth: f64,
309    /// Disk health status
310    pub disk_health: Vec<DiskHealthStatus>,
311
312    // Latest memory values (bytes)
313    pub mem_total: u64,
314    pub mem_used: u64,
315    pub mem_available: u64,
316    pub mem_cached: u64,
317    pub mem_free: u64,
318    pub swap_total: u64,
319    pub swap_used: u64,
320
321    // Network totals (bytes)
322    pub net_rx_total: u64,
323    pub net_tx_total: u64,
324    pub net_interface_ip: String,
325
326    // Network peak tracking
327    pub net_rx_peak: f64,
328    pub net_tx_peak: f64,
329    pub net_rx_peak_time: Instant,
330    pub net_tx_peak_time: Instant,
331
332    // Network Exploded Mode Features (SPEC-TDD)
333    /// Network errors count
334    pub net_errors: u64,
335    /// Network drops count
336    pub net_drops: u64,
337    /// Established connection count
338    pub net_established: u64,
339    /// Listening port count
340    pub net_listening: u64,
341
342    // UI state
343    pub panels: PanelVisibility,
344    pub process_selected: usize,
345    pub process_scroll_offset: usize,
346    pub sort_column: ProcessSortColumn,
347    pub sort_descending: bool,
348    pub filter: String,
349    pub show_filter_input: bool,
350    pub show_help: bool,
351    pub show_tree: bool,
352
353    // Signal/kill mode
354    pub show_signal_menu: bool,
355    pub pending_signal: Option<(u32, String, SignalType)>, // (pid, name, signal)
356    pub signal_result: Option<(bool, String, Instant)>,     // (success, message, timestamp)
357
358    // Panel focus/explode state
359    pub focused_panel: Option<PanelType>,
360    pub exploded_panel: Option<PanelType>,
361
362    // Files panel view mode (SIZE/ENTROPY/IO)
363    pub files_view_mode: crate::state::FilesViewMode,
364
365    // Frame timing
366    pub frame_id: u64,
367    pub last_collect: Instant,
368    pub avg_frame_time_us: u64,
369    pub max_frame_time_us: u64,
370    pub show_fps: bool,
371
372    // Mode flags
373    pub deterministic: bool,
374
375    // Mock data for testing (dependency injection)
376    pub mock_gpus: Vec<MockGpuData>,
377    pub mock_battery: Option<MockBatteryData>,
378    pub mock_sensors: Vec<MockSensorData>,
379    pub mock_containers: Vec<MockContainerData>,
380}
381
382impl App {
383    /// Get current swap thrashing severity
384    pub fn thrashing_severity(&self) -> ThrashingSeverity {
385        self.swap_analyzer.detect_thrashing()
386    }
387
388    /// Check if system has ZRAM configured
389    pub fn has_zram(&self) -> bool {
390        self.swap_analyzer.has_zram()
391    }
392
393    /// Get ZRAM compression ratio (combined across all devices)
394    pub fn zram_ratio(&self) -> f64 {
395        self.swap_analyzer.zram_compression_ratio()
396    }
397}
398
399impl App {
400    /// Create a new application instance
401    pub fn new(deterministic: bool, show_fps: bool) -> Self {
402        use trueno_viz::monitor::debug::{self, Level, TimingGuard};
403
404        debug::log(Level::Debug, "app", "Initializing CPU collector...");
405        let _t = TimingGuard::new("app", "CpuCollector::new");
406        let cpu = CpuCollector::new();
407        drop(_t);
408        debug::log(Level::Info, "app", &format!("CPU: {} cores", cpu.core_count()));
409
410        debug::log(Level::Debug, "app", "Initializing Memory collector...");
411        let memory = MemoryCollector::new();
412
413        debug::log(Level::Debug, "app", "Initializing Disk collector...");
414        let disk = DiskCollector::new();
415
416        debug::log(Level::Debug, "app", "Initializing Network collector...");
417        let network = NetworkCollector::new();
418
419        debug::log(Level::Debug, "app", "Initializing Process collector...");
420        let process = ProcessCollector::new();
421
422        debug::log(Level::Debug, "app", "Initializing Sensors collector...");
423        let sensors = SensorCollector::new();
424
425        debug::log(Level::Debug, "app", "Initializing Battery collector...");
426        let battery = BatteryCollector::new();
427
428        #[cfg(feature = "nvidia")]
429        let nvidia_gpu = {
430            debug::log(Level::Debug, "app", "Initializing NVIDIA GPU collector...");
431            NvidiaGpuCollector::new()
432        };
433
434        #[cfg(target_os = "linux")]
435        let amd_gpu = {
436            debug::log(Level::Debug, "app", "Initializing AMD GPU collector...");
437            AmdGpuCollector::new()
438        };
439
440        #[cfg(target_os = "macos")]
441        let apple_gpu = {
442            debug::log(Level::Debug, "app", "Initializing Apple GPU collector...");
443            let _t = TimingGuard::new("app", "AppleGpuCollector::new");
444            let g = AppleGpuCollector::new();
445            drop(_t);
446            debug::log(Level::Info, "app", &format!("Apple GPU: {} devices", g.gpus().len()));
447            g
448        };
449
450        debug::log(Level::Debug, "app", "All collectors initialized");
451
452        let mut app = Self {
453            cpu,
454            memory,
455            disk,
456            network,
457            process,
458            sensors,
459            battery,
460
461            #[cfg(feature = "nvidia")]
462            nvidia_gpu,
463
464            #[cfg(target_os = "linux")]
465            amd_gpu,
466
467            #[cfg(target_os = "macos")]
468            apple_gpu,
469
470            // Initialize advanced analyzers
471            swap_analyzer: SwapAnalyzer::default(),
472            disk_io_analyzer: DiskIoAnalyzer::default(),
473            storage_analyzer: StorageAnalyzer::default(),
474            connection_analyzer: crate::analyzers::ConnectionAnalyzer::default(),
475            treemap_analyzer: crate::analyzers::TreemapAnalyzer::new("/"),
476            gpu_process_analyzer: GpuProcessAnalyzer::default(),
477            psi_analyzer: PsiAnalyzer::default(),
478            container_analyzer: ContainerAnalyzer::default(),
479            network_stats: NetworkStatsAnalyzer::default(),
480            disk_entropy: DiskEntropyAnalyzer::new(),
481            process_extra: crate::analyzers::ProcessExtraAnalyzer::new(),
482            file_analyzer: crate::analyzers::FileAnalyzer::new(),
483            sensor_health: SensorHealthAnalyzer::default(),
484
485            cpu_history: Vec::with_capacity(300),
486            mem_history: Vec::with_capacity(300),
487            mem_available_history: Vec::with_capacity(300),
488            mem_cached_history: Vec::with_capacity(300),
489            mem_free_history: Vec::with_capacity(300),
490            swap_history: Vec::with_capacity(300),
491            net_rx_history: Vec::with_capacity(300),
492            net_tx_history: Vec::with_capacity(300),
493            per_core_percent: Vec::new(),
494
495            // CPU Exploded Mode Features
496            per_core_history: Vec::new(),
497            per_core_state: Vec::new(),
498            freq_history: Vec::with_capacity(60),
499            top_process_per_core: Vec::new(),
500            thermal_throttle_active: None,
501
502            // Memory Exploded Mode Features
503            mem_pressure_history: Vec::with_capacity(60),
504            mem_reclaim_rate: 0.0,
505            top_mem_consumers: Vec::new(),
506            swap_trend: SwapTrend::Stable,
507
508            // Disk Exploded Mode Features
509            disk_latency_history: Vec::with_capacity(60),
510            disk_read_iops: 0.0,
511            disk_write_iops: 0.0,
512            disk_queue_depth: 0.0,
513            disk_health: Vec::new(),
514
515            mem_total: 0,
516            mem_used: 0,
517            mem_available: 0,
518            mem_cached: 0,
519            mem_free: 0,
520            swap_total: 0,
521            swap_used: 0,
522
523            net_rx_total: 0,
524            net_tx_total: 0,
525            net_interface_ip: String::new(),
526
527            net_rx_peak: 0.0,
528            net_tx_peak: 0.0,
529            net_rx_peak_time: Instant::now(),
530            net_tx_peak_time: Instant::now(),
531
532            // Network Exploded Mode Features
533            net_errors: 0,
534            net_drops: 0,
535            net_established: 0,
536            net_listening: 0,
537
538            panels: PanelVisibility::default(),
539            process_selected: 0,
540            process_scroll_offset: 0,
541            sort_column: ProcessSortColumn::Cpu,
542            sort_descending: true,
543            filter: String::new(),
544            show_filter_input: false,
545            show_help: false,
546            show_tree: false,
547
548            show_signal_menu: false,
549            pending_signal: None,
550            signal_result: None,
551
552            focused_panel: None,
553            exploded_panel: None,
554
555            files_view_mode: crate::state::FilesViewMode::default(),
556
557            frame_id: 0,
558            last_collect: Instant::now(),
559            avg_frame_time_us: 0,
560            max_frame_time_us: 0,
561            show_fps,
562
563            deterministic,
564
565            // No mock data in production mode
566            mock_gpus: Vec::new(),
567            mock_battery: None,
568            mock_sensors: Vec::new(),
569            mock_containers: Vec::new(),
570        };
571
572        // Initial collection (need 2 for deltas)
573        debug::log(Level::Debug, "app", "Initial metrics collection (1/2)...");
574        app.collect_metrics();
575        debug::log(Level::Debug, "app", "Initial metrics collection (2/2)...");
576        app.collect_metrics();
577        debug::log(Level::Info, "app", "App initialization complete");
578
579        app
580    }
581
582    /// Create a mock application instance for testing
583    /// This creates an App with default collectors and populated test data
584    /// without making real system calls.
585    /// Available for integration tests and benchmarks.
586    pub fn new_mock() -> Self {
587        Self {
588            cpu: CpuCollector::default(),
589            memory: MemoryCollector::default(),
590            disk: DiskCollector::default(),
591            network: NetworkCollector::default(),
592            process: ProcessCollector::default(),
593            sensors: SensorCollector::default(),
594            battery: BatteryCollector::default(),
595
596            #[cfg(feature = "nvidia")]
597            nvidia_gpu: NvidiaGpuCollector::default(),
598
599            #[cfg(target_os = "linux")]
600            amd_gpu: AmdGpuCollector::default(),
601
602            #[cfg(target_os = "macos")]
603            apple_gpu: AppleGpuCollector::default(),
604
605            swap_analyzer: SwapAnalyzer::new(),
606            disk_io_analyzer: DiskIoAnalyzer::new(),
607            storage_analyzer: StorageAnalyzer::new(),
608            connection_analyzer: crate::analyzers::ConnectionAnalyzer::new(),
609            treemap_analyzer: crate::analyzers::TreemapAnalyzer::new("/tmp"),
610            gpu_process_analyzer: GpuProcessAnalyzer::new(),
611            psi_analyzer: PsiAnalyzer::new(),
612            container_analyzer: ContainerAnalyzer::new(),
613            network_stats: NetworkStatsAnalyzer::new(),
614            disk_entropy: DiskEntropyAnalyzer::new(),
615            process_extra: crate::analyzers::ProcessExtraAnalyzer::new(),
616            file_analyzer: crate::analyzers::FileAnalyzer::new(),
617            sensor_health: SensorHealthAnalyzer::new(),
618
619            // Populate with test data
620            cpu_history: vec![0.25, 0.30, 0.35, 0.40, 0.45, 0.50, 0.45, 0.40],
621            mem_history: vec![0.60, 0.61, 0.62, 0.63, 0.64, 0.65, 0.64, 0.63],
622            mem_available_history: vec![0.40, 0.39, 0.38, 0.37, 0.36, 0.35, 0.36, 0.37],
623            mem_cached_history: vec![0.20, 0.21, 0.22, 0.23, 0.22, 0.21, 0.20, 0.21],
624            mem_free_history: vec![0.10, 0.09, 0.08, 0.07, 0.08, 0.09, 0.10, 0.09],
625            swap_history: vec![0.05, 0.06, 0.07, 0.08, 0.07, 0.06, 0.05, 0.06],
626            net_rx_history: vec![0.01, 0.02, 0.03, 0.04, 0.03, 0.02, 0.01, 0.02],
627            net_tx_history: vec![0.005, 0.01, 0.015, 0.02, 0.015, 0.01, 0.005, 0.01],
628            per_core_percent: vec![25.0, 30.0, 35.0, 40.0, 45.0, 50.0, 55.0, 60.0],
629
630            // CPU Exploded Mode Features (mock data with proper capacity)
631            per_core_history: {
632                let mut histories = Vec::with_capacity(8);
633                for i in 0..8 {
634                    let mut h = Vec::with_capacity(60);
635                    let base = 20.0 + (i as f64 * 5.0);
636                    for j in 0..6 {
637                        h.push(base + (j as f64 * 0.5));
638                    }
639                    histories.push(h);
640                }
641                histories
642            },
643            per_core_state: vec![
644                CpuCoreState { user: 20.0, system: 3.0, iowait: 2.0, idle: 75.0 },
645                CpuCoreState { user: 25.0, system: 3.0, iowait: 2.0, idle: 70.0 },
646                CpuCoreState { user: 28.0, system: 4.0, iowait: 3.0, idle: 65.0 },
647                CpuCoreState { user: 32.0, system: 5.0, iowait: 3.0, idle: 60.0 },
648                CpuCoreState { user: 35.0, system: 6.0, iowait: 4.0, idle: 55.0 },
649                CpuCoreState { user: 38.0, system: 7.0, iowait: 5.0, idle: 50.0 },
650                CpuCoreState { user: 42.0, system: 8.0, iowait: 5.0, idle: 45.0 },
651                CpuCoreState { user: 45.0, system: 9.0, iowait: 6.0, idle: 40.0 },
652            ],
653            freq_history: {
654                let mut h = Vec::with_capacity(60);
655                h.extend_from_slice(&[3200.0, 3400.0, 3600.0, 3800.0, 4000.0, 4200.0]);
656                h
657            },
658            top_process_per_core: vec![
659                TopProcessForCore { pid: 1234, name: "firefox".to_string(), cpu_percent: 15.0 },
660                TopProcessForCore { pid: 5678, name: "chrome".to_string(), cpu_percent: 12.0 },
661                TopProcessForCore { pid: 9012, name: "code".to_string(), cpu_percent: 10.0 },
662                TopProcessForCore { pid: 3456, name: "rustc".to_string(), cpu_percent: 25.0 },
663                TopProcessForCore { pid: 7890, name: "cargo".to_string(), cpu_percent: 20.0 },
664                TopProcessForCore { pid: 1111, name: "node".to_string(), cpu_percent: 8.0 },
665                TopProcessForCore { pid: 2222, name: "python".to_string(), cpu_percent: 6.0 },
666                TopProcessForCore { pid: 3333, name: "java".to_string(), cpu_percent: 5.0 },
667            ],
668            thermal_throttle_active: Some(false),
669
670            // Memory Exploded Mode Features (mock data)
671            mem_pressure_history: {
672                let mut h = Vec::with_capacity(60);
673                h.extend_from_slice(&[5.0, 8.0, 12.0, 15.0, 10.0, 7.0]);
674                h
675            },
676            mem_reclaim_rate: 1250.0,  // 1250 pages/sec
677            top_mem_consumers: vec![
678                TopMemConsumer { pid: 1234, name: "firefox".to_string(), mem_bytes: 2 * 1024 * 1024 * 1024, mem_percent: 12.5 },
679                TopMemConsumer { pid: 5678, name: "chrome".to_string(), mem_bytes: 1500 * 1024 * 1024, mem_percent: 9.4 },
680                TopMemConsumer { pid: 9012, name: "code".to_string(), mem_bytes: 800 * 1024 * 1024, mem_percent: 5.0 },
681                TopMemConsumer { pid: 3456, name: "slack".to_string(), mem_bytes: 600 * 1024 * 1024, mem_percent: 3.8 },
682            ],
683            swap_trend: SwapTrend::Stable,
684
685            // Disk Exploded Mode Features (mock data)
686            disk_latency_history: {
687                let mut h = Vec::with_capacity(60);
688                h.extend_from_slice(&[2.5, 3.0, 4.5, 8.0, 5.5, 3.2]);
689                h
690            },
691            disk_read_iops: 1250.0,
692            disk_write_iops: 850.0,
693            disk_queue_depth: 2.5,
694            disk_health: vec![
695                DiskHealthStatus {
696                    device: "nvme0n1".to_string(),
697                    status: DiskHealth::Good,
698                    temperature: Some(42.0),
699                    reallocated_sectors: 0,
700                },
701            ],
702
703            mem_total: 16 * 1024 * 1024 * 1024,  // 16 GB
704            mem_used: 10 * 1024 * 1024 * 1024,   // 10 GB
705            mem_available: 6 * 1024 * 1024 * 1024, // 6 GB
706            mem_cached: 3 * 1024 * 1024 * 1024,  // 3 GB
707            mem_free: 2 * 1024 * 1024 * 1024,    // 2 GB
708            swap_total: 4 * 1024 * 1024 * 1024,  // 4 GB
709            swap_used: 500 * 1024 * 1024,        // 500 MB
710
711            net_rx_total: 1024 * 1024 * 1024,    // 1 GB
712            net_tx_total: 512 * 1024 * 1024,     // 512 MB
713            net_interface_ip: "192.168.1.100".to_string(),
714
715            net_rx_peak: 100_000_000.0,  // 100 MB/s
716            net_tx_peak: 50_000_000.0,   // 50 MB/s
717            net_rx_peak_time: Instant::now(),
718            net_tx_peak_time: Instant::now(),
719
720            // Network Exploded Mode Features (mock data)
721            net_errors: 5,
722            net_drops: 2,
723            net_established: 42,
724            net_listening: 15,
725
726            panels: PanelVisibility::default(),
727            process_selected: 0,
728            process_scroll_offset: 0,
729            sort_column: ProcessSortColumn::Cpu,
730            sort_descending: true,
731            filter: String::new(),
732            show_filter_input: false,
733            show_help: false,
734            show_tree: false,
735
736            show_signal_menu: false,
737            pending_signal: None,
738            signal_result: None,
739
740            focused_panel: None,
741            exploded_panel: None,
742
743            files_view_mode: crate::state::FilesViewMode::default(),
744
745            frame_id: 100,
746            last_collect: Instant::now(),
747            avg_frame_time_us: 1000,
748            max_frame_time_us: 2000,
749            show_fps: false,
750
751            deterministic: true,
752
753            // Populate mock data for testing hardware-dependent panel rendering
754            mock_gpus: vec![
755                MockGpuData {
756                    name: "NVIDIA RTX 4090".to_string(),
757                    gpu_util: 75.0,
758                    vram_used: 20 * 1024 * 1024 * 1024, // 20 GB
759                    vram_total: 24 * 1024 * 1024 * 1024, // 24 GB
760                    temperature: 72.0,
761                    power_watts: 350,
762                    power_limit_watts: 450,
763                    clock_mhz: 2520,
764                    history: vec![0.65, 0.70, 0.75, 0.80, 0.75, 0.70, 0.75, 0.80],
765                },
766                MockGpuData {
767                    name: "NVIDIA RTX 3080".to_string(),
768                    gpu_util: 45.0,
769                    vram_used: 6 * 1024 * 1024 * 1024, // 6 GB
770                    vram_total: 10 * 1024 * 1024 * 1024, // 10 GB
771                    temperature: 65.0,
772                    power_watts: 220,
773                    power_limit_watts: 320,
774                    clock_mhz: 1950,
775                    history: vec![0.40, 0.45, 0.50, 0.45, 0.40, 0.45, 0.50, 0.45],
776                },
777            ],
778            mock_battery: Some(MockBatteryData {
779                percent: 72.5,
780                charging: false,
781                time_remaining_mins: Some(185),
782                power_watts: 15.2,
783                health_percent: 94.0,
784                cycle_count: 342,
785            }),
786            mock_sensors: vec![
787                MockSensorData {
788                    name: "coretemp/temp1".to_string(),
789                    label: "Package".to_string(),
790                    value: 65.0,
791                    max: Some(100.0),
792                    crit: Some(105.0),
793                    sensor_type: MockSensorType::Temperature,
794                },
795                MockSensorData {
796                    name: "coretemp/temp2".to_string(),
797                    label: "Core 0".to_string(),
798                    value: 62.0,
799                    max: Some(100.0),
800                    crit: Some(105.0),
801                    sensor_type: MockSensorType::Temperature,
802                },
803                MockSensorData {
804                    name: "coretemp/temp3".to_string(),
805                    label: "Core 1".to_string(),
806                    value: 64.0,
807                    max: Some(100.0),
808                    crit: Some(105.0),
809                    sensor_type: MockSensorType::Temperature,
810                },
811                MockSensorData {
812                    name: "nct6798/fan1".to_string(),
813                    label: "CPU Fan".to_string(),
814                    value: 1200.0,
815                    max: Some(3000.0),
816                    crit: None,
817                    sensor_type: MockSensorType::Fan,
818                },
819                MockSensorData {
820                    name: "nct6798/fan2".to_string(),
821                    label: "Chassis Fan".to_string(),
822                    value: 800.0,
823                    max: Some(2000.0),
824                    crit: None,
825                    sensor_type: MockSensorType::Fan,
826                },
827                MockSensorData {
828                    name: "nct6798/in0".to_string(),
829                    label: "Vcore".to_string(),
830                    value: 1.25,
831                    max: Some(1.50),
832                    crit: None,
833                    sensor_type: MockSensorType::Voltage,
834                },
835            ],
836            mock_containers: vec![
837                MockContainerData {
838                    name: "nginx-proxy".to_string(),
839                    status: "running".to_string(),
840                    cpu_percent: 2.5,
841                    mem_used: 128 * 1024 * 1024, // 128 MB
842                    mem_limit: 512 * 1024 * 1024, // 512 MB
843                },
844                MockContainerData {
845                    name: "postgres-db".to_string(),
846                    status: "running".to_string(),
847                    cpu_percent: 8.2,
848                    mem_used: 512 * 1024 * 1024, // 512 MB
849                    mem_limit: 2 * 1024 * 1024 * 1024, // 2 GB
850                },
851                MockContainerData {
852                    name: "redis-cache".to_string(),
853                    status: "running".to_string(),
854                    cpu_percent: 1.1,
855                    mem_used: 64 * 1024 * 1024, // 64 MB
856                    mem_limit: 256 * 1024 * 1024, // 256 MB
857                },
858            ],
859        }
860    }
861
862    /// Collect metrics from all collectors
863    pub fn collect_metrics(&mut self) {
864        use trueno_viz::monitor::debug::{self, Level};
865
866        self.frame_id += 1;
867
868        // Skip real collection in deterministic/mock mode - data is pre-populated
869        if self.deterministic {
870            return;
871        }
872
873        let is_first = self.frame_id <= 2;
874
875        // CPU
876        if is_first { debug::log(Level::Trace, "collect", "cpu..."); }
877        if self.cpu.is_available() {
878            if let Ok(metrics) = self.cpu.collect() {
879                if let Some(total) = metrics.get_gauge("cpu.total") {
880                    Self::push_to_history(&mut self.cpu_history, total / 100.0);
881                }
882
883                // Per-core percentages
884                self.per_core_percent.clear();
885                for i in 0..self.cpu.core_count() {
886                    if let Some(percent) = metrics.get_gauge(&format!("cpu.core.{i}")) {
887                        self.per_core_percent.push(percent);
888                    }
889                }
890            }
891        }
892
893        // Memory
894        if is_first { debug::log(Level::Trace, "collect", "memory..."); }
895        if self.memory.is_available() {
896            if let Ok(metrics) = self.memory.collect() {
897                // Cache raw values first
898                if let Some(total) = metrics.get_counter("memory.total") {
899                    self.mem_total = total;
900                }
901                if let Some(used) = metrics.get_counter("memory.used") {
902                    self.mem_used = used;
903                }
904                if let Some(available) = metrics.get_counter("memory.available") {
905                    self.mem_available = available;
906                }
907                if let Some(cached) = metrics.get_counter("memory.cached") {
908                    self.mem_cached = cached;
909                }
910                if let Some(free) = metrics.get_counter("memory.free") {
911                    self.mem_free = free;
912                }
913                if let Some(swap_total) = metrics.get_counter("memory.swap.total") {
914                    self.swap_total = swap_total;
915                }
916                if let Some(swap_used) = metrics.get_counter("memory.swap.used") {
917                    self.swap_used = swap_used;
918                }
919
920                // Track history for all memory metrics (normalized 0-1 relative to total)
921                if self.mem_total > 0 {
922                    let total = self.mem_total as f64;
923
924                    // Used percentage history
925                    if let Some(percent) = metrics.get_gauge("memory.used.percent") {
926                        Self::push_to_history(&mut self.mem_history, percent / 100.0);
927                    }
928
929                    // Available percentage history
930                    let avail_pct = self.mem_available as f64 / total;
931                    Self::push_to_history(&mut self.mem_available_history, avail_pct);
932
933                    // Cached percentage history
934                    let cached_pct = self.mem_cached as f64 / total;
935                    Self::push_to_history(&mut self.mem_cached_history, cached_pct);
936
937                    // Free percentage history
938                    let free_pct = self.mem_free as f64 / total;
939                    Self::push_to_history(&mut self.mem_free_history, free_pct);
940                }
941
942                // Swap percentage history
943                if let Some(swap_percent) = metrics.get_gauge("memory.swap.percent") {
944                    Self::push_to_history(&mut self.swap_history, swap_percent / 100.0);
945                }
946            }
947        }
948
949        // Network
950        if is_first { debug::log(Level::Trace, "collect", "network..."); }
951        if self.network.is_available() {
952            let _ = self.network.collect();
953            if let Some(iface) = self.network.current_interface() {
954                if let Some(rates) = self.network.all_rates().get(iface) {
955                    // Normalize to 0-1 range (assume max 1 GB/s for scaling)
956                    let rx_norm = (rates.rx_bytes_per_sec / 1_000_000_000.0).min(1.0);
957                    let tx_norm = (rates.tx_bytes_per_sec / 1_000_000_000.0).min(1.0);
958                    Self::push_to_history(&mut self.net_rx_history, rx_norm);
959                    Self::push_to_history(&mut self.net_tx_history, tx_norm);
960
961                    // Accumulate total bytes (approximate from rates)
962                    // This is reset on app start, but gives session totals
963                    self.net_rx_total += rates.rx_bytes_per_sec as u64;
964                    self.net_tx_total += rates.tx_bytes_per_sec as u64;
965
966                    // Track peak rates
967                    if rates.rx_bytes_per_sec > self.net_rx_peak {
968                        self.net_rx_peak = rates.rx_bytes_per_sec;
969                        self.net_rx_peak_time = Instant::now();
970                    }
971                    if rates.tx_bytes_per_sec > self.net_tx_peak {
972                        self.net_tx_peak = rates.tx_bytes_per_sec;
973                        self.net_tx_peak_time = Instant::now();
974                    }
975                }
976            }
977        }
978
979        // Disk
980        if is_first { debug::log(Level::Trace, "collect", "disk..."); }
981        if self.disk.is_available() {
982            let _ = self.disk.collect();
983        }
984
985        // Process
986        if is_first { debug::log(Level::Trace, "collect", "process..."); }
987        if self.process.is_available() {
988            let _ = self.process.collect();
989        }
990
991        // Sensors
992        if is_first { debug::log(Level::Trace, "collect", "sensors..."); }
993        if self.sensors.is_available() {
994            let _ = self.sensors.collect();
995        }
996
997        // Battery
998        if is_first { debug::log(Level::Trace, "collect", "battery..."); }
999        if self.battery.is_available() {
1000            let _ = self.battery.collect();
1001        }
1002
1003        // GPU
1004        if is_first { debug::log(Level::Trace, "collect", "gpu..."); }
1005        #[cfg(feature = "nvidia")]
1006        if self.nvidia_gpu.is_available() {
1007            let _ = self.nvidia_gpu.collect();
1008        }
1009
1010        #[cfg(target_os = "linux")]
1011        if self.amd_gpu.is_available() {
1012            let _ = self.amd_gpu.collect();
1013        }
1014
1015        #[cfg(target_os = "macos")]
1016        if self.apple_gpu.is_available() {
1017            let _ = self.apple_gpu.collect();
1018        }
1019
1020        // Advanced analyzers (ttop-improve.md spec)
1021        if is_first { debug::log(Level::Trace, "collect", "swap_analyzer..."); }
1022        self.swap_analyzer.collect();
1023
1024        if is_first { debug::log(Level::Trace, "collect", "disk_io_analyzer..."); }
1025        self.disk_io_analyzer.collect();
1026
1027        // Disk entropy analysis (rate-limited internally)
1028        if is_first { debug::log(Level::Trace, "collect", "disk_entropy..."); }
1029        let mount_paths: Vec<String> = self.disk.mounts().iter().map(|m| m.mount_point.clone()).collect();
1030        self.disk_entropy.collect(&mount_paths);
1031
1032        if is_first { debug::log(Level::Trace, "collect", "storage_analyzer..."); }
1033        self.storage_analyzer.collect();
1034
1035        if is_first { debug::log(Level::Trace, "collect", "connection_analyzer..."); }
1036        self.connection_analyzer.collect();
1037
1038        if is_first { debug::log(Level::Trace, "collect", "treemap_analyzer..."); }
1039        self.treemap_analyzer.collect();
1040
1041        if is_first { debug::log(Level::Trace, "collect", "gpu_process_analyzer..."); }
1042        self.gpu_process_analyzer.collect();
1043
1044        if is_first { debug::log(Level::Trace, "collect", "psi_analyzer..."); }
1045        self.psi_analyzer.collect();
1046
1047        if is_first { debug::log(Level::Trace, "collect", "container_analyzer..."); }
1048        self.container_analyzer.collect();
1049
1050        // Linux network stats (protocol counts, errors, latency)
1051        #[cfg(target_os = "linux")]
1052        {
1053            if is_first { debug::log(Level::Trace, "collect", "network_stats..."); }
1054            self.network_stats.collect();
1055        }
1056
1057        // Extended process info (cgroups, FDs, CPU history)
1058        if is_first { debug::log(Level::Trace, "collect", "process_extra..."); }
1059        let pids: Vec<u32> = self.process.processes().keys().copied().collect();
1060        let cpu_percents: std::collections::HashMap<u32, f64> = self.process.processes()
1061            .iter()
1062            .map(|(&pid, p)| (pid, p.cpu_percent))
1063            .collect();
1064        self.process_extra.collect(&pids, &cpu_percents);
1065
1066        // File analyzer for treemap enhancements (rate-limited internally)
1067        if is_first { debug::log(Level::Trace, "collect", "file_analyzer..."); }
1068        self.file_analyzer.collect("/");
1069
1070        // Sensor health analysis (outliers, drift, staleness)
1071        if is_first { debug::log(Level::Trace, "collect", "sensor_health..."); }
1072        let _ = self.sensor_health.collect();
1073
1074        self.last_collect = Instant::now();
1075    }
1076
1077    fn push_to_history(history: &mut Vec<f64>, value: f64) {
1078        history.push(value);
1079        if history.len() > 300 {
1080            history.remove(0);
1081        }
1082    }
1083
1084    /// Update frame timing statistics
1085    pub fn update_frame_stats(&mut self, frame_times: &[Duration]) {
1086        if frame_times.is_empty() {
1087            return;
1088        }
1089
1090        let total: u128 = frame_times.iter().map(|d| d.as_micros()).sum();
1091        self.avg_frame_time_us = (total / frame_times.len() as u128) as u64;
1092        self.max_frame_time_us = frame_times
1093            .iter()
1094            .map(|d| d.as_micros() as u64)
1095            .max()
1096            .unwrap_or(0);
1097    }
1098
1099    /// Handle keyboard input. Returns true if app should quit.
1100    pub fn handle_key(&mut self, code: KeyCode, modifiers: KeyModifiers) -> bool {
1101        // Signal confirmation mode (Y/n prompt)
1102        if self.pending_signal.is_some() {
1103            match code {
1104                KeyCode::Char('y') | KeyCode::Char('Y') | KeyCode::Enter => {
1105                    self.confirm_signal();
1106                }
1107                KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
1108                    self.cancel_signal();
1109                }
1110                _ => {}
1111            }
1112            return false;
1113        }
1114
1115        // Signal menu mode (pick signal type)
1116        if self.show_signal_menu {
1117            match code {
1118                KeyCode::Esc => {
1119                    self.show_signal_menu = false;
1120                }
1121                KeyCode::Char('x') => {
1122                    self.show_signal_menu = false;
1123                    self.request_signal(SignalType::Term);
1124                }
1125                KeyCode::Char('K') => {
1126                    self.show_signal_menu = false;
1127                    self.request_signal(SignalType::Kill);
1128                }
1129                KeyCode::Char('H') => {
1130                    self.show_signal_menu = false;
1131                    self.request_signal(SignalType::Hup);
1132                }
1133                KeyCode::Char('i') => {
1134                    self.show_signal_menu = false;
1135                    self.request_signal(SignalType::Int);
1136                }
1137                KeyCode::Char('p') => {
1138                    self.show_signal_menu = false;
1139                    self.request_signal(SignalType::Stop);
1140                }
1141                KeyCode::Char('c') => {
1142                    self.show_signal_menu = false;
1143                    self.request_signal(SignalType::Cont);
1144                }
1145                _ => {}
1146            }
1147            return false;
1148        }
1149
1150        // Filter input mode
1151        if self.show_filter_input {
1152            match code {
1153                KeyCode::Esc => {
1154                    self.show_filter_input = false;
1155                    self.filter.clear();
1156                }
1157                KeyCode::Enter => {
1158                    self.show_filter_input = false;
1159                }
1160                KeyCode::Backspace => {
1161                    self.filter.pop();
1162                }
1163                KeyCode::Char(c) => {
1164                    self.filter.push(c);
1165                }
1166                _ => {}
1167            }
1168            return false;
1169        }
1170
1171        // ESC handling: exit explode -> clear focus -> quit
1172        if code == KeyCode::Esc {
1173            if self.exploded_panel.is_some() {
1174                self.exploded_panel = None;
1175                return false;
1176            }
1177            if self.focused_panel.is_some() {
1178                self.focused_panel = None;
1179                return false;
1180            }
1181            return true; // Quit
1182        }
1183
1184        // Ctrl+C always quits
1185        if code == KeyCode::Char('c') && modifiers.contains(KeyModifiers::CONTROL) {
1186            return true;
1187        }
1188
1189        // EXPLODED MODE: pass most keys through to panel controls
1190        // Only handle exit keys (Enter/z/ESC already handled above for ESC)
1191        if self.exploded_panel.is_some() {
1192            match code {
1193                // Exit explode with Enter or z
1194                KeyCode::Enter | KeyCode::Char('z') => {
1195                    self.exploded_panel = None;
1196                    return false;
1197                }
1198                // All other keys fall through to normal handling (process nav, sort, etc.)
1199                _ => {}
1200            }
1201        }
1202        // FOCUSED MODE (not exploded): arrow/hjkl navigate between panels
1203        else if self.focused_panel.is_some() {
1204            match code {
1205                // Explode with Enter or z
1206                KeyCode::Enter | KeyCode::Char('z') => {
1207                    if let Some(panel) = self.focused_panel {
1208                        self.exploded_panel = Some(panel);
1209                    }
1210                    return false;
1211                }
1212                // Arrow navigation between panels
1213                KeyCode::Left | KeyCode::Right | KeyCode::Up | KeyCode::Down => {
1214                    self.navigate_panel_focus(code);
1215                    return false;
1216                }
1217                // hjkl navigation between panels
1218                KeyCode::Char('h') => {
1219                    self.navigate_panel_focus(KeyCode::Left);
1220                    return false;
1221                }
1222                KeyCode::Char('l') => {
1223                    self.navigate_panel_focus(KeyCode::Right);
1224                    return false;
1225                }
1226                KeyCode::Char('j') => {
1227                    self.navigate_panel_focus(KeyCode::Down);
1228                    return false;
1229                }
1230                KeyCode::Char('k') => {
1231                    self.navigate_panel_focus(KeyCode::Up);
1232                    return false;
1233                }
1234                _ => {}
1235            }
1236        }
1237        // NOT FOCUSED: h/l start panel focus, j/k navigate process list
1238        else {
1239            match code {
1240                KeyCode::Char('h') => {
1241                    self.focused_panel = Some(self.first_visible_panel());
1242                    return false;
1243                }
1244                KeyCode::Char('l') => {
1245                    self.focused_panel = Some(self.first_visible_panel());
1246                    return false;
1247                }
1248                _ => {}
1249            }
1250        }
1251
1252        match code {
1253            // Quit
1254            KeyCode::Char('q') => return true,
1255
1256            // Help
1257            KeyCode::Char('?') | KeyCode::F(1) => self.show_help = !self.show_help,
1258
1259            // Panel toggles (1-8)
1260            KeyCode::Char('1') => self.panels.cpu = !self.panels.cpu,
1261            KeyCode::Char('2') => self.panels.memory = !self.panels.memory,
1262            KeyCode::Char('3') => self.panels.disk = !self.panels.disk,
1263            KeyCode::Char('4') => self.panels.network = !self.panels.network,
1264            KeyCode::Char('5') => self.panels.process = !self.panels.process,
1265            KeyCode::Char('6') => self.panels.gpu = !self.panels.gpu,
1266            KeyCode::Char('7') => self.panels.battery = !self.panels.battery,
1267            KeyCode::Char('8') => self.panels.sensors = !self.panels.sensors,
1268            KeyCode::Char('9') => self.panels.files = !self.panels.files,
1269
1270            // Files view mode cycle (v = view mode: SIZE -> ENTROPY -> I/O)
1271            KeyCode::Char('v') => self.files_view_mode = self.files_view_mode.next(),
1272
1273            // Process navigation (when no panel focused, or when exploded)
1274            KeyCode::Down if self.focused_panel.is_none() || self.exploded_panel.is_some() => {
1275                self.navigate_process(1)
1276            }
1277            KeyCode::Up if self.focused_panel.is_none() || self.exploded_panel.is_some() => {
1278                self.navigate_process(-1)
1279            }
1280            // j/k for process navigation when exploded
1281            KeyCode::Char('j') if self.exploded_panel.is_some() => self.navigate_process(1),
1282            KeyCode::Char('k') if self.exploded_panel.is_some() => self.navigate_process(-1),
1283            KeyCode::PageDown => self.navigate_process(10),
1284            KeyCode::PageUp => self.navigate_process(-10),
1285            KeyCode::Home | KeyCode::Char('g') => self.process_selected = 0,
1286            KeyCode::Char('G') => {
1287                let count = self.process_count();
1288                if count > 0 {
1289                    self.process_selected = count - 1;
1290                }
1291            }
1292
1293            // Sorting
1294            KeyCode::Tab | KeyCode::Char('s') => {
1295                self.sort_column = self.sort_column.next();
1296            }
1297            KeyCode::Char('r') => self.sort_descending = !self.sort_descending,
1298
1299            // Tree view
1300            KeyCode::Char('t') => self.show_tree = !self.show_tree,
1301
1302            // Signal menu (kill process) - 'k' key opens signal menu
1303            // Quick kill shortcuts (no menu): x=TERM, X=KILL
1304            KeyCode::Char('X') if self.focused_panel.is_none() || self.exploded_panel == Some(PanelType::Process) => {
1305                // Quick SIGKILL (uppercase X)
1306                self.request_signal(SignalType::Kill);
1307            }
1308            KeyCode::Char('x') if self.focused_panel.is_none() => {
1309                // Quick SIGTERM (lowercase x)
1310                self.request_signal(SignalType::Term);
1311            }
1312
1313            // z key starts focus when nothing is focused/exploded
1314            KeyCode::Char('z') if self.focused_panel.is_none() && self.exploded_panel.is_none() => {
1315                self.focused_panel = Some(self.first_visible_panel());
1316            }
1317
1318            // Filter
1319            KeyCode::Char('f') | KeyCode::Char('/') => {
1320                self.show_filter_input = true;
1321            }
1322            KeyCode::Delete => self.filter.clear(),
1323
1324            // Reset view
1325            KeyCode::Char('0') => {
1326                self.panels = PanelVisibility::default();
1327                self.focused_panel = None;
1328                self.exploded_panel = None;
1329            }
1330
1331            _ => {}
1332        }
1333
1334        false
1335    }
1336
1337    /// Navigate panel focus with arrow keys
1338    fn navigate_panel_focus(&mut self, direction: KeyCode) {
1339        let visible = self.visible_panels();
1340        if visible.is_empty() {
1341            return;
1342        }
1343
1344        let current = self.focused_panel.unwrap_or_else(|| self.first_visible_panel());
1345        let current_idx = visible.iter().position(|&p| p == current).unwrap_or(0);
1346
1347        let new_idx = match direction {
1348            KeyCode::Left | KeyCode::Up => {
1349                if current_idx == 0 {
1350                    visible.len() - 1
1351                } else {
1352                    current_idx - 1
1353                }
1354            }
1355            KeyCode::Right | KeyCode::Down => {
1356                if current_idx >= visible.len() - 1 {
1357                    0
1358                } else {
1359                    current_idx + 1
1360                }
1361            }
1362            _ => current_idx,
1363        };
1364
1365        self.focused_panel = Some(visible[new_idx]);
1366    }
1367
1368    /// Get list of currently visible panels
1369    pub fn visible_panels(&self) -> Vec<PanelType> {
1370        let mut visible = Vec::new();
1371        if self.panels.cpu {
1372            visible.push(PanelType::Cpu);
1373        }
1374        if self.panels.memory {
1375            visible.push(PanelType::Memory);
1376        }
1377        if self.panels.disk {
1378            visible.push(PanelType::Disk);
1379        }
1380        if self.panels.network {
1381            visible.push(PanelType::Network);
1382        }
1383        if self.panels.process {
1384            visible.push(PanelType::Process);
1385        }
1386        if self.panels.gpu && self.has_gpu() {
1387            visible.push(PanelType::Gpu);
1388        }
1389        if self.panels.battery && self.battery.is_available() {
1390            visible.push(PanelType::Battery);
1391        }
1392        if self.panels.sensors && self.sensors.is_available() {
1393            visible.push(PanelType::Sensors);
1394        }
1395        if self.panels.files {
1396            visible.push(PanelType::Files);
1397        }
1398        visible
1399    }
1400
1401    /// Get first visible panel (for default focus)
1402    fn first_visible_panel(&self) -> PanelType {
1403        self.visible_panels().first().copied().unwrap_or(PanelType::Cpu)
1404    }
1405
1406    /// Check if a specific panel is visible
1407    pub fn is_panel_visible(&self, panel: PanelType) -> bool {
1408        match panel {
1409            PanelType::Cpu => self.panels.cpu,
1410            PanelType::Memory => self.panels.memory,
1411            PanelType::Disk => self.panels.disk,
1412            PanelType::Network => self.panels.network,
1413            PanelType::Process => self.panels.process,
1414            PanelType::Gpu => self.panels.gpu && self.has_gpu(),
1415            PanelType::Battery => self.panels.battery && self.battery.is_available(),
1416            PanelType::Sensors => self.panels.sensors && self.sensors.is_available(),
1417            PanelType::Files => self.panels.files,
1418        }
1419    }
1420
1421    fn navigate_process(&mut self, delta: isize) {
1422        let count = self.process_count();
1423        if count == 0 {
1424            return;
1425        }
1426
1427        let current = self.process_selected;
1428        let new = if delta > 0 {
1429            (current + delta as usize).min(count - 1)
1430        } else {
1431            current.saturating_sub((-delta) as usize)
1432        };
1433        self.process_selected = new;
1434    }
1435
1436    fn process_count(&self) -> usize {
1437        let filter_lower = self.filter.to_lowercase();
1438        self.process
1439            .processes()
1440            .values()
1441            .filter(|p| {
1442                if filter_lower.is_empty() {
1443                    true
1444                } else {
1445                    // Use allocation-free case-insensitive contains
1446                    contains_ignore_case(&p.name, &filter_lower)
1447                        || contains_ignore_case(&p.cmdline, &filter_lower)
1448                }
1449            })
1450            .count()
1451    }
1452
1453    /// Get sorted and filtered processes
1454    pub fn sorted_processes(&self) -> Vec<&trueno_viz::monitor::collectors::process::ProcessInfo> {
1455        // Cache lowercase filter once per call
1456        let filter_lower = self.filter.to_lowercase();
1457
1458        let mut procs: Vec<_> = self
1459            .process
1460            .processes()
1461            .values()
1462            .filter(|p| {
1463                if filter_lower.is_empty() {
1464                    true
1465                } else {
1466                    // Use allocation-free case-insensitive contains
1467                    contains_ignore_case(&p.name, &filter_lower)
1468                        || contains_ignore_case(&p.cmdline, &filter_lower)
1469                }
1470            })
1471            .collect();
1472
1473        procs.sort_by(|a, b| {
1474            let cmp = match self.sort_column {
1475                ProcessSortColumn::Pid => a.pid.cmp(&b.pid),
1476                ProcessSortColumn::Name => a.name.cmp(&b.name),
1477                ProcessSortColumn::Cpu => a
1478                    .cpu_percent
1479                    .partial_cmp(&b.cpu_percent)
1480                    .unwrap_or(std::cmp::Ordering::Equal),
1481                ProcessSortColumn::Mem => a
1482                    .mem_percent
1483                    .partial_cmp(&b.mem_percent)
1484                    .unwrap_or(std::cmp::Ordering::Equal),
1485                ProcessSortColumn::State => a.state.as_char().cmp(&b.state.as_char()),
1486                ProcessSortColumn::User => a.user.cmp(&b.user),
1487                ProcessSortColumn::Threads => a.threads.cmp(&b.threads),
1488            };
1489            if self.sort_descending {
1490                cmp.reverse()
1491            } else {
1492                cmp
1493            }
1494        });
1495
1496        procs
1497    }
1498
1499    /// Check if any GPU is available
1500    pub fn has_gpu(&self) -> bool {
1501        #[cfg(feature = "nvidia")]
1502        if self.nvidia_gpu.is_available() {
1503            return true;
1504        }
1505
1506        #[cfg(target_os = "linux")]
1507        if self.amd_gpu.is_available() {
1508            return true;
1509        }
1510
1511        #[cfg(target_os = "macos")]
1512        if self.apple_gpu.is_available() {
1513            return true;
1514        }
1515
1516        false
1517    }
1518
1519    /// Send a signal to a process
1520    #[cfg(unix)]
1521    pub fn send_signal(&mut self, pid: u32, signal: SignalType) -> Result<(), String> {
1522        use std::process::Command;
1523
1524        let signal_num = signal.number();
1525        let result = Command::new("kill")
1526            .arg(format!("-{}", signal_num))
1527            .arg(pid.to_string())
1528            .output();
1529
1530        match result {
1531            Ok(output) => {
1532                if output.status.success() {
1533                    self.signal_result = Some((
1534                        true,
1535                        format!("Sent {} to PID {}", signal.name(), pid),
1536                        Instant::now(),
1537                    ));
1538                    Ok(())
1539                } else {
1540                    let stderr = String::from_utf8_lossy(&output.stderr);
1541                    let msg = format!("Failed to send {} to {}: {}", signal.name(), pid, stderr.trim());
1542                    self.signal_result = Some((false, msg.clone(), Instant::now()));
1543                    Err(msg)
1544                }
1545            }
1546            Err(e) => {
1547                let msg = format!("Failed to execute kill: {}", e);
1548                self.signal_result = Some((false, msg.clone(), Instant::now()));
1549                Err(msg)
1550            }
1551        }
1552    }
1553
1554    #[cfg(not(unix))]
1555    pub fn send_signal(&mut self, _pid: u32, _signal: SignalType) -> Result<(), String> {
1556        Err("Signal sending not supported on this platform".to_string())
1557    }
1558
1559    /// Get the currently selected process info (pid, name)
1560    pub fn selected_process(&self) -> Option<(u32, String)> {
1561        let procs = self.sorted_processes();
1562        procs.get(self.process_selected).map(|p| (p.pid, p.name.clone()))
1563    }
1564
1565    /// Request to send a signal to the selected process (shows confirmation)
1566    pub fn request_signal(&mut self, signal: SignalType) {
1567        if let Some((pid, name)) = self.selected_process() {
1568            self.pending_signal = Some((pid, name, signal));
1569        }
1570    }
1571
1572    /// Confirm and send the pending signal
1573    pub fn confirm_signal(&mut self) {
1574        if let Some((pid, _name, signal)) = self.pending_signal.take() {
1575            let _ = self.send_signal(pid, signal);
1576        }
1577    }
1578
1579    /// Cancel the pending signal
1580    pub fn cancel_signal(&mut self) {
1581        self.pending_signal = None;
1582    }
1583
1584    /// Clear old signal results (after 3 seconds)
1585    pub fn clear_old_signal_result(&mut self) {
1586        if let Some((_, _, timestamp)) = &self.signal_result {
1587            if timestamp.elapsed() > Duration::from_secs(3) {
1588                self.signal_result = None;
1589            }
1590        }
1591    }
1592}
1593
1594#[cfg(test)]
1595mod tests {
1596    use super::*;
1597    use crossterm::event::{KeyCode, KeyModifiers};
1598
1599    #[test]
1600    fn test_panel_visibility_default() {
1601        let vis = PanelVisibility::default();
1602        assert!(vis.cpu);
1603        assert!(vis.memory);
1604        assert!(vis.disk);
1605        assert!(vis.network);
1606        assert!(vis.process);
1607        assert!(vis.gpu);
1608        assert!(vis.battery);
1609        assert!(vis.sensors);
1610        assert!(!vis.files); // Off by default
1611    }
1612
1613    #[test]
1614    fn test_mock_app_creation() {
1615        let app = App::new_mock();
1616        assert!(app.deterministic);
1617        assert_eq!(app.frame_id, 100);
1618        assert_eq!(app.avg_frame_time_us, 1000);
1619        assert_eq!(app.max_frame_time_us, 2000);
1620        assert!(!app.show_fps);
1621    }
1622
1623    #[test]
1624    fn test_mock_app_history_populated() {
1625        let app = App::new_mock();
1626        assert_eq!(app.cpu_history.len(), 8);
1627        assert_eq!(app.mem_history.len(), 8);
1628        assert_eq!(app.per_core_percent.len(), 8);
1629    }
1630
1631    #[test]
1632    fn test_mock_app_memory_values() {
1633        let app = App::new_mock();
1634        assert_eq!(app.mem_total, 16 * 1024 * 1024 * 1024);
1635        assert_eq!(app.mem_used, 10 * 1024 * 1024 * 1024);
1636        assert_eq!(app.mem_available, 6 * 1024 * 1024 * 1024);
1637    }
1638
1639    #[test]
1640    fn test_update_frame_stats_empty() {
1641        let mut app = App::new_mock();
1642        app.update_frame_stats(&[]);
1643        // Should not panic, values unchanged
1644    }
1645
1646    #[test]
1647    fn test_update_frame_stats_single() {
1648        let mut app = App::new_mock();
1649        app.update_frame_stats(&[Duration::from_micros(500)]);
1650        assert_eq!(app.avg_frame_time_us, 500);
1651        assert_eq!(app.max_frame_time_us, 500);
1652    }
1653
1654    #[test]
1655    fn test_update_frame_stats_multiple() {
1656        let mut app = App::new_mock();
1657        let times = vec![
1658            Duration::from_micros(100),
1659            Duration::from_micros(200),
1660            Duration::from_micros(300),
1661        ];
1662        app.update_frame_stats(&times);
1663        assert_eq!(app.avg_frame_time_us, 200); // (100+200+300)/3
1664        assert_eq!(app.max_frame_time_us, 300);
1665    }
1666
1667    #[test]
1668    fn test_visible_panels_default() {
1669        let app = App::new_mock();
1670        let visible = app.visible_panels();
1671        // Default: cpu, memory, disk, network, process (not files, battery/sensors may vary)
1672        assert!(visible.contains(&PanelType::Cpu));
1673        assert!(visible.contains(&PanelType::Memory));
1674        assert!(visible.contains(&PanelType::Disk));
1675        assert!(visible.contains(&PanelType::Network));
1676        assert!(visible.contains(&PanelType::Process));
1677        assert!(!visible.contains(&PanelType::Files)); // Off by default
1678    }
1679
1680    #[test]
1681    fn test_visible_panels_with_files() {
1682        let mut app = App::new_mock();
1683        app.panels.files = true;
1684        let visible = app.visible_panels();
1685        assert!(visible.contains(&PanelType::Files));
1686    }
1687
1688    #[test]
1689    fn test_visible_panels_all_disabled() {
1690        let mut app = App::new_mock();
1691        app.panels.cpu = false;
1692        app.panels.memory = false;
1693        app.panels.disk = false;
1694        app.panels.network = false;
1695        app.panels.process = false;
1696        app.panels.gpu = false;
1697        app.panels.battery = false;
1698        app.panels.sensors = false;
1699        app.panels.files = false;
1700        let visible = app.visible_panels();
1701        assert!(visible.is_empty());
1702    }
1703
1704    #[test]
1705    fn test_first_visible_panel_default() {
1706        let app = App::new_mock();
1707        let first = app.first_visible_panel();
1708        assert_eq!(first, PanelType::Cpu);
1709    }
1710
1711    #[test]
1712    fn test_first_visible_panel_when_cpu_disabled() {
1713        let mut app = App::new_mock();
1714        app.panels.cpu = false;
1715        let first = app.first_visible_panel();
1716        assert_eq!(first, PanelType::Memory);
1717    }
1718
1719    #[test]
1720    fn test_is_panel_visible() {
1721        let app = App::new_mock();
1722        assert!(app.is_panel_visible(PanelType::Cpu));
1723        assert!(app.is_panel_visible(PanelType::Memory));
1724        assert!(!app.is_panel_visible(PanelType::Files));
1725    }
1726
1727    #[test]
1728    fn test_handle_key_quit_q() {
1729        let mut app = App::new_mock();
1730        let quit = app.handle_key(KeyCode::Char('q'), KeyModifiers::NONE);
1731        assert!(quit);
1732    }
1733
1734    #[test]
1735    fn test_handle_key_quit_ctrl_c() {
1736        let mut app = App::new_mock();
1737        let quit = app.handle_key(KeyCode::Char('c'), KeyModifiers::CONTROL);
1738        assert!(quit);
1739    }
1740
1741    #[test]
1742    fn test_handle_key_quit_esc() {
1743        let mut app = App::new_mock();
1744        let quit = app.handle_key(KeyCode::Esc, KeyModifiers::NONE);
1745        assert!(quit);
1746    }
1747
1748    #[test]
1749    fn test_handle_key_help_toggle() {
1750        let mut app = App::new_mock();
1751        assert!(!app.show_help);
1752        app.handle_key(KeyCode::Char('?'), KeyModifiers::NONE);
1753        assert!(app.show_help);
1754        app.handle_key(KeyCode::Char('?'), KeyModifiers::NONE);
1755        assert!(!app.show_help);
1756    }
1757
1758    #[test]
1759    fn test_handle_key_panel_toggles() {
1760        let mut app = App::new_mock();
1761
1762        // Toggle CPU off
1763        assert!(app.panels.cpu);
1764        app.handle_key(KeyCode::Char('1'), KeyModifiers::NONE);
1765        assert!(!app.panels.cpu);
1766
1767        // Toggle memory off
1768        assert!(app.panels.memory);
1769        app.handle_key(KeyCode::Char('2'), KeyModifiers::NONE);
1770        assert!(!app.panels.memory);
1771
1772        // Toggle files on (off by default)
1773        assert!(!app.panels.files);
1774        app.handle_key(KeyCode::Char('9'), KeyModifiers::NONE);
1775        assert!(app.panels.files);
1776    }
1777
1778    #[test]
1779    fn test_handle_key_filter_mode() {
1780        let mut app = App::new_mock();
1781        assert!(!app.show_filter_input);
1782
1783        // Enter filter mode
1784        app.handle_key(KeyCode::Char('/'), KeyModifiers::NONE);
1785        assert!(app.show_filter_input);
1786
1787        // Type some text
1788        app.handle_key(KeyCode::Char('t'), KeyModifiers::NONE);
1789        app.handle_key(KeyCode::Char('e'), KeyModifiers::NONE);
1790        app.handle_key(KeyCode::Char('s'), KeyModifiers::NONE);
1791        app.handle_key(KeyCode::Char('t'), KeyModifiers::NONE);
1792        assert_eq!(app.filter, "test");
1793
1794        // Backspace
1795        app.handle_key(KeyCode::Backspace, KeyModifiers::NONE);
1796        assert_eq!(app.filter, "tes");
1797
1798        // Escape clears and exits
1799        app.handle_key(KeyCode::Esc, KeyModifiers::NONE);
1800        assert!(!app.show_filter_input);
1801        assert_eq!(app.filter, "");
1802    }
1803
1804    #[test]
1805    fn test_handle_key_filter_enter_confirm() {
1806        let mut app = App::new_mock();
1807        app.handle_key(KeyCode::Char('f'), KeyModifiers::NONE);
1808        app.handle_key(KeyCode::Char('a'), KeyModifiers::NONE);
1809        app.handle_key(KeyCode::Enter, KeyModifiers::NONE);
1810        assert!(!app.show_filter_input);
1811        assert_eq!(app.filter, "a"); // Preserved
1812    }
1813
1814    #[test]
1815    fn test_handle_key_sort_toggle() {
1816        let mut app = App::new_mock();
1817        assert_eq!(app.sort_column, ProcessSortColumn::Cpu);
1818
1819        app.handle_key(KeyCode::Tab, KeyModifiers::NONE);
1820        assert_eq!(app.sort_column, ProcessSortColumn::Mem);
1821
1822        app.handle_key(KeyCode::Char('s'), KeyModifiers::NONE);
1823        assert_eq!(app.sort_column, ProcessSortColumn::State);
1824    }
1825
1826    #[test]
1827    fn test_handle_key_sort_reverse() {
1828        let mut app = App::new_mock();
1829        assert!(app.sort_descending);
1830        app.handle_key(KeyCode::Char('r'), KeyModifiers::NONE);
1831        assert!(!app.sort_descending);
1832    }
1833
1834    #[test]
1835    fn test_handle_key_tree_toggle() {
1836        let mut app = App::new_mock();
1837        assert!(!app.show_tree);
1838        app.handle_key(KeyCode::Char('t'), KeyModifiers::NONE);
1839        assert!(app.show_tree);
1840    }
1841
1842    #[test]
1843    fn test_handle_key_reset_view() {
1844        let mut app = App::new_mock();
1845        app.panels.cpu = false;
1846        app.focused_panel = Some(PanelType::Memory);
1847        app.exploded_panel = Some(PanelType::Disk);
1848
1849        app.handle_key(KeyCode::Char('0'), KeyModifiers::NONE);
1850
1851        assert!(app.panels.cpu);
1852        assert!(app.focused_panel.is_none());
1853        assert!(app.exploded_panel.is_none());
1854    }
1855
1856    #[test]
1857    fn test_handle_key_focus_start_h() {
1858        let mut app = App::new_mock();
1859        assert!(app.focused_panel.is_none());
1860
1861        app.handle_key(KeyCode::Char('h'), KeyModifiers::NONE);
1862        assert!(app.focused_panel.is_some());
1863    }
1864
1865    #[test]
1866    fn test_handle_key_focus_start_l() {
1867        let mut app = App::new_mock();
1868        assert!(app.focused_panel.is_none());
1869
1870        app.handle_key(KeyCode::Char('l'), KeyModifiers::NONE);
1871        assert!(app.focused_panel.is_some());
1872    }
1873
1874    #[test]
1875    fn test_handle_key_focus_navigation() {
1876        let mut app = App::new_mock();
1877        app.focused_panel = Some(PanelType::Cpu);
1878
1879        // Navigate right
1880        app.handle_key(KeyCode::Right, KeyModifiers::NONE);
1881        assert_eq!(app.focused_panel, Some(PanelType::Memory));
1882
1883        // Navigate left
1884        app.handle_key(KeyCode::Left, KeyModifiers::NONE);
1885        assert_eq!(app.focused_panel, Some(PanelType::Cpu));
1886    }
1887
1888    #[test]
1889    fn test_handle_key_explode_panel() {
1890        let mut app = App::new_mock();
1891        app.focused_panel = Some(PanelType::Cpu);
1892
1893        // Explode with Enter
1894        app.handle_key(KeyCode::Enter, KeyModifiers::NONE);
1895        assert_eq!(app.exploded_panel, Some(PanelType::Cpu));
1896
1897        // Un-explode with Enter
1898        app.handle_key(KeyCode::Enter, KeyModifiers::NONE);
1899        assert!(app.exploded_panel.is_none());
1900    }
1901
1902    #[test]
1903    fn test_handle_key_explode_with_z() {
1904        let mut app = App::new_mock();
1905        app.focused_panel = Some(PanelType::Memory);
1906
1907        app.handle_key(KeyCode::Char('z'), KeyModifiers::NONE);
1908        assert_eq!(app.exploded_panel, Some(PanelType::Memory));
1909    }
1910
1911    #[test]
1912    fn test_handle_key_esc_unexplode() {
1913        let mut app = App::new_mock();
1914        app.exploded_panel = Some(PanelType::Cpu);
1915
1916        let quit = app.handle_key(KeyCode::Esc, KeyModifiers::NONE);
1917        assert!(!quit);
1918        assert!(app.exploded_panel.is_none());
1919    }
1920
1921    #[test]
1922    fn test_handle_key_esc_unfocus() {
1923        let mut app = App::new_mock();
1924        app.focused_panel = Some(PanelType::Cpu);
1925
1926        let quit = app.handle_key(KeyCode::Esc, KeyModifiers::NONE);
1927        assert!(!quit);
1928        assert!(app.focused_panel.is_none());
1929    }
1930
1931    #[test]
1932    fn test_handle_key_files_view_mode() {
1933        let mut app = App::new_mock();
1934        use crate::state::FilesViewMode;
1935
1936        assert_eq!(app.files_view_mode, FilesViewMode::Size);
1937        app.handle_key(KeyCode::Char('v'), KeyModifiers::NONE);
1938        assert_eq!(app.files_view_mode, FilesViewMode::Entropy);
1939    }
1940
1941    #[test]
1942    fn test_navigate_panel_focus_wrap_right() {
1943        let mut app = App::new_mock();
1944        // Only keep CPU visible - disable everything else
1945        app.panels.memory = false;
1946        app.panels.disk = false;
1947        app.panels.network = false;
1948        app.panels.process = false;
1949        app.panels.gpu = false;
1950        app.panels.battery = false;
1951        app.panels.sensors = false;
1952        app.panels.files = false;
1953
1954        app.focused_panel = Some(PanelType::Cpu);
1955        app.navigate_panel_focus(KeyCode::Right);
1956        // Should wrap to CPU (only visible panel)
1957        assert_eq!(app.focused_panel, Some(PanelType::Cpu));
1958    }
1959
1960    #[test]
1961    fn test_navigate_panel_focus_wrap_left() {
1962        let mut app = App::new_mock();
1963        app.focused_panel = Some(PanelType::Cpu);
1964        app.navigate_panel_focus(KeyCode::Left);
1965        // Should wrap to last visible panel
1966        assert!(app.focused_panel.is_some());
1967    }
1968
1969    #[test]
1970    fn test_navigate_panel_focus_empty() {
1971        let mut app = App::new_mock();
1972        app.panels.cpu = false;
1973        app.panels.memory = false;
1974        app.panels.disk = false;
1975        app.panels.network = false;
1976        app.panels.process = false;
1977        app.panels.gpu = false;
1978        app.panels.battery = false;
1979        app.panels.sensors = false;
1980
1981        app.navigate_panel_focus(KeyCode::Right);
1982        // Should not panic
1983    }
1984
1985    #[test]
1986    fn test_navigate_process_empty() {
1987        let mut app = App::new_mock();
1988        // Mock has no real processes
1989        app.navigate_process(1);
1990        app.navigate_process(-1);
1991        // Should not panic
1992    }
1993
1994    #[test]
1995    fn test_process_count_empty() {
1996        let app = App::new_mock();
1997        assert_eq!(app.process_count(), 0);
1998    }
1999
2000    #[test]
2001    fn test_sorted_processes_empty() {
2002        let app = App::new_mock();
2003        let procs = app.sorted_processes();
2004        assert!(procs.is_empty());
2005    }
2006
2007    #[test]
2008    fn test_has_gpu_mock() {
2009        let app = App::new_mock();
2010        // Mock collectors are not "available"
2011        // This tests the has_gpu() method runs without panic
2012        let _has_gpu = app.has_gpu();
2013    }
2014
2015    #[test]
2016    fn test_thrashing_severity() {
2017        let app = App::new_mock();
2018        let severity = app.thrashing_severity();
2019        assert_eq!(severity, ThrashingSeverity::None);
2020    }
2021
2022    #[test]
2023    fn test_has_zram() {
2024        let app = App::new_mock();
2025        let _has = app.has_zram();
2026        // Just verify it doesn't panic
2027    }
2028
2029    #[test]
2030    fn test_zram_ratio() {
2031        let app = App::new_mock();
2032        let ratio = app.zram_ratio();
2033        assert!(ratio >= 0.0);
2034    }
2035
2036    #[test]
2037    fn test_selected_process_none() {
2038        let app = App::new_mock();
2039        assert!(app.selected_process().is_none());
2040    }
2041
2042    #[test]
2043    fn test_request_signal_no_process() {
2044        let mut app = App::new_mock();
2045        app.request_signal(SignalType::Term);
2046        assert!(app.pending_signal.is_none()); // No process selected
2047    }
2048
2049    #[test]
2050    fn test_cancel_signal() {
2051        let mut app = App::new_mock();
2052        app.pending_signal = Some((1234, "test".to_string(), SignalType::Term));
2053        app.cancel_signal();
2054        assert!(app.pending_signal.is_none());
2055    }
2056
2057    #[test]
2058    fn test_confirm_signal_none() {
2059        let mut app = App::new_mock();
2060        app.confirm_signal(); // No pending signal
2061        // Should not panic
2062    }
2063
2064    #[test]
2065    fn test_clear_old_signal_result_none() {
2066        let mut app = App::new_mock();
2067        app.clear_old_signal_result();
2068        // Should not panic when no result
2069    }
2070
2071    #[test]
2072    fn test_clear_old_signal_result_recent() {
2073        let mut app = App::new_mock();
2074        app.signal_result = Some((true, "test".to_string(), Instant::now()));
2075        app.clear_old_signal_result();
2076        assert!(app.signal_result.is_some()); // Not old enough
2077    }
2078
2079    #[test]
2080    fn test_signal_menu_handling() {
2081        let mut app = App::new_mock();
2082        app.show_signal_menu = true;
2083
2084        // ESC closes menu
2085        let quit = app.handle_key(KeyCode::Esc, KeyModifiers::NONE);
2086        assert!(!quit);
2087        assert!(!app.show_signal_menu);
2088    }
2089
2090    #[test]
2091    fn test_signal_menu_keys() {
2092        let mut app = App::new_mock();
2093
2094        // Test various signal menu keys
2095        app.show_signal_menu = true;
2096        app.handle_key(KeyCode::Char('x'), KeyModifiers::NONE);
2097        assert!(!app.show_signal_menu);
2098
2099        app.show_signal_menu = true;
2100        app.handle_key(KeyCode::Char('K'), KeyModifiers::NONE);
2101        assert!(!app.show_signal_menu);
2102
2103        app.show_signal_menu = true;
2104        app.handle_key(KeyCode::Char('H'), KeyModifiers::NONE);
2105        assert!(!app.show_signal_menu);
2106    }
2107
2108    #[test]
2109    fn test_pending_signal_confirmation() {
2110        let mut app = App::new_mock();
2111        app.pending_signal = Some((1234, "test".to_string(), SignalType::Term));
2112
2113        // Y confirms
2114        let quit = app.handle_key(KeyCode::Char('y'), KeyModifiers::NONE);
2115        assert!(!quit);
2116        assert!(app.pending_signal.is_none());
2117    }
2118
2119    #[test]
2120    fn test_pending_signal_cancel() {
2121        let mut app = App::new_mock();
2122        app.pending_signal = Some((1234, "test".to_string(), SignalType::Term));
2123
2124        // N cancels
2125        let quit = app.handle_key(KeyCode::Char('n'), KeyModifiers::NONE);
2126        assert!(!quit);
2127        assert!(app.pending_signal.is_none());
2128    }
2129
2130    #[test]
2131    fn test_pending_signal_esc_cancels() {
2132        let mut app = App::new_mock();
2133        app.pending_signal = Some((1234, "test".to_string(), SignalType::Term));
2134
2135        let quit = app.handle_key(KeyCode::Esc, KeyModifiers::NONE);
2136        assert!(!quit);
2137        assert!(app.pending_signal.is_none());
2138    }
2139
2140    #[test]
2141    fn test_process_navigation_keys() {
2142        let mut app = App::new_mock();
2143
2144        // Home key
2145        app.process_selected = 5;
2146        app.handle_key(KeyCode::Home, KeyModifiers::NONE);
2147        assert_eq!(app.process_selected, 0);
2148
2149        // g key
2150        app.process_selected = 5;
2151        app.handle_key(KeyCode::Char('g'), KeyModifiers::NONE);
2152        assert_eq!(app.process_selected, 0);
2153    }
2154
2155    #[test]
2156    fn test_delete_clears_filter() {
2157        let mut app = App::new_mock();
2158        app.filter = "test".to_string();
2159        app.handle_key(KeyCode::Delete, KeyModifiers::NONE);
2160        assert!(app.filter.is_empty());
2161    }
2162
2163    #[test]
2164    fn test_hjkl_focus_navigation() {
2165        let mut app = App::new_mock();
2166        app.focused_panel = Some(PanelType::Cpu);
2167
2168        // l moves right in focus mode
2169        app.handle_key(KeyCode::Char('l'), KeyModifiers::NONE);
2170        assert_eq!(app.focused_panel, Some(PanelType::Memory));
2171
2172        // h moves left
2173        app.handle_key(KeyCode::Char('h'), KeyModifiers::NONE);
2174        assert_eq!(app.focused_panel, Some(PanelType::Cpu));
2175    }
2176
2177    #[test]
2178    fn test_jk_process_nav_in_explode() {
2179        let mut app = App::new_mock();
2180        app.exploded_panel = Some(PanelType::Process);
2181
2182        // j/k should navigate processes in explode mode
2183        app.handle_key(KeyCode::Char('j'), KeyModifiers::NONE);
2184        app.handle_key(KeyCode::Char('k'), KeyModifiers::NONE);
2185        // Should not panic
2186    }
2187
2188    #[test]
2189    fn test_z_starts_focus() {
2190        let mut app = App::new_mock();
2191        assert!(app.focused_panel.is_none());
2192        assert!(app.exploded_panel.is_none());
2193
2194        app.handle_key(KeyCode::Char('z'), KeyModifiers::NONE);
2195        assert!(app.focused_panel.is_some());
2196    }
2197
2198    /// Test collect_metrics with real system data (for coverage)
2199    #[test]
2200    fn test_collect_metrics_real() {
2201        let mut app = App::new(false, false);
2202        // Run one real collection cycle for coverage
2203        app.collect_metrics();
2204        // Should complete without panic
2205        assert!(app.frame_id >= 1);
2206    }
2207
2208    /// Test collect_metrics multiple cycles
2209    #[test]
2210    fn test_collect_metrics_cycles() {
2211        let mut app = App::new(false, false);
2212        let initial_frame = app.frame_id;
2213        app.collect_metrics();
2214        app.collect_metrics();
2215        assert_eq!(app.frame_id, initial_frame + 2);
2216    }
2217
2218    /// Test history update in collect_metrics
2219    #[test]
2220    fn test_collect_metrics_history() {
2221        let mut app = App::new(false, false);
2222        let initial_cpu_len = app.cpu_history.len();
2223        app.collect_metrics();
2224        // History should have been updated (may or may not grow depending on collector state)
2225        assert!(app.cpu_history.len() >= initial_cpu_len);
2226    }
2227
2228    /// Test push_to_history helper
2229    #[test]
2230    fn test_push_to_history() {
2231        let mut history = Vec::new();
2232        App::push_to_history(&mut history, 0.5);
2233        assert_eq!(history.len(), 1);
2234        assert_eq!(history[0], 0.5);
2235
2236        // Push more values
2237        for i in 0..400 {
2238            App::push_to_history(&mut history, i as f64 / 400.0);
2239        }
2240        // Should be capped at 300
2241        assert_eq!(history.len(), 300);
2242    }
2243
2244    // === Micro-benchmark Performance Tests ===
2245
2246    /// Verify collect_metrics completes within reasonable time
2247    /// Note: Real metrics collection involves many system calls (reading /proc, /sys, etc.)
2248    #[test]
2249    fn test_collect_metrics_performance() {
2250        use std::time::Instant;
2251
2252        let mut app = App::new(false, false);
2253        let iterations = 5;
2254        let start = Instant::now();
2255
2256        for _ in 0..iterations {
2257            app.collect_metrics();
2258        }
2259
2260        let elapsed = start.elapsed();
2261        let avg_ms = elapsed.as_millis() / iterations as u128;
2262
2263        // Real metrics collection with all collectors can take several seconds
2264        // on systems with many disks, network interfaces, and processes
2265        assert!(avg_ms < 5000, "collect_metrics too slow: {}ms avg", avg_ms);
2266    }
2267
2268    /// Verify history push is O(1) amortized
2269    #[test]
2270    fn test_history_push_performance() {
2271        use std::time::Instant;
2272
2273        let mut history = Vec::new();
2274        let iterations = 10000;
2275        let start = Instant::now();
2276
2277        for i in 0..iterations {
2278            App::push_to_history(&mut history, i as f64 / iterations as f64);
2279        }
2280
2281        let elapsed = start.elapsed();
2282        let per_op_ns = elapsed.as_nanos() / iterations as u128;
2283
2284        // Each push should be sub-microsecond
2285        assert!(per_op_ns < 1000, "push_to_history too slow: {}ns per op", per_op_ns);
2286    }
2287
2288    /// Verify App::new_mock is reasonably fast for testing
2289    /// Note: Mock creation initializes many analyzers which may read system state
2290    #[test]
2291    fn test_app_mock_creation_performance() {
2292        use std::time::Instant;
2293
2294        let iterations = 50;
2295        let start = Instant::now();
2296
2297        for _ in 0..iterations {
2298            let _ = App::new_mock();
2299        }
2300
2301        let elapsed = start.elapsed();
2302        let avg_us = elapsed.as_micros() / iterations as u128;
2303
2304        // Mock creation includes initializing analyzers which may touch system state
2305        // Allow up to 100ms each
2306        assert!(avg_us < 100000, "new_mock too slow: {}us avg", avg_us);
2307    }
2308
2309    // === Additional Coverage Tests ===
2310
2311    #[test]
2312    fn test_visible_panels_files_enabled() {
2313        let mut app = App::new_mock();
2314        app.panels.files = true;
2315        let panels = app.visible_panels();
2316        assert!(panels.contains(&PanelType::Files));
2317    }
2318
2319    #[test]
2320    fn test_cancel_signal_with_pending() {
2321        let mut app = App::new_mock();
2322        app.pending_signal = Some((1234, "test_proc".to_string(), SignalType::Term));
2323        app.cancel_signal();
2324        assert!(app.pending_signal.is_none());
2325    }
2326
2327    #[test]
2328    fn test_pending_signal_hup() {
2329        let mut app = App::new_mock();
2330        app.pending_signal = Some((1234, "proc".to_string(), SignalType::Hup));
2331        assert!(app.pending_signal.is_some());
2332        if let Some((pid, name, signal)) = &app.pending_signal {
2333            assert_eq!(*pid, 1234);
2334            assert_eq!(name, "proc");
2335            assert_eq!(*signal, SignalType::Hup);
2336        }
2337    }
2338
2339    #[test]
2340    fn test_pending_signal_int() {
2341        let mut app = App::new_mock();
2342        app.pending_signal = Some((5678, "daemon".to_string(), SignalType::Int));
2343        if let Some((_, _, signal)) = &app.pending_signal {
2344            assert_eq!(*signal, SignalType::Int);
2345        }
2346    }
2347
2348    #[test]
2349    fn test_pending_signal_usr1() {
2350        let mut app = App::new_mock();
2351        app.pending_signal = Some((1000, "app".to_string(), SignalType::Usr1));
2352        if let Some((_, _, signal)) = &app.pending_signal {
2353            assert_eq!(*signal, SignalType::Usr1);
2354        }
2355    }
2356
2357    #[test]
2358    fn test_pending_signal_usr2() {
2359        let mut app = App::new_mock();
2360        app.pending_signal = Some((2000, "service".to_string(), SignalType::Usr2));
2361        if let Some((_, _, signal)) = &app.pending_signal {
2362            assert_eq!(*signal, SignalType::Usr2);
2363        }
2364    }
2365
2366    #[test]
2367    fn test_pending_signal_stop() {
2368        let mut app = App::new_mock();
2369        app.pending_signal = Some((3000, "worker".to_string(), SignalType::Stop));
2370        if let Some((_, _, signal)) = &app.pending_signal {
2371            assert_eq!(*signal, SignalType::Stop);
2372        }
2373    }
2374
2375    #[test]
2376    fn test_pending_signal_cont() {
2377        let mut app = App::new_mock();
2378        app.pending_signal = Some((4000, "bg_task".to_string(), SignalType::Cont));
2379        if let Some((_, _, signal)) = &app.pending_signal {
2380            assert_eq!(*signal, SignalType::Cont);
2381        }
2382    }
2383
2384    #[test]
2385    fn test_pending_signal_kill() {
2386        let mut app = App::new_mock();
2387        app.pending_signal = Some((9999, "zombie".to_string(), SignalType::Kill));
2388        if let Some((_, _, signal)) = &app.pending_signal {
2389            assert_eq!(*signal, SignalType::Kill);
2390        }
2391    }
2392
2393    #[test]
2394    fn test_signal_menu_key_i_int() {
2395        let mut app = App::new_mock();
2396        app.process_selected = 0;
2397        app.show_signal_menu = true;
2398        app.handle_key(KeyCode::Char('i'), KeyModifiers::NONE);
2399        assert!(!app.show_signal_menu);
2400    }
2401
2402    #[test]
2403    fn test_signal_menu_key_p_stop() {
2404        let mut app = App::new_mock();
2405        app.process_selected = 0;
2406        app.show_signal_menu = true;
2407        app.handle_key(KeyCode::Char('p'), KeyModifiers::NONE);
2408        assert!(!app.show_signal_menu);
2409    }
2410
2411    #[test]
2412    fn test_signal_menu_key_c_cont() {
2413        let mut app = App::new_mock();
2414        app.process_selected = 0;
2415        app.show_signal_menu = true;
2416        app.handle_key(KeyCode::Char('c'), KeyModifiers::NONE);
2417        assert!(!app.show_signal_menu);
2418    }
2419
2420    #[test]
2421    fn test_signal_menu_key_unknown_ignored() {
2422        let mut app = App::new_mock();
2423        app.show_signal_menu = true;
2424        // Unknown key should not close menu
2425        app.handle_key(KeyCode::Char('z'), KeyModifiers::NONE);
2426        assert!(app.show_signal_menu);
2427    }
2428
2429    #[test]
2430    fn test_filter_input_escape_clears() {
2431        let mut app = App::new_mock();
2432        app.show_filter_input = true;
2433        app.filter = "test".to_string();
2434        app.handle_key(KeyCode::Esc, KeyModifiers::NONE);
2435        assert!(!app.show_filter_input);
2436        assert!(app.filter.is_empty());
2437    }
2438
2439    #[test]
2440    fn test_filter_input_backspace() {
2441        let mut app = App::new_mock();
2442        app.show_filter_input = true;
2443        app.filter = "test".to_string();
2444        app.handle_key(KeyCode::Backspace, KeyModifiers::NONE);
2445        assert_eq!(app.filter, "tes");
2446    }
2447
2448    #[test]
2449    fn test_filter_input_char_append() {
2450        let mut app = App::new_mock();
2451        app.show_filter_input = true;
2452        app.filter = "te".to_string();
2453        app.handle_key(KeyCode::Char('s'), KeyModifiers::NONE);
2454        app.handle_key(KeyCode::Char('t'), KeyModifiers::NONE);
2455        assert_eq!(app.filter, "test");
2456    }
2457
2458    #[test]
2459    fn test_filter_input_enter_closes() {
2460        let mut app = App::new_mock();
2461        app.show_filter_input = true;
2462        app.filter = "search".to_string();
2463        app.handle_key(KeyCode::Enter, KeyModifiers::NONE);
2464        assert!(!app.show_filter_input);
2465        assert_eq!(app.filter, "search"); // Filter kept
2466    }
2467
2468    // === Mock Data Verification Tests ===
2469
2470    #[test]
2471    fn test_mock_gpus_populated() {
2472        let app = App::new_mock();
2473        assert!(!app.mock_gpus.is_empty(), "mock_gpus should be populated");
2474        assert_eq!(app.mock_gpus.len(), 2, "should have 2 mock GPUs");
2475        assert!(app.mock_gpus[0].name.contains("RTX"));
2476    }
2477
2478    #[test]
2479    fn test_mock_battery_populated() {
2480        let app = App::new_mock();
2481        assert!(app.mock_battery.is_some(), "mock_battery should be populated");
2482        let bat = app.mock_battery.as_ref().expect("battery");
2483        assert!(bat.percent > 0.0);
2484        assert!(bat.health_percent > 0.0);
2485    }
2486
2487    #[test]
2488    fn test_mock_sensors_populated() {
2489        let app = App::new_mock();
2490        assert!(!app.mock_sensors.is_empty(), "mock_sensors should be populated");
2491        assert!(app.mock_sensors.len() >= 3);
2492    }
2493
2494    #[test]
2495    fn test_mock_containers_populated() {
2496        let app = App::new_mock();
2497        assert!(!app.mock_containers.is_empty(), "mock_containers should be populated");
2498        assert_eq!(app.mock_containers.len(), 3);
2499    }
2500
2501    // === Additional Key Handling Tests ===
2502
2503    #[test]
2504    fn test_panel_toggle_keys() {
2505        let mut app = App::new_mock();
2506
2507        // Test panel 3 (disk)
2508        let original = app.panels.disk;
2509        app.handle_key(KeyCode::Char('3'), KeyModifiers::NONE);
2510        assert_ne!(app.panels.disk, original);
2511
2512        // Test panel 4 (network)
2513        let original = app.panels.network;
2514        app.handle_key(KeyCode::Char('4'), KeyModifiers::NONE);
2515        assert_ne!(app.panels.network, original);
2516
2517        // Test panel 5 (process)
2518        let original = app.panels.process;
2519        app.handle_key(KeyCode::Char('5'), KeyModifiers::NONE);
2520        assert_ne!(app.panels.process, original);
2521
2522        // Test panel 6 (gpu)
2523        let original = app.panels.gpu;
2524        app.handle_key(KeyCode::Char('6'), KeyModifiers::NONE);
2525        assert_ne!(app.panels.gpu, original);
2526
2527        // Test panel 7 (battery)
2528        let original = app.panels.battery;
2529        app.handle_key(KeyCode::Char('7'), KeyModifiers::NONE);
2530        assert_ne!(app.panels.battery, original);
2531
2532        // Test panel 8 (sensors)
2533        let original = app.panels.sensors;
2534        app.handle_key(KeyCode::Char('8'), KeyModifiers::NONE);
2535        assert_ne!(app.panels.sensors, original);
2536    }
2537
2538    #[test]
2539    fn test_navigation_when_focused() {
2540        let mut app = App::new_mock();
2541        app.focused_panel = Some(PanelType::Cpu);
2542
2543        // h should navigate left
2544        app.handle_key(KeyCode::Char('h'), KeyModifiers::NONE);
2545        // l should navigate right
2546        app.handle_key(KeyCode::Char('l'), KeyModifiers::NONE);
2547        // j should navigate down
2548        app.handle_key(KeyCode::Char('j'), KeyModifiers::NONE);
2549        // k should navigate up
2550        app.handle_key(KeyCode::Char('k'), KeyModifiers::NONE);
2551    }
2552
2553    #[test]
2554    fn test_navigation_when_not_focused() {
2555        let mut app = App::new_mock();
2556        app.focused_panel = None;
2557
2558        // h should start focus
2559        app.handle_key(KeyCode::Char('h'), KeyModifiers::NONE);
2560        assert!(app.focused_panel.is_some());
2561
2562        // Reset and try l
2563        app.focused_panel = None;
2564        app.handle_key(KeyCode::Char('l'), KeyModifiers::NONE);
2565        assert!(app.focused_panel.is_some());
2566    }
2567
2568    #[test]
2569    fn test_process_navigation_pageup_pagedown() {
2570        let mut app = App::new_mock();
2571        app.process_selected = 5;
2572
2573        // Just test that keys are handled without panicking
2574        app.handle_key(KeyCode::PageDown, KeyModifiers::NONE);
2575        app.handle_key(KeyCode::PageUp, KeyModifiers::NONE);
2576    }
2577
2578    #[test]
2579    fn test_process_navigation_home_end() {
2580        let mut app = App::new_mock();
2581        app.process_selected = 5;
2582
2583        // g should go to start
2584        app.handle_key(KeyCode::Char('g'), KeyModifiers::NONE);
2585        assert_eq!(app.process_selected, 0);
2586
2587        // G should go to end
2588        app.handle_key(KeyCode::Char('G'), KeyModifiers::NONE);
2589    }
2590
2591    #[test]
2592    fn test_process_navigation_arrow_keys() {
2593        let mut app = App::new_mock();
2594        app.focused_panel = None;
2595        app.exploded_panel = None;
2596        app.process_selected = 5;
2597
2598        // Just test that arrow keys are handled without panicking
2599        app.handle_key(KeyCode::Down, KeyModifiers::NONE);
2600        app.handle_key(KeyCode::Up, KeyModifiers::NONE);
2601    }
2602
2603    #[test]
2604    fn test_process_navigation_with_exploded_panel() {
2605        let mut app = App::new_mock();
2606        app.exploded_panel = Some(PanelType::Process);
2607        app.process_selected = 0;
2608
2609        // Just test that j/k are handled
2610        app.handle_key(KeyCode::Char('j'), KeyModifiers::NONE);
2611        app.handle_key(KeyCode::Char('k'), KeyModifiers::NONE);
2612    }
2613
2614    #[test]
2615    fn test_help_toggle() {
2616        let mut app = App::new_mock();
2617        assert!(!app.show_help);
2618
2619        app.handle_key(KeyCode::Char('?'), KeyModifiers::NONE);
2620        assert!(app.show_help);
2621
2622        app.handle_key(KeyCode::Char('?'), KeyModifiers::NONE);
2623        assert!(!app.show_help);
2624    }
2625
2626    #[test]
2627    fn test_f1_help_toggle() {
2628        let mut app = App::new_mock();
2629        assert!(!app.show_help);
2630
2631        app.handle_key(KeyCode::F(1), KeyModifiers::NONE);
2632        assert!(app.show_help);
2633    }
2634
2635    #[test]
2636    fn test_quit_key() {
2637        let mut app = App::new_mock();
2638        let quit = app.handle_key(KeyCode::Char('q'), KeyModifiers::NONE);
2639        assert!(quit);
2640    }
2641
2642    #[test]
2643    fn test_esc_clears_focus() {
2644        let mut app = App::new_mock();
2645        app.focused_panel = Some(PanelType::Cpu);
2646
2647        app.handle_key(KeyCode::Esc, KeyModifiers::NONE);
2648        assert!(app.focused_panel.is_none());
2649    }
2650
2651    #[test]
2652    fn test_ctrl_c_returns_true() {
2653        let mut app = App::new_mock();
2654        let quit = app.handle_key(KeyCode::Char('c'), KeyModifiers::CONTROL);
2655        assert!(quit);
2656    }
2657
2658    #[test]
2659    fn test_view_mode_cycle() {
2660        let mut app = App::new_mock();
2661        let original = app.files_view_mode;
2662
2663        app.handle_key(KeyCode::Char('v'), KeyModifiers::NONE);
2664        // Should have cycled to next mode
2665        assert_ne!(app.files_view_mode, original);
2666    }
2667
2668    #[test]
2669    fn test_pending_signal_confirm() {
2670        let mut app = App::new_mock();
2671        app.pending_signal = Some((1234, "test".to_string(), SignalType::Term));
2672
2673        // y confirms
2674        app.handle_key(KeyCode::Char('y'), KeyModifiers::NONE);
2675        assert!(app.pending_signal.is_none());
2676    }
2677
2678    #[test]
2679    fn test_pending_signal_cancel_with_n_key() {
2680        let mut app = App::new_mock();
2681        app.pending_signal = Some((1234, "test".to_string(), SignalType::Term));
2682
2683        // n cancels
2684        app.handle_key(KeyCode::Char('n'), KeyModifiers::NONE);
2685        assert!(app.pending_signal.is_none());
2686    }
2687
2688    #[test]
2689    fn test_panel_explode_toggle() {
2690        let mut app = App::new_mock();
2691        app.focused_panel = Some(PanelType::Cpu);
2692        app.exploded_panel = None;
2693
2694        // z or Enter should toggle explode
2695        app.handle_key(KeyCode::Char('z'), KeyModifiers::NONE);
2696        assert!(app.exploded_panel.is_some());
2697
2698        app.handle_key(KeyCode::Char('z'), KeyModifiers::NONE);
2699        assert!(app.exploded_panel.is_none());
2700    }
2701
2702    #[test]
2703    fn test_signal_menu_keys_huk() {
2704        let mut app = App::new_mock();
2705        app.show_signal_menu = true;
2706        app.process_selected = 0;
2707
2708        // Test H for HUP
2709        app.handle_key(KeyCode::Char('H'), KeyModifiers::NONE);
2710
2711        app.show_signal_menu = true;
2712        // Test u for USR1
2713        app.handle_key(KeyCode::Char('u'), KeyModifiers::NONE);
2714
2715        app.show_signal_menu = true;
2716        // Test U for USR2
2717        app.handle_key(KeyCode::Char('U'), KeyModifiers::NONE);
2718        // These keys should be handled without panicking
2719    }
2720
2721    // === Additional Edge Case Tests ===
2722
2723    #[test]
2724    fn test_signal_menu_all_signal_types() {
2725        let mut app = App::new_mock();
2726
2727        // Test all signal menu keys
2728        for (key, _) in [
2729            ('x', SignalType::Term),
2730            ('K', SignalType::Kill),
2731            ('i', SignalType::Int),
2732            ('p', SignalType::Stop),
2733            ('c', SignalType::Cont),
2734        ] {
2735            app.show_signal_menu = true;
2736            app.pending_signal = None;
2737            app.handle_key(KeyCode::Char(key), KeyModifiers::NONE);
2738            assert!(!app.show_signal_menu);
2739        }
2740    }
2741
2742    #[test]
2743    fn test_signal_menu_esc_closes_menu() {
2744        let mut app = App::new_mock();
2745        app.show_signal_menu = true;
2746
2747        app.handle_key(KeyCode::Esc, KeyModifiers::NONE);
2748        assert!(!app.show_signal_menu);
2749    }
2750
2751    #[test]
2752    fn test_filter_input_backspace_removal() {
2753        let mut app = App::new_mock();
2754        app.show_filter_input = true;
2755        app.filter = "test".to_string();
2756
2757        app.handle_key(KeyCode::Backspace, KeyModifiers::NONE);
2758        assert_eq!(app.filter, "tes");
2759    }
2760
2761    #[test]
2762    fn test_filter_input_esc_clears_text() {
2763        let mut app = App::new_mock();
2764        app.show_filter_input = true;
2765        app.filter = "test".to_string();
2766
2767        app.handle_key(KeyCode::Esc, KeyModifiers::NONE);
2768        assert!(!app.show_filter_input);
2769        assert!(app.filter.is_empty());
2770    }
2771
2772    #[test]
2773    fn test_filter_input_enter_preserves() {
2774        let mut app = App::new_mock();
2775        app.show_filter_input = true;
2776        app.filter = "test".to_string();
2777
2778        app.handle_key(KeyCode::Enter, KeyModifiers::NONE);
2779        assert!(!app.show_filter_input);
2780        assert_eq!(app.filter, "test"); // Filter preserved
2781    }
2782
2783    #[test]
2784    fn test_filter_input_add_char() {
2785        let mut app = App::new_mock();
2786        app.show_filter_input = true;
2787        app.filter = String::new();
2788
2789        app.handle_key(KeyCode::Char('a'), KeyModifiers::NONE);
2790        assert_eq!(app.filter, "a");
2791    }
2792
2793    #[test]
2794    fn test_pending_signal_enter_confirms() {
2795        let mut app = App::new_mock();
2796        app.pending_signal = Some((1234, "test".to_string(), SignalType::Term));
2797
2798        app.handle_key(KeyCode::Enter, KeyModifiers::NONE);
2799        assert!(app.pending_signal.is_none());
2800    }
2801
2802    #[test]
2803    fn test_pending_signal_capital_y_confirms() {
2804        let mut app = App::new_mock();
2805        app.pending_signal = Some((1234, "test".to_string(), SignalType::Term));
2806
2807        app.handle_key(KeyCode::Char('Y'), KeyModifiers::NONE);
2808        assert!(app.pending_signal.is_none());
2809    }
2810
2811    #[test]
2812    fn test_pending_signal_capital_n_cancels() {
2813        let mut app = App::new_mock();
2814        app.pending_signal = Some((1234, "test".to_string(), SignalType::Term));
2815
2816        app.handle_key(KeyCode::Char('N'), KeyModifiers::NONE);
2817        assert!(app.pending_signal.is_none());
2818    }
2819
2820    #[test]
2821    fn test_pending_signal_esc_cancels_prompt() {
2822        let mut app = App::new_mock();
2823        app.pending_signal = Some((1234, "test".to_string(), SignalType::Term));
2824
2825        app.handle_key(KeyCode::Esc, KeyModifiers::NONE);
2826        assert!(app.pending_signal.is_none());
2827    }
2828
2829    #[test]
2830    fn test_exploded_panel_enter_exits() {
2831        let mut app = App::new_mock();
2832        app.exploded_panel = Some(PanelType::Cpu);
2833
2834        app.handle_key(KeyCode::Enter, KeyModifiers::NONE);
2835        assert!(app.exploded_panel.is_none());
2836    }
2837
2838    #[test]
2839    fn test_focused_panel_arrow_navigation() {
2840        let mut app = App::new_mock();
2841        app.focused_panel = Some(PanelType::Cpu);
2842        app.exploded_panel = None;
2843
2844        // Test all arrow directions
2845        app.handle_key(KeyCode::Left, KeyModifiers::NONE);
2846        app.handle_key(KeyCode::Right, KeyModifiers::NONE);
2847        app.handle_key(KeyCode::Up, KeyModifiers::NONE);
2848        app.handle_key(KeyCode::Down, KeyModifiers::NONE);
2849    }
2850
2851    #[test]
2852    fn test_focused_panel_hjkl_navigation() {
2853        let mut app = App::new_mock();
2854        app.focused_panel = Some(PanelType::Memory);
2855        app.exploded_panel = None;
2856
2857        // Test h/l navigation
2858        app.handle_key(KeyCode::Char('h'), KeyModifiers::NONE);
2859        app.handle_key(KeyCode::Char('l'), KeyModifiers::NONE);
2860    }
2861
2862    #[test]
2863    fn test_focused_panel_enter_explodes() {
2864        let mut app = App::new_mock();
2865        app.focused_panel = Some(PanelType::Disk);
2866        app.exploded_panel = None;
2867
2868        app.handle_key(KeyCode::Enter, KeyModifiers::NONE);
2869        assert_eq!(app.exploded_panel, Some(PanelType::Disk));
2870    }
2871
2872    #[test]
2873    fn test_unfocused_process_navigation_jk() {
2874        let mut app = App::new_mock();
2875        app.focused_panel = None;
2876        app.exploded_panel = None;
2877        app.process_selected = 5;
2878
2879        app.handle_key(KeyCode::Char('j'), KeyModifiers::NONE);
2880        app.handle_key(KeyCode::Char('k'), KeyModifiers::NONE);
2881    }
2882
2883    #[test]
2884    fn test_push_to_history_overflow() {
2885        let mut history = Vec::new();
2886        for i in 0..305 {
2887            App::push_to_history(&mut history, i as f64);
2888        }
2889        assert_eq!(history.len(), 300);
2890        assert_eq!(history[0], 5.0); // First 5 elements should be removed
2891    }
2892
2893    #[test]
2894    fn test_panel_visibility_all_fields() {
2895        let vis = PanelVisibility {
2896            cpu: false,
2897            memory: false,
2898            disk: false,
2899            network: false,
2900            process: false,
2901            gpu: false,
2902            battery: false,
2903            sensors: false,
2904            files: true,
2905        };
2906        assert!(!vis.cpu);
2907        assert!(vis.files);
2908    }
2909
2910    #[test]
2911    fn test_mock_gpu_data_debug() {
2912        let gpu = MockGpuData {
2913            name: "Test GPU".to_string(),
2914            gpu_util: 50.0,
2915            vram_used: 1000,
2916            vram_total: 8000,
2917            temperature: 65.0,
2918            power_watts: 150,
2919            power_limit_watts: 300,
2920            clock_mhz: 1500,
2921            history: vec![0.5],
2922        };
2923        let debug = format!("{:?}", gpu);
2924        assert!(debug.contains("Test GPU"));
2925    }
2926
2927    #[test]
2928    fn test_mock_battery_data_debug() {
2929        let bat = MockBatteryData {
2930            percent: 75.0,
2931            charging: true,
2932            time_remaining_mins: Some(120),
2933            power_watts: 45.0,
2934            health_percent: 95.0,
2935            cycle_count: 500,
2936        };
2937        let debug = format!("{:?}", bat);
2938        assert!(debug.contains("75"));
2939    }
2940
2941    #[test]
2942    fn test_mock_sensor_data_debug() {
2943        let sensor = MockSensorData {
2944            name: "cpu/temp1".to_string(),
2945            label: "CPU".to_string(),
2946            value: 65.0,
2947            max: Some(90.0),
2948            crit: Some(100.0),
2949            sensor_type: MockSensorType::Temperature,
2950        };
2951        let debug = format!("{:?}", sensor);
2952        assert!(debug.contains("cpu/temp1"));
2953    }
2954
2955    #[test]
2956    fn test_mock_container_data_debug() {
2957        let container = MockContainerData {
2958            name: "nginx".to_string(),
2959            status: "running".to_string(),
2960            cpu_percent: 5.0,
2961            mem_used: 100_000_000,
2962            mem_limit: 1_000_000_000,
2963        };
2964        let debug = format!("{:?}", container);
2965        assert!(debug.contains("nginx"));
2966    }
2967
2968    #[test]
2969    fn test_mock_sensor_type_equality() {
2970        assert_eq!(MockSensorType::Temperature, MockSensorType::Temperature);
2971        assert_ne!(MockSensorType::Temperature, MockSensorType::Fan);
2972        assert_ne!(MockSensorType::Voltage, MockSensorType::Power);
2973    }
2974
2975    #[test]
2976    fn test_toggle_panel_9_files_toggle() {
2977        let mut app = App::new_mock();
2978        let initial = app.panels.files;
2979
2980        app.handle_key(KeyCode::Char('9'), KeyModifiers::NONE);
2981        assert_ne!(app.panels.files, initial);
2982    }
2983
2984    // =========================================================================
2985    // TUI Load Testing (probar integration)
2986    // =========================================================================
2987
2988    /// Test filter performance with large dataset using probar's TUI load testing.
2989    /// This test uses probar's synthetic data generator and detects hangs.
2990    #[test]
2991    fn test_filter_no_hang_with_5000_items() {
2992        use jugar_probar::tui_load::{DataGenerator, TuiLoadTest};
2993        use std::time::{Duration, Instant};
2994
2995        // Generate 5000 synthetic process-like items
2996        let generator = DataGenerator::new(5000);
2997        let items = generator.generate();
2998
2999        // Test filter performance with timeout
3000        let timeout = Duration::from_millis(1000);
3001        let filters = ["", "a", "sys", "chrome", "nonexistent_long_filter_string"];
3002
3003        for filter in filters {
3004            let filter_lower = filter.to_lowercase();
3005            let start = Instant::now();
3006
3007            // Simulate what sorted_processes does: filter then collect
3008            let filtered: Vec<_> = items
3009                .iter()
3010                .filter(|item| {
3011                    if filter_lower.is_empty() {
3012                        true
3013                    } else {
3014                        item.name.to_lowercase().contains(&filter_lower)
3015                            || item.description.to_lowercase().contains(&filter_lower)
3016                    }
3017                })
3018                .collect();
3019
3020            let elapsed = start.elapsed();
3021
3022            assert!(
3023                elapsed < timeout,
3024                "Filter '{}' took {:?} (timeout: {:?}) - HANG DETECTED with {} items, {} results",
3025                filter, elapsed, timeout, items.len(), filtered.len()
3026            );
3027        }
3028    }
3029
3030    /// Test that filter performance is O(n) not O(n²) using probar load testing.
3031    #[test]
3032    fn test_filter_scales_linearly() {
3033        use jugar_probar::tui_load::DataGenerator;
3034        use std::time::Instant;
3035
3036        let sizes = [100, 500, 1000, 2000, 5000];
3037        let mut times_us = Vec::new();
3038
3039        for size in sizes {
3040            let items = DataGenerator::new(size).generate();
3041            let filter_lower = "sys".to_lowercase();
3042
3043            let start = Instant::now();
3044            // Run filter 10 times for stable measurement
3045            for _ in 0..10 {
3046                let _: Vec<_> = items
3047                    .iter()
3048                    .filter(|item| {
3049                        item.name.to_lowercase().contains(&filter_lower)
3050                            || item.description.to_lowercase().contains(&filter_lower)
3051                    })
3052                    .collect();
3053            }
3054            let elapsed = start.elapsed().as_micros() as u64;
3055            times_us.push((size, elapsed));
3056        }
3057
3058        // Check that time grows roughly linearly (< 3x for 5x data)
3059        // From 1000 to 5000 items should take roughly 5x longer (with tolerance)
3060        let time_1k = times_us.iter().find(|(s, _)| *s == 1000).map(|(_, t)| *t).unwrap_or(1);
3061        let time_5k = times_us.iter().find(|(s, _)| *s == 5000).map(|(_, t)| *t).unwrap_or(1);
3062
3063        let ratio = time_5k as f64 / time_1k as f64;
3064
3065        // Should scale roughly linearly: 5x data = ~5x time (allow up to 8x for overhead)
3066        assert!(
3067            ratio < 8.0,
3068            "Filter time scaled {}x from 1K to 5K items (expected ~5x). \
3069             Times: {:?}. May indicate O(n²) complexity.",
3070            ratio, times_us
3071        );
3072    }
3073
3074    /// Stress test with probar's TuiLoadTest harness - tests for hangs, not microbenchmarks
3075    #[test]
3076    fn test_filter_stress_with_probar() {
3077        use jugar_probar::tui_load::TuiLoadTest;
3078
3079        let load_test = TuiLoadTest::new()
3080            .with_item_count(5000)     // Test with 5000 items
3081            .with_timeout_ms(2000)      // 2 second timeout per frame
3082            .with_frames_per_filter(3);
3083
3084        // Run filter stress test using allocation-free matching (like ttop's optimized code)
3085        let result = load_test.run_filter_stress(|items, filter| {
3086            let filter_lower = filter.to_lowercase();
3087            items
3088                .iter()
3089                .filter(|item| {
3090                    if filter_lower.is_empty() {
3091                        true
3092                    } else {
3093                        // Use allocation-free case-insensitive contains
3094                        contains_ignore_case(&item.name, &filter_lower)
3095                            || contains_ignore_case(&item.description, &filter_lower)
3096                    }
3097                })
3098                .cloned()
3099                .collect()
3100        });
3101
3102        // The main assertion: no frame should timeout (no hangs)
3103        assert!(result.is_ok(), "TUI filter stress test detected hang: {:?}", result.err());
3104
3105        // Verify we actually ran all the filters
3106        let results = result.expect("result should be ok");
3107        assert!(!results.is_empty(), "Should have run at least one filter");
3108
3109        // Log performance for manual review (not a hard failure)
3110        for (filter, metrics) in &results {
3111            let avg = metrics.avg_frame_ms();
3112            assert!(
3113                avg < 500.0,
3114                "Filter '{}' took {:.1}ms avg - too slow for responsive UI",
3115                filter, avg
3116            );
3117        }
3118    }
3119
3120    /// Integration load test that tests REAL App with REAL collectors.
3121    ///
3122    /// This test would have caught the container_analyzer hang because it:
3123    /// 1. Creates a real App (not mock)
3124    /// 2. Calls real collect methods
3125    /// 3. Measures component-level timings
3126    /// 4. Enforces per-component budgets (container_analyzer: 200ms max)
3127    ///
3128    /// The synthetic load tests missed the hang because they only tested
3129    /// filter performance with fake data, not actual system calls.
3130    #[test]
3131    fn test_integration_load_real_app_no_hang() {
3132        use jugar_probar::tui_load::{ComponentTimings, IntegrationLoadTest};
3133
3134        // Set up integration test with component budgets
3135        let test = IntegrationLoadTest::new()
3136            .with_frame_budget_ms(500.0)   // 500ms total frame budget
3137            .with_timeout_ms(5000)          // 5 second timeout for hang detection
3138            .with_frame_count(3)            // Test 3 frames
3139            // Per-component budgets - this is what would catch the container_analyzer issue
3140            .with_component_budget("container_analyzer", 200.0)  // Max 200ms
3141            .with_component_budget("disk_analyzer", 200.0)
3142            .with_component_budget("network_analyzer", 200.0)
3143            .with_component_budget("sensor_analyzer", 100.0);
3144
3145        // Track whether we're on first frame (initialization is slower)
3146        let first_frame = std::sync::atomic::AtomicBool::new(true);
3147        let app = std::sync::Mutex::new(None::<App>);
3148
3149        let result = test.run(|| {
3150            let mut timings = ComponentTimings::new();
3151
3152            // Get or create app
3153            let mut guard = app.lock().expect("lock");
3154            let app = guard.get_or_insert_with(|| {
3155                // First frame includes App::new() which is slower
3156                App::new(false, false) // deterministic=false, show_fps=false
3157            });
3158
3159            // Measure individual analyzer times
3160            let start = Instant::now();
3161            app.container_analyzer.collect();
3162            timings.record("container_analyzer", start.elapsed().as_secs_f64() * 1000.0);
3163
3164            let start = Instant::now();
3165            app.disk_io_analyzer.collect();
3166            timings.record("disk_analyzer", start.elapsed().as_secs_f64() * 1000.0);
3167
3168            let start = Instant::now();
3169            app.network_stats.collect();
3170            timings.record("network_analyzer", start.elapsed().as_secs_f64() * 1000.0);
3171
3172            let start = Instant::now();
3173            app.sensor_health.collect();
3174            timings.record("sensor_analyzer", start.elapsed().as_secs_f64() * 1000.0);
3175
3176            // Skip strict budget check on first frame (initialization)
3177            if first_frame.swap(false, std::sync::atomic::Ordering::SeqCst) {
3178                // Return empty timings for first frame so budget checks are skipped
3179                return ComponentTimings::new();
3180            }
3181
3182            timings
3183        });
3184
3185        // This assertion WOULD HAVE FAILED before fixing container_analyzer
3186        // because docker stats --no-stream was blocking for 1.5+ seconds
3187        assert!(
3188            result.is_ok(),
3189            "Integration load test failed! This catches real collector hangs: {:?}",
3190            result.err()
3191        );
3192
3193        let metrics = result.expect("test passed");
3194        assert!(
3195            metrics.p95_frame_ms() < 1000.0,
3196            "Frame time p95 {:.1}ms is too slow",
3197            metrics.p95_frame_ms()
3198        );
3199    }
3200
3201    // =========================================================================
3202    // EXTREME TDD: Exploded Panel Features - Tests Written FIRST
3203    // =========================================================================
3204    //
3205    // SPEC: CPU Panel Exploded Mode Features
3206    // 1. Per-core sparkline history (60 samples per core)
3207    // 2. CPU state breakdown (user/system/iowait/idle per core)
3208    // 3. Frequency timeline (history of freq changes)
3209    // 4. Top process per core
3210    // 5. Thermal throttling indicator
3211    //
3212    // PMAT Requirements:
3213    // - Performance: O(1) per-core history update
3214    // - Maintainability: Clear data structures
3215    // - Accessibility: Data available via public getters
3216    // - Testing: 95% coverage requirement
3217
3218    /// TDD Test 1: Per-core history storage exists and is populated
3219    #[test]
3220    fn test_per_core_history_exists() {
3221        let app = App::new_mock();
3222        // Per-core history should exist for each core
3223        assert!(!app.per_core_history.is_empty(), "per_core_history must exist");
3224        assert_eq!(app.per_core_history.len(), app.per_core_percent.len(),
3225            "per_core_history should have one entry per core");
3226    }
3227
3228    /// TDD Test 2: Per-core history has correct capacity
3229    #[test]
3230    fn test_per_core_history_capacity() {
3231        let app = App::new_mock();
3232        // Each core should have history capacity for 60 samples (60 seconds at 1Hz)
3233        for (i, history) in app.per_core_history.iter().enumerate() {
3234            assert!(history.capacity() >= 60,
3235                "Core {} history should have capacity for 60 samples, got {}",
3236                i, history.capacity());
3237        }
3238    }
3239
3240    /// TDD Test 3: Per-core history values are valid percentages
3241    #[test]
3242    fn test_per_core_history_values_valid() {
3243        let app = App::new_mock();
3244        for (i, history) in app.per_core_history.iter().enumerate() {
3245            for (j, &value) in history.iter().enumerate() {
3246                assert!(value >= 0.0 && value <= 100.0,
3247                    "Core {} history[{}] = {} is invalid (must be 0-100)",
3248                    i, j, value);
3249            }
3250        }
3251    }
3252
3253    /// TDD Test 4: CPU state breakdown per core exists
3254    #[test]
3255    fn test_cpu_state_breakdown_exists() {
3256        let app = App::new_mock();
3257        // Should have breakdown for each core
3258        assert!(!app.per_core_state.is_empty(), "per_core_state must exist");
3259        assert_eq!(app.per_core_state.len(), app.per_core_percent.len(),
3260            "per_core_state should have one entry per core");
3261    }
3262
3263    /// TDD Test 5: CPU state breakdown has all components
3264    #[test]
3265    fn test_cpu_state_breakdown_components() {
3266        let app = App::new_mock();
3267        for (i, state) in app.per_core_state.iter().enumerate() {
3268            // Each state should have user, system, iowait, idle
3269            let total = state.user + state.system + state.iowait + state.idle;
3270            assert!((total - 100.0).abs() < 1.0,
3271                "Core {} state breakdown should sum to ~100%, got {:.1}%",
3272                i, total);
3273        }
3274    }
3275
3276    /// TDD Test 6: Frequency history exists
3277    #[test]
3278    fn test_freq_history_exists() {
3279        let app = App::new_mock();
3280        // Should have frequency history for trend tracking
3281        assert!(app.freq_history.capacity() >= 60,
3282            "freq_history should have capacity for 60 samples");
3283    }
3284
3285    /// TDD Test 7: Top process per core tracking
3286    #[test]
3287    fn test_top_process_per_core_exists() {
3288        let app = App::new_mock();
3289        // Should track which process is using each core most
3290        assert!(!app.top_process_per_core.is_empty(),
3291            "top_process_per_core must exist");
3292    }
3293
3294    /// TDD Test 8: Thermal throttling state exists
3295    #[test]
3296    fn test_thermal_throttling_state_exists() {
3297        let app = App::new_mock();
3298        // Should track throttling state
3299        assert!(app.thermal_throttle_active.is_some() || !app.thermal_throttle_active.is_some(),
3300            "thermal_throttle_active field must exist (can be None)");
3301    }
3302
3303    /// TDD Test 9: Probar load test for per-core history update performance
3304    #[test]
3305    fn test_per_core_history_update_performance() {
3306        use std::time::Instant;
3307
3308        let mut app = App::new_mock();
3309        let core_count = 48; // Simulate high core count
3310
3311        // Initialize per-core history for many cores
3312        app.per_core_history = vec![Vec::with_capacity(60); core_count];
3313        app.per_core_percent = vec![50.0; core_count];
3314
3315        // Measure update performance
3316        let start = Instant::now();
3317        for _ in 0..1000 {
3318            // Simulate history update
3319            for (i, history) in app.per_core_history.iter_mut().enumerate() {
3320                let value = app.per_core_percent.get(i).copied().unwrap_or(0.0);
3321                if history.len() >= 60 {
3322                    history.remove(0);
3323                }
3324                history.push(value);
3325            }
3326        }
3327        let elapsed = start.elapsed();
3328
3329        // Should complete 1000 updates in under 100ms
3330        assert!(elapsed.as_millis() < 100,
3331            "1000 per-core history updates took {:?} (should be < 100ms)",
3332            elapsed);
3333    }
3334
3335    /// TDD Test 10: CpuCoreState struct validation
3336    #[test]
3337    fn test_cpu_core_state_struct() {
3338        let state = CpuCoreState {
3339            user: 25.0,
3340            system: 10.0,
3341            iowait: 5.0,
3342            idle: 60.0,
3343        };
3344        assert_eq!(state.user, 25.0);
3345        assert_eq!(state.system, 10.0);
3346        assert_eq!(state.iowait, 5.0);
3347        assert_eq!(state.idle, 60.0);
3348        assert!((state.total_busy() - 40.0).abs() < 0.1);
3349    }
3350
3351    // =========================================================================
3352    // EXTREME TDD: Memory Panel Exploded Mode Features - Tests Written FIRST
3353    // =========================================================================
3354    //
3355    // SPEC: Memory Panel Exploded Mode Features
3356    // 1. Memory pressure (PSI) history (60 samples)
3357    // 2. Memory breakdown categories (used/cached/buffers/free)
3358    // 3. Swap thrashing detection with trend
3359    // 4. Memory reclaim rate tracking
3360    // 5. Top memory consumers with trend
3361    //
3362    // PMAT Requirements:
3363    // - Performance: O(1) history updates
3364    // - Maintainability: Clear data structures
3365    // - Accessibility: Data available via public getters
3366    // - Testing: 95% coverage requirement
3367
3368    /// TDD Test 11: Memory pressure history exists
3369    #[test]
3370    fn test_mem_pressure_history_exists() {
3371        let app = App::new_mock();
3372        assert!(app.mem_pressure_history.capacity() >= 60,
3373            "mem_pressure_history should have capacity for 60 samples");
3374    }
3375
3376    /// TDD Test 12: Memory reclaim rate tracking
3377    #[test]
3378    fn test_mem_reclaim_rate_exists() {
3379        let app = App::new_mock();
3380        // Should track memory reclaim rate (can be 0.0 if not available)
3381        assert!(app.mem_reclaim_rate >= 0.0, "mem_reclaim_rate should be >= 0");
3382    }
3383
3384    /// TDD Test 13: Top memory consumers tracking
3385    #[test]
3386    fn test_top_mem_consumers_exists() {
3387        let app = App::new_mock();
3388        assert!(!app.top_mem_consumers.is_empty(),
3389            "top_mem_consumers should have at least one entry");
3390    }
3391
3392    /// TDD Test 14: Memory breakdown struct
3393    #[test]
3394    fn test_mem_breakdown_struct() {
3395        let breakdown = MemoryBreakdown {
3396            used_bytes: 8 * 1024 * 1024 * 1024,
3397            cached_bytes: 4 * 1024 * 1024 * 1024,
3398            buffers_bytes: 512 * 1024 * 1024,
3399            free_bytes: 3 * 1024 * 1024 * 1024,
3400        };
3401        assert_eq!(breakdown.used_bytes, 8 * 1024 * 1024 * 1024);
3402        assert_eq!(breakdown.cached_bytes, 4 * 1024 * 1024 * 1024);
3403    }
3404
3405    /// TDD Test 15: Swap trend indicator
3406    #[test]
3407    fn test_swap_trend_exists() {
3408        let app = App::new_mock();
3409        // Swap trend can be Rising, Falling, or Stable
3410        match app.swap_trend {
3411            SwapTrend::Rising | SwapTrend::Falling | SwapTrend::Stable => (),
3412        }
3413    }
3414
3415    // =========================================================================
3416    // EXTREME TDD: Disk Panel Exploded Mode Features - Tests Written FIRST
3417    // =========================================================================
3418    //
3419    // SPEC: Disk Panel Exploded Mode Features
3420    // 1. Per-mount I/O history (60 samples)
3421    // 2. Latency history tracking
3422    // 3. Queue depth tracking
3423    // 4. Device health status
3424    // 5. IOPS breakdown (read/write)
3425
3426    /// TDD Test 16: Disk latency history exists
3427    #[test]
3428    fn test_disk_latency_history_exists() {
3429        let app = App::new_mock();
3430        assert!(app.disk_latency_history.capacity() >= 60,
3431            "disk_latency_history should have capacity for 60 samples");
3432    }
3433
3434    /// TDD Test 17: Disk IOPS breakdown exists
3435    #[test]
3436    fn test_disk_iops_breakdown_exists() {
3437        let app = App::new_mock();
3438        assert!(app.disk_read_iops >= 0.0, "disk_read_iops should be >= 0");
3439        assert!(app.disk_write_iops >= 0.0, "disk_write_iops should be >= 0");
3440    }
3441
3442    /// TDD Test 18: Disk queue depth tracking
3443    #[test]
3444    fn test_disk_queue_depth_exists() {
3445        let app = App::new_mock();
3446        assert!(app.disk_queue_depth >= 0.0, "disk_queue_depth should be >= 0");
3447    }
3448
3449    /// TDD Test 19: Disk health struct
3450    #[test]
3451    fn test_disk_health_struct() {
3452        let health = DiskHealthStatus {
3453            device: "sda".to_string(),
3454            status: DiskHealth::Good,
3455            temperature: Some(35.0),
3456            reallocated_sectors: 0,
3457        };
3458        assert_eq!(health.device, "sda");
3459        assert_eq!(health.status, DiskHealth::Good);
3460    }
3461
3462    /// TDD Test 20: Disk health status field
3463    #[test]
3464    fn test_disk_health_status_exists() {
3465        let app = App::new_mock();
3466        // disk_health can be empty if no SMART data available
3467        assert!(app.disk_health.is_empty() || !app.disk_health.is_empty(),
3468            "disk_health field must exist");
3469    }
3470
3471    // =========================================================================
3472    // EXTREME TDD: Network Panel Exploded Mode Features - Tests Written FIRST
3473    // =========================================================================
3474    //
3475    // SPEC: Network Panel Exploded Mode Features
3476    // 1. Per-interface history (60 samples)
3477    // 2. Bandwidth utilization percentage
3478    // 3. Connection count by state
3479    // 4. Error/drop rate tracking
3480    // 5. Latency estimation
3481
3482    /// TDD Test 21: Network per-interface history
3483    #[test]
3484    fn test_net_interface_history_exists() {
3485        let app = App::new_mock();
3486        assert!(app.net_rx_history.capacity() >= 8,
3487            "net_rx_history should have capacity for samples");
3488        assert!(app.net_tx_history.capacity() >= 8,
3489            "net_tx_history should have capacity for samples");
3490    }
3491
3492    /// TDD Test 22: Network error tracking
3493    #[test]
3494    fn test_net_error_counts_exists() {
3495        let app = App::new_mock();
3496        assert!(app.net_errors >= 0, "net_errors should be >= 0");
3497        assert!(app.net_drops >= 0, "net_drops should be >= 0");
3498    }
3499
3500    /// TDD Test 23: Network connection state counts
3501    #[test]
3502    fn test_net_connection_states_exists() {
3503        let app = App::new_mock();
3504        assert!(app.net_established >= 0, "net_established should be >= 0");
3505        assert!(app.net_listening >= 0, "net_listening should be >= 0");
3506    }
3507
3508    // =========================================================================
3509    // EXTREME TDD: Process Panel Exploded Mode Features - Tests Written FIRST
3510    // =========================================================================
3511    //
3512    // SPEC: Process Panel Exploded Mode Features
3513    // 1. Process tree view
3514    // 2. Per-process history (CPU/memory)
3515    // 3. I/O rates per process
3516    // 4. Thread count tracking
3517    // 5. File descriptor usage
3518
3519    /// TDD Test 24: Process tree data exists
3520    #[test]
3521    fn test_process_tree_data_exists() {
3522        let app = App::new_mock();
3523        // show_tree is the toggle
3524        assert!(!app.show_tree || app.show_tree, "show_tree field must exist");
3525    }
3526
3527    /// TDD Test 25: Process additional fields tracked
3528    #[test]
3529    fn test_process_extra_fields_exist() {
3530        let app = App::new_mock();
3531        // These are tracked by process_extra analyzer
3532        // Just verify the field exists (analyzer is Default-able)
3533        let _ = &app.process_extra;
3534        assert!(true, "process_extra analyzer must exist");
3535    }
3536}