//! Query Executor - Executes query plans across heterogeneous stores
//!
//! Supports two execution modes:
//! - **Batch**: `execute()` runs the full plan and returns a `QueryResult`.
//! - **Streaming**: `execute_stream()` uses a bounded `sync_channel` for
//!   back-pressure controlled iteration, avoiding loading all records into memory.

use std::collections::HashMap;
use std::sync::{Arc, mpsc};

use super::error::{FederationError, Result};
use super::traits::Store;
use super::types::{Record, QueryResult, QueryStats};
use super::query_planner::{QueryPlan, QueryPlanNode, ResultTransform};

/// Query executor configuration
#[derive(Debug, Clone)]
pub struct ExecutorConfig {
    /// Maximum parallel queries
    pub max_parallel: usize,
    /// Query timeout in milliseconds
    pub timeout_ms: u64,
    /// Batch size for streaming
    pub batch_size: usize,
    /// Enable result caching
    pub enable_cache: bool,
}

impl Default for ExecutorConfig {
    fn default() -> Self {
        Self {
            max_parallel: 4,
            timeout_ms: 30000,
            batch_size: 1000,
            enable_cache: true,
        }
    }
}

/// Query executor for running query plans
pub struct QueryExecutor {
    config: ExecutorConfig,
    stores: HashMap<String, Arc<dyn Store>>,
}

impl QueryExecutor {
    /// Create a new query executor
    pub fn new(config: ExecutorConfig) -> Self {
        Self {
            config,
            stores: HashMap::new(),
        }
    }
    
    /// Register a store for execution
    pub fn register_store(&mut self, alias: &str, store: Arc<dyn Store>) {
        self.stores.insert(alias.to_string(), store);
    }
    
    /// Execute a query plan
    pub fn execute(&self, plan: &QueryPlan) -> Result<QueryResult> {
        let start = std::time::Instant::now();
        
        // Execute the plan tree
        let records = self.execute_node(&plan.root)?;
        
        // Apply result transformation if specified
        let records = if let Some(transform) = &plan.transform {
            self.apply_transform(records, transform)?
        } else {
            records
        };
        
        let records_len = records.len();
        let stats = QueryStats {
            execution_time_ms: start.elapsed().as_millis() as u64,
            records_scanned: records_len,
            records_returned: records_len,
            join_strategy: None,
            stores_involved: plan.stores_involved.clone(),
        };
        
        Ok(QueryResult {
            records,
            total_count: Some(records_len),
            has_more: false,
            stats,
        })
    }
    
    /// Execute with streaming results using backpressure-controlled channel.
    ///
    /// Records are produced in a background thread and sent through a bounded
    /// `sync_channel(batch_size)`. When the channel buffer is full the producer
    /// blocks, preventing unbounded memory growth (pull-based backpressure).
    ///
    /// The returned iterator yields `Result<Record>` items. If the producer
    /// encounters an error it sends it as the last item, then exits.
    pub fn execute_stream(&self, plan: &QueryPlan) -> Result<Box<dyn Iterator<Item = Result<Record>> + Send>> {
        let buffer_size = self.config.batch_size;

        // Clone what the background thread needs (plan root + store refs)
        let root = plan.root.clone();
        let stores = self.stores.clone();
        let transform = plan.transform.clone();

        let (tx, rx) = mpsc::sync_channel::<Result<Record>>(buffer_size);

        std::thread::spawn(move || {
            // Build a temporary executor for the background thread
            let executor = QueryExecutor {
                config: ExecutorConfig { batch_size: buffer_size, ..ExecutorConfig::default() },
                stores,
            };

            match executor.execute_node(&root) {
                Ok(mut records) => {
                    // Apply transform if present
                    if let Some(ref tf) = transform {
                        match executor.apply_transform(records, tf) {
                            Ok(transformed) => records = transformed,
                            Err(e) => {
                                let _ = tx.send(Err(e));
                                return;
                            }
                        }
                    }

                    for record in records {
                        // send() blocks when the channel is full → backpressure
                        if tx.send(Ok(record)).is_err() {
                            // Receiver dropped (consumer stopped reading) → stop
                            return;
                        }
                    }
                }
                Err(e) => {
                    let _ = tx.send(Err(e));
                }
            }
            // tx drops here → iterator returns None on next call
        });

        Ok(Box::new(StreamingIterator { rx }))
    }
    
