//! Cache invalidation middleware for data consistency

use anyhow::Result;
use chrono::{DateTime, Utc};
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use tracing::{debug, warn};
use uuid::Uuid;

/// Cache invalidation event
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InvalidationEvent {
    pub event_id: Uuid,
    pub event_type: InvalidationEventType,
    pub entity_type: String,
    pub entity_id: Option<Uuid>,
    pub operation: String,
    pub timestamp: DateTime<Utc>,
    pub affected_caches: Vec<String>,
    pub metadata: HashMap<String, serde_json::Value>,
}

/// Types of invalidation events
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum InvalidationEventType {
    /// Entity was created
    Created,
    /// Entity was updated
    Updated,
    /// Entity was deleted
    Deleted,
    /// Entity was completed
    Completed,
    /// Bulk operation occurred
    BulkOperation,
    /// Cache was manually invalidated
    ManualInvalidation,
    /// Cache expired
    Expired,
    /// Cascade invalidation
    CascadeInvalidation,
}

impl std::fmt::Display for InvalidationEventType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            InvalidationEventType::Created => write!(f, "Created"),
            InvalidationEventType::Updated => write!(f, "Updated"),
            InvalidationEventType::Deleted => write!(f, "Deleted"),
            InvalidationEventType::Completed => write!(f, "Completed"),
            InvalidationEventType::BulkOperation => write!(f, "BulkOperation"),
            InvalidationEventType::ManualInvalidation => write!(f, "ManualInvalidation"),
            InvalidationEventType::Expired => write!(f, "Expired"),
            InvalidationEventType::CascadeInvalidation => write!(f, "CascadeInvalidation"),
        }
    }
}

/// Cache invalidation rule
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InvalidationRule {
    pub rule_id: Uuid,
    pub name: String,
    pub description: String,
    pub entity_type: String,
    pub operations: Vec<String>,
    pub affected_cache_types: Vec<String>,
    pub invalidation_strategy: InvalidationStrategy,
    pub enabled: bool,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
}

/// Invalidation strategies
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum InvalidationStrategy {
    /// Invalidate all caches
    InvalidateAll,
    /// Invalidate specific cache types
    InvalidateSpecific(Vec<String>),
    /// Invalidate by entity ID
    InvalidateByEntity,
    /// Invalidate by pattern
    InvalidateByPattern(String),
    /// Cascade invalidation (invalidate dependent entities)
    CascadeInvalidation,
}

/// Cache invalidation middleware
pub struct CacheInvalidationMiddleware {
    /// Invalidation rules
    rules: Arc<RwLock<HashMap<String, InvalidationRule>>>,
    /// Event history
    events: Arc<RwLock<Vec<InvalidationEvent>>>,
    /// Cache invalidation handlers
    handlers: Arc<RwLock<HashMap<String, Box<dyn CacheInvalidationHandler + Send + Sync>>>>,
    /// Configuration
    config: InvalidationConfig,
    /// Statistics
    stats: Arc<RwLock<InvalidationStats>>,
}

/// Cache invalidation handler trait
pub trait CacheInvalidationHandler {
    /// Handle cache invalidation
    ///
    /// # Errors
    ///
    /// This function will return an error if the invalidation fails
    fn invalidate(&self, event: &InvalidationEvent) -> Result<()>;

    /// Get cache type name
    fn cache_type(&self) -> &str;

    /// Check if this handler can handle the event
    fn can_handle(&self, event: &InvalidationEvent) -> bool;
}

/// Invalidation configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InvalidationConfig {
    /// Maximum number of events to keep in history
    pub max_events: usize,
    /// Event retention duration
    pub event_retention: Duration,
    /// Enable cascade invalidation
    pub enable_cascade: bool,
    /// Cascade invalidation depth
    pub cascade_depth: u32,
    /// Enable event batching
    pub enable_batching: bool,
    /// Batch size
    pub batch_size: usize,
    /// Batch timeout
    pub batch_timeout: Duration,
}

impl Default for InvalidationConfig {
    fn default() -> Self {
        Self {
            max_events: 10000,
            event_retention: Duration::from_secs(86400), // 24 hours
            enable_cascade: true,
            cascade_depth: 3,
            enable_batching: true,
            batch_size: 100,
            batch_timeout: Duration::from_secs(5),
        }
    }
}

