//! In-memory `ReplayDefense` adapter for engine integration tests.

use std::collections::HashMap;
use std::sync::Mutex;
use std::time::{Duration, Instant};

use ppoppo_token::{ReplayDefense, ReplayDefenseError};

#[derive(Debug)]
pub struct MemoryReplayDefense {
    seen: Mutex<HashMap<String, Instant>>,
    failing: bool,
}

impl MemoryReplayDefense {
    /// Working substrate — first observation admits, subsequent
    /// observations within TTL reject with `Replayed`. Expired entries
    /// fall through and re-record (matches KVRocks key-expiry semantics).
    pub fn new() -> Self {
        Self {
            seen: Mutex::new(HashMap::new()),
            failing: false,
        }
    }

    /// Always-failing substrate — every call returns `Transient`. Used
    /// by integration tests to prove the engine fails closed when the
    /// replay-cache substrate is unreachable.
    pub fn failing() -> Self {
        Self {
            seen: Mutex::new(HashMap::new()),
            failing: true,
        }
    }
}

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

#[async_trait::async_trait]
impl ReplayDefense for MemoryReplayDefense {
    async fn check_and_record(
        &self,
        jti_hash: &str,
        ttl: Duration,
    ) -> Result<(), ReplayDefenseError> {
        if self.failing {
            return Err(ReplayDefenseError::Transient(
                "MemoryReplayDefense::failing()".to_string(),
            ));
        }
        let now = Instant::now();
        let mut seen = self.seen.lock().unwrap_or_else(|p| p.into_inner());
        if let Some(deadline) = seen.get(jti_hash)
            && *deadline > now
        {
            return Err(ReplayDefenseError::Replayed);
        }
        seen.insert(jti_hash.to_string(), now + ttl);
        Ok(())
    }
}
