speedtest-tui 0.1.1

A terminal-based network speed test tool with real-time gauges and graphs
use ratatui::{
    prelude::*,
    widgets::{Axis, Chart, Dataset, GraphType},
    Frame,
};

use super::Theme;
use crate::network::SpeedSample;

pub struct GraphWidget<'a> {
    samples: &'a [SpeedSample],
    max_scale: f64,
    theme: Option<&'a Theme>,
}

impl<'a> GraphWidget<'a> {
    pub fn new(samples: &'a [SpeedSample], max_scale: f64) -> Self {
        Self {
            samples,
            max_scale,
            theme: None,
        }
    }

    pub fn with_theme(mut self, theme: &'a Theme) -> Self {
        self.theme = Some(theme);
        self
    }

    pub fn render(self, frame: &mut Frame, area: Rect) {
        let default_theme = Theme::default();
        let theme = self.theme.unwrap_or(&default_theme);

        if self.samples.is_empty() {
            // Render placeholder when no data
            let placeholder = ratatui::widgets::Paragraph::new("No data")
                .style(Style::default().fg(theme.muted))
                .alignment(Alignment::Center);
            frame.render_widget(placeholder, area);
            return;
        }

        // Convert samples to chart data points
        let data: Vec<(f64, f64)> = self
            .samples
            .iter()
            .map(|s| (s.timestamp_ms as f64 / 1000.0, s.speed_mbps))
            .collect();

        // Calculate time bounds
        let max_time = data.iter().map(|(t, _)| *t).fold(0.0_f64, f64::max);
        let min_time = data.iter().map(|(t, _)| *t).fold(f64::INFINITY, f64::min);

        // Ensure we have at least some range
        let time_range = if max_time - min_time < 1.0 {
            (0.0, 15.0) // Default to 15 seconds
        } else {
            (min_time, max_time)
        };

        let datasets = vec![Dataset::default()
            .name("Speed")
            .marker(symbols::Marker::Braille)
            .graph_type(GraphType::Line)
            .style(Style::default().fg(theme.primary))
            .data(&data)];

        // Create X axis labels
        let x_labels = vec![
            Span::styled(
                format!("{:.0}s", time_range.0),
                Style::default().fg(theme.muted),
            ),
            Span::styled(
                format!("{:.0}s", (time_range.0 + time_range.1) / 2.0),
                Style::default().fg(theme.muted),
            ),
            Span::styled(
                format!("{:.0}s", time_range.1),
                Style::default().fg(theme.muted),
            ),
        ];

        // Create Y axis labels
        let y_labels = vec![
            Span::styled("0", Style::default().fg(theme.muted)),
            Span::styled(
                format!("{:.0}", self.max_scale / 2.0),
                Style::default().fg(theme.muted),
            ),
            Span::styled(
                format!("{:.0}", self.max_scale),
                Style::default().fg(theme.muted),
            ),
        ];

        let chart = Chart::new(datasets)
            .style(Style::default().bg(theme.background))
            .x_axis(
                Axis::default()
                    .style(Style::default().fg(theme.border))
                    .bounds([time_range.0, time_range.1])
                    .labels(x_labels),
            )
            .y_axis(
                Axis::default()
                    .style(Style::default().fg(theme.border))
                    .bounds([0.0, self.max_scale])
                    .labels(y_labels),
            );

        frame.render_widget(chart, area);
    }
}

#[allow(dead_code)]
pub struct SampleBuffer {
    samples: Vec<SpeedSample>,
    max_samples: usize,
}

#[allow(dead_code)]
impl SampleBuffer {
    pub fn new(max_samples: usize) -> Self {
        Self {
            samples: Vec::with_capacity(max_samples),
            max_samples,
        }
    }

    pub fn push(&mut self, sample: SpeedSample) {
        if self.samples.len() >= self.max_samples {
            self.samples.remove(0);
        }
        self.samples.push(sample);
    }

    pub fn clear(&mut self) {
        self.samples.clear();
    }

    pub fn samples(&self) -> &[SpeedSample] {
        &self.samples
    }

    pub fn last(&self) -> Option<&SpeedSample> {
        self.samples.last()
    }

    pub fn average(&self) -> f64 {
        if self.samples.is_empty() {
            return 0.0;
        }
        self.samples.iter().map(|s| s.speed_mbps).sum::<f64>() / self.samples.len() as f64
    }

    pub fn peak(&self) -> f64 {
        self.samples
            .iter()
            .map(|s| s.speed_mbps)
            .fold(0.0_f64, f64::max)
    }
}

impl Default for SampleBuffer {
    fn default() -> Self {
        Self::new(100)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_sample_buffer() {
        let mut buffer = SampleBuffer::new(5);

        for i in 0..10 {
            buffer.push(SpeedSample {
                timestamp_ms: i * 1000,
                speed_mbps: i as f64 * 10.0,
            });
        }

        // Should only have last 5 samples
        assert_eq!(buffer.samples().len(), 5);

        // First sample should be at 5000ms (index 5)
        assert_eq!(buffer.samples()[0].timestamp_ms, 5000);
    }

    #[test]
    fn test_sample_buffer_stats() {
        let mut buffer = SampleBuffer::new(10);

        buffer.push(SpeedSample {
            timestamp_ms: 0,
            speed_mbps: 100.0,
        });
        buffer.push(SpeedSample {
            timestamp_ms: 1000,
            speed_mbps: 200.0,
        });
        buffer.push(SpeedSample {
            timestamp_ms: 2000,
            speed_mbps: 150.0,
        });

        assert_eq!(buffer.average(), 150.0);
        assert_eq!(buffer.peak(), 200.0);
    }
}