/// Invalidation statistics
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct InvalidationStats {
    pub total_events: u64,
    pub successful_invalidations: u64,
    pub failed_invalidations: u64,
    pub cascade_invalidations: u64,
    pub manual_invalidations: u64,
    pub expired_invalidations: u64,
    pub average_processing_time_ms: f64,
    pub last_invalidation: Option<DateTime<Utc>>,
}

impl CacheInvalidationMiddleware {
    /// Create a new cache invalidation middleware
    #[must_use]
    pub fn new(config: InvalidationConfig) -> Self {
        Self {
            rules: Arc::new(RwLock::new(HashMap::new())),
            events: Arc::new(RwLock::new(Vec::new())),
            handlers: Arc::new(RwLock::new(HashMap::new())),
            config,
            stats: Arc::new(RwLock::new(InvalidationStats::default())),
        }
    }

    /// Create a new middleware with default configuration
    #[must_use]
    pub fn new_default() -> Self {
        Self::new(InvalidationConfig::default())
    }

    /// Register a cache invalidation handler
    pub fn register_handler(&self, handler: Box<dyn CacheInvalidationHandler + Send + Sync>) {
        let mut handlers = self.handlers.write();
        handlers.insert(handler.cache_type().to_string(), handler);
    }

    /// Add an invalidation rule
    pub fn add_rule(&self, rule: InvalidationRule) {
        let mut rules = self.rules.write();
        rules.insert(rule.name.clone(), rule);
    }

    /// Process an invalidation event
    ///
    /// # Errors
    ///
    /// This function will return an error if the event processing fails
    pub async fn process_event(&self, event: InvalidationEvent) -> Result<()> {
        let start_time = std::time::Instant::now();

        // Store the event
        self.store_event(&event);

        // Find applicable rules
        let applicable_rules = self.find_applicable_rules(&event);

        // Process invalidation for each rule
        for rule in applicable_rules {
            if let Err(e) = self.process_rule(&event, &rule).await {
                warn!("Failed to process invalidation rule {}: {}", rule.name, e);
                self.record_failed_invalidation();
            } else {
                self.record_successful_invalidation();
            }
        }

        // Handle cascade invalidation if enabled
        if self.config.enable_cascade {
            self.handle_cascade_invalidation(&event).await?;
        }

        // Update statistics
        #[allow(clippy::cast_precision_loss)]
        let processing_time = start_time.elapsed().as_millis().min(u128::from(u64::MAX)) as f64;
        {
            let mut stats = self.stats.write();
            stats.total_events += 1;
        }
        self.update_processing_time(processing_time);

        debug!(
            "Processed invalidation event: {} for entity: {}:{}",
            event.event_type,
            event.entity_type,
            event
                .entity_id
                .map_or_else(|| "none".to_string(), |id| id.to_string())
        );

        Ok(())
    }

    /// Manually invalidate caches
    ///
    /// # Errors
    ///
    /// This function will return an error if the manual invalidation fails
    pub async fn manual_invalidate(
        &self,
        entity_type: &str,
        entity_id: Option<Uuid>,
        cache_types: Option<Vec<String>>,
    ) -> Result<()> {
        let event = InvalidationEvent {
            event_id: Uuid::new_v4(),
            event_type: InvalidationEventType::ManualInvalidation,
            entity_type: entity_type.to_string(),
            entity_id,
            operation: "manual_invalidation".to_string(),
            timestamp: Utc::now(),
            affected_caches: cache_types.unwrap_or_default(),
            metadata: HashMap::new(),
        };

        self.process_event(event).await?;
        self.record_manual_invalidation();
        Ok(())
    }

    /// Get invalidation statistics
    #[must_use]
    pub fn get_stats(&self) -> InvalidationStats {
        self.stats.read().clone()
    }

    /// Get recent invalidation events
    #[must_use]
    pub fn get_recent_events(&self, limit: usize) -> Vec<InvalidationEvent> {
        let events = self.events.read();
        events.iter().rev().take(limit).cloned().collect()
    }

