//! SSE keep-alive
//!
//! Many proxies (nginx, AWS ALB, Cloudflare) close idle SSE connections.
//! `KeepAlive` periodically writes a comment frame (`: keep-alive\n\n`) to
//! the response stream so the connection stays open through those proxies.

use crate::upgrade::sse::SseSender;
use std::time::Duration;

/// Default keep-alive interval (15 seconds).
pub const DEFAULT_KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(15);

/// Keep-alive configuration
#[derive(Debug, Clone, Copy)]
pub struct KeepAlive {
    /// Interval between keep-alive comments
    interval: Duration,
}

impl KeepAlive {
    /// Create keep-alive with a custom interval.
    pub fn new(interval: Duration) -> Self {
        Self { interval }
    }

    /// Configured interval.
    pub fn interval(&self) -> Duration {
        self.interval
    }

    /// Spawn a background task that sends a comment frame on every tick
    /// until the receiver is dropped (client disconnect).
    pub(crate) fn spawn(self, sender: SseSender) {
        tokio::spawn(async move {
            let mut ticker = tokio::time::interval(self.interval);
            // First tick fires immediately; skip it so the first keep-alive
            // arrives one interval after the response starts.
            ticker.tick().await;
            loop {
                ticker.tick().await;
                if sender.send_comment("keep-alive").await.is_err() {
                    break;
                }
            }
        });
    }
}

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

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

    #[test]
    fn test_default_interval() {
        let ka = KeepAlive::default();
        assert_eq!(ka.interval(), DEFAULT_KEEP_ALIVE_INTERVAL);
    }

    #[test]
    fn test_custom_interval() {
        let ka = KeepAlive::new(Duration::from_secs(5));
        assert_eq!(ka.interval(), Duration::from_secs(5));
    }

    #[tokio::test]
    async fn test_spawn_sends_keep_alive() {
        use tokio::sync::mpsc;

        let (tx, mut rx) = mpsc::unbounded_channel();
        let sender = SseSender::new(tx);

        KeepAlive::new(Duration::from_millis(20)).spawn(sender);

        // Give the keep-alive task time to fire at least once.
        let frame = tokio::time::timeout(Duration::from_millis(200), rx.recv())
            .await
            .expect("keep-alive should have fired")
            .expect("channel should not be closed");
        assert_eq!(frame, ": keep-alive\n\n");
    }

    #[tokio::test]
    async fn test_spawn_stops_on_disconnect() {
        use tokio::sync::mpsc;

        let (tx, rx) = mpsc::unbounded_channel::<String>();
        let sender = SseSender::new(tx);

        KeepAlive::new(Duration::from_millis(10)).spawn(sender);

        // Drop the receiver to simulate client disconnect.
        drop(rx);

        // Task should exit cleanly without panicking. Give it a moment
        // to attempt a send and detect the closed channel.
        tokio::time::sleep(Duration::from_millis(50)).await;
    }
}