    // ===== Private Execution Methods =====
    
    fn execute_node(&self, node: &QueryPlanNode) -> Result<Vec<Record>> {
        match node {
            QueryPlanNode::StoreScan { store_alias, query, .. } => {
                self.execute_store_scan(store_alias, query)
            }
            QueryPlanNode::GraphTraverse { store_alias, start_ids, max_depth, relation_types } => {
                self.execute_graph_traverse(store_alias, start_ids, *max_depth, relation_types)
            }
            QueryPlanNode::IndexLookup { index_name, keys } => {
                self.execute_index_lookup(index_name, keys)
            }
            QueryPlanNode::HashJoin { left, right, left_key, right_key, join_type, .. } => {
                self.execute_hash_join(left, right, left_key, right_key, join_type)
            }
            QueryPlanNode::NestedLoopJoin { outer, inner, left_key, right_key, join_type, .. } => {
                self.execute_nested_loop(outer, inner, left_key, right_key, join_type)
            }
            QueryPlanNode::Filter { input, predicate } => {
                self.execute_filter(input, predicate)
            }
            QueryPlanNode::Project { input, fields } => {
                self.execute_project(input, fields)
            }
            QueryPlanNode::Sort { input, order_by } => {
                self.execute_sort(input, order_by)
            }
            QueryPlanNode::LimitOffset { input, limit, offset } => {
                self.execute_limit_offset(input, *limit, *offset)
            }
            QueryPlanNode::VectorSearch { store_alias, query_vector, top_k, min_score, .. } => {
                self.execute_vector_search(store_alias, query_vector, *top_k, *min_score)
            }
            QueryPlanNode::VectorSimilarityJoin {
                left, vector_store_alias, query_vector, top_k, min_score,
                left_key, right_key, join_type,
            } => {
                self.execute_vector_similarity_join(
                    left, vector_store_alias, query_vector,
                    *top_k, *min_score, left_key, right_key, join_type,
                )
            }
            QueryPlanNode::Union { inputs } => {
                self.execute_union(inputs)
            }
        }
    }
    
    fn execute_vector_search(
        &self,
        store_alias: &str,
        query_vector: &[f32],
        top_k: usize,
        min_score: Option<f32>,
    ) -> Result<Vec<Record>> {
        let store = self.stores.get(store_alias)
            .ok_or_else(|| FederationError::StoreNotFound(store_alias.to_string()))?;
        
        // Build a UniversalQuery with vector_search spec
        let query = super::types::UniversalQuery {
            target: String::new(),
            filters: vec![],
            limit: None,
            offset: None,
            order_by: None,
            traverse: None,
            vector_search: Some(super::types::VectorSearchSpec {
                query_vector: query_vector.to_vec(),
                top_k,
                min_score,
                metadata_filters: vec![],
            }),
        };
        
        store.query(&query)
    }
    
    fn execute_store_scan(&self, store_alias: &str, query: &super::types::UniversalQuery) -> Result<Vec<Record>> {
        let store = self.stores.get(store_alias)
            .ok_or_else(|| FederationError::StoreNotFound(store_alias.to_string()))?;
        
        store.query(query)
    }
    
    fn execute_graph_traverse(
        &self,
        store_alias: &str,
        start_ids: &[String],
        _max_depth: u32,
        _relation_types: &[String],
    ) -> Result<Vec<Record>> {
        let store = self.stores.get(store_alias)
            .ok_or_else(|| FederationError::StoreNotFound(store_alias.to_string()))?;
        
        // Collect all traversed entities
        let mut results = Vec::new();
        
        for id in start_ids {
            if let Some(record) = store.get(id)? {
                results.push(record);
            }
        }
        
        // TODO: Implement actual graph traversal with BFS/DFS
        // This requires access to the graph store's relation API
        
        Ok(results)
    }
    