    /// Get events by entity type
    #[must_use]
    pub fn get_events_by_entity_type(&self, entity_type: &str) -> Vec<InvalidationEvent> {
        let events = self.events.read();
        events
            .iter()
            .filter(|event| event.entity_type == entity_type)
            .cloned()
            .collect()
    }

    /// Store an invalidation event
    fn store_event(&self, event: &InvalidationEvent) {
        let mut events = self.events.write();
        events.push(event.clone());

        // Trim events if we exceed max_events
        if events.len() > self.config.max_events {
            let excess = events.len() - self.config.max_events;
            events.drain(0..excess);
        }

        // Remove old events based on retention policy
        let cutoff_time = Utc::now()
            - chrono::Duration::from_std(self.config.event_retention).unwrap_or_default();
        events.retain(|event| event.timestamp > cutoff_time);
    }

    /// Find applicable invalidation rules
    fn find_applicable_rules(&self, event: &InvalidationEvent) -> Vec<InvalidationRule> {
        let rules = self.rules.read();
        rules
            .values()
            .filter(|rule| {
                rule.enabled
                    && rule.entity_type == event.entity_type
                    && (rule.operations.is_empty() || rule.operations.contains(&event.operation))
            })
            .cloned()
            .collect()
    }

    /// Process an invalidation rule
    async fn process_rule(&self, event: &InvalidationEvent, rule: &InvalidationRule) -> Result<()> {
        match &rule.invalidation_strategy {
            InvalidationStrategy::InvalidateAll => {
                // Invalidate all registered caches
                let handlers_guard = self.handlers.read();
                for handler in handlers_guard.values() {
                    if handler.can_handle(event) {
                        handler.invalidate(event)?;
                    }
                }
            }
            InvalidationStrategy::InvalidateSpecific(cache_types) => {
                // Invalidate specific cache types
                let handlers_guard = self.handlers.read();
                for cache_type in cache_types {
                    if let Some(handler) = handlers_guard.get(cache_type) {
                        if handler.can_handle(event) {
                            handler.invalidate(event)?;
                        }
                    }
                }
            }
            InvalidationStrategy::InvalidateByEntity => {
                // Invalidate by entity ID
                if let Some(_entity_id) = event.entity_id {
                    let handlers_guard = self.handlers.read();
                    for handler in handlers_guard.values() {
                        if handler.can_handle(event) {
                            handler.invalidate(event)?;
                        }
                    }
                }
            }
            InvalidationStrategy::InvalidateByPattern(pattern) => {
                // Invalidate by pattern matching
                let handlers_guard = self.handlers.read();
                for handler in handlers_guard.values() {
                    if handler.can_handle(event) && Self::matches_pattern(event, pattern) {
                        handler.invalidate(event)?;
                    }
                }
            }
            InvalidationStrategy::CascadeInvalidation => {
                // Handle cascade invalidation
                self.handle_cascade_invalidation(event).await?;
            }
        }

        Ok(())
    }

    /// Handle cascade invalidation
    async fn handle_cascade_invalidation(&self, event: &InvalidationEvent) -> Result<()> {
        // Find dependent entities and invalidate them
        let dependent_entities = Self::find_dependent_entities(event);

        for dependent_entity in dependent_entities {
            let dependent_event = InvalidationEvent {
                event_id: Uuid::new_v4(),
                event_type: InvalidationEventType::CascadeInvalidation,
                entity_type: dependent_entity.entity_type.clone(),
                entity_id: dependent_entity.entity_id,
                operation: "cascade_invalidation".to_string(),
                timestamp: Utc::now(),
                affected_caches: dependent_entity.affected_caches.clone(),
                metadata: HashMap::new(),
            };

            Box::pin(self.process_event(dependent_event)).await?;
            self.record_cascade_invalidation();
        }

        Ok(())
    }

