//! Database and Service Fixtures for Integration Testing
//! 
//! Manages test database setup, cleanup, and service lifecycle
//! for reproducible integration testing.

use anyhow::Result;
use std::process::Command;
use std::time::Duration;
use tokio::time::timeout;

/// Test fixtures manager for database and service setup
pub struct TestFixtures {
    postgres_container: Option<String>,
    influxdb_container: Option<String>,
    redis_container: Option<String>,
}

impl TestFixtures {
    /// Create new test fixtures manager
    pub fn new() -> Result<Self> {
        Ok(Self {
            postgres_container: None,
            influxdb_container: None,
            redis_container: None,
        })
    }

    /// Setup all test databases and services
    pub async fn setup(&mut self) -> Result<()> {
        println!("Setting up test fixtures...");

        // Start PostgreSQL container
        self.setup_postgres().await?;
        
        // Start InfluxDB container
        self.setup_influxdb().await?;
        
        // Start Redis container
        self.setup_redis().await?;
        
        // Wait for all services to be ready
        self.wait_for_services().await?;
        
        // Initialize database schemas
        self.initialize_schemas().await?;
        
        println!("Test fixtures setup complete");
        Ok(())
    }

    /// Cleanup all test resources
    pub async fn cleanup(&mut self) -> Result<()> {
        println!("Cleaning up test fixtures...");

        // Stop and remove containers
        if let Some(container_id) = &self.postgres_container {
            self.stop_container(container_id).await?;
        }
        
        if let Some(container_id) = &self.influxdb_container {
            self.stop_container(container_id).await?;
        }
        
        if let Some(container_id) = &self.redis_container {
            self.stop_container(container_id).await?;
        }

        // Clean up test data directories
        tokio::fs::remove_dir_all("/tmp/test_data").await.ok();
        tokio::fs::remove_dir_all("/tmp/test_models").await.ok();

        println!("Test fixtures cleanup complete");
        Ok(())
    }

    /// Setup PostgreSQL test database
    async fn setup_postgres(&mut self) -> Result<()> {
        println!("Starting PostgreSQL container...");
        
        let output = Command::new("docker")
            .args([
                "run", "-d",
                "--name", "foxhunt-test-postgres",
                "-e", "POSTGRES_DB=foxhunt_test",
                "-e", "POSTGRES_USER=test",
                "-e", "POSTGRES_PASSWORD=test",
                "-p", "5432:5432",
                "--rm",
                "postgres:15-alpine"
            ])
            .output()?;

        if !output.status.success() {
            return Err(anyhow::anyhow!(
                "Failed to start PostgreSQL container: {}",
                String::from_utf8_lossy(&output.stderr)
            ));
        }

        let container_id = String::from_utf8_lossy(&output.stdout).trim().to_string();
        self.postgres_container = Some(container_id);
        
        println!("PostgreSQL container started");
        Ok(())
    }

    /// Setup InfluxDB test database
    async fn setup_influxdb(&mut self) -> Result<()> {
        println!("Starting InfluxDB container...");
        
        let output = Command::new("docker")
            .args([
                "run", "-d",
                "--name", "foxhunt-test-influxdb",
                "-e", "DOCKER_INFLUXDB_INIT_MODE=setup",
                "-e", "DOCKER_INFLUXDB_INIT_USERNAME=test",
                "-e", "DOCKER_INFLUXDB_INIT_PASSWORD=test123456",
                "-e", "DOCKER_INFLUXDB_INIT_ORG=foxhunt",
                "-e", "DOCKER_INFLUXDB_INIT_BUCKET=test",
                "-p", "8086:8086",
                "--rm",
                "influxdb:2.7-alpine"
            ])
            .output()?;

        if !output.status.success() {
            return Err(anyhow::anyhow!(
                "Failed to start InfluxDB container: {}",
                String::from_utf8_lossy(&output.stderr)
            ));
        }

        let container_id = String::from_utf8_lossy(&output.stdout).trim().to_string();
        self.influxdb_container = Some(container_id);
        
        println!("InfluxDB container started");
        Ok(())
    }