    fn execute_index_lookup(&self, _index_name: &str, _keys: &[String]) -> Result<Vec<Record>> {
        // TODO: Implement index lookup
        // This would query the cross-db index for entity locations
        Ok(Vec::new())
    }
    
    fn execute_hash_join(
        &self,
        left: &QueryPlanNode,
        right: &QueryPlanNode,
        left_key: &str,
        right_key: &str,
        join_type: &super::types::JoinType,
    ) -> Result<Vec<Record>> {
        let left_records = self.execute_node(left)?;
        let right_records = self.execute_node(right)?;
        
        // Build hash table from right side keyed by right_key
        let mut hash_table: HashMap<String, Vec<usize>> = HashMap::new();
        for (idx, record) in right_records.iter().enumerate() {
            if let Some(key) = self.get_join_key(record, right_key) {
                hash_table.entry(key).or_default().push(idx);
            }
        }
        
        let mut results = Vec::new();
        let mut right_matched = vec![false; right_records.len()];
        
        for left_record in &left_records {
            if let Some(key) = self.get_join_key(left_record, left_key) {
                if let Some(indices) = hash_table.get(&key) {
                    for &idx in indices {
                        right_matched[idx] = true;
                        let merged = self.merge_records(left_record, &right_records[idx]);
                        results.push(merged);
                    }
                } else if matches!(join_type, super::types::JoinType::Left | super::types::JoinType::Full) {
                    // LEFT / FULL: include left row with null right side
                    results.push(left_record.clone());
                }
            } else if matches!(join_type, super::types::JoinType::Left | super::types::JoinType::Full) {
                results.push(left_record.clone());
            }
        }
        
        // RIGHT / FULL: include unmatched right rows
        if matches!(join_type, super::types::JoinType::Right | super::types::JoinType::Full) {
            for (idx, matched) in right_matched.iter().enumerate() {
                if !matched {
                    results.push(right_records[idx].clone());
                }
            }
        }
        
        Ok(results)
    }
    
    fn execute_nested_loop(
        &self,
        outer: &QueryPlanNode,
        inner: &QueryPlanNode,
        left_key: &str,
        right_key: &str,
        join_type: &super::types::JoinType,
    ) -> Result<Vec<Record>> {
        let outer_records = self.execute_node(outer)?;
        let inner_records = self.execute_node(inner)?;
        
        let mut results = Vec::new();
        let mut inner_matched = vec![false; inner_records.len()];
        
        for outer_record in &outer_records {
            let left_val = self.get_join_key(outer_record, left_key);
            let mut had_match = false;
            
            for (idx, inner_record) in inner_records.iter().enumerate() {
                let right_val = self.get_join_key(inner_record, right_key);
                
                if left_val.is_some() && left_val == right_val {
                    had_match = true;
                    inner_matched[idx] = true;
                    let merged = self.merge_records(outer_record, inner_record);
                    results.push(merged);
                }
            }
            
            if !had_match && matches!(join_type, super::types::JoinType::Left | super::types::JoinType::Full) {
                results.push(outer_record.clone());
            }
        }
        
        if matches!(join_type, super::types::JoinType::Right | super::types::JoinType::Full) {
            for (idx, matched) in inner_matched.iter().enumerate() {
                if !matched {
                    results.push(inner_records[idx].clone());
                }
            }
        }
        
        Ok(results)
    }
    
    fn execute_filter(&self, input: &QueryPlanNode, predicate: &str) -> Result<Vec<Record>> {
        let records = self.execute_node(input)?;
        
        if predicate.is_empty() {
            return Ok(records);
        }
        
        // Parse simple predicate expressions like "field = value", "field > value", etc.
        // Supports AND-separated conditions
        let conditions: Vec<&str> = predicate.split(" AND ").collect();
        
        Ok(records.into_iter().filter(|record| {
            conditions.iter().all(|cond| self.evaluate_condition(record, cond.trim()))
        }).collect())
    }
    