    /// Find dependent entities for cascade invalidation.
    ///
    /// The fan-out is type-driven (a task event implies project + area
    /// dependents; an area event implies project + task dependents). When the
    /// caller populates `event.metadata` with structured hints, the dependent
    /// events carry concrete entity IDs instead of `None`. Recognised keys:
    ///
    /// - `"project_uuid"` — UUID string of the project the entity belongs to.
    /// - `"area_uuid"` — UUID string of the area the entity belongs to.
    ///
    /// Missing or unparseable metadata falls back to `entity_id: None`
    /// (back-compatible with pre-#93 callers).
    fn find_dependent_entities(event: &InvalidationEvent) -> Vec<DependentEntity> {
        let mut dependent_entities = Vec::new();
        let project_uuid = Self::metadata_uuid(event, "project_uuid");
        let area_uuid = Self::metadata_uuid(event, "area_uuid");

        match event.entity_type.as_str() {
            "task" if event.entity_id.is_some() => {
                dependent_entities.push(DependentEntity {
                    entity_type: "project".to_string(),
                    entity_id: project_uuid,
                    affected_caches: vec![],
                });
                dependent_entities.push(DependentEntity {
                    entity_type: "area".to_string(),
                    entity_id: area_uuid,
                    affected_caches: vec![],
                });
            }
            "project" if event.entity_id.is_some() => {
                dependent_entities.push(DependentEntity {
                    entity_type: "task".to_string(),
                    entity_id: None,
                    affected_caches: vec![],
                });
                dependent_entities.push(DependentEntity {
                    entity_type: "area".to_string(),
                    entity_id: area_uuid,
                    affected_caches: vec![],
                });
            }
            "area" if event.entity_id.is_some() => {
                dependent_entities.push(DependentEntity {
                    entity_type: "project".to_string(),
                    entity_id: None,
                    affected_caches: vec![],
                });
                dependent_entities.push(DependentEntity {
                    entity_type: "task".to_string(),
                    entity_id: None,
                    affected_caches: vec![],
                });
            }
            _ => {}
        }

        dependent_entities
    }

    /// Extract a UUID from `event.metadata[key]`, returning `None` if missing
    /// or unparseable.
    fn metadata_uuid(event: &InvalidationEvent, key: &str) -> Option<Uuid> {
        event
            .metadata
            .get(key)
            .and_then(serde_json::Value::as_str)
            .and_then(|s| Uuid::parse_str(s).ok())
    }

    /// Check if event matches a pattern
    fn matches_pattern(event: &InvalidationEvent, pattern: &str) -> bool {
        // Simple pattern matching - in production, use regex or more sophisticated matching
        event.entity_type.contains(pattern) || event.operation.contains(pattern)
    }

    /// Record successful invalidation
    fn record_successful_invalidation(&self) {
        let mut stats = self.stats.write();
        stats.successful_invalidations += 1;
        stats.last_invalidation = Some(Utc::now());
    }

    /// Record failed invalidation
    fn record_failed_invalidation(&self) {
        let mut stats = self.stats.write();
        stats.failed_invalidations += 1;
    }

    /// Record cascade invalidation
    fn record_cascade_invalidation(&self) {
        let mut stats = self.stats.write();
        stats.cascade_invalidations += 1;
    }

    /// Record manual invalidation
    fn record_manual_invalidation(&self) {
        let mut stats = self.stats.write();
        stats.manual_invalidations += 1;
    }

    /// Update average processing time
    fn update_processing_time(&self, processing_time: f64) {
        let mut stats = self.stats.write();

        // Update running average
        #[allow(clippy::cast_precision_loss)]
        let total_events = stats.total_events as f64;
        stats.average_processing_time_ms =
            (stats.average_processing_time_ms * (total_events - 1.0) + processing_time)
                / total_events;
    }
}

/// Dependent entity for cascade invalidation
#[derive(Debug, Clone)]
struct DependentEntity {
    entity_type: String,
    entity_id: Option<Uuid>,
    affected_caches: Vec<String>,
}

