//! Positive LRU cache mapping `scroll_id` to the backend that holds the
//! authoritative session metadata, populated on the first successful read.

use std::num::NonZeroUsize;

use lru::LruCache;
use tokio::sync::Mutex;
use uuid::Uuid;

const DEFAULT_CAPACITY: usize = 10_000;

pub struct ReadCache {
    inner: Mutex<LruCache<Uuid, String>>,
}

impl ReadCache {
    pub fn new() -> Self {
        Self::with_capacity(DEFAULT_CAPACITY)
    }

    pub fn with_capacity(capacity: usize) -> Self {
        let cap = NonZeroUsize::new(capacity.max(1)).unwrap();
        Self {
            inner: Mutex::new(LruCache::new(cap)),
        }
    }

    pub async fn get(&self, scroll_id: Uuid) -> Option<String> {
        let mut guard = self.inner.lock().await;
        guard.get(&scroll_id).cloned()
    }

    pub async fn put(&self, scroll_id: Uuid, backend_name: String) {
        let mut guard = self.inner.lock().await;
        guard.put(scroll_id, backend_name);
    }

    pub async fn invalidate(&self, scroll_id: Uuid) {
        let mut guard = self.inner.lock().await;
        guard.pop(&scroll_id);
    }

    pub async fn rewrite(&self, scroll_id: Uuid, new_backend: String) {
        let mut guard = self.inner.lock().await;
        guard.put(scroll_id, new_backend);
    }

    pub async fn clear(&self) {
        let mut guard = self.inner.lock().await;
        guard.clear();
    }

    pub async fn len(&self) -> usize {
        let guard = self.inner.lock().await;
        guard.len()
    }
}

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

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

    fn id(b: u8) -> Uuid {
        Uuid::from_bytes([b; 16])
    }

    #[tokio::test]
    async fn put_then_get() {
        let c = ReadCache::new();
        c.put(id(1), "main".into()).await;
        assert_eq!(c.get(id(1)).await.as_deref(), Some("main"));
        assert!(c.get(id(2)).await.is_none());
    }

    #[tokio::test]
    async fn invalidate_removes_entry() {
        let c = ReadCache::new();
        c.put(id(1), "main".into()).await;
        c.invalidate(id(1)).await;
        assert!(c.get(id(1)).await.is_none());
    }

    #[tokio::test]
    async fn rewrite_changes_backend() {
        let c = ReadCache::new();
        c.put(id(1), "a".into()).await;
        c.rewrite(id(1), "b".into()).await;
        assert_eq!(c.get(id(1)).await.as_deref(), Some("b"));
    }

    #[tokio::test]
    async fn lru_evicts_oldest() {
        let c = ReadCache::with_capacity(2);
        c.put(id(1), "a".into()).await;
        c.put(id(2), "b".into()).await;
        c.put(id(3), "c".into()).await; // evicts id(1)
        assert!(c.get(id(1)).await.is_none());
        assert_eq!(c.get(id(2)).await.as_deref(), Some("b"));
        assert_eq!(c.get(id(3)).await.as_deref(), Some("c"));
    }

    #[tokio::test]
    async fn clear_empties() {
        let c = ReadCache::new();
        c.put(id(1), "a".into()).await;
        c.put(id(2), "b".into()).await;
        c.clear().await;
        assert_eq!(c.len().await, 0);
    }
}