    fn execute_project(&self, input: &QueryPlanNode, fields: &[String]) -> Result<Vec<Record>> {
        let records = self.execute_node(input)?;
        
        if fields.is_empty() {
            return Ok(records);
        }
        
        // For merged document records, project specific fields from the content
        Ok(records.into_iter().map(|record| {
            match record {
                Record::Document { id, doc_type, content, tags, created_at, updated_at, .. } if doc_type == "merged" => {
                    // Project fields from merged content
                    let mut projected = serde_json::Map::new();
                    if let serde_json::Value::Object(ref obj) = content {
                        for field in fields {
                            // Strip prefix for lookup: "person.name" → look in "left"/"right"
                            let (prefix, bare_field) = if let Some(pos) = field.find('.') {
                                (Some(&field[..pos]), &field[pos + 1..])
                            } else {
                                (None, field.as_str())
                            };
                            
                            // Search in left and right sub-objects
                            for side in &["left", "right"] {
                                if let Some(side_val) = obj.get(*side) {
                                    if let Some(val) = self.extract_field(side_val, bare_field, prefix) {
                                        projected.insert(field.clone(), val);
                                        break;
                                    }
                                }
                            }
                            
                            // Also check top-level
                            if !projected.contains_key(field) {
                                if let Some(val) = obj.get(field) {
                                    projected.insert(field.clone(), val.clone());
                                }
                            }
                        }
                    }
                    Record::Document {
                        id,
                        doc_type: "projected".to_string(),
                        content: serde_json::Value::Object(projected),
                        tags,
                        created_at,
                        updated_at,
                        ownership: None,
                    }
                }
                other => other, // Non-merged records pass through unchanged
            }
        }).collect())
    }
    
    fn execute_sort(&self, input: &QueryPlanNode, order_by: &[(String, bool)]) -> Result<Vec<Record>> {
        let mut records = self.execute_node(input)?;
        
        if let Some((field, ascending)) = order_by.first() {
            records.sort_by(|a, b| {
                let cmp = self.compare_by_field(a, b, field);
                if *ascending { cmp } else { cmp.reverse() }
            });
        }
        
        Ok(records)
    }
    
    fn execute_limit_offset(&self, input: &QueryPlanNode, limit: Option<usize>, offset: Option<usize>) -> Result<Vec<Record>> {
        let records = self.execute_node(input)?;
        
        let offset = offset.unwrap_or(0);
        let limit = limit.unwrap_or(usize::MAX);
        
        Ok(records.into_iter().skip(offset).take(limit).collect())
    }
    
    /// Execute a vector similarity join.
    ///
    /// 1. Perform a vector similarity search to get top-K vector results.
    /// 2. Execute the left side (e.g., graph scan).
    /// 3. Hash-join the two result sets on the specified keys.
    ///
    /// This is the key operation for Graph-Vector cross-store queries,
    /// allowing patterns like:
    /// - "Find people whose embeddings are similar to this query"
    /// - "Enrich vector search results with graph entity details"
    fn execute_vector_similarity_join(
        &self,
        left: &QueryPlanNode,
        vector_store_alias: &str,
        query_vector: &[f32],
        top_k: usize,
        min_score: Option<f32>,
        left_key: &str,
        right_key: &str,
        join_type: &super::types::JoinType,
    ) -> Result<Vec<Record>> {
        // Phase 1: Execute vector similarity search
        let vector_records = self.execute_vector_search(
            vector_store_alias, query_vector, top_k, min_score,
        )?;

        // Phase 2: Execute left side (e.g., graph entities)
        let left_records = self.execute_node(left)?;

        // Phase 3: Hash-join on keys
        // Build hash table from vector results keyed by right_key
        let mut hash_table: HashMap<String, Vec<usize>> = HashMap::new();
        for (idx, record) in vector_records.iter().enumerate() {
            if let Some(key) = self.get_join_key(record, right_key) {
                hash_table.entry(key).or_default().push(idx);
            }
        }

        let mut results = Vec::new();
        let mut right_matched = vec![false; vector_records.len()];

        for left_record in &left_records {
            if let Some(key) = self.get_join_key(left_record, left_key) {
                if let Some(indices) = hash_table.get(&key) {
                    for &idx in indices {
                        right_matched[idx] = true;
                        let merged = self.merge_records_with_score(left_record, &vector_records[idx]);
                        results.push(merged);
                    }
                } else if matches!(join_type, super::types::JoinType::Left | super::types::JoinType::Full) {
                    results.push(left_record.clone());
                }
            } else if matches!(join_type, super::types::JoinType::Left | super::types::JoinType::Full) {
                results.push(left_record.clone());
            }
        }

        // RIGHT / FULL: include unmatched vector results
        if matches!(join_type, super::types::JoinType::Right | super::types::JoinType::Full) {
            for (idx, matched) in right_matched.iter().enumerate() {
                if !matched {
                    results.push(vector_records[idx].clone());
                }
            }
        }

        Ok(results)
    }