/// Bridge handler that routes [`InvalidationEvent`]s into a [`ThingsCache`]'s
/// dependency-aware invalidation methods.
///
/// Register one of these on a [`CacheInvalidationMiddleware`] to make
/// `process_event(...)` actually evict cached entries:
///
/// ```ignore
/// let cache = Arc::new(ThingsCache::new_default());
/// let handler = ThingsCacheInvalidationHandler::new(Arc::clone(&cache));
/// middleware.register_handler(Box::new(handler));
/// ```
///
/// Spawns the actual eviction onto the current Tokio runtime so the
/// synchronous trait method can return immediately. Callers that need to
/// observe the post-invalidation state (e.g. tests) should yield once with
/// `tokio::task::yield_now().await` after `process_event` returns.
pub struct ThingsCacheInvalidationHandler {
    cache: Arc<crate::cache::ThingsCache>,
    cache_type: String,
}

impl ThingsCacheInvalidationHandler {
    /// Construct a handler with the default cache type label `"things_cache"`.
    #[must_use]
    pub fn new(cache: Arc<crate::cache::ThingsCache>) -> Self {
        Self {
            cache,
            cache_type: "things_cache".to_string(),
        }
    }

    /// Construct a handler with a caller-supplied cache type label.
    #[must_use]
    pub fn with_cache_type(cache: Arc<crate::cache::ThingsCache>, cache_type: String) -> Self {
        Self { cache, cache_type }
    }
}

impl CacheInvalidationHandler for ThingsCacheInvalidationHandler {
    fn invalidate(&self, event: &InvalidationEvent) -> Result<()> {
        // Route only by `(entity_type, entity_id)`. Operation matching is a
        // category-level fallback that would over-evict siblings of the
        // mutated entity (every task entry lists `task_updated`), so we leave
        // it for callers that explicitly want coarse invalidation.
        let cache = Arc::clone(&self.cache);
        let entity_type = event.entity_type.clone();
        let entity_id = event.entity_id;
        tokio::spawn(async move {
            cache
                .invalidate_by_entity(&entity_type, entity_id.as_ref())
                .await;
        });
        Ok(())
    }

    fn cache_type(&self) -> &str {
        &self.cache_type
    }

    fn can_handle(&self, _event: &InvalidationEvent) -> bool {
        true
    }
}

