oxur_cli/repl/
stats.rs

1//! Statistics display and formatting for REPL sessions
2//!
3//! Provides display functions for REPL statistics using OxurTable formatting.
4//! Core data collection is in oxur-repl; this module handles presentation only.
5
6use crate::table::{OxurTable, Tabled};
7use oxur_repl::cache::CacheStats as ArtifactCacheStats;
8use oxur_repl::eval::{get_resource_stats, EvalMetrics, ExecutionTier};
9use oxur_repl::metrics::SessionStatsSnapshot;
10use oxur_repl::session::DirStats;
11
12// Display functions
13
14/// Show comprehensive statistics (all stats combined)
15#[allow(clippy::too_many_arguments)]
16pub fn show_all_stats(
17    collector: &EvalMetrics,
18    dir_stats: Option<&DirStats>,
19    cache_stats: Option<&ArtifactCacheStats>,
20    server_snapshot: Option<&oxur_repl::metrics::ServerMetricsSnapshot>,
21    client_snapshot: Option<&oxur_repl::metrics::ClientMetricsSnapshot>,
22    subprocess_snapshot: Option<&oxur_repl::metrics::SubprocessMetricsSnapshot>,
23    usage_snapshot: Option<&oxur_repl::metrics::UsageMetricsSnapshot>,
24    color_enabled: bool,
25) -> String {
26    let mut output = String::new();
27
28    // 1. Execution Statistics (full detailed view)
29    output.push_str(&show_execution_details(collector, color_enabled));
30
31    // 2. Cache Statistics (full view)
32    output.push_str(&show_cache_stats(collector, color_enabled));
33
34    // 3. Resource Usage (full view if available)
35    if dir_stats.is_some() || cache_stats.is_some() {
36        output.push('\n');
37        output.push_str(&show_resource_stats(dir_stats, cache_stats, color_enabled));
38    }
39
40    // 4. Client Statistics (full view if available)
41    if let Some(client) = client_snapshot {
42        output.push_str(&show_client_stats(client, color_enabled));
43    }
44
45    // 5. Usage Statistics (full view if available)
46    if let Some(usage) = usage_snapshot {
47        output.push('\n');
48        output.push_str(&show_usage_stats(usage, color_enabled));
49    }
50
51    // 6. Subprocess Statistics (full view if available)
52    if let Some(subprocess) = subprocess_snapshot {
53        output.push_str(&show_subprocess_stats(subprocess, color_enabled));
54    }
55
56    // 7. Server Statistics (full view if available)
57    if let Some(server) = server_snapshot {
58        output.push('\n');
59        output.push_str(&show_server_stats(server, color_enabled));
60    }
61
62    output
63}
64
65/// Show detailed execution breakdown
66pub fn show_execution_details(collector: &EvalMetrics, color_enabled: bool) -> String {
67    let mut output = String::new();
68
69    output.push_str(&header("Execution Statistics", color_enabled));
70    output.push('\n');
71
72    for tier in [ExecutionTier::Calculator, ExecutionTier::CachedLoaded, ExecutionTier::JustInTime]
73    {
74        if let Some(p) = collector.percentiles(tier) {
75            #[derive(Tabled)]
76            struct Metric {
77                #[tabled(rename = "Metric")]
78                metric: String,
79                #[tabled(rename = "Value (ms) ")]
80                value: String,
81            }
82
83            let metrics = vec![
84                Metric { metric: " Count ".to_string(), value: format!(" {} ", p.count) },
85                Metric { metric: " Min ".to_string(), value: format!(" {:.2} ", p.min) },
86                Metric { metric: " p50 (median) ".to_string(), value: format!(" {:.2} ", p.p50) },
87                Metric { metric: " p95 ".to_string(), value: format!(" {:.2} ", p.p95) },
88                Metric { metric: " p99 ".to_string(), value: format!(" {:.2} ", p.p99) },
89                Metric { metric: " Max ".to_string(), value: format!(" {:.2} ", p.max) },
90            ];
91
92            output.push_str(
93                &OxurTable::new(metrics).with_title(tier_name(tier)).with_footer().render(),
94            );
95            output.push_str("\n\n");
96        }
97    }
98
99    output
100}
101
102/// Show cache statistics
103pub fn show_cache_stats(collector: &EvalMetrics, color_enabled: bool) -> String {
104    let mut output = String::new();
105
106    output.push_str(&header("Cache Statistics", color_enabled));
107    output.push('\n');
108
109    // Evaluation cache
110    let cache = collector.cache_stats();
111
112    #[derive(Tabled)]
113    struct CacheMetric {
114        #[tabled(rename = "Metric")]
115        metric: String,
116        #[tabled(rename = "Value ")]
117        value: String,
118    }
119
120    let metrics = vec![
121        CacheMetric { metric: " Hits ".to_string(), value: format!(" {} ", cache.hits) },
122        CacheMetric { metric: " Misses ".to_string(), value: format!(" {} ", cache.misses) },
123        CacheMetric {
124            metric: " Hit Rate ".to_string(),
125            value: format!(" {:.1}% ", cache.hit_rate),
126        },
127    ];
128
129    output.push_str(&OxurTable::new(metrics).with_title("EVALUATION CACHE").with_footer().render());
130    output.push('\n');
131
132    output
133}
134
135/// Show resource usage statistics
136pub fn show_resource_stats(
137    dir_stats: Option<&DirStats>,
138    cache_stats: Option<&ArtifactCacheStats>,
139    color_enabled: bool,
140) -> String {
141    let mut output = String::new();
142
143    output.push_str(&header("Resource Usage", color_enabled));
144    output.push('\n');
145
146    // Memory section
147    if let Some(resource_stats) = get_resource_stats() {
148        #[derive(Tabled)]
149        struct MemoryMetric {
150            #[tabled(rename = "Metric")]
151            metric: String,
152            #[tabled(rename = "Value ")]
153            value: String,
154        }
155
156        let metrics = vec![
157            MemoryMetric {
158                metric: " Process RSS ".to_string(),
159                value: format!(" {} ", format_bytes(resource_stats.process_memory_bytes)),
160            },
161            MemoryMetric {
162                metric: " Virtual Memory ".to_string(),
163                value: format!(" {} ", format_bytes(resource_stats.virtual_memory_bytes)),
164            },
165            MemoryMetric {
166                metric: " Process ID ".to_string(),
167                value: format!(" {} ", resource_stats.pid),
168            },
169        ];
170
171        output.push_str(&OxurTable::new(metrics).with_title("MEMORY").with_footer().render());
172        output.push_str("\n\n");
173    } else {
174        output.push_str("Memory stats unavailable\n\n");
175    }
176
177    // Session directory section
178    if let Some(dir_stats) = dir_stats {
179        let location_type = if dir_stats.is_tmpfs { " (tmpfs)" } else { "" };
180
181        #[derive(Tabled)]
182        struct DirMetric {
183            #[tabled(rename = "Metric")]
184            metric: String,
185            #[tabled(rename = "Value ")]
186            value: String,
187        }
188
189        let metrics = vec![
190            DirMetric {
191                metric: " Location ".to_string(),
192                value: format!(" {}{} ", dir_stats.path.display(), location_type),
193            },
194            DirMetric {
195                metric: " Files ".to_string(),
196                value: format!(" {} ", dir_stats.file_count),
197            },
198            DirMetric {
199                metric: " Disk Usage ".to_string(),
200                value: format!(" {} ", format_bytes(dir_stats.total_bytes)),
201            },
202        ];
203
204        output.push_str(
205            &OxurTable::new(metrics).with_title("SESSION DIRECTORY").with_footer().render(),
206        );
207        output.push_str("\n\n");
208    } else {
209        output.push_str("Session directory not initialized\n\n");
210    }
211
212    // Artifact cache section
213    if let Some(cache_stats) = cache_stats {
214        #[derive(Tabled)]
215        struct ArtifactMetric {
216            #[tabled(rename = "Metric")]
217            metric: String,
218            #[tabled(rename = "Value ")]
219            value: String,
220        }
221
222        let age_seconds = if cache_stats.entry_count > 0 {
223            let now = std::time::SystemTime::now()
224                .duration_since(std::time::UNIX_EPOCH)
225                .unwrap()
226                .as_secs();
227            now.saturating_sub(cache_stats.oldest_entry_secs)
228        } else {
229            0
230        };
231
232        let metrics = vec![
233            ArtifactMetric {
234                metric: " Entries ".to_string(),
235                value: format!(" {} ", cache_stats.entry_count),
236            },
237            ArtifactMetric {
238                metric: " Total Size ".to_string(),
239                value: format!(" {} ", format_bytes(cache_stats.total_size_bytes)),
240            },
241            ArtifactMetric {
242                metric: " Oldest Entry ".to_string(),
243                value: if cache_stats.entry_count > 0 {
244                    format!(" {} ", format_duration(age_seconds))
245                } else {
246                    " N/A ".to_string()
247                },
248            },
249            ArtifactMetric {
250                metric: " Cache Directory ".to_string(),
251                value: format!(" {} ", cache_stats.cache_dir.display()),
252            },
253        ];
254
255        output.push_str(
256            &OxurTable::new(metrics).with_title("ARTIFACT CACHE (Global)").with_footer().render(),
257        );
258        output.push_str("\n\n");
259    } else {
260        output.push_str("Artifact cache not initialized\n\n");
261    }
262
263    output
264}
265
266/// Show all sessions with their statistics
267pub fn show_sessions(
268    sessions: &[oxur_repl::server::SessionInfo],
269    current_session_id: &oxur_repl::protocol::SessionId,
270    color_enabled: bool,
271) -> String {
272    let mut output = String::new();
273
274    output.push_str(&header("Sessions", color_enabled));
275    output.push('\n');
276
277    if sessions.is_empty() {
278        output.push_str("No active sessions\n");
279        return output;
280    }
281
282    #[derive(Tabled)]
283    struct SessionRow {
284        #[tabled(rename = "ID")]
285        id: String,
286        #[tabled(rename = "Name")]
287        name: String,
288        #[tabled(rename = "Active")]
289        active: String,
290        #[tabled(rename = "Evals")]
291        evals: String,
292        #[tabled(rename = "Last Active")]
293        last_active: String,
294    }
295
296    let rows: Vec<SessionRow> = sessions
297        .iter()
298        .map(|s| {
299            let is_current = s.id == *current_session_id;
300            let active_marker = if is_current { " * " } else { " " };
301
302            // Format last active time
303            let now = std::time::SystemTime::now()
304                .duration_since(std::time::UNIX_EPOCH)
305                .unwrap()
306                .as_millis() as u64;
307            let elapsed_ms = now.saturating_sub(s.last_active_at);
308            let last_active = if elapsed_ms < 60_000 {
309                " just now ".to_string()
310            } else if elapsed_ms < 3_600_000 {
311                format!(" {} min ago ", elapsed_ms / 60_000)
312            } else if elapsed_ms < 86_400_000 {
313                format!(" {} hr ago ", elapsed_ms / 3_600_000)
314            } else {
315                format!(" {} days ago ", elapsed_ms / 86_400_000)
316            };
317
318            SessionRow {
319                id: format!(" {} ", s.id),
320                name: format!(" {} ", s.name.clone().unwrap_or_else(|| "-".to_string())),
321                active: active_marker.to_string(),
322                evals: format!(" {} ", s.eval_count),
323                last_active,
324            }
325        })
326        .collect();
327
328    output.push_str(&OxurTable::new(rows).with_title("ACTIVE SESSIONS").with_footer().render());
329    output.push('\n');
330
331    output
332}
333
334/// Show usage metrics
335pub fn show_usage_stats(
336    usage_snapshot: &oxur_repl::metrics::UsageMetricsSnapshot,
337    color_enabled: bool,
338) -> String {
339    let mut output = String::new();
340
341    output.push_str(&header("Usage Statistics", color_enabled));
342    output.push('\n');
343
344    #[derive(Tabled)]
345    struct CommandMetric {
346        #[tabled(rename = "Command")]
347        command: String,
348        #[tabled(rename = "Count")]
349        count: String,
350        #[tabled(rename = "Percentage")]
351        percentage: String,
352    }
353
354    let total = usage_snapshot.total_commands as f64;
355    let calc_pct = |count: u64| {
356        if total > 0.0 {
357            format!(" {:.1}% ", (count as f64 / total) * 100.0)
358        } else {
359            " 0.0% ".to_string()
360        }
361    };
362
363    let mut metrics = vec![
364        CommandMetric {
365            command: " Eval ".to_string(),
366            count: format!(" {} ", usage_snapshot.eval_count),
367            percentage: calc_pct(usage_snapshot.eval_count),
368        },
369        CommandMetric {
370            command: " Help ".to_string(),
371            count: format!(" {} ", usage_snapshot.help_count),
372            percentage: calc_pct(usage_snapshot.help_count),
373        },
374        CommandMetric {
375            command: " Stats ".to_string(),
376            count: format!(" {} ", usage_snapshot.stats_count),
377            percentage: calc_pct(usage_snapshot.stats_count),
378        },
379        CommandMetric {
380            command: " Info ".to_string(),
381            count: format!(" {} ", usage_snapshot.info_count),
382            percentage: calc_pct(usage_snapshot.info_count),
383        },
384        CommandMetric {
385            command: " Sessions ".to_string(),
386            count: format!(" {} ", usage_snapshot.sessions_count),
387            percentage: calc_pct(usage_snapshot.sessions_count),
388        },
389        CommandMetric {
390            command: " Clear ".to_string(),
391            count: format!(" {} ", usage_snapshot.clear_count),
392            percentage: calc_pct(usage_snapshot.clear_count),
393        },
394        CommandMetric {
395            command: " Banner ".to_string(),
396            count: format!(" {} ", usage_snapshot.banner_count),
397            percentage: calc_pct(usage_snapshot.banner_count),
398        },
399    ];
400
401    // Add total as a footer-like row
402    metrics.push(CommandMetric {
403        command: " Total Commands: ".to_string(),
404        count: format!(" {} ", usage_snapshot.total_commands),
405        percentage: " ".to_string(),
406    });
407
408    output
409        .push_str(&OxurTable::new(metrics).with_title("COMMAND FREQUENCY").with_footer().render());
410    output.push_str("\n\n");
411
412    output
413}
414
415/// Show client metrics
416pub fn show_client_stats(
417    client_snapshot: &oxur_repl::metrics::ClientMetricsSnapshot,
418    color_enabled: bool,
419) -> String {
420    let mut output = String::new();
421
422    output.push_str(&header("Client Statistics", color_enabled));
423    output.push('\n');
424
425    #[derive(Tabled)]
426    struct RequestMetric {
427        #[tabled(rename = "Metric")]
428        metric: String,
429        #[tabled(rename = "Value ")]
430        value: String,
431    }
432
433    // Request/Response stats
434    let metrics = vec![
435        RequestMetric {
436            metric: " Total Requests ".to_string(),
437            value: format!(" {} ", client_snapshot.requests_total),
438        },
439        RequestMetric {
440            metric: " Total Responses ".to_string(),
441            value: format!(" {} ", client_snapshot.responses_total),
442        },
443        RequestMetric {
444            metric: " Success Responses ".to_string(),
445            value: format!(" {} ", client_snapshot.responses_success),
446        },
447        RequestMetric {
448            metric: " Error Responses ".to_string(),
449            value: format!(" {} ", client_snapshot.responses_error),
450        },
451    ];
452
453    output.push_str(
454        &OxurTable::new(metrics).with_title("REQUESTS & RESPONSES").with_footer().render(),
455    );
456    output.push_str("\n\n");
457
458    #[derive(Tabled)]
459    struct LatencyMetric {
460        #[tabled(rename = "Metric")]
461        metric: String,
462        #[tabled(rename = "Value (ms)")]
463        value: String,
464    }
465
466    // Latency stats
467    let latency_metrics = vec![
468        LatencyMetric {
469            metric: " Average ".to_string(),
470            value: format!(" {:.2} ", client_snapshot.average_latency_ms),
471        },
472        LatencyMetric {
473            metric: " P50 ".to_string(),
474            value: format!(" {:.2} ", client_snapshot.p50_latency_ms),
475        },
476        LatencyMetric {
477            metric: " P95 ".to_string(),
478            value: format!(" {:.2} ", client_snapshot.p95_latency_ms),
479        },
480        LatencyMetric {
481            metric: " P99 ".to_string(),
482            value: format!(" {:.2} ", client_snapshot.p99_latency_ms),
483        },
484        LatencyMetric {
485            metric: " Min ".to_string(),
486            value: format!(" {:.2} ", client_snapshot.min_latency_ms),
487        },
488        LatencyMetric {
489            metric: " Max ".to_string(),
490            value: format!(" {:.2} ", client_snapshot.max_latency_ms),
491        },
492    ];
493
494    output.push_str(
495        &OxurTable::new(latency_metrics).with_title("LATENCY DISTRIBUTION").with_footer().render(),
496    );
497    output.push('\n');
498
499    output
500}
501
502/// Show server metrics
503pub fn show_server_stats(
504    server_snapshot: &oxur_repl::metrics::ServerMetricsSnapshot,
505    color_enabled: bool,
506) -> String {
507    let mut output = String::new();
508
509    output.push_str(&header("Server Statistics", color_enabled));
510    output.push('\n');
511
512    #[derive(Tabled)]
513    struct ConnectionMetric {
514        #[tabled(rename = "Metric")]
515        metric: String,
516        #[tabled(rename = "Value ")]
517        value: String,
518    }
519
520    // Connection stats
521    let metrics = vec![
522        ConnectionMetric {
523            metric: " Total Connections ".to_string(),
524            value: format!(" {} ", server_snapshot.connections_total),
525        },
526        ConnectionMetric {
527            metric: " Active Connections ".to_string(),
528            value: format!(" {} ", server_snapshot.connections_active),
529        },
530    ];
531
532    output.push_str(&OxurTable::new(metrics).with_title("CONNECTIONS").with_footer().render());
533    output.push_str("\n\n");
534
535    // Session stats
536    let metrics = vec![
537        ConnectionMetric {
538            metric: " Total Sessions ".to_string(),
539            value: format!(" {} ", server_snapshot.sessions_total),
540        },
541        ConnectionMetric {
542            metric: " Active Sessions ".to_string(),
543            value: format!(" {} ", server_snapshot.sessions_active),
544        },
545    ];
546
547    output.push_str(&OxurTable::new(metrics).with_title("SESSIONS").with_footer().render());
548    output.push_str("\n\n");
549
550    // Request/Response stats
551    let success_rate = if server_snapshot.responses_total > 0 {
552        (server_snapshot.responses_success as f64 / server_snapshot.responses_total as f64) * 100.0
553    } else {
554        0.0
555    };
556
557    let metrics = vec![
558        ConnectionMetric {
559            metric: " Total Requests ".to_string(),
560            value: format!(" {} ", server_snapshot.requests_total),
561        },
562        ConnectionMetric {
563            metric: " Total Responses ".to_string(),
564            value: format!(" {} ", server_snapshot.responses_total),
565        },
566        ConnectionMetric {
567            metric: " Successful ".to_string(),
568            value: format!(" {} ", server_snapshot.responses_success),
569        },
570        ConnectionMetric {
571            metric: " Errors ".to_string(),
572            value: format!(" {} ", server_snapshot.responses_error),
573        },
574        ConnectionMetric {
575            metric: " Success Rate ".to_string(),
576            value: format!(" {:.1}% ", success_rate),
577        },
578    ];
579
580    output.push_str(
581        &OxurTable::new(metrics).with_title("REQUESTS & RESPONSES").with_footer().render(),
582    );
583    output.push_str("\n\n");
584
585    output
586}
587
588/// Show subprocess metrics
589pub fn show_subprocess_stats(
590    subprocess_snapshot: &oxur_repl::metrics::SubprocessMetricsSnapshot,
591    color_enabled: bool,
592) -> String {
593    let mut output = String::new();
594
595    output.push_str(&header("Subprocess Statistics", color_enabled));
596    output.push('\n');
597
598    #[derive(Tabled)]
599    struct SubprocessMetric {
600        #[tabled(rename = "Metric")]
601        metric: String,
602        #[tabled(rename = "Value ")]
603        value: String,
604    }
605
606    let status = if subprocess_snapshot.is_running { "Running" } else { "Stopped" };
607    let uptime = format_uptime_seconds(subprocess_snapshot.uptime_seconds);
608    let last_reason = subprocess_snapshot
609        .last_restart_reason
610        .map(|r| r.to_string())
611        .unwrap_or_else(|| "N/A".to_string());
612
613    let metrics = vec![
614        SubprocessMetric { metric: " Status ".to_string(), value: format!(" {} ", status) },
615        SubprocessMetric { metric: " Uptime ".to_string(), value: format!(" {} ", uptime) },
616        SubprocessMetric {
617            metric: " Restart Count ".to_string(),
618            value: format!(" {} ", subprocess_snapshot.restart_count),
619        },
620        SubprocessMetric {
621            metric: " Last Restart Reason ".to_string(),
622            value: format!(" {} ", last_reason),
623        },
624    ];
625
626    output.push_str(&OxurTable::new(metrics).with_title("STATUS").with_footer().render());
627    output.push_str("\n\n");
628
629    output
630}
631
632// ============================================================================
633// Snapshot-based display functions (for remote/protocol mode)
634// ============================================================================
635
636/// Show session summary from snapshot (for remote mode)
637///
638/// Uses the serialized SessionStatsSnapshot instead of direct EvalMetrics access.
639pub fn show_session_summary_from_snapshot(
640    snapshot: &SessionStatsSnapshot,
641    color_enabled: bool,
642) -> String {
643    let mut output = String::new();
644
645    // Header
646    output.push_str(&header("Session Statistics", color_enabled));
647    output.push('\n');
648
649    // Overall summary
650    output.push_str(&section("SUMMARY", color_enabled));
651    output.push_str(&format!("Total Evaluations: {}\n", snapshot.total_evaluations));
652    output.push_str(&format!(
653        "Cache Hit Rate: {:.1}% ({} hits, {} misses)\n\n",
654        snapshot.cache.hit_rate, snapshot.cache.hits, snapshot.cache.misses
655    ));
656
657    // Execution tiers table
658    #[derive(Tabled)]
659    struct TierMetric {
660        #[tabled(rename = "Tier")]
661        tier: String,
662        #[tabled(rename = "Count ")]
663        count: String,
664        #[tabled(rename = "P50 (ms)")]
665        p50: String,
666        #[tabled(rename = "P95 (ms) ")]
667        p95: String,
668        #[tabled(rename = "P99 (ms) ")]
669        p99: String,
670    }
671
672    let mut metrics = Vec::new();
673
674    // Tier 1
675    if let Some(ref p) = snapshot.tier1_percentiles {
676        metrics.push(TierMetric {
677            tier: " Calculator ".to_string(),
678            count: format!(" {} ", p.count),
679            p50: format!(" {:.2} ", p.p50),
680            p95: format!(" {:.2} ", p.p95),
681            p99: format!(" {:.2} ", p.p99),
682        });
683    }
684
685    // Tier 2
686    if let Some(ref p) = snapshot.tier2_percentiles {
687        metrics.push(TierMetric {
688            tier: " Cached ".to_string(),
689            count: format!(" {} ", p.count),
690            p50: format!(" {:.2} ", p.p50),
691            p95: format!(" {:.2} ", p.p95),
692            p99: format!(" {:.2} ", p.p99),
693        });
694    }
695
696    // Tier 3
697    if let Some(ref p) = snapshot.tier3_percentiles {
698        metrics.push(TierMetric {
699            tier: " JIT ".to_string(),
700            count: format!(" {} ", p.count),
701            p50: format!(" {:.2} ", p.p50),
702            p95: format!(" {:.2} ", p.p95),
703            p99: format!(" {:.2} ", p.p99),
704        });
705    }
706
707    if !metrics.is_empty() {
708        output.push_str(
709            &OxurTable::new(metrics).with_title("EXECUTION TIERS").with_footer().render(),
710        );
711        output.push('\n');
712    } else {
713        output.push_str("No execution data yet.\n\n");
714    }
715
716    output
717}
718
719/// Show detailed execution breakdown from snapshot (for remote mode)
720pub fn show_execution_from_snapshot(
721    snapshot: &SessionStatsSnapshot,
722    color_enabled: bool,
723) -> String {
724    let mut output = String::new();
725
726    output.push_str(&header("Execution Statistics", color_enabled));
727    output.push('\n');
728
729    // Helper to display a tier's percentiles
730    let display_tier =
731        |output: &mut String, name: &str, percentiles: &Option<oxur_repl::metrics::Percentiles>| {
732            if let Some(ref p) = percentiles {
733                #[derive(Tabled)]
734                struct Metric {
735                    #[tabled(rename = "Metric")]
736                    metric: String,
737                    #[tabled(rename = "Value (ms)")]
738                    value: String,
739                }
740
741                let metrics = vec![
742                    Metric { metric: " Count ".to_string(), value: format!(" {} ", p.count) },
743                    Metric { metric: " Min ".to_string(), value: format!(" {:.2} ", p.min) },
744                    Metric {
745                        metric: " p50 (median) ".to_string(),
746                        value: format!(" {:.2} ", p.p50),
747                    },
748                    Metric { metric: " p95 ".to_string(), value: format!(" {:.2} ", p.p95) },
749                    Metric { metric: " p99 ".to_string(), value: format!(" {:.2} ", p.p99) },
750                    Metric { metric: " Max ".to_string(), value: format!(" {:.2} ", p.max) },
751                ];
752
753                output.push_str(&OxurTable::new(metrics).with_title(name).with_footer().render());
754                output.push_str("\n\n");
755            }
756        };
757
758    display_tier(&mut output, "TIER 1: CALCULATOR (~1ms)", &snapshot.tier1_percentiles);
759    display_tier(&mut output, "TIER 2: CACHED LOADED (~1-5ms)", &snapshot.tier2_percentiles);
760    display_tier(&mut output, "TIER 3: JUST-IN-TIME (~50-300ms)", &snapshot.tier3_percentiles);
761
762    output
763}
764
765/// Show cache statistics from snapshot (for remote mode)
766pub fn show_cache_from_snapshot(snapshot: &SessionStatsSnapshot, color_enabled: bool) -> String {
767    let mut output = String::new();
768
769    output.push_str(&header("Cache Statistics", color_enabled));
770    output.push('\n');
771
772    // Evaluation cache
773    #[derive(Tabled)]
774    struct CacheMetric {
775        #[tabled(rename = "Metric")]
776        metric: String,
777        #[tabled(rename = "Value")]
778        value: String,
779    }
780
781    let metrics = vec![
782        CacheMetric { metric: " Hits ".to_string(), value: format!(" {} ", snapshot.cache.hits) },
783        CacheMetric {
784            metric: " Misses ".to_string(),
785            value: format!(" {} ", snapshot.cache.misses),
786        },
787        CacheMetric {
788            metric: " Hit Rate ".to_string(),
789            value: format!(" {:.1}% ", snapshot.cache.hit_rate),
790        },
791    ];
792
793    output.push_str(&OxurTable::new(metrics).with_title("EVALUATION CACHE").with_footer().render());
794    output.push('\n');
795
796    output
797}
798
799// ============================================================================
800// Stats command parsing
801// ============================================================================
802
803/// Parse stats commands
804///
805/// Recognizes:
806/// - `(stats)` - Session summary
807/// - `(stats execution)` - Detailed tier breakdown
808/// - `(stats cache)` - Cache metrics
809pub fn parse_stats_command(
810    input: &str,
811    collector: &EvalMetrics,
812    color_enabled: bool,
813) -> Option<String> {
814    if input == "(stats)" {
815        return Some(show_all_stats(collector, None, None, None, None, None, None, color_enabled));
816    }
817
818    if input == "(stats execution)" {
819        return Some(show_execution_details(collector, color_enabled));
820    }
821
822    if input == "(stats cache)" {
823        return Some(show_cache_stats(collector, color_enabled));
824    }
825
826    None
827}
828
829/// Parse stats command with resource stats
830///
831/// Extended version that handles `(stats resources)` command
832pub fn parse_stats_command_with_resources(
833    input: &str,
834    collector: &EvalMetrics,
835    dir_stats: Option<&DirStats>,
836    cache_stats: Option<&ArtifactCacheStats>,
837    color_enabled: bool,
838) -> Option<String> {
839    if input == "(stats resources)" {
840        return Some(show_resource_stats(dir_stats, cache_stats, color_enabled));
841    }
842
843    // Fall back to regular stats commands
844    parse_stats_command(input, collector, color_enabled)
845}
846
847// Helper functions
848
849fn header(text: &str, color_enabled: bool) -> String {
850    if color_enabled {
851        format!("\x1b[1;36m{}\x1b[0m\n{}\n", text, "═".repeat(text.len()))
852    } else {
853        format!("{}\n{}\n", text, "=".repeat(text.len()))
854    }
855}
856
857fn section(title: &str, color_enabled: bool) -> String {
858    if color_enabled {
859        format!("\x1b[1;36m{}\x1b[0m\n{}\n", title, "─".repeat(title.len()))
860    } else {
861        format!("{}\n{}\n", title, "-".repeat(title.len()))
862    }
863}
864
865fn tier_name(tier: ExecutionTier) -> String {
866    match tier {
867        ExecutionTier::Calculator => "TIER 1: CALCULATOR (~1ms)".to_string(),
868        ExecutionTier::CachedLoaded => "TIER 2: CACHED LOADED (~1-5ms)".to_string(),
869        ExecutionTier::JustInTime => "TIER 3: JUST-IN-TIME (~50-300ms)".to_string(),
870        _ => "UNKNOWN TIER".to_string(), // Handle future variants
871    }
872}
873
874fn format_bytes(bytes: u64) -> String {
875    const KB: u64 = 1024;
876    const MB: u64 = KB * 1024;
877    const GB: u64 = MB * 1024;
878
879    if bytes >= GB {
880        format!("{:.2} GB", bytes as f64 / GB as f64)
881    } else if bytes >= MB {
882        format!("{:.2} MB", bytes as f64 / MB as f64)
883    } else if bytes >= KB {
884        format!("{:.2} KB", bytes as f64 / KB as f64)
885    } else {
886        format!("{} bytes", bytes)
887    }
888}
889
890fn format_duration(seconds: u64) -> String {
891    const MINUTE: u64 = 60;
892    const HOUR: u64 = MINUTE * 60;
893    const DAY: u64 = HOUR * 24;
894
895    if seconds >= DAY {
896        format!("{} days ago", seconds / DAY)
897    } else if seconds >= HOUR {
898        format!("{} hours ago", seconds / HOUR)
899    } else if seconds >= MINUTE {
900        format!("{} minutes ago", seconds / MINUTE)
901    } else {
902        format!("{} seconds ago", seconds)
903    }
904}
905
906fn format_uptime_seconds(seconds: f64) -> String {
907    let secs = seconds as u64;
908    const MINUTE: u64 = 60;
909    const HOUR: u64 = MINUTE * 60;
910    const DAY: u64 = HOUR * 24;
911
912    if secs >= DAY {
913        let days = secs / DAY;
914        let hours = (secs % DAY) / HOUR;
915        format!("{}d {}h", days, hours)
916    } else if secs >= HOUR {
917        let hours = secs / HOUR;
918        let mins = (secs % HOUR) / MINUTE;
919        format!("{}h {}m", hours, mins)
920    } else if secs >= MINUTE {
921        let mins = secs / MINUTE;
922        let s = secs % MINUTE;
923        format!("{}m {}s", mins, s)
924    } else {
925        format!("{:.1}s", seconds)
926    }
927}
928
929#[cfg(test)]
930mod tests {
931    use super::*;
932    use std::time::Duration;
933
934    #[test]
935    fn test_display_session_summary() {
936        let mut collector = EvalMetrics::new("test");
937        collector.record(ExecutionTier::Calculator, false, Duration::from_millis(1));
938        collector.record(ExecutionTier::CachedLoaded, true, Duration::from_millis(2));
939
940        let output = show_all_stats(&collector, None, None, None, None, None, None, false);
941
942        // Should contain all the major sections
943        assert!(output.contains("Execution Statistics"));
944        assert!(output.contains("Cache Statistics"));
945        assert!(output.contains("TIER 1: CALCULATOR"));
946        assert!(output.contains("EVALUATION CACHE"));
947    }
948
949    #[test]
950    fn test_display_execution_details() {
951        let mut collector = EvalMetrics::new("test");
952        collector.record(ExecutionTier::Calculator, false, Duration::from_millis(1));
953        collector.record(ExecutionTier::Calculator, false, Duration::from_millis(2));
954
955        let output = show_execution_details(&collector, false);
956
957        assert!(output.contains("Execution Statistics"));
958        assert!(output.contains("TIER 1: CALCULATOR"));
959    }
960
961    #[test]
962    fn test_display_cache_stats() {
963        let mut collector = EvalMetrics::new("test");
964        collector.record(ExecutionTier::CachedLoaded, true, Duration::from_millis(2));
965
966        let output = show_cache_stats(&collector, false);
967
968        assert!(output.contains("Cache Statistics"));
969        assert!(output.contains("EVALUATION CACHE"));
970    }
971
972    #[test]
973    fn test_parse_stats_command_summary() {
974        let collector = EvalMetrics::new("test");
975
976        let result = parse_stats_command("(stats)", &collector, false);
977        assert!(result.is_some());
978        // Should contain multiple sections
979        let output = result.unwrap();
980        assert!(output.contains("Execution Statistics"));
981        assert!(output.contains("Cache Statistics"));
982    }
983
984    #[test]
985    fn test_parse_stats_command_execution() {
986        let collector = EvalMetrics::new("test");
987
988        let result = parse_stats_command("(stats execution)", &collector, false);
989        assert!(result.is_some());
990        assert!(result.unwrap().contains("Execution Statistics"));
991    }
992
993    #[test]
994    fn test_parse_stats_command_cache() {
995        let collector = EvalMetrics::new("test");
996
997        let result = parse_stats_command("(stats cache)", &collector, false);
998        assert!(result.is_some());
999        assert!(result.unwrap().contains("Cache Statistics"));
1000    }
1001
1002    #[test]
1003    fn test_parse_stats_command_invalid() {
1004        let collector = EvalMetrics::new("test");
1005
1006        let result = parse_stats_command("(stats invalid)", &collector, false);
1007        assert!(result.is_none());
1008
1009        let result = parse_stats_command("(not-stats)", &collector, false);
1010        assert!(result.is_none());
1011    }
1012
1013    // ===== Helper function tests =====
1014
1015    #[test]
1016    fn test_format_bytes_bytes() {
1017        assert_eq!(format_bytes(0), "0 bytes");
1018        assert_eq!(format_bytes(100), "100 bytes");
1019        assert_eq!(format_bytes(1023), "1023 bytes");
1020    }
1021
1022    #[test]
1023    fn test_format_bytes_kilobytes() {
1024        assert_eq!(format_bytes(1024), "1.00 KB");
1025        assert_eq!(format_bytes(2048), "2.00 KB");
1026        assert_eq!(format_bytes(1536), "1.50 KB");
1027        assert_eq!(format_bytes(1024 * 1024 - 1), "1024.00 KB");
1028    }
1029
1030    #[test]
1031    fn test_format_bytes_megabytes() {
1032        assert_eq!(format_bytes(1024 * 1024), "1.00 MB");
1033        assert_eq!(format_bytes(2 * 1024 * 1024), "2.00 MB");
1034        assert_eq!(format_bytes(1024 * 1024 * 1024 - 1), "1024.00 MB");
1035    }
1036
1037    #[test]
1038    fn test_format_bytes_gigabytes() {
1039        assert_eq!(format_bytes(1024 * 1024 * 1024), "1.00 GB");
1040        assert_eq!(format_bytes(2 * 1024 * 1024 * 1024), "2.00 GB");
1041    }
1042
1043    #[test]
1044    fn test_format_duration_seconds() {
1045        assert_eq!(format_duration(0), "0 seconds ago");
1046        assert_eq!(format_duration(30), "30 seconds ago");
1047        assert_eq!(format_duration(59), "59 seconds ago");
1048    }
1049
1050    #[test]
1051    fn test_format_duration_minutes() {
1052        assert_eq!(format_duration(60), "1 minutes ago");
1053        assert_eq!(format_duration(120), "2 minutes ago");
1054        assert_eq!(format_duration(3599), "59 minutes ago");
1055    }
1056
1057    #[test]
1058    fn test_format_duration_hours() {
1059        assert_eq!(format_duration(3600), "1 hours ago");
1060        assert_eq!(format_duration(7200), "2 hours ago");
1061        assert_eq!(format_duration(86399), "23 hours ago");
1062    }
1063
1064    #[test]
1065    fn test_format_duration_days() {
1066        assert_eq!(format_duration(86400), "1 days ago");
1067        assert_eq!(format_duration(172800), "2 days ago");
1068        assert_eq!(format_duration(604800), "7 days ago");
1069    }
1070
1071    #[test]
1072    fn test_format_uptime_seconds_subsecond() {
1073        assert_eq!(format_uptime_seconds(0.5), "0.5s");
1074        assert_eq!(format_uptime_seconds(30.5), "30.5s");
1075        assert_eq!(format_uptime_seconds(59.9), "59.9s");
1076    }
1077
1078    #[test]
1079    fn test_format_uptime_seconds_minutes() {
1080        assert_eq!(format_uptime_seconds(60.0), "1m 0s");
1081        assert_eq!(format_uptime_seconds(90.0), "1m 30s");
1082        assert_eq!(format_uptime_seconds(3599.0), "59m 59s");
1083    }
1084
1085    #[test]
1086    fn test_format_uptime_seconds_hours() {
1087        assert_eq!(format_uptime_seconds(3600.0), "1h 0m");
1088        assert_eq!(format_uptime_seconds(5400.0), "1h 30m");
1089        assert_eq!(format_uptime_seconds(86399.0), "23h 59m");
1090    }
1091
1092    #[test]
1093    fn test_format_uptime_seconds_days() {
1094        assert_eq!(format_uptime_seconds(86400.0), "1d 0h");
1095        assert_eq!(format_uptime_seconds(129600.0), "1d 12h");
1096        assert_eq!(format_uptime_seconds(172800.0), "2d 0h");
1097    }
1098
1099    #[test]
1100    fn test_header_with_color() {
1101        let result = header("Test", true);
1102        assert!(result.contains("Test"));
1103        assert!(result.contains("\x1b[1;36m")); // cyan color code
1104        assert!(result.contains("\x1b[0m")); // reset code
1105        assert!(result.contains("═")); // unicode equals
1106    }
1107
1108    #[test]
1109    fn test_header_without_color() {
1110        let result = header("Test", false);
1111        assert!(result.contains("Test"));
1112        assert!(!result.contains("\x1b")); // no escape codes
1113        assert!(result.contains("=")); // ASCII equals
1114    }
1115
1116    #[test]
1117    fn test_section_with_color() {
1118        let result = section("Section", true);
1119        assert!(result.contains("Section"));
1120        assert!(result.contains("\x1b[1;36m")); // cyan color code
1121        assert!(result.contains("─")); // unicode dash
1122    }
1123
1124    #[test]
1125    fn test_section_without_color() {
1126        let result = section("Section", false);
1127        assert!(result.contains("Section"));
1128        assert!(!result.contains("\x1b")); // no escape codes
1129        assert!(result.contains("-")); // ASCII dash
1130    }
1131
1132    #[test]
1133    fn test_tier_name_calculator() {
1134        let name = tier_name(ExecutionTier::Calculator);
1135        assert!(name.contains("CALCULATOR"));
1136        assert!(name.contains("TIER 1"));
1137    }
1138
1139    #[test]
1140    fn test_tier_name_cached() {
1141        let name = tier_name(ExecutionTier::CachedLoaded);
1142        assert!(name.contains("CACHED"));
1143        assert!(name.contains("TIER 2"));
1144    }
1145
1146    #[test]
1147    fn test_tier_name_jit() {
1148        let name = tier_name(ExecutionTier::JustInTime);
1149        assert!(name.contains("JUST-IN-TIME"));
1150        assert!(name.contains("TIER 3"));
1151    }
1152
1153    // ===== Display function tests =====
1154
1155    #[test]
1156    fn test_show_resource_stats_no_data() {
1157        let output = show_resource_stats(None, None, false);
1158        // When no data is provided, should show messages about unavailability
1159        assert!(output.contains("Resource Usage"));
1160        assert!(output.contains("Session directory not initialized"));
1161        assert!(output.contains("Artifact cache not initialized"));
1162    }
1163
1164    #[test]
1165    fn test_show_sessions_empty() {
1166        let sessions: Vec<oxur_repl::server::SessionInfo> = vec![];
1167        let current = oxur_repl::protocol::SessionId::new("test");
1168
1169        let output = show_sessions(&sessions, &current, false);
1170        assert!(output.contains("Sessions"));
1171        assert!(output.contains("No active sessions"));
1172    }
1173
1174    #[test]
1175    fn test_show_usage_stats() {
1176        let usage = oxur_repl::metrics::UsageMetricsSnapshot {
1177            session_id: "test".to_string(),
1178            eval_count: 10,
1179            help_count: 5,
1180            stats_count: 3,
1181            info_count: 2,
1182            sessions_count: 1,
1183            clear_count: 0,
1184            banner_count: 1,
1185            total_commands: 22,
1186        };
1187
1188        let output = show_usage_stats(&usage, false);
1189        assert!(output.contains("Usage Statistics"));
1190        assert!(output.contains("COMMAND FREQUENCY"));
1191        assert!(output.contains("Eval"));
1192        assert!(output.contains("10"));
1193    }
1194
1195    #[test]
1196    fn test_show_client_stats() {
1197        let client = oxur_repl::metrics::ClientMetricsSnapshot {
1198            requests_total: 100,
1199            responses_total: 98,
1200            responses_success: 95,
1201            responses_error: 3,
1202            average_latency_ms: 10.5,
1203            p50_latency_ms: 8.0,
1204            p95_latency_ms: 25.0,
1205            p99_latency_ms: 50.0,
1206            min_latency_ms: 1.0,
1207            max_latency_ms: 100.0,
1208        };
1209
1210        let output = show_client_stats(&client, false);
1211        assert!(output.contains("Client Statistics"));
1212        assert!(output.contains("REQUESTS & RESPONSES"));
1213        assert!(output.contains("LATENCY DISTRIBUTION"));
1214        assert!(output.contains("100")); // requests_total
1215    }
1216
1217    #[test]
1218    fn test_show_server_stats() {
1219        let server = oxur_repl::metrics::ServerMetricsSnapshot {
1220            connections_total: 50,
1221            connections_active: 2,
1222            sessions_total: 30,
1223            sessions_active: 5,
1224            requests_total: 1000,
1225            responses_total: 998,
1226            responses_success: 990,
1227            responses_error: 8,
1228        };
1229
1230        let output = show_server_stats(&server, false);
1231        assert!(output.contains("Server Statistics"));
1232        assert!(output.contains("CONNECTIONS"));
1233        assert!(output.contains("SESSIONS"));
1234        assert!(output.contains("Success Rate"));
1235    }
1236
1237    #[test]
1238    fn test_show_subprocess_stats_running() {
1239        let subprocess = oxur_repl::metrics::SubprocessMetricsSnapshot {
1240            is_running: true,
1241            uptime_seconds: 3600.0,
1242            restart_count: 2,
1243            last_restart_reason: None,
1244        };
1245
1246        let output = show_subprocess_stats(&subprocess, false);
1247        assert!(output.contains("Subprocess Statistics"));
1248        assert!(output.contains("Running"));
1249        assert!(output.contains("1h 0m")); // uptime
1250    }
1251
1252    #[test]
1253    fn test_show_subprocess_stats_stopped() {
1254        let subprocess = oxur_repl::metrics::SubprocessMetricsSnapshot {
1255            is_running: false,
1256            uptime_seconds: 0.0,
1257            restart_count: 0,
1258            last_restart_reason: None,
1259        };
1260
1261        let output = show_subprocess_stats(&subprocess, false);
1262        assert!(output.contains("Stopped"));
1263    }
1264
1265    #[test]
1266    fn test_parse_stats_command_with_resources() {
1267        let collector = EvalMetrics::new("test");
1268
1269        // Test resources command
1270        let result =
1271            parse_stats_command_with_resources("(stats resources)", &collector, None, None, false);
1272        assert!(result.is_some());
1273        assert!(result.unwrap().contains("Resource Usage"));
1274
1275        // Test fallback to regular stats
1276        let result = parse_stats_command_with_resources("(stats)", &collector, None, None, false);
1277        assert!(result.is_some());
1278
1279        // Test invalid command
1280        let result =
1281            parse_stats_command_with_resources("(stats invalid)", &collector, None, None, false);
1282        assert!(result.is_none());
1283    }
1284
1285    // ===== Snapshot-based display tests =====
1286
1287    #[test]
1288    fn test_show_session_summary_from_snapshot_empty() {
1289        let snapshot = SessionStatsSnapshot {
1290            session_id: "test".to_string(),
1291            total_evaluations: 0,
1292            cache: oxur_repl::metrics::CacheStats { hits: 0, misses: 0, hit_rate: 0.0 },
1293            tier1_percentiles: None,
1294            tier2_percentiles: None,
1295            tier3_percentiles: None,
1296            parse_errors: 0,
1297            compile_errors: 0,
1298            runtime_errors: 0,
1299            average_eval_time_ms: 0.0,
1300        };
1301
1302        let output = show_session_summary_from_snapshot(&snapshot, false);
1303        assert!(output.contains("Session Statistics"));
1304        assert!(output.contains("Total Evaluations: 0"));
1305        assert!(output.contains("No execution data yet"));
1306    }
1307
1308    #[test]
1309    fn test_show_session_summary_from_snapshot_with_data() {
1310        let snapshot = SessionStatsSnapshot {
1311            session_id: "test".to_string(),
1312            total_evaluations: 100,
1313            cache: oxur_repl::metrics::CacheStats { hits: 80, misses: 20, hit_rate: 80.0 },
1314            tier1_percentiles: Some(oxur_repl::metrics::Percentiles {
1315                count: 50,
1316                min: 0.5,
1317                p50: 1.0,
1318                p95: 2.0,
1319                p99: 3.0,
1320                max: 5.0,
1321            }),
1322            tier2_percentiles: None,
1323            tier3_percentiles: None,
1324            parse_errors: 0,
1325            compile_errors: 0,
1326            runtime_errors: 0,
1327            average_eval_time_ms: 1.5,
1328        };
1329
1330        let output = show_session_summary_from_snapshot(&snapshot, false);
1331        assert!(output.contains("Total Evaluations: 100"));
1332        assert!(output.contains("80.0%")); // hit rate
1333        assert!(output.contains("EXECUTION TIERS"));
1334        assert!(output.contains("Calculator"));
1335    }
1336
1337    #[test]
1338    fn test_show_execution_from_snapshot() {
1339        let snapshot = SessionStatsSnapshot {
1340            session_id: "test".to_string(),
1341            total_evaluations: 10,
1342            cache: oxur_repl::metrics::CacheStats { hits: 5, misses: 5, hit_rate: 50.0 },
1343            tier1_percentiles: Some(oxur_repl::metrics::Percentiles {
1344                count: 5,
1345                min: 0.5,
1346                p50: 1.0,
1347                p95: 2.0,
1348                p99: 3.0,
1349                max: 5.0,
1350            }),
1351            tier2_percentiles: Some(oxur_repl::metrics::Percentiles {
1352                count: 3,
1353                min: 1.0,
1354                p50: 2.0,
1355                p95: 4.0,
1356                p99: 5.0,
1357                max: 8.0,
1358            }),
1359            tier3_percentiles: Some(oxur_repl::metrics::Percentiles {
1360                count: 2,
1361                min: 50.0,
1362                p50: 100.0,
1363                p95: 200.0,
1364                p99: 250.0,
1365                max: 300.0,
1366            }),
1367            parse_errors: 0,
1368            compile_errors: 0,
1369            runtime_errors: 0,
1370            average_eval_time_ms: 50.0,
1371        };
1372
1373        let output = show_execution_from_snapshot(&snapshot, false);
1374        assert!(output.contains("Execution Statistics"));
1375        assert!(output.contains("TIER 1: CALCULATOR"));
1376        assert!(output.contains("TIER 2: CACHED LOADED"));
1377        assert!(output.contains("TIER 3: JUST-IN-TIME"));
1378    }
1379
1380    #[test]
1381    fn test_show_cache_from_snapshot() {
1382        let snapshot = SessionStatsSnapshot {
1383            session_id: "test".to_string(),
1384            total_evaluations: 100,
1385            cache: oxur_repl::metrics::CacheStats { hits: 75, misses: 25, hit_rate: 75.0 },
1386            tier1_percentiles: None,
1387            tier2_percentiles: None,
1388            tier3_percentiles: None,
1389            parse_errors: 0,
1390            compile_errors: 0,
1391            runtime_errors: 0,
1392            average_eval_time_ms: 0.0,
1393        };
1394
1395        let output = show_cache_from_snapshot(&snapshot, false);
1396        assert!(output.contains("Cache Statistics"));
1397        assert!(output.contains("EVALUATION CACHE"));
1398        assert!(output.contains("75")); // hits
1399        assert!(output.contains("25")); // misses
1400        assert!(output.contains("75.0%")); // hit rate
1401    }
1402
1403    #[test]
1404    fn test_show_all_stats_with_optional_data() {
1405        let collector = EvalMetrics::new("test");
1406
1407        // Test with all options as Some
1408        let server = oxur_repl::metrics::ServerMetricsSnapshot {
1409            connections_total: 10,
1410            connections_active: 1,
1411            sessions_total: 5,
1412            sessions_active: 1,
1413            requests_total: 100,
1414            responses_total: 100,
1415            responses_success: 99,
1416            responses_error: 1,
1417        };
1418
1419        let client = oxur_repl::metrics::ClientMetricsSnapshot {
1420            requests_total: 50,
1421            responses_total: 50,
1422            responses_success: 49,
1423            responses_error: 1,
1424            average_latency_ms: 5.0,
1425            p50_latency_ms: 4.0,
1426            p95_latency_ms: 10.0,
1427            p99_latency_ms: 20.0,
1428            min_latency_ms: 1.0,
1429            max_latency_ms: 30.0,
1430        };
1431
1432        let subprocess = oxur_repl::metrics::SubprocessMetricsSnapshot {
1433            is_running: true,
1434            uptime_seconds: 120.0,
1435            restart_count: 1,
1436            last_restart_reason: None,
1437        };
1438
1439        let usage = oxur_repl::metrics::UsageMetricsSnapshot {
1440            session_id: "test".to_string(),
1441            eval_count: 25,
1442            help_count: 5,
1443            stats_count: 3,
1444            info_count: 2,
1445            sessions_count: 1,
1446            clear_count: 0,
1447            banner_count: 1,
1448            total_commands: 37,
1449        };
1450
1451        let output = show_all_stats(
1452            &collector,
1453            None,
1454            None,
1455            Some(&server),
1456            Some(&client),
1457            Some(&subprocess),
1458            Some(&usage),
1459            false,
1460        );
1461
1462        assert!(output.contains("Execution Statistics"));
1463        assert!(output.contains("Cache Statistics"));
1464        assert!(output.contains("Server Statistics"));
1465        assert!(output.contains("Client Statistics"));
1466        assert!(output.contains("Subprocess Statistics"));
1467        assert!(output.contains("Usage Statistics"));
1468    }
1469
1470    // ===== Additional coverage tests =====
1471
1472    #[test]
1473    fn test_show_sessions_with_data() {
1474        use oxur_repl::protocol::ReplMode;
1475        use std::time::{SystemTime, UNIX_EPOCH};
1476
1477        let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() as u64;
1478
1479        let sessions = vec![
1480            oxur_repl::server::SessionInfo {
1481                id: oxur_repl::protocol::SessionId::new("session-1"),
1482                name: Some("Main".to_string()),
1483                mode: ReplMode::Lisp,
1484                eval_count: 42,
1485                created_at: now - 3600_000,   // 1 hour ago
1486                last_active_at: now - 30_000, // 30 seconds ago - "just now"
1487                timeout_ms: 300_000,
1488            },
1489            oxur_repl::server::SessionInfo {
1490                id: oxur_repl::protocol::SessionId::new("session-2"),
1491                name: None, // No name - tests the "-" fallback
1492                mode: ReplMode::Sexpr,
1493                eval_count: 10,
1494                created_at: now - 7200_000,
1495                last_active_at: now - 120_000, // 2 min ago
1496                timeout_ms: 300_000,
1497            },
1498            oxur_repl::server::SessionInfo {
1499                id: oxur_repl::protocol::SessionId::new("session-3"),
1500                name: Some("Old".to_string()),
1501                mode: ReplMode::Lisp,
1502                eval_count: 5,
1503                created_at: now - 172800_000,
1504                last_active_at: now - 7200_000, // 2 hr ago
1505                timeout_ms: 300_000,
1506            },
1507            oxur_repl::server::SessionInfo {
1508                id: oxur_repl::protocol::SessionId::new("session-4"),
1509                name: Some("Very Old".to_string()),
1510                mode: ReplMode::Lisp,
1511                eval_count: 1,
1512                created_at: now - 259200_000,
1513                last_active_at: now - 172800_000, // 2 days ago
1514                timeout_ms: 300_000,
1515            },
1516        ];
1517        let current = oxur_repl::protocol::SessionId::new("session-1");
1518
1519        let output = show_sessions(&sessions, &current, false);
1520        assert!(output.contains("Sessions"));
1521        assert!(output.contains("ACTIVE SESSIONS"));
1522        assert!(output.contains("session-1"));
1523        assert!(output.contains("Main"));
1524        assert!(output.contains("42")); // eval_count
1525        assert!(output.contains("just now")); // <1 min
1526        assert!(output.contains("min ago")); // 2 min
1527        assert!(output.contains("hr ago")); // 2 hr
1528        assert!(output.contains("days ago")); // 2 days
1529    }
1530
1531    #[test]
1532    fn test_show_resource_stats_with_dir_stats() {
1533        use std::path::PathBuf;
1534
1535        let dir_stats = oxur_repl::session::DirStats {
1536            file_count: 15,
1537            total_bytes: 102400,
1538            is_tmpfs: true,
1539            path: PathBuf::from("/tmp/oxur-session"),
1540        };
1541
1542        let output = show_resource_stats(Some(&dir_stats), None, false);
1543        assert!(output.contains("Resource Usage"));
1544        assert!(output.contains("SESSION DIRECTORY"));
1545        assert!(output.contains("15")); // file_count
1546        assert!(output.contains("100.00 KB")); // total_bytes formatted
1547        assert!(output.contains("tmpfs")); // is_tmpfs flag
1548    }
1549
1550    #[test]
1551    fn test_show_resource_stats_with_cache_stats() {
1552        use std::path::PathBuf;
1553        use std::time::{SystemTime, UNIX_EPOCH};
1554
1555        let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
1556
1557        let cache_stats = oxur_repl::cache::CacheStats {
1558            entry_count: 25,
1559            total_size_bytes: 5242880,
1560            oldest_entry_secs: now - 3600, // 1 hour ago
1561            newest_entry_secs: now,
1562            cache_dir: PathBuf::from("/home/user/.cache/oxur"),
1563        };
1564
1565        let output = show_resource_stats(None, Some(&cache_stats), false);
1566        assert!(output.contains("Resource Usage"));
1567        assert!(output.contains("ARTIFACT CACHE"));
1568        assert!(output.contains("25")); // entry_count
1569        assert!(output.contains("5.00 MB")); // total_size formatted
1570    }
1571
1572    #[test]
1573    fn test_show_resource_stats_with_empty_cache() {
1574        use std::path::PathBuf;
1575
1576        let cache_stats = oxur_repl::cache::CacheStats {
1577            entry_count: 0,
1578            total_size_bytes: 0,
1579            oldest_entry_secs: 0,
1580            newest_entry_secs: 0,
1581            cache_dir: PathBuf::from("/home/user/.cache/oxur"),
1582        };
1583
1584        let output = show_resource_stats(None, Some(&cache_stats), false);
1585        assert!(output.contains("ARTIFACT CACHE"));
1586        assert!(output.contains("N/A")); // oldest entry shows N/A when empty
1587    }
1588
1589    #[test]
1590    fn test_show_resource_stats_dir_not_tmpfs() {
1591        use std::path::PathBuf;
1592
1593        let dir_stats = oxur_repl::session::DirStats {
1594            file_count: 5,
1595            total_bytes: 1024,
1596            is_tmpfs: false, // Not on tmpfs
1597            path: PathBuf::from("/var/oxur-session"),
1598        };
1599
1600        let output = show_resource_stats(Some(&dir_stats), None, false);
1601        assert!(output.contains("SESSION DIRECTORY"));
1602        assert!(!output.contains("tmpfs")); // No tmpfs indicator
1603    }
1604
1605    #[test]
1606    fn test_show_usage_stats_zero_commands() {
1607        let usage = oxur_repl::metrics::UsageMetricsSnapshot {
1608            session_id: "test".to_string(),
1609            eval_count: 0,
1610            help_count: 0,
1611            stats_count: 0,
1612            info_count: 0,
1613            sessions_count: 0,
1614            clear_count: 0,
1615            banner_count: 0,
1616            total_commands: 0,
1617        };
1618
1619        let output = show_usage_stats(&usage, false);
1620        assert!(output.contains("Usage Statistics"));
1621        assert!(output.contains("0.0%")); // All percentages should be 0.0%
1622    }
1623
1624    #[test]
1625    fn test_show_subprocess_stats_with_restart_reason() {
1626        let subprocess = oxur_repl::metrics::SubprocessMetricsSnapshot {
1627            is_running: true,
1628            uptime_seconds: 7200.0,
1629            restart_count: 3,
1630            last_restart_reason: Some(oxur_repl::metrics::RestartReason::UserRequested),
1631        };
1632
1633        let output = show_subprocess_stats(&subprocess, false);
1634        assert!(output.contains("Subprocess Statistics"));
1635        assert!(output.contains("2h 0m")); // 2 hours
1636        assert!(output.contains("3")); // restart count
1637        assert!(output.contains("user requested")); // UserRequested displays
1638    }
1639
1640    #[test]
1641    fn test_show_server_stats_zero_responses() {
1642        let server = oxur_repl::metrics::ServerMetricsSnapshot {
1643            connections_total: 5,
1644            connections_active: 0,
1645            sessions_total: 2,
1646            sessions_active: 0,
1647            requests_total: 10,
1648            responses_total: 0, // No responses yet
1649            responses_success: 0,
1650            responses_error: 0,
1651        };
1652
1653        let output = show_server_stats(&server, false);
1654        assert!(output.contains("Server Statistics"));
1655        assert!(output.contains("0.0%")); // Success rate with no responses
1656    }
1657
1658    #[test]
1659    fn test_show_session_summary_from_snapshot_with_tier2_and_tier3() {
1660        let snapshot = SessionStatsSnapshot {
1661            session_id: "test".to_string(),
1662            total_evaluations: 100,
1663            cache: oxur_repl::metrics::CacheStats { hits: 50, misses: 50, hit_rate: 50.0 },
1664            tier1_percentiles: None, // No tier1
1665            tier2_percentiles: Some(oxur_repl::metrics::Percentiles {
1666                count: 30,
1667                min: 1.0,
1668                p50: 5.0,
1669                p95: 10.0,
1670                p99: 15.0,
1671                max: 20.0,
1672            }),
1673            tier3_percentiles: Some(oxur_repl::metrics::Percentiles {
1674                count: 20,
1675                min: 50.0,
1676                p50: 100.0,
1677                p95: 200.0,
1678                p99: 300.0,
1679                max: 400.0,
1680            }),
1681            parse_errors: 5,
1682            compile_errors: 3,
1683            runtime_errors: 2,
1684            average_eval_time_ms: 25.0,
1685        };
1686
1687        let output = show_session_summary_from_snapshot(&snapshot, false);
1688        assert!(output.contains("Cached")); // Tier 2
1689        assert!(output.contains("JIT")); // Tier 3
1690    }
1691}