    /// Merge a left record with a vector record, preserving similarity score.
    fn merge_records_with_score(&self, left: &Record, vector: &Record) -> Record {
        let mut content = serde_json::Map::new();

        content.insert("left".to_string(), serde_json::to_value(left).unwrap_or_default());
        content.insert("right".to_string(), serde_json::to_value(vector).unwrap_or_default());

        // Extract and surface the similarity score at the top level
        if let Record::Vector { score, .. } = vector {
            if let Some(s) = score {
                content.insert("similarity_score".to_string(), serde_json::json!(s));
            }
        }

        Record::Document {
            id: format!("{}-{}", self.get_record_id(left), self.get_record_id(vector)),
            doc_type: "merged".to_string(),
            content: serde_json::Value::Object(content),
            tags: vec![],
            created_at: 0,
            updated_at: 0,
            ownership: None,
        }
    }

    fn execute_union(&self, inputs: &[QueryPlanNode]) -> Result<Vec<Record>> {
        let mut results = Vec::new();
        
        for input in inputs {
            let records = self.execute_node(input)?;
            results.extend(records);
        }
        
        Ok(results)
    }
    
    // ===== Helper Methods =====
    
    fn get_join_key(&self, record: &Record, key_field: &str) -> Option<String> {
        match record {
            Record::GraphEntity { id, name, properties, .. } => {
                if key_field == "id" {
                    Some(id.clone())
                } else if key_field == "name" {
                    Some(name.clone())
                } else {
                    properties.get(key_field).map(|v: &serde_json::Value| v.to_string())
                }
            }
            Record::GraphRelation { source, target, .. } => {
                if key_field == "source" || key_field == "source_id" {
                    Some(source.clone())
                } else if key_field == "target" || key_field == "target_id" {
                    Some(target.clone())
                } else {
                    None
                }
            }
            Record::Document { id, doc_type, content, tags, .. } => {
                if key_field == "id" {
                    Some(id.clone())
                } else if key_field == "type" || key_field == "doc_type" {
                    Some(doc_type.clone())
                } else if key_field == "tags" {
                    Some(tags.join(","))
                } else if let serde_json::Value::Object(ref obj) = content {
                    // Look up field in document content
                    obj.get(key_field).map(|v: &serde_json::Value| {
                        match v {
                            serde_json::Value::String(s) => s.clone(),
                            other => other.to_string(),
                        }
                    })
                } else {
                    None
                }
            }
            Record::Vector { id, metadata, score, .. } => {
                if key_field == "id" {
                    Some(id.clone())
                } else if key_field == "score" {
                    score.map(|s| s.to_string())
                } else {
                    // Look up in vector metadata for arbitrary join keys
                    metadata.get(key_field).map(|v| {
                        match v {
                            serde_json::Value::String(s) => s.clone(),
                            other => other.to_string(),
                        }
                    })
                }
            }
        }
    }
    
