//! In-memory scan result cache for the Aphoria scan history endpoint.
//!
//! Caches the last N scan results to provide a scan history API.

use std::collections::VecDeque;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use tokio::sync::RwLock;

use crate::dto::aphoria::{ListScansResponse, ScanListItem, ScanResponse, ScanSummaryDto};

/// Maximum number of scans to cache.
const MAX_CACHED_SCANS: usize = 100;

/// Thread-safe scan cache that stores recent scan results.
#[derive(Clone)]
pub struct ScanCache {
    inner: Arc<RwLock<ScanCacheInner>>,
}

struct ScanCacheInner {
    scans: VecDeque<ScanListItem>,
}

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

impl ScanCache {
    /// Create a new empty scan cache.
    pub fn new() -> Self {
        Self {
            inner: Arc::new(RwLock::new(ScanCacheInner {
                scans: VecDeque::with_capacity(MAX_CACHED_SCANS),
            })),
        }
    }

    /// Record a new scan result.
    ///
    /// Adds the scan to the front of the cache, removing the oldest
    /// scan if we exceed MAX_CACHED_SCANS.
    pub async fn record(&self, response: &ScanResponse) {
        let timestamp =
            SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0);

        let item = ScanListItem {
            scan_id: response.scan_id.clone(),
            project: response.project.clone(),
            files_scanned: response.files_scanned,
            claims_extracted: response.claims_extracted,
            summary: ScanSummaryDto {
                total: response.summary.total,
                blocked: response.summary.blocked,
                flagged: response.summary.flagged,
                passed: response.summary.passed,
                acknowledged: response.summary.acknowledged,
            },
            timestamp,
            findings: response.findings.clone(),
        };

        let mut inner = self.inner.write().await;
        inner.scans.push_front(item);
        if inner.scans.len() > MAX_CACHED_SCANS {
            inner.scans.pop_back();
        }
    }

    /// List all cached scans, newest first.
    pub async fn list(&self) -> ListScansResponse {
        let inner = self.inner.read().await;
        ListScansResponse { scans: inner.scans.iter().cloned().collect() }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::dto::aphoria::ScanSummaryDto;

    fn make_scan(id: &str) -> ScanResponse {
        ScanResponse {
            project: "test-project".to_string(),
            scan_id: id.to_string(),
            files_scanned: 10,
            claims_extracted: 5,
            findings: vec![],
            summary: ScanSummaryDto {
                total: 0,
                blocked: 0,
                flagged: 0,
                passed: 0,
                acknowledged: 0,
            },
        }
    }

    #[tokio::test]
    async fn test_record_and_list() {
        let cache = ScanCache::new();

        cache.record(&make_scan("scan-1")).await;
        cache.record(&make_scan("scan-2")).await;

        let list = cache.list().await;
        assert_eq!(list.scans.len(), 2);
        assert_eq!(list.scans[0].scan_id, "scan-2");
        assert_eq!(list.scans[1].scan_id, "scan-1");
    }

    #[tokio::test]
    async fn test_cache_eviction() {
        let cache = ScanCache::new();

        // Fill cache beyond limit (MAX_CACHED_SCANS = 100)
        for i in 0..105 {
            cache.record(&make_scan(&format!("scan-{}", i))).await;
        }

        let list = cache.list().await;
        // Should be capped at MAX_CACHED_SCANS
        assert_eq!(list.scans.len(), MAX_CACHED_SCANS);
        // Newest first
        assert_eq!(list.scans[0].scan_id, "scan-104");
        // Oldest retained (first 5 were evicted: scan-0 through scan-4)
        assert_eq!(list.scans[99].scan_id, "scan-5");
    }

    #[tokio::test]
    async fn test_empty_cache() {
        let cache = ScanCache::new();
        let list = cache.list().await;
        assert!(list.scans.is_empty());
    }
}