    /// Setup Redis test cache
    async fn setup_redis(&mut self) -> Result<()> {
        println!("Starting Redis container...");
        
        let output = Command::new("docker")
            .args([
                "run", "-d",
                "--name", "foxhunt-test-redis",
                "-p", "6379:6379",
                "--rm",
                "redis:7-alpine"
            ])
            .output()?;

        if !output.status.success() {
            return Err(anyhow::anyhow!(
                "Failed to start Redis container: {}",
                String::from_utf8_lossy(&output.stderr)
            ));
        }

        let container_id = String::from_utf8_lossy(&output.stdout).trim().to_string();
        self.redis_container = Some(container_id);
        
        println!("Redis container started");
        Ok(())
    }

    /// Wait for all services to be ready
    async fn wait_for_services(&self) -> Result<()> {
        println!("Waiting for services to be ready...");
        
        let timeout_duration = Duration::from_secs(60);
        
        // Wait for PostgreSQL
        timeout(timeout_duration, async {
            loop {
                if self.check_postgres_ready().await {
                    break;
                }
                tokio::time::sleep(Duration::from_millis(1000)).await;
            }
        }).await?;
        
        // Wait for InfluxDB
        timeout(timeout_duration, async {
            loop {
                if self.check_influxdb_ready().await {
                    break;
                }
                tokio::time::sleep(Duration::from_millis(1000)).await;
            }
        }).await?;
        
        // Wait for Redis
        timeout(timeout_duration, async {
            loop {
                if self.check_redis_ready().await {
                    break;
                }
                tokio::time::sleep(Duration::from_millis(1000)).await;
            }
        }).await?;
        
        println!("All services are ready");
        Ok(())
    }

    /// Check if PostgreSQL is ready
    async fn check_postgres_ready(&self) -> bool {
        let output = Command::new("docker")
            .args([
                "exec", "foxhunt-test-postgres",
                "pg_isready", "-U", "test", "-d", "foxhunt_test"
            ])
            .output();

        matches!(output, Ok(output) if output.status.success())
    }

    /// Check if InfluxDB is ready
    async fn check_influxdb_ready(&self) -> bool {
        let output = Command::new("curl")
            .args(["-f", "http://localhost:8086/health"])
            .output();

        matches!(output, Ok(output) if output.status.success())
    }

    /// Check if Redis is ready
    async fn check_redis_ready(&self) -> bool {
        let output = Command::new("docker")
            .args([
                "exec", "foxhunt-test-redis",
                "redis-cli", "ping"
            ])
            .output();

        matches!(output, Ok(output) if output.status.success() && 
                 String::from_utf8_lossy(&output.stdout).trim() == "PONG")
    }

    /// Initialize database schemas
    async fn initialize_schemas(&self) -> Result<()> {
        println!("Initializing database schemas...");
        
        // Create PostgreSQL tables
        self.create_postgres_tables().await?;
        
        // Create InfluxDB buckets and measurements
        self.create_influxdb_schema().await?;
        
        println!("Database schemas initialized");
        Ok(())
    }

