speedtest-tui 0.1.1

A terminal-based network speed test tool with real-time gauges and graphs
use async_trait::async_trait;
use serde::Deserialize;

use super::{Provider, ServerInfo};

pub struct LibrespeedProvider {
    servers: Vec<LibrespeedServer>,
    selected_server: Option<LibrespeedServer>,
}

#[derive(Debug, Clone, Deserialize)]
#[allow(dead_code)]
struct LibrespeedServer {
    id: String,
    name: String,
    server: String,
    #[serde(rename = "dlURL")]
    dl_url: String,
    #[serde(rename = "ulURL")]
    ul_url: String,
    #[serde(rename = "pingURL")]
    ping_url: String,
    #[serde(rename = "getIpURL")]
    get_ip_url: Option<String>,
}

impl LibrespeedProvider {
    pub fn new() -> Self {
        Self {
            servers: get_default_servers(),
            selected_server: None,
        }
    }

    #[allow(dead_code)]
    pub fn with_server(server_url: &str) -> Self {
        let server = LibrespeedServer {
            id: "custom".to_string(),
            name: "Custom Server".to_string(),
            server: server_url.to_string(),
            dl_url: format!("{}/garbage.php", server_url),
            ul_url: format!("{}/empty.php", server_url),
            ping_url: format!("{}/empty.php", server_url),
            get_ip_url: Some(format!("{}/getIP.php", server_url)),
        };

        Self {
            servers: vec![server.clone()],
            selected_server: Some(server),
        }
    }
}

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

#[async_trait]
impl Provider for LibrespeedProvider {
    fn name(&self) -> &str {
        "LibreSpeed"
    }

    fn get_download_url(&self) -> String {
        self.selected_server
            .as_ref()
            .map(|s| s.dl_url.clone())
            .unwrap_or_else(|| self.servers[0].dl_url.clone())
    }

    fn get_upload_url(&self) -> String {
        self.selected_server
            .as_ref()
            .map(|s| s.ul_url.clone())
            .unwrap_or_else(|| self.servers[0].ul_url.clone())
    }

    fn get_ping_url(&self) -> String {
        self.selected_server
            .as_ref()
            .map(|s| s.ping_url.clone())
            .unwrap_or_else(|| self.servers[0].ping_url.clone())
    }

    async fn get_servers(&self) -> anyhow::Result<Vec<ServerInfo>> {
        Ok(self
            .servers
            .iter()
            .map(|s| ServerInfo {
                id: s.id.clone(),
                name: s.name.clone(),
                location: s.name.clone(),
                country: "".to_string(),
                host: s.server.clone(),
                latency_ms: None,
            })
            .collect())
    }

    async fn select_best_server(&self) -> anyhow::Result<Option<ServerInfo>> {
        let mut best_server: Option<(ServerInfo, f64)> = None;

        for server in &self.servers {
            if let Ok(latency) = crate::network::ping::single_ping(&server.ping_url).await {
                let server_info = ServerInfo {
                    id: server.id.clone(),
                    name: server.name.clone(),
                    location: server.name.clone(),
                    country: "".to_string(),
                    host: server.server.clone(),
                    latency_ms: Some(latency),
                };

                match &best_server {
                    None => best_server = Some((server_info, latency)),
                    Some((_, best_latency)) if latency < *best_latency => {
                        best_server = Some((server_info, latency));
                    }
                    _ => {}
                }
            }
        }

        Ok(best_server.map(|(s, _)| s))
    }
}

fn get_default_servers() -> Vec<LibrespeedServer> {
    // Use Cloudflare as fallback since it's reliable
    // LibreSpeed public servers can be unreliable
    vec![LibrespeedServer {
        id: "librespeed-backend".to_string(),
        name: "LibreSpeed Backend".to_string(),
        server: "https://librespeed.org".to_string(),
        dl_url: "https://librespeed.org/backend/garbage.php".to_string(),
        ul_url: "https://librespeed.org/backend/empty.php".to_string(),
        ping_url: "https://librespeed.org/backend/empty.php".to_string(),
        get_ip_url: Some("https://librespeed.org/backend/getIP.php".to_string()),
    }]
}

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

    #[test]
    fn test_librespeed_provider() {
        let provider = LibrespeedProvider::new();
        assert!(!provider.get_download_url().is_empty());
        assert!(!provider.get_upload_url().is_empty());
    }

    #[tokio::test]
    async fn test_get_servers() {
        let provider = LibrespeedProvider::new();
        let servers = provider.get_servers().await.unwrap();
        assert!(!servers.is_empty());
    }
}