//! Shared types for deferred realtime projections.
//!
//! Identity state (canonical_entities + identity_links) is committed synchronously
//! in the resolve hot path. Everything else - relationship edges, parent_entity_id
//! updates, entity events, webhooks - is enqueued to Redis and consumed by the
//! worker's projection loop. This keeps the hot path to one pool connection at a time.

use serde::{Deserialize, Serialize};
use uuid::Uuid;

/// Redis list key for the realtime projection queue.
pub const RT_PROJECTION_QUEUE: &str = "q:rt_projections";

/// Envelope pushed to Redis after a realtime resolve commits identity state.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RealtimeProjection {
    pub tenant_id: Uuid,
    pub tasks: Vec<ProjectionTask>,
}

/// Individual deferred task within a projection envelope.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ProjectionTask {
    /// Insert or update a relationship edge between two canonical entities.
    RelateEdge {
        entity_a_id: Uuid,
        entity_b_id: Uuid,
        weight: f64,
        relationship_type: String,
        attributes: serde_json::Value,
    },
    /// Set parent_entity_id on a canonical entity (hierarchy from directed relations).
    SetParent {
        child_entity_id: Uuid,
        parent_entity_id: Uuid,
    },
    /// Record an entity lifecycle event + fan out webhooks.
    EntityEvent {
        entity_id: Uuid,
        event_type: String,
        source: String,
    },
}

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

    fn test_tenant_id() -> Uuid {
        Uuid::parse_str("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee").unwrap()
    }

    fn test_entity_a() -> Uuid {
        Uuid::parse_str("11111111-1111-1111-1111-111111111111").unwrap()
    }

    fn test_entity_b() -> Uuid {
        Uuid::parse_str("22222222-2222-2222-2222-222222222222").unwrap()
    }

    #[test]
    fn test_queue_constant() {
        assert_eq!(RT_PROJECTION_QUEUE, "q:rt_projections");
    }

    #[test]
    fn test_relate_edge_serde_round_trip() {
        let task = ProjectionTask::RelateEdge {
            entity_a_id: test_entity_a(),
            entity_b_id: test_entity_b(),
            weight: 0.87,
            relationship_type: "parent_child".to_string(),
            attributes: serde_json::json!({
                "source": "realtime_semantic",
                "confidence": 0.87,
                "direction": "a_to_b",
            }),
        };
        let envelope = RealtimeProjection {
            tenant_id: test_tenant_id(),
            tasks: vec![task],
        };

        let json = serde_json::to_string(&envelope).unwrap();
        let parsed: RealtimeProjection = serde_json::from_str(&json).unwrap();

        assert_eq!(parsed.tenant_id, test_tenant_id());
        assert_eq!(parsed.tasks.len(), 1);
        match &parsed.tasks[0] {
            ProjectionTask::RelateEdge { entity_a_id, entity_b_id, weight, relationship_type, attributes } => {
                assert_eq!(*entity_a_id, test_entity_a());
                assert_eq!(*entity_b_id, test_entity_b());
                assert!((weight - 0.87).abs() < f64::EPSILON);
                assert_eq!(relationship_type, "parent_child");
                assert_eq!(attributes["direction"], "a_to_b");
            }
            _ => panic!("Expected RelateEdge variant"),
        }
    }

    #[test]
    fn test_set_parent_serde_round_trip() {
        let task = ProjectionTask::SetParent {
            child_entity_id: test_entity_a(),
            parent_entity_id: test_entity_b(),
        };
        let envelope = RealtimeProjection {
            tenant_id: test_tenant_id(),
            tasks: vec![task],
        };

        let json = serde_json::to_string(&envelope).unwrap();
        let parsed: RealtimeProjection = serde_json::from_str(&json).unwrap();

        match &parsed.tasks[0] {
            ProjectionTask::SetParent { child_entity_id, parent_entity_id } => {
                assert_eq!(*child_entity_id, test_entity_a());
                assert_eq!(*parent_entity_id, test_entity_b());
            }
            _ => panic!("Expected SetParent variant"),
        }
    }

    #[test]
    fn test_entity_event_serde_round_trip() {
        let task = ProjectionTask::EntityEvent {
            entity_id: test_entity_a(),
            event_type: "created".to_string(),
            source: "realtime_new".to_string(),
        };
        let envelope = RealtimeProjection {
            tenant_id: test_tenant_id(),
            tasks: vec![task],
        };

        let json = serde_json::to_string(&envelope).unwrap();
        let parsed: RealtimeProjection = serde_json::from_str(&json).unwrap();

        match &parsed.tasks[0] {
            ProjectionTask::EntityEvent { entity_id, event_type, source } => {
                assert_eq!(*entity_id, test_entity_a());
                assert_eq!(event_type, "created");
                assert_eq!(source, "realtime_new");
            }
            _ => panic!("Expected EntityEvent variant"),
        }
    }

    #[test]
    fn test_empty_tasks_serde_round_trip() {
        let envelope = RealtimeProjection {
            tenant_id: test_tenant_id(),
            tasks: vec![],
        };

        let json = serde_json::to_string(&envelope).unwrap();
        let parsed: RealtimeProjection = serde_json::from_str(&json).unwrap();

        assert_eq!(parsed.tenant_id, test_tenant_id());
        assert!(parsed.tasks.is_empty());
    }

    #[test]
    fn test_mixed_tasks_serde_round_trip() {
        let envelope = RealtimeProjection {
            tenant_id: test_tenant_id(),
            tasks: vec![
                ProjectionTask::RelateEdge {
                    entity_a_id: test_entity_a(),
                    entity_b_id: test_entity_b(),
                    weight: 0.95,
                    relationship_type: "sibling".to_string(),
                    attributes: serde_json::json!({}),
                },
                ProjectionTask::SetParent {
                    child_entity_id: test_entity_a(),
                    parent_entity_id: test_entity_b(),
                },
                ProjectionTask::EntityEvent {
                    entity_id: test_entity_a(),
                    event_type: "updated".to_string(),
                    source: "realtime_link".to_string(),
                },
            ],
        };

        let json = serde_json::to_string(&envelope).unwrap();
        let parsed: RealtimeProjection = serde_json::from_str(&json).unwrap();

        assert_eq!(parsed.tasks.len(), 3);
        assert!(matches!(&parsed.tasks[0], ProjectionTask::RelateEdge { .. }));
        assert!(matches!(&parsed.tasks[1], ProjectionTask::SetParent { .. }));
        assert!(matches!(&parsed.tasks[2], ProjectionTask::EntityEvent { .. }));
    }

    #[test]
    fn test_deserialize_from_known_json() {
        // Simulate what the worker would receive from Redis BLPOP.
        let json = r#"{
            "tenant_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
            "tasks": [
                {
                    "EntityEvent": {
                        "entity_id": "11111111-1111-1111-1111-111111111111",
                        "event_type": "created",
                        "source": "realtime_new"
                    }
                }
            ]
        }"#;

        let parsed: RealtimeProjection = serde_json::from_str(json).unwrap();
        assert_eq!(parsed.tenant_id, test_tenant_id());
        assert_eq!(parsed.tasks.len(), 1);
        match &parsed.tasks[0] {
            ProjectionTask::EntityEvent { entity_id, event_type, source } => {
                assert_eq!(*entity_id, test_entity_a());
                assert_eq!(event_type, "created");
                assert_eq!(source, "realtime_new");
            }
            _ => panic!("Expected EntityEvent"),
        }
    }

    #[test]
    fn test_clone_preserves_values() {
        let original = RealtimeProjection {
            tenant_id: test_tenant_id(),
            tasks: vec![ProjectionTask::SetParent {
                child_entity_id: test_entity_a(),
                parent_entity_id: test_entity_b(),
            }],
        };

        let cloned = original.clone();
        assert_eq!(cloned.tenant_id, original.tenant_id);
        assert_eq!(cloned.tasks.len(), original.tasks.len());
    }

    #[test]
    fn test_relate_edge_with_zero_weight() {
        let task = ProjectionTask::RelateEdge {
            entity_a_id: test_entity_a(),
            entity_b_id: test_entity_b(),
            weight: 0.0,
            relationship_type: "unknown".to_string(),
            attributes: serde_json::json!(null),
        };

        let json = serde_json::to_string(&task).unwrap();
        let parsed: ProjectionTask = serde_json::from_str(&json).unwrap();
        match parsed {
            ProjectionTask::RelateEdge { weight, attributes, .. } => {
                assert!((weight - 0.0).abs() < f64::EPSILON);
                assert!(attributes.is_null());
            }
            _ => panic!("Expected RelateEdge"),
        }
    }
}