    /// Create PostgreSQL test tables
    async fn create_postgres_tables(&self) -> Result<()> {
        let sql_commands = vec![
            // Trading tables
            r#"
            CREATE TABLE IF NOT EXISTS trades (
                id BIGSERIAL PRIMARY KEY,
                symbol VARCHAR(10) NOT NULL,
                price DECIMAL(18,8) NOT NULL,
                quantity DECIMAL(18,8) NOT NULL,
                side VARCHAR(4) NOT NULL CHECK (side IN ('BUY', 'SELL')),
                timestamp TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
                order_id UUID,
                model_id VARCHAR(100),
                created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
            );
            "#,
            
            // ML model registry
            r#"
            CREATE TABLE IF NOT EXISTS ml_models (
                id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
                model_name VARCHAR(100) NOT NULL,
                model_type VARCHAR(50) NOT NULL,
                version VARCHAR(20) NOT NULL,
                symbol VARCHAR(10) NOT NULL,
                status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
                performance_metrics JSONB,
                hyperparameters JSONB,
                model_path TEXT,
                created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
                updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
                UNIQUE(model_name, version, symbol)
            );
            "#,
            
            // Training jobs
            r#"
            CREATE TABLE IF NOT EXISTS training_jobs (
                id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
                job_id VARCHAR(100) UNIQUE NOT NULL,
                model_name VARCHAR(100) NOT NULL,
                status VARCHAR(20) NOT NULL DEFAULT 'QUEUED',
                progress_percentage DECIMAL(5,2) DEFAULT 0.0,
                current_epoch INTEGER DEFAULT 0,
                total_epochs INTEGER DEFAULT 0,
                hyperparameters JSONB,
                resource_requirements JSONB,
                error_message TEXT,
                started_at TIMESTAMP WITH TIME ZONE,
                completed_at TIMESTAMP WITH TIME ZONE,
                created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
                updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
            );
            "#,
            
            // Market data
            r#"
            CREATE TABLE IF NOT EXISTS market_data (
                id BIGSERIAL PRIMARY KEY,
                symbol VARCHAR(10) NOT NULL,
                timestamp TIMESTAMP WITH TIME ZONE NOT NULL,
                price DECIMAL(18,8) NOT NULL,
                volume BIGINT NOT NULL,
                bid DECIMAL(18,8),
                ask DECIMAL(18,8),
                created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
            );
            "#,
            
            // Create indexes
            "CREATE INDEX IF NOT EXISTS idx_trades_symbol_timestamp ON trades(symbol, timestamp DESC);",
            "CREATE INDEX IF NOT EXISTS idx_trades_model_id ON trades(model_id);",
            "CREATE INDEX IF NOT EXISTS idx_ml_models_name_version ON ml_models(model_name, version);",
            "CREATE INDEX IF NOT EXISTS idx_training_jobs_status ON training_jobs(status);",
            "CREATE INDEX IF NOT EXISTS idx_market_data_symbol_timestamp ON market_data(symbol, timestamp DESC);",
        ];

        for sql in sql_commands {
            let output = Command::new("docker")
                .args([
                    "exec", "foxhunt-test-postgres",
                    "psql", "-U", "test", "-d", "foxhunt_test",
                    "-c", sql
                ])
                .output()?;

            if !output.status.success() {
                eprintln!("Failed to execute SQL: {}", sql);
                eprintln!("Error: {}", String::from_utf8_lossy(&output.stderr));
                return Err(anyhow::anyhow!("Failed to create PostgreSQL tables"));
            }
        }

        Ok(())
    }

    /// Create InfluxDB schema
    async fn create_influxdb_schema(&self) -> Result<()> {
        // InfluxDB 2.x uses buckets instead of databases
        // The bucket was already created during container initialization
        // Here we can create retention policies or other schema elements if needed
        
        Ok(())
    }

    /// Stop and remove a Docker container
    async fn stop_container(&self, container_id: &str) -> Result<()> {
        let output = Command::new("docker")
            .args(["stop", container_id])
            .output()?;

        if !output.status.success() {
            eprintln!("Warning: Failed to stop container {}: {}", 
                      container_id, String::from_utf8_lossy(&output.stderr));
        }

        Ok(())
    }

    /// Insert test data into PostgreSQL
    pub async fn insert_test_trades(&self, trades: &[TestTrade]) -> Result<()> {
        for trade in trades {
            let sql = format!(
                "INSERT INTO trades (symbol, price, quantity, side, timestamp, model_id) VALUES ('{}', {}, {}, '{}', '{}', '{}')",
                trade.symbol, trade.price, trade.quantity, trade.side, trade.timestamp, trade.model_id.as_deref().unwrap_or("NULL")
            );

            let output = Command::new("docker")
                .args([
                    "exec", "foxhunt-test-postgres",
                    "psql", "-U", "test", "-d", "foxhunt_test",
                    "-c", &sql
                ])
                .output()?;

            if !output.status.success() {
                return Err(anyhow::anyhow!(
                    "Failed to insert test trade: {}",
                    String::from_utf8_lossy(&output.stderr)
                ));
            }
        }

        Ok(())
    }