/// Cascade invalidation event type
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum CascadeInvalidationEvent {
    /// Invalidate all dependent entities
    InvalidateAll,
    /// Invalidate specific dependent entities
    InvalidateSpecific(Vec<String>),
    /// Invalidate by dependency level
    InvalidateByLevel(u32),
}

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

    // Mock cache invalidation handler for testing
    struct MockCacheHandler {
        cache_type: String,
        invalidated_events: Arc<RwLock<Vec<InvalidationEvent>>>,
    }

    impl MockCacheHandler {
        fn new(cache_type: &str) -> Self {
            Self {
                cache_type: cache_type.to_string(),
                invalidated_events: Arc::new(RwLock::new(Vec::new())),
            }
        }

        fn _get_invalidated_events(&self) -> Vec<InvalidationEvent> {
            self.invalidated_events.read().clone()
        }
    }

    impl CacheInvalidationHandler for MockCacheHandler {
        fn invalidate(&self, event: &InvalidationEvent) -> Result<()> {
            let mut events = self.invalidated_events.write();
            events.push(event.clone());
            Ok(())
        }

        fn cache_type(&self) -> &str {
            &self.cache_type
        }

        fn can_handle(&self, event: &InvalidationEvent) -> bool {
            event.affected_caches.is_empty() || event.affected_caches.contains(&self.cache_type)
        }
    }

    #[tokio::test]
    async fn test_invalidation_middleware_basic() {
        let middleware = CacheInvalidationMiddleware::new_default();

        // Register mock handlers
        let _l1_handler = Arc::new(MockCacheHandler::new("l1"));
        let _l2_handler = Arc::new(MockCacheHandler::new("l2"));

        middleware.register_handler(Box::new(MockCacheHandler::new("l1")));
        middleware.register_handler(Box::new(MockCacheHandler::new("l2")));

        // Add rules for task, project, and area entities
        let task_rule = InvalidationRule {
            rule_id: Uuid::new_v4(),
            name: "task_rule".to_string(),
            description: "Rule for task invalidation".to_string(),
            entity_type: "task".to_string(),
            operations: vec!["updated".to_string()],
            affected_cache_types: vec!["l1".to_string(), "l2".to_string()],
            invalidation_strategy: InvalidationStrategy::InvalidateAll,
            enabled: true,
            created_at: Utc::now(),
            updated_at: Utc::now(),
        };
        middleware.add_rule(task_rule);

        let project_rule = InvalidationRule {
            rule_id: Uuid::new_v4(),
            name: "project_rule".to_string(),
            description: "Rule for project invalidation".to_string(),
            entity_type: "project".to_string(),
            operations: vec!["cascade_invalidation".to_string()],
            affected_cache_types: vec!["l1".to_string(), "l2".to_string()],
            invalidation_strategy: InvalidationStrategy::InvalidateAll,
            enabled: true,
            created_at: Utc::now(),
            updated_at: Utc::now(),
        };
        middleware.add_rule(project_rule);

        let area_rule = InvalidationRule {
            rule_id: Uuid::new_v4(),
            name: "area_rule".to_string(),
            description: "Rule for area invalidation".to_string(),
            entity_type: "area".to_string(),
            operations: vec!["cascade_invalidation".to_string()],
            affected_cache_types: vec!["l1".to_string(), "l2".to_string()],
            invalidation_strategy: InvalidationStrategy::InvalidateAll,
            enabled: true,
            created_at: Utc::now(),
            updated_at: Utc::now(),
        };
        middleware.add_rule(area_rule);

        // Create an invalidation event
        let event = InvalidationEvent {
            event_id: Uuid::new_v4(),
            event_type: InvalidationEventType::Updated,
            entity_type: "task".to_string(),
            entity_id: Some(Uuid::new_v4()),
            operation: "updated".to_string(),
            timestamp: Utc::now(),
            affected_caches: vec!["l1".to_string(), "l2".to_string()],
            metadata: HashMap::new(),
        };

        // Process the event
        middleware.process_event(event).await.unwrap();

        // Check statistics
        let stats = middleware.get_stats();
        assert_eq!(stats.total_events, 3); // 1 original + 2 cascade events
        assert_eq!(stats.successful_invalidations, 3);
    }

    #[tokio::test]
    async fn test_manual_invalidation() {
        let middleware = CacheInvalidationMiddleware::new_default();

        middleware.register_handler(Box::new(MockCacheHandler::new("l1")));

        // Manual invalidation
        middleware
            .manual_invalidate("task", Some(Uuid::new_v4()), None)
            .await
            .unwrap();

        let stats = middleware.get_stats();
        assert_eq!(stats.manual_invalidations, 1);
    }

    #[tokio::test]
    async fn test_event_storage() {
        let middleware = CacheInvalidationMiddleware::new_default();

        let event = InvalidationEvent {
            event_id: Uuid::new_v4(),
            event_type: InvalidationEventType::Created,
            entity_type: "task".to_string(),
            entity_id: Some(Uuid::new_v4()),
            operation: "created".to_string(),
            timestamp: Utc::now(),
            affected_caches: vec![],
            metadata: HashMap::new(),
        };

        middleware.store_event(&event);

        let recent_events = middleware.get_recent_events(1);
        assert_eq!(recent_events.len(), 1);
        assert_eq!(recent_events[0].entity_type, "task");
    }

    #[tokio::test]
    async fn test_invalidation_middleware_creation() {
        let middleware = CacheInvalidationMiddleware::new_default();
        let stats = middleware.get_stats();

        assert_eq!(stats.total_events, 0);
        assert_eq!(stats.successful_invalidations, 0);
        assert_eq!(stats.failed_invalidations, 0);
        assert_eq!(stats.manual_invalidations, 0);
    }

    #[tokio::test]
    async fn test_invalidation_middleware_with_config() {
        let config = InvalidationConfig {
            enable_cascade: true,
            max_events: 1000,
            event_retention: Duration::from_secs(3600),
            batch_size: 10,
            batch_timeout: Duration::from_secs(30),
            cascade_depth: 3,
            enable_batching: true,
        };

        let middleware = CacheInvalidationMiddleware::new(config);
        let stats = middleware.get_stats();

        assert_eq!(stats.total_events, 0);
    }

    #[tokio::test]
    async fn test_add_rule() {
        let middleware = CacheInvalidationMiddleware::new_default();

        let rule = InvalidationRule {
            rule_id: Uuid::new_v4(),
            name: "test_rule".to_string(),
            description: "Test rule".to_string(),
            entity_type: "task".to_string(),
            operations: vec!["updated".to_string()],
            affected_cache_types: vec!["task_cache".to_string()],
            invalidation_strategy: InvalidationStrategy::InvalidateAll,
            enabled: true,
            created_at: Utc::now(),
            updated_at: Utc::now(),
        };

        middleware.add_rule(rule);

        // Rules are stored internally, we can't directly test them
        // but we can test that the method doesn't panic
    }

    #[tokio::test]
    async fn test_register_handler() {
        let middleware = CacheInvalidationMiddleware::new_default();
        let handler = Box::new(MockCacheHandler::new("test_cache"));

        middleware.register_handler(handler);

        // Handler is stored internally, we can't directly test it
        // but we can test that the method doesn't panic
    }

    #[tokio::test]
    async fn test_process_event_with_handler() {
        let middleware = CacheInvalidationMiddleware::new_default();
        let handler = Box::new(MockCacheHandler::new("test_cache"));
        let l1_handler = Box::new(MockCacheHandler::new("l1"));
        let l2_handler = Box::new(MockCacheHandler::new("l2"));

        middleware.register_handler(handler);
        middleware.register_handler(l1_handler);
        middleware.register_handler(l2_handler);

        let rule = InvalidationRule {
            rule_id: Uuid::new_v4(),
            name: "test_rule".to_string(),
            description: "Test rule".to_string(),
            entity_type: "task".to_string(),
            operations: vec!["created".to_string(), "updated".to_string()],
            affected_cache_types: vec!["test_cache".to_string()],
            invalidation_strategy: InvalidationStrategy::InvalidateAll,
            enabled: true,
            created_at: Utc::now(),
            updated_at: Utc::now(),
        };
        middleware.add_rule(rule);

        // Add rules for project and area entities to handle cascade events
        // Note: cascade events use "l1" and "l2" as affected caches
        let project_rule = InvalidationRule {
            rule_id: Uuid::new_v4(),
            name: "project_rule".to_string(),
            description: "Rule for project invalidation".to_string(),
            entity_type: "project".to_string(),
            operations: vec!["cascade_invalidation".to_string()],
            affected_cache_types: vec!["l1".to_string(), "l2".to_string()],
            invalidation_strategy: InvalidationStrategy::InvalidateAll,
            enabled: true,
            created_at: Utc::now(),
            updated_at: Utc::now(),
        };
        middleware.add_rule(project_rule);

        let area_rule = InvalidationRule {
            rule_id: Uuid::new_v4(),
            name: "area_rule".to_string(),
            description: "Rule for area invalidation".to_string(),
            entity_type: "area".to_string(),
            operations: vec!["cascade_invalidation".to_string()],
            affected_cache_types: vec!["l1".to_string(), "l2".to_string()],
            invalidation_strategy: InvalidationStrategy::InvalidateAll,
            enabled: true,
            created_at: Utc::now(),
            updated_at: Utc::now(),
        };
        middleware.add_rule(area_rule);

        let event = InvalidationEvent {
            event_id: Uuid::new_v4(),
            event_type: InvalidationEventType::Created,
            entity_type: "task".to_string(),
            entity_id: Some(Uuid::new_v4()),
            operation: "created".to_string(),
            timestamp: Utc::now(),
            affected_caches: vec!["test_cache".to_string()],
            metadata: HashMap::new(),
        };

        let _ = middleware.process_event(event).await;

        let stats = middleware.get_stats();
        assert_eq!(stats.total_events, 3); // 1 original + 2 cascade events
        assert_eq!(stats.successful_invalidations, 3);
    }

    #[tokio::test]
    async fn test_get_recent_events() {
        let middleware = CacheInvalidationMiddleware::new_default();

        // Add multiple events
        for i in 0..5 {
            let event = InvalidationEvent {
                event_id: Uuid::new_v4(),
                event_type: InvalidationEventType::Created,
                entity_type: format!("task_{i}"),
                entity_id: Some(Uuid::new_v4()),
                operation: "created".to_string(),
                timestamp: Utc::now(),
                affected_caches: vec![],
                metadata: HashMap::new(),
            };
            middleware.store_event(&event);
        }

        // Get recent events
        let recent_events = middleware.get_recent_events(3);
        assert_eq!(recent_events.len(), 3);

        // Get all events
        let all_events = middleware.get_recent_events(10);
        assert_eq!(all_events.len(), 5);
    }

    fn task_event_with_metadata(
        entity_id: Uuid,
        metadata: HashMap<String, serde_json::Value>,
    ) -> InvalidationEvent {
        InvalidationEvent {
            event_id: Uuid::new_v4(),
            event_type: InvalidationEventType::Updated,
            entity_type: "task".to_string(),
            entity_id: Some(entity_id),
            operation: "task_updated".to_string(),
            timestamp: Utc::now(),
            affected_caches: vec![],
            metadata,
        }
    }

    #[test]
    fn test_cascade_uses_project_uuid_metadata() {
        let task_id = Uuid::new_v4();
        let project_id = Uuid::new_v4();
        let mut metadata = HashMap::new();
        metadata.insert(
            "project_uuid".to_string(),
            serde_json::Value::String(project_id.to_string()),
        );
        let event = task_event_with_metadata(task_id, metadata);

        let dependents = CacheInvalidationMiddleware::find_dependent_entities(&event);
        let project_dep = dependents
            .iter()
            .find(|d| d.entity_type == "project")
            .expect("task event must fan out to project");
        assert_eq!(project_dep.entity_id, Some(project_id));
        let area_dep = dependents
            .iter()
            .find(|d| d.entity_type == "area")
            .expect("task event must fan out to area");
        assert_eq!(area_dep.entity_id, None, "no area_uuid in metadata");
    }

    #[test]
    fn test_cascade_falls_back_when_metadata_missing() {
        let event = task_event_with_metadata(Uuid::new_v4(), HashMap::new());
        let dependents = CacheInvalidationMiddleware::find_dependent_entities(&event);
        assert!(dependents.iter().all(|d| d.entity_id.is_none()));
    }

    #[tokio::test]
    async fn test_things_cache_handler_routes_to_invalidate_by_entity() {
        use crate::cache::ThingsCache;
        use crate::test_utils::create_mock_tasks;

        let cache = Arc::new(ThingsCache::new_default());
        let target_id = Uuid::new_v4();
        let other_id = Uuid::new_v4();

        let mut target_task = create_mock_tasks().into_iter().next().unwrap();
        target_task.uuid = target_id;
        target_task.project_uuid = None;
        target_task.area_uuid = None;
        let mut other_task = target_task.clone();
        other_task.uuid = other_id;

        cache
            .get_tasks("target_key", || async { Ok(vec![target_task]) })
            .await
            .unwrap();
        cache
            .get_tasks("other_key", || async { Ok(vec![other_task]) })
            .await
            .unwrap();

        let handler = ThingsCacheInvalidationHandler::new(Arc::clone(&cache));
        let event = InvalidationEvent {
            event_id: Uuid::new_v4(),
            event_type: InvalidationEventType::Updated,
            entity_type: "task".to_string(),
            entity_id: Some(target_id),
            operation: "task_updated".to_string(),
            timestamp: Utc::now(),
            affected_caches: vec![],
            metadata: HashMap::new(),
        };
        handler.invalidate(&event).unwrap();

        // The handler spawns the eviction; give the runtime enough time to
        // complete it before asserting. yield_now is insufficient on loaded
        // machines — a brief sleep is more robust.
        tokio::time::sleep(std::time::Duration::from_millis(10)).await;

        // Re-fetch via the cache: the target key should miss (returning the
        // empty fetcher result), the other key should still hit its cached row.
        let target = cache
            .get_tasks("target_key", || async { Ok(vec![]) })
            .await
            .unwrap();
        let other = cache
            .get_tasks("other_key", || async { Ok(vec![]) })
            .await
            .unwrap();
        assert!(target.is_empty(), "target should have been invalidated");
        assert_eq!(other.len(), 1, "other task should still be cached");
        assert_eq!(other[0].uuid, other_id);
    }
}
