//! Network transport classification for the WiFi-only download gate.
//!
//! This answers "what kind of connection are we on?", which is orthogonal to
//! the `ConnectivityMonitor`'s "is the server reachable?". The download queue
//! pump consults this before starting pending rows when the user has enabled
//! WiFi-only downloads.
//!
//! On Android the real transport is read from `NetworkCapabilities` in
//! `NetworkTypeMonitor.kt` and pushed in from the frontend. On desktop there is
//! no metered-connection concept worth enforcing, so we report `Ethernet`,
//! which is always acceptable — gating desktop downloads would be a regression.

use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::RwLock;

/// Kind of network transport currently active.
///
/// Mirrors the string constants in `NetworkTypeMonitor.kt`; the two must stay
/// in sync (the serde rename below is what the frontend sends).
///
/// TRACES: UR-053 | DR-074
#[derive(specta::Type, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum NetworkType {
    /// No active network.
    None,
    /// WiFi (may still be metered — check `unmetered`).
    Wifi,
    /// Wired ethernet, typical on Android TV and desktop.
    Ethernet,
    /// Mobile data — never acceptable when wifi-only is enabled.
    Cellular,
    /// Some other transport (VPN over unknown carrier, Bluetooth tethering, …).
    Other,
    /// Could not determine the transport.
    Unknown,
}

/// Current network transport plus whether it is metered.
///
/// TRACES: UR-053 | DR-074
#[derive(specta::Type, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NetworkState {
    pub network_type: NetworkType,
    /// Whether the active network is unmetered (Android `NET_CAPABILITY_NOT_METERED`).
    pub unmetered: bool,
}

impl Default for NetworkState {
    fn default() -> Self {
        // Desktop default: wired and unmetered, so the gate never blocks there.
        // Android overwrites this as soon as the frontend reports the real state.
        Self {
            network_type: NetworkType::Ethernet,
            unmetered: true,
        }
    }
}

impl NetworkState {
    /// Whether downloads may run right now given the wifi-only preference.
    ///
    /// Ethernet counts as acceptable — it is unmetered in practice and is what
    /// Android TV devices use. Cellular never does. `None`/`Unknown` fail
    /// closed: if we cannot tell what we are on, we do not spend the user's
    /// mobile data to find out.
    ///
    /// TRACES: UR-053 | DR-074
    pub fn allows_download(&self, wifi_only: bool) -> bool {
        if !wifi_only {
            return true;
        }
        match self.network_type {
            NetworkType::Cellular | NetworkType::None | NetworkType::Unknown => false,
            // Require unmetered so metered WiFi hotspots (backed by the very
            // cellular data this setting protects) are excluded too.
            NetworkType::Wifi | NetworkType::Ethernet | NetworkType::Other => self.unmetered,
        }
    }
}

/// Shared, mutable view of the current network transport.
///
/// Cheap to clone; the frontend updates it via `set_network_state` whenever
/// Android reports a network change.
#[derive(Clone, Default)]
pub struct NetworkStateHandle {
    state: Arc<RwLock<NetworkState>>,
}

impl NetworkStateHandle {
    pub fn new() -> Self {
        Self {
            state: Arc::new(RwLock::new(NetworkState::default())),
        }
    }

    pub async fn get(&self) -> NetworkState {
        *self.state.read().await
    }

    pub async fn set(&self, new_state: NetworkState) {
        *self.state.write().await = new_state;
    }

    /// Whether downloads may run right now given the wifi-only preference.
    pub async fn allows_download(&self, wifi_only: bool) -> bool {
        self.state.read().await.allows_download(wifi_only)
    }
}

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

    fn state(network_type: NetworkType, unmetered: bool) -> NetworkState {
        NetworkState {
            network_type,
            unmetered,
        }
    }

    #[test]
    fn wifi_only_off_allows_every_transport() {
        for t in [
            NetworkType::None,
            NetworkType::Wifi,
            NetworkType::Ethernet,
            NetworkType::Cellular,
            NetworkType::Other,
            NetworkType::Unknown,
        ] {
            assert!(
                state(t, false).allows_download(false),
                "{t:?} should be allowed when wifi_only is off"
            );
        }
    }

    #[test]
    fn cellular_is_blocked_when_wifi_only() {
        // Even if somehow flagged unmetered, cellular is never acceptable.
        assert!(!state(NetworkType::Cellular, true).allows_download(true));
        assert!(!state(NetworkType::Cellular, false).allows_download(true));
    }

    #[test]
    fn unmetered_wifi_and_ethernet_are_allowed() {
        assert!(state(NetworkType::Wifi, true).allows_download(true));
        assert!(state(NetworkType::Ethernet, true).allows_download(true));
    }

    #[test]
    fn metered_wifi_is_blocked() {
        // A phone hotspot reports as WiFi but is metered — blocking it is the
        // whole point of checking NOT_METERED rather than the transport alone.
        assert!(!state(NetworkType::Wifi, false).allows_download(true));
    }

    #[test]
    fn unknown_and_none_fail_closed() {
        assert!(!state(NetworkType::Unknown, true).allows_download(true));
        assert!(!state(NetworkType::None, true).allows_download(true));
    }

    #[test]
    fn desktop_default_is_never_gated() {
        assert!(NetworkState::default().allows_download(true));
    }

    #[tokio::test]
    async fn handle_roundtrips_state() {
        let handle = NetworkStateHandle::new();
        assert!(handle.allows_download(true).await);

        handle.set(state(NetworkType::Cellular, false)).await;
        assert!(!handle.allows_download(true).await);
        assert!(handle.allows_download(false).await);
        assert_eq!(handle.get().await.network_type, NetworkType::Cellular);
    }

    #[test]
    fn network_type_serializes_lowercase() {
        // Must match the string constants in NetworkTypeMonitor.kt.
        assert_eq!(
            serde_json::to_string(&NetworkType::Wifi).unwrap(),
            "\"wifi\""
        );
        assert_eq!(
            serde_json::to_string(&NetworkType::Cellular).unwrap(),
            "\"cellular\""
        );
    }
}
