//! Test procedure support for TCK tests.
//!
//! This module provides a global registry for test procedures used in openCypher TCK tests.
//! Test procedures return static data and don't require graph access.

use std::collections::HashMap;
use std::sync::{OnceLock, RwLock};

use crate::error::ExecutionResult;
use crate::result::{CypherValue, Record};

/// Information about a test procedure parameter.
#[derive(Debug, Clone)]
pub struct TestParameterInfo {
    pub name: String,
    pub typ: String,
    pub optional: bool,
}

/// Information about a test procedure yield column.
#[derive(Debug, Clone)]
pub struct TestYieldInfo {
    pub name: String,
    pub typ: String,
}

/// A test procedure that returns static data.
#[derive(Debug, Clone)]
pub struct TestProcedure {
    pub name: String,
    pub namespace: Vec<String>,
    pub parameters: Vec<TestParameterInfo>,
    pub yields: Vec<TestYieldInfo>,
    pub data: Vec<Record>,
}

impl TestProcedure {
    /// Get the fully qualified procedure name.
    pub fn full_name(&self) -> String {
        if self.namespace.is_empty() {
            self.name.clone()
        } else {
            format!("{}.{}", self.namespace.join("."), self.name)
        }
    }

    /// Call the procedure with the given arguments.
    /// For test procedures, arguments are used to filter the static data.
    /// Returns only YIELD columns (procedure outputs), not parameter columns.
    pub fn call(&self, args: Vec<CypherValue>) -> ExecutionResult<Vec<Record>> {
        // Filter rows by arguments
        let filtered_rows = if self.parameters.is_empty() || args.is_empty() {
            // No filtering needed
            self.data.clone()
        } else {
            // Filter data based on input parameters
            let param_names: Vec<&str> = self.parameters.iter()
                .map(|p| p.name.as_str())
                .collect();

            let filtered: Vec<Record> = self.data.iter()
                .filter(|row| {
                    // Check if this row matches the input arguments
                    for (i, arg) in args.iter().enumerate() {
                        if i >= param_names.len() {
                            break;
                        }
                        let param_name = param_names[i];

                        if let Some(row_value) = row.get(param_name) {
                            // Use value equality
                            if !values_equal(row_value, arg) {
                                return false;
                            }
                        } else {
                            // Row is missing this parameter column - exclude it
                            return false;
                        }
                    }
                    true
                })
                .cloned()
                .collect();

            filtered
        };

        // Project to only yield columns
        let yield_names: Vec<&str> = self.yields.iter()
            .map(|y| y.name.as_str())
            .collect();

        let projected: Vec<Record> = filtered_rows.iter()
            .map(|row| {
                let mut new_record = Record::new();
                for yield_name in &yield_names {
                    if let Some(value) = row.get(yield_name) {
                        new_record.add(yield_name.to_string(), value.clone());
                    }
                }
                new_record
            })
            .collect();

        Ok(projected)
    }
}

/// Check if two CypherValues are equal for filtering purposes.
fn values_equal(a: &CypherValue, b: &CypherValue) -> bool {
    match (a, b) {
        (CypherValue::Null, CypherValue::Null) => true,
        (CypherValue::Boolean(a), CypherValue::Boolean(b)) => a == b,
        (CypherValue::Integer(a), CypherValue::Integer(b)) => a == b,
        (CypherValue::Float(a), CypherValue::Float(b)) => (a - b).abs() < f64::EPSILON,
        (CypherValue::Integer(a), CypherValue::Float(b)) => (*a as f64 - b).abs() < f64::EPSILON,
        (CypherValue::Float(a), CypherValue::Integer(b)) => (a - *b as f64).abs() < f64::EPSILON,
        (CypherValue::String(a), CypherValue::String(b)) => a == b,
        (CypherValue::List(a), CypherValue::List(b)) => {
            a.len() == b.len() && a.iter().zip(b.iter()).all(|(x, y)| values_equal(x, y))
        }
        _ => false,
    }
}

/// Global registry for test procedures.
#[derive(Debug, Default)]
pub struct TestProcedureRegistry {
    procedures: RwLock<HashMap<String, TestProcedure>>,
}

impl TestProcedureRegistry {
    /// Create a new empty registry.
    pub fn new() -> Self {
        Self {
            procedures: RwLock::new(HashMap::new()),
        }
    }

    /// Register a test procedure.
    pub fn register(&self, procedure: TestProcedure) {
        let name = procedure.full_name();
        let mut procs = self.procedures.write().unwrap();
        procs.insert(name, procedure);
    }

    /// Look up a procedure by its fully qualified name.
    pub fn get(&self, name: &str) -> Option<TestProcedure> {
        let procs = self.procedures.read().unwrap();
        procs.get(name).cloned()
    }

    /// Check if a procedure exists.
    pub fn exists(&self, name: &str) -> bool {
        let procs = self.procedures.read().unwrap();
        procs.contains_key(name)
    }

    /// Clear all procedures (useful for resetting between tests).
    pub fn clear(&self) {
        let mut procs = self.procedures.write().unwrap();
        procs.clear();
    }

    /// List all registered procedures.
    pub fn list(&self) -> Vec<String> {
        let procs = self.procedures.read().unwrap();
        procs.keys().cloned().collect()
    }

    /// Get the number of registered procedures.
    pub fn len(&self) -> usize {
        let procs = self.procedures.read().unwrap();
        procs.len()
    }

    /// Check if the registry is empty.
    pub fn is_empty(&self) -> bool {
        self.len() == 0
    }
}

// Global static registry
static GLOBAL_TEST_REGISTRY: OnceLock<TestProcedureRegistry> = OnceLock::new();

/// Get the global test procedure registry.
pub fn global_test_registry() -> &'static TestProcedureRegistry {
    GLOBAL_TEST_REGISTRY.get_or_init(TestProcedureRegistry::new)
}

/// Register a test procedure in the global registry.
pub fn register_test_procedure(procedure: TestProcedure) {
    global_test_registry().register(procedure);
}

/// Look up a test procedure in the global registry.
pub fn get_test_procedure(name: &str) -> Option<TestProcedure> {
    global_test_registry().get(name)
}

/// Clear all test procedures (useful for resetting between tests).
pub fn clear_test_procedures() {
    global_test_registry().clear();
}
