//! Stop signalling for in-flight downloads.
//!
//! TRACES: UR-055 | DR-168
//!
//! Pausing and cancelling used to be database-only: `pause_download` wrote
//! `status = 'paused'` and nothing else. No cancellation existed anywhere in the
//! download stack — no token, no flag, no abort — so the streaming task kept
//! running, kept writing bytes, and on finishing overwrote the row with
//! `completed` or `failed`. The row flicked to "paused" and then undid itself,
//! which is precisely the reported "pause does not work".
//!
//! This is the missing half: a flag per in-flight download that the worker reads
//! between chunks. Setting it makes the worker return [`Stopped`] promptly and
//! leave the `.part` file **intact**, which is what lets a resume pick up from
//! where it stopped via the existing HTTP Range request.
//!
//! Kept as a module-level registry rather than on `DownloadManager` because the
//! two sides never meet: the command handler holds the manager's lock, while the
//! worker runs detached inside `tauri::async_runtime::spawn` with no access to
//! Tauri state. A registry both can reach is the smallest thing that works.
//!
//! [`Stopped`]: crate::download::worker::DownloadError::Stopped

use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, OnceLock};

use crate::utils::lock::MutexSafe;

/// download id → its stop flag, for downloads currently in flight.
fn registry() -> &'static Mutex<HashMap<i64, Arc<AtomicBool>>> {
    static REGISTRY: OnceLock<Mutex<HashMap<i64, Arc<AtomicBool>>>> = OnceLock::new();
    REGISTRY.get_or_init(|| Mutex::new(HashMap::new()))
}

/// Register `download_id` as in-flight and hand back its stop flag.
///
/// Called by the worker as it starts. A previous flag for the same id is
/// replaced, so a download that is paused and later resumed does not inherit the
/// set flag from its last run and stop immediately.
pub fn register(download_id: i64) -> Arc<AtomicBool> {
    let flag = Arc::new(AtomicBool::new(false));
    registry().lock_safe().insert(download_id, flag.clone());
    flag
}

/// Ask an in-flight download to stop.
///
/// Returns whether one was actually in flight — the caller uses this to tell a
/// running download (which will stop shortly) from a merely queued one (which
/// the database update alone has already handled).
pub fn signal(download_id: i64) -> bool {
    match registry().lock_safe().get(&download_id) {
        Some(flag) => {
            flag.store(true, Ordering::SeqCst);
            true
        }
        None => false,
    }
}

/// Forget a download's flag. Called when its task finishes, however it ended.
pub fn clear(download_id: i64) {
    registry().lock_safe().remove(&download_id);
}

/// Whether a stop has been requested for `download_id`.
///
/// The worker reads its own `Arc<AtomicBool>` directly rather than looking the id
/// up, so this exists for the tests that assert the registry's behaviour.
#[cfg(test)]
pub fn is_stopping(download_id: i64) -> bool {
    registry()
        .lock_safe()
        .get(&download_id)
        .map(|f| f.load(Ordering::SeqCst))
        .unwrap_or(false)
}

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

    /// Ids are per-test so the shared registry cannot leak between them.
    fn unique_id(seed: i64) -> i64 {
        900_000 + seed
    }

    #[test]
    fn test_a_registered_download_starts_unflagged() {
        let id = unique_id(1);
        let flag = register(id);
        assert!(!flag.load(Ordering::SeqCst));
        assert!(!is_stopping(id));
        clear(id);
    }

    #[test]
    fn test_signal_sets_the_flag_the_worker_reads() {
        let id = unique_id(2);
        let flag = register(id);

        assert!(signal(id), "a registered download reports as in flight");
        assert!(
            flag.load(Ordering::SeqCst),
            "the worker's own handle sees it"
        );
        assert!(is_stopping(id));

        clear(id);
    }

    /// The pump only needs to abort a task that exists; a queued row is handled
    /// by its database status alone.
    #[test]
    fn test_signalling_an_unregistered_download_reports_not_in_flight() {
        assert!(!signal(unique_id(3)));
    }

    #[test]
    fn test_clear_forgets_the_download() {
        let id = unique_id(4);
        register(id);
        signal(id);
        clear(id);

        assert!(!is_stopping(id));
        assert!(!signal(id), "a cleared download is no longer in flight");
    }

    /// The bug this guards: pause sets the flag, and resume re-runs the same
    /// download id. If registering reused the old flag, the resumed run would see
    /// a set flag and stop instantly — a download that could never be resumed.
    #[test]
    fn test_reregistering_clears_a_previous_stop() {
        let id = unique_id(5);
        register(id);
        signal(id);
        assert!(is_stopping(id));

        let fresh = register(id);
        assert!(!fresh.load(Ordering::SeqCst));
        assert!(
            !is_stopping(id),
            "a resumed download must not inherit the pause"
        );

        clear(id);
    }
}