    /// Insert test ML model records
    pub async fn insert_test_models(&self, models: &[TestModel]) -> Result<()> {
        for model in models {
            let performance_json = serde_json::to_string(&model.performance_metrics)?;
            let hyperparams_json = serde_json::to_string(&model.hyperparameters)?;
            
            let sql = format!(
                "INSERT INTO ml_models (model_name, model_type, version, symbol, status, performance_metrics, hyperparameters, model_path) VALUES ('{}', '{}', '{}', '{}', '{}', '{}', '{}', '{}')",
                model.model_name, model.model_type, model.version, model.symbol, model.status,
                performance_json, hyperparams_json, model.model_path.as_deref().unwrap_or("")
            );

            let output = Command::new("docker")
                .args([
                    "exec", "foxhunt-test-postgres",
                    "psql", "-U", "test", "-d", "foxhunt_test",
                    "-c", &sql
                ])
                .output()?;

            if !output.status.success() {
                return Err(anyhow::anyhow!(
                    "Failed to insert test model: {}",
                    String::from_utf8_lossy(&output.stderr)
                ));
            }
        }

        Ok(())
    }

    /// Clean all test data from databases
    pub async fn clean_test_data(&self) -> Result<()> {
        let tables = vec!["trades", "ml_models", "training_jobs", "market_data"];
        
        for table in tables {
            let sql = format!("TRUNCATE TABLE {} RESTART IDENTITY CASCADE", table);
            
            let output = Command::new("docker")
                .args([
                    "exec", "foxhunt-test-postgres",
                    "psql", "-U", "test", "-d", "foxhunt_test",
                    "-c", &sql
                ])
                .output()?;

            if !output.status.success() {
                eprintln!("Warning: Failed to clean table {}: {}", 
                          table, String::from_utf8_lossy(&output.stderr));
            }
        }

        Ok(())
    }
}

/// Test trade data structure
#[derive(Debug, Clone)]
pub struct TestTrade {
    pub symbol: String,
    pub price: f64,
    pub quantity: f64,
    pub side: String,
    pub timestamp: chrono::DateTime<chrono::Utc>,
    pub model_id: Option<String>,
}

/// Test ML model data structure
#[derive(Debug, Clone)]
pub struct TestModel {
    pub model_name: String,
    pub model_type: String,
    pub version: String,
    pub symbol: String,
    pub status: String,
    pub performance_metrics: std::collections::HashMap<String, f64>,
    pub hyperparameters: std::collections::HashMap<String, String>,
    pub model_path: Option<String>,
}

impl Default for TestTrade {
    fn default() -> Self {
        Self {
            symbol: "AAPL".to_string(),
            price: 150.0,
            quantity: 100.0,
            side: "BUY".to_string(),
            timestamp: chrono::Utc::now(),
            model_id: Some("test_model_v1".to_string()),
        }
    }
}

impl Default for TestModel {
    fn default() -> Self {
        let mut performance_metrics = std::collections::HashMap::new();
        performance_metrics.insert("accuracy".to_string(), 0.75);
        performance_metrics.insert("sharpe_ratio".to_string(), 1.2);
        
        let mut hyperparameters = std::collections::HashMap::new();
        hyperparameters.insert("learning_rate".to_string(), "0.001".to_string());
        hyperparameters.insert("batch_size".to_string(), "32".to_string());
        
        Self {
            model_name: "test_model".to_string(),
            model_type: "DQN".to_string(),
            version: "1.0.0".to_string(),
            symbol: "AAPL".to_string(),
            status: "ACTIVE".to_string(),
            performance_metrics,
            hyperparameters,
            model_path: Some("/tmp/test_models/test_model.pkl".to_string()),
        }
    }
}