    fn merge_records(&self, left: &Record, right: &Record) -> Record {
        // For simplicity, return a document record with merged data
        // A proper implementation would have a more sophisticated merge strategy
        let mut content = serde_json::Map::new();
        
        content.insert("left".to_string(), serde_json::to_value(left).unwrap_or_default());
        content.insert("right".to_string(), serde_json::to_value(right).unwrap_or_default());
        
        Record::Document {
            id: format!("{}-{}", self.get_record_id(left), self.get_record_id(right)),
            doc_type: "merged".to_string(),
            content: serde_json::Value::Object(content),
            tags: vec![],
            created_at: 0,
            updated_at: 0,
            ownership: None,
        }
    }
    
    fn get_record_id(&self, record: &Record) -> String {
        match record {
            Record::GraphEntity { id, .. } => id.clone(),
            Record::GraphRelation { id, .. } => id.to_string(),
            Record::Document { id, .. } => id.clone(),
            Record::Vector { id, .. } => id.clone(),
        }
    }
    
    fn compare_by_field(&self, a: &Record, b: &Record, field: &str) -> std::cmp::Ordering {
        let a_val = self.get_join_key(a, field);
        let b_val = self.get_join_key(b, field);
        a_val.cmp(&b_val)
    }
    
    fn apply_transform(&self, records: Vec<Record>, transform: &ResultTransform) -> Result<Vec<Record>> {
        if transform.fields.is_empty() && transform.renames.is_empty() && transform.computed.is_empty() {
            return Ok(records);
        }
        
        Ok(records.into_iter().map(|record| {
            // For now, apply field selection by delegating to project logic
            match record {
                Record::Document { id, content, tags, created_at, updated_at, .. } => {
                    let mut projected = serde_json::Map::new();
                    if let serde_json::Value::Object(ref obj) = content {
                        for field in &transform.fields {
                            if let Some(val) = obj.get(field) {
                                // Apply rename if applicable
                                let out_name = transform.renames.get(field).unwrap_or(field);
                                projected.insert(out_name.clone(), val.clone());
                            }
                        }
                    }
                    Record::Document {
                        id,
                        doc_type: "transformed".to_string(),
                        content: serde_json::Value::Object(projected),
                        tags,
                        created_at,
                        updated_at,
                        ownership: None,
                    }
                }
                other => other,
            }
        }).collect())
    }
    
    /// Evaluate a simple condition string against a record.
    ///
    /// Supports: `field = value`, `field != value`, `field > value`,
    /// `field >= value`, `field < value`, `field <= value`,
    /// `field CONTAINS 'text'`
    fn evaluate_condition(&self, record: &Record, condition: &str) -> bool {
        // Try different operators (order matters: >= before >, != before =)
        let ops: &[(&str, fn(&str, &str) -> bool)] = &[
            (" >= ", |a, b| a >= b),
            (" <= ", |a, b| a <= b),
            (" != ", |a, b| a != b),
            (" > ",  |a, b| a > b),
            (" < ",  |a, b| a < b),
            (" = ",  |a, b| a == b),
        ];
        
        for &(op_str, cmp_fn) in ops {
            if let Some(pos) = condition.find(op_str) {
                let field = condition[..pos].trim();
                let value = condition[pos + op_str.len()..].trim().trim_matches('"');
                
                if let Some(record_val) = self.get_join_key(record, field) {
                    return cmp_fn(&record_val, value);
                }
                return false;
            }
        }
        
        // CONTAINS operator
        if let Some(pos) = condition.find(" CONTAINS ") {
            let field = condition[..pos].trim();
            let value = condition[pos + 10..].trim().trim_matches('\'').trim_matches('"');
            
            if let Some(record_val) = self.get_join_key(record, field) {
                return record_val.to_lowercase().contains(&value.to_lowercase());
            }
            return false;
        }
        
        // Unknown condition → pass through
        true
    }
    
    /// Extract a field value from a JSON sub-object.
    ///
    /// For merged records, `side_val` is the serialized left/right Record.
    /// If `prefix` is given, only match if the record's type matches.
    fn extract_field(
        &self,
        side_val: &serde_json::Value,
        bare_field: &str,
        _prefix: Option<&str>,
    ) -> Option<serde_json::Value> {
        if let serde_json::Value::Object(obj) = side_val {
            // Direct field access (works for serialized Record variants)
            if let Some(val) = obj.get(bare_field) {
                return Some(val.clone());
            }
            // Check in nested "properties" or "content" (from Record::GraphEntity / Document)
            if let Some(serde_json::Value::Object(props)) = obj.get("properties") {
                if let Some(val) = props.get(bare_field) {
                    return Some(val.clone());
                }
            }
            if let Some(content) = obj.get("content") {
                if let serde_json::Value::Object(cobj) = content {
                    if let Some(val) = cobj.get(bare_field) {
                        return Some(val.clone());
                    }
                }
            }
        }
        None
    }
}

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

/// Pull-based streaming iterator backed by a bounded `sync_channel`.
///
/// The producer thread pushes `Result<Record>` items into the channel.
/// When the channel buffer is full, the producer blocks (backpressure).
/// When the consumer drops the iterator, the producer detects a send error
/// and stops.
struct StreamingIterator {
    rx: mpsc::Receiver<Result<Record>>,
}

impl Iterator for StreamingIterator {
    type Item = Result<Record>;

    fn next(&mut self) -> Option<Self::Item> {
        self.rx.recv().ok()
    }
}

// Safety: mpsc::Receiver is Send when T: Send, and Result<Record> is Send.
unsafe impl Send for StreamingIterator {}

#[cfg(test)]
mod tests {
    use super::*;
    use super::super::types::{StoreType, UniversalQuery};
    
    #[test]
    fn test_executor_creation() {
        let executor = QueryExecutor::default();
        assert_eq!(executor.config.max_parallel, 4);
    }

    #[test]
    fn test_streaming_basic() {
        // Create an executor with a small buffer to test backpressure
        let executor = QueryExecutor::new(ExecutorConfig {
            batch_size: 2, // small buffer → backpressure kicks in quickly
            ..ExecutorConfig::default()
        });

        // Build a plan that returns records directly (Union of nothing = empty)
        let plan = QueryPlan {
            root: QueryPlanNode::Union { inputs: vec![] },
            transform: None,
            estimated_cost: 0.0,
            estimated_rows: 0,
            stores_involved: vec![],
        };

        let stream = executor.execute_stream(&plan).unwrap();
        let collected: Vec<_> = stream.collect();
        assert!(collected.is_empty());
    }

    #[test]
    fn test_streaming_with_records() {
        // Build an executor with a real store scan that produces records
        let executor = QueryExecutor::new(ExecutorConfig {
            batch_size: 3,
            ..ExecutorConfig::default()
        });

        // Use a StoreScan that will fail (no store registered) to verify
        // error propagation through the stream
        let plan = QueryPlan {
            root: QueryPlanNode::StoreScan {
                store_alias: "nonexistent".to_string(),
                store_type: StoreType::Graph,
                query: UniversalQuery::default(),
                estimated_rows: 10,
            },
            transform: None,
            estimated_cost: 1.0,
            estimated_rows: 10,
            stores_involved: vec!["nonexistent".to_string()],
        };

        let stream = executor.execute_stream(&plan).unwrap();
        let results: Vec<_> = stream.collect();
        // Should get exactly one error item
        assert_eq!(results.len(), 1);
        assert!(results[0].is_err());
    }

    #[test]
    fn test_streaming_backpressure_consumer_drop() {
        // Verify that dropping the iterator (consumer) stops the producer
        let executor = QueryExecutor::new(ExecutorConfig {
            batch_size: 1, // very small buffer
            ..ExecutorConfig::default()
        });

        let plan = QueryPlan {
            root: QueryPlanNode::Union { inputs: vec![] },
            transform: None,
            estimated_cost: 0.0,
            estimated_rows: 0,
            stores_involved: vec![],
        };

        // Create and immediately drop the stream
        let stream = executor.execute_stream(&plan).unwrap();
        drop(stream);
        // If this doesn't hang, the producer thread correctly detected the drop
    }
}
