#![allow(
    clippy::unwrap_used,
    clippy::expect_used,
    clippy::indexing_slicing,
    clippy::useless_vec
)]
//! Comprehensive Orchestrator Tests for ML Training Service
//!
//! Tests covering job lifecycle, checkpoint/versioning through storage,
//! and basic orchestrator functionality.

use std::collections::HashMap;
use std::sync::Arc;

use ml::training_pipeline::{
    FinancialValidationConfig, ModelArchitectureConfig, PerformanceConfig,
    ProductionTrainingConfig, TrainingHyperparameters,
};
use ml_training_service::{
    orchestrator::{JobStatus, TrainingJob},
    storage::{ModelStorageManager, StorageConfig},
};
use tempfile::TempDir;
use uuid::Uuid;

/// Helper to create test training config
fn create_test_training_config() -> ProductionTrainingConfig {
    ProductionTrainingConfig {
        model_config: ModelArchitectureConfig {
            input_dim: 50,
            output_dim: 1,
            hidden_dims: vec![128, 64],
            dropout_rate: 0.1,
            activation: "relu".to_string(),
            batch_norm: false,
            residual_connections: false,
        },
        training_params: TrainingHyperparameters {
            learning_rate: 0.001,
            batch_size: 32,
            max_epochs: 10,
            patience: 3,
            validation_split: 0.2,
            l2_regularization: 0.0001,
            lr_decay_factor: 0.1,
            lr_decay_patience: 5,
        },
        safety_config: ml::safety::MLSafetyConfig::default(),
        gradient_config: ml::safety::GradientSafetyConfig::default(),
        financial_config: FinancialValidationConfig {
            max_prediction_multiple: 2.0,
            min_prediction_confidence: 0.6,
            validate_position_sizing: true,
            max_position_fraction: 0.25,
            min_sharpe_threshold: 0.5,
        },
        performance_config: PerformanceConfig {
            device_preference: "cpu".to_string(),
            max_memory_bytes: 8 * 1024 * 1024 * 1024, // 8GB
            num_workers: 4,
            gradient_accumulation_steps: 1,
        },
    }
}

// ============================================================================
// Training Job Tests
// ============================================================================

#[test]
fn test_training_job_creation() {
    let config = create_test_training_config();
    let model_type = "test_model".to_string();
    let description = "Test training job".to_string();
    let tags = HashMap::from([("env".to_string(), "test".to_string())]);

    let job = TrainingJob::new(
        model_type.clone(),
        config.clone(),
        description.clone(),
        tags.clone(),
    );

    assert_eq!(job.model_type, model_type);
    assert_eq!(job.status, JobStatus::Pending);
    assert_eq!(job.description, description);
    assert_eq!(job.tags, tags);
    assert_eq!(job.progress_percentage, 0.0);
    assert_eq!(job.current_epoch, 0);
    assert_eq!(job.total_epochs, 0);
    assert!(job.started_at.is_none());
    assert!(job.completed_at.is_none());
    assert!(job.error_message.is_none());
    assert!(job.model_artifact_path.is_none());
}

#[test]
fn test_training_job_has_unique_ids() {
    let config = create_test_training_config();
    let job1 = TrainingJob::new(
        "model1".to_string(),
        config.clone(),
        "Job 1".to_string(),
        HashMap::new(),
    );
    let job2 = TrainingJob::new(
        "model2".to_string(),
        config,
        "Job 2".to_string(),
        HashMap::new(),
    );

    assert_ne!(job1.id, job2.id);
}

#[test]
fn test_training_job_creation_time() {
    let config = create_test_training_config();
    let before_creation = chrono::Utc::now();

    let job = TrainingJob::new(
        "test_model".to_string(),
        config,
        "Test job".to_string(),
        HashMap::new(),
    );

    let after_creation = chrono::Utc::now();

    assert!(job.created_at >= before_creation);
    assert!(job.created_at <= after_creation);
}

#[test]
fn test_job_status_variants() {
    // Test all status variants exist
    let statuses = vec![
        JobStatus::Pending,
        JobStatus::Running,
        JobStatus::Completed,
        JobStatus::Failed,
        JobStatus::Stopped,
        JobStatus::Paused,
    ];

    // All statuses should be distinct
    for i in 0..statuses.len() {
        for j in i + 1..statuses.len() {
            assert_ne!(statuses[i], statuses[j]);
        }
    }
}

#[test]
fn test_job_status_equality() {
    assert_eq!(JobStatus::Pending, JobStatus::Pending);
    assert_eq!(JobStatus::Running, JobStatus::Running);
    assert_eq!(JobStatus::Completed, JobStatus::Completed);
    assert_eq!(JobStatus::Failed, JobStatus::Failed);
    assert_eq!(JobStatus::Stopped, JobStatus::Stopped);
    assert_eq!(JobStatus::Paused, JobStatus::Paused);
}

#[test]
fn test_training_job_with_empty_tags() {
    let config = create_test_training_config();
    let job = TrainingJob::new(
        "test_model".to_string(),
        config,
        "Test job".to_string(),
        HashMap::new(),
    );

    assert_eq!(job.tags.len(), 0);
}

#[test]
fn test_training_job_with_multiple_tags() {
    let config = create_test_training_config();
    let tags = HashMap::from([
        ("env".to_string(), "production".to_string()),
        ("version".to_string(), "1.0".to_string()),
        ("team".to_string(), "ml".to_string()),
    ]);

    let job = TrainingJob::new(
        "test_model".to_string(),
        config,
        "Test job".to_string(),
        tags.clone(),
    );

    assert_eq!(job.tags.len(), 3);
    assert_eq!(job.tags.get("env"), Some(&"production".to_string()));
    assert_eq!(job.tags.get("version"), Some(&"1.0".to_string()));
    assert_eq!(job.tags.get("team"), Some(&"ml".to_string()));
}

#[test]
fn test_training_job_initial_metrics_empty() {
    let config = create_test_training_config();
    let job = TrainingJob::new(
        "test_model".to_string(),
        config,
        "Test job".to_string(),
        HashMap::new(),
    );

    assert_eq!(job.metrics.len(), 0);
}

// ============================================================================
// Model Versioning Tests (via Storage)
// ============================================================================

#[tokio::test]
async fn test_checkpoint_versioning_simulation() {
    // Simulate checkpoint versioning using storage directly
    let temp_dir = TempDir::new().unwrap();
    let config = StorageConfig {
        storage_type: "local".to_string(),
        local_base_path: Some(temp_dir.path().to_path_buf()),
        enable_compression: false,
    };

    let storage = Arc::new(ModelStorageManager::new(config).await.unwrap());

    let job_id = Uuid::new_v4();

    // Simulate saving multiple checkpoints (versioning)
    let checkpoint_v1 = storage
        .store_model(job_id, b"checkpoint version 1")
        .await
        .unwrap();
    tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;

    let checkpoint_v2 = storage
        .store_model(job_id, b"checkpoint version 2")
        .await
        .unwrap();
    tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;

    let checkpoint_v3 = storage
        .store_model(job_id, b"checkpoint version 3")
        .await
        .unwrap();

    // All versions should be unique
    assert_ne!(checkpoint_v1, checkpoint_v2);
    assert_ne!(checkpoint_v2, checkpoint_v3);
    assert_ne!(checkpoint_v1, checkpoint_v3);

    // All versions should exist
    assert!(storage.model_exists(&checkpoint_v1).await.unwrap());
    assert!(storage.model_exists(&checkpoint_v2).await.unwrap());
    assert!(storage.model_exists(&checkpoint_v3).await.unwrap());

    // Can retrieve specific versions
    let v1_data = storage.retrieve_model(&checkpoint_v1).await.unwrap();
    let v2_data = storage.retrieve_model(&checkpoint_v2).await.unwrap();
    let v3_data = storage.retrieve_model(&checkpoint_v3).await.unwrap();

    assert_eq!(v1_data, b"checkpoint version 1");
    assert_eq!(v2_data, b"checkpoint version 2");
    assert_eq!(v3_data, b"checkpoint version 3");
}

#[tokio::test]
async fn test_model_artifact_save_and_load() {
    let temp_dir = TempDir::new().unwrap();
    let config = StorageConfig {
        storage_type: "local".to_string(),
        local_base_path: Some(temp_dir.path().to_path_buf()),
        enable_compression: false,
    };

    let storage = Arc::new(ModelStorageManager::new(config).await.unwrap());

    let job_id = Uuid::new_v4();
    let model_data = b"trained model weights and biases";

    // Save model artifact
    let artifact_path = storage.store_model(job_id, model_data).await.unwrap();

    // Load model artifact
    let loaded_data = storage.retrieve_model(&artifact_path).await.unwrap();

    assert_eq!(loaded_data, model_data);
}

#[tokio::test]
async fn test_checkpoint_rollback_scenario() {
    // Test scenario: save checkpoints, then rollback to earlier version
    let temp_dir = TempDir::new().unwrap();
    let config = StorageConfig {
        storage_type: "local".to_string(),
        local_base_path: Some(temp_dir.path().to_path_buf()),
        enable_compression: false,
    };

    let storage = Arc::new(ModelStorageManager::new(config).await.unwrap());

    let job_id = Uuid::new_v4();

    // Save checkpoints at different epochs
    let epoch_5 = storage
        .store_model(job_id, b"model at epoch 5")
        .await
        .unwrap();
    tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;

    let epoch_10 = storage
        .store_model(job_id, b"model at epoch 10")
        .await
        .unwrap();
    tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;

    let _epoch_15 = storage
        .store_model(job_id, b"model at epoch 15")
        .await
        .unwrap();

    // Simulate rollback: load earlier checkpoint
    let rollback_data = storage.retrieve_model(&epoch_5).await.unwrap();
    assert_eq!(rollback_data, b"model at epoch 5");

    // Can still load epoch 10
    let epoch_10_data = storage.retrieve_model(&epoch_10).await.unwrap();
    assert_eq!(epoch_10_data, b"model at epoch 10");
}

#[tokio::test]
async fn test_checkpoint_cleanup_scenario() {
    // Test scenario: keep only last N checkpoints
    let temp_dir = TempDir::new().unwrap();
    let config = StorageConfig {
        storage_type: "local".to_string(),
        local_base_path: Some(temp_dir.path().to_path_buf()),
        enable_compression: false,
    };

    let storage = Arc::new(ModelStorageManager::new(config).await.unwrap());

    let job_id = Uuid::new_v4();

    // Save 5 checkpoints
    let cp1 = storage.store_model(job_id, b"cp1").await.unwrap();
    tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
    let cp2 = storage.store_model(job_id, b"cp2").await.unwrap();
    tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
    let cp3 = storage.store_model(job_id, b"cp3").await.unwrap();
    tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
    let cp4 = storage.store_model(job_id, b"cp4").await.unwrap();
    tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
    let cp5 = storage.store_model(job_id, b"cp5").await.unwrap();

    // Keep only last 3 checkpoints (delete cp1, cp2)
    storage.delete_model(&cp1).await.unwrap();
    storage.delete_model(&cp2).await.unwrap();

    // Verify deletions
    assert!(!storage.model_exists(&cp1).await.unwrap());
    assert!(!storage.model_exists(&cp2).await.unwrap());

    // Verify remaining checkpoints still exist
    assert!(storage.model_exists(&cp3).await.unwrap());
    assert!(storage.model_exists(&cp4).await.unwrap());
    assert!(storage.model_exists(&cp5).await.unwrap());
}

#[tokio::test]
async fn test_feature_engineering_edge_case_empty_data() {
    // Test edge case: empty training data
    let temp_dir = TempDir::new().unwrap();
    let config = StorageConfig {
        storage_type: "local".to_string(),
        local_base_path: Some(temp_dir.path().to_path_buf()),
        enable_compression: false,
    };

    let storage = Arc::new(ModelStorageManager::new(config).await.unwrap());

    let job_id = Uuid::new_v4();
    let empty_model = b"";

    // Should handle empty model
    let path = storage.store_model(job_id, empty_model).await.unwrap();
    let retrieved = storage.retrieve_model(&path).await.unwrap();
    assert_eq!(retrieved, empty_model);
}

#[tokio::test]
async fn test_feature_engineering_edge_case_large_model() {
    // Test edge case: very large model
    let temp_dir = TempDir::new().unwrap();
    let config = StorageConfig {
        storage_type: "local".to_string(),
        local_base_path: Some(temp_dir.path().to_path_buf()),
        enable_compression: true, // Use compression for large models
    };

    let storage = Arc::new(ModelStorageManager::new(config).await.unwrap());

    let job_id = Uuid::new_v4();
    // 5MB model
    let large_model = vec![42u8; 5 * 1024 * 1024];

    // Should handle large model with compression
    let path = storage.store_model(job_id, &large_model).await.unwrap();
    let retrieved = storage.retrieve_model(&path).await.unwrap();
    assert_eq!(retrieved.len(), large_model.len());
    assert_eq!(retrieved, large_model);
}

#[tokio::test]
async fn test_checkpoint_with_corrupted_data_detection() {
    // Test edge case: detect corrupted checkpoint
    let temp_dir = TempDir::new().unwrap();
    let config = StorageConfig {
        storage_type: "local".to_string(),
        local_base_path: Some(temp_dir.path().to_path_buf()),
        enable_compression: false,
    };

    let storage = Arc::new(ModelStorageManager::new(config).await.unwrap());

    // Try to load non-existent checkpoint
    let result = storage.retrieve_model("nonexistent/checkpoint.bin").await;
    assert!(result.is_err());
}

#[tokio::test]
async fn test_concurrent_checkpoint_saves() {
    // Test edge case: concurrent checkpoint saves
    let temp_dir = TempDir::new().unwrap();
    let config = StorageConfig {
        storage_type: "local".to_string(),
        local_base_path: Some(temp_dir.path().to_path_buf()),
        enable_compression: false,
    };

    let storage = Arc::new(ModelStorageManager::new(config).await.unwrap());

    let job_id = Uuid::new_v4();

    // Simulate concurrent saves
    let storage1 = storage.clone();
    let storage2 = storage.clone();
    let storage3 = storage.clone();

    let handle1 =
        tokio::spawn(async move { storage1.store_model(job_id, b"concurrent 1").await.unwrap() });

    let handle2 =
        tokio::spawn(async move { storage2.store_model(job_id, b"concurrent 2").await.unwrap() });

    let handle3 =
        tokio::spawn(async move { storage3.store_model(job_id, b"concurrent 3").await.unwrap() });

    let paths = futures::future::join_all(vec![handle1, handle2, handle3])
        .await
        .into_iter()
        .map(|r| r.unwrap())
        .collect::<Vec<_>>();

    // All should succeed with unique paths
    assert_eq!(paths.len(), 3);
    let unique_paths: std::collections::HashSet<_> = paths.iter().collect();
    assert_eq!(unique_paths.len(), 3);
}

#[tokio::test]
async fn test_training_config_serialization() {
    // Test that training config can be serialized/deserialized
    let config = create_test_training_config();

    let serialized = serde_json::to_string(&config).expect("INVARIANT: Serialization should succeed for valid types");
    let deserialized: ProductionTrainingConfig = serde_json::from_str(&serialized).expect("INVARIANT: Deserialization should succeed for valid JSON");

    assert_eq!(
        config.model_config.input_dim,
        deserialized.model_config.input_dim
    );
    assert_eq!(
        config.training_params.learning_rate,
        deserialized.training_params.learning_rate
    );
    assert_eq!(
        config.financial_config.max_prediction_multiple,
        deserialized.financial_config.max_prediction_multiple
    );
}

#[test]
fn test_job_with_special_characters_in_description() {
    let config = create_test_training_config();
    let description = "Test job with special chars: !@#$%^&*()_+-={}[]|\\:\";<>?,./".to_string();

    let job = TrainingJob::new(
        "test_model".to_string(),
        config,
        description.clone(),
        HashMap::new(),
    );

    assert_eq!(job.description, description);
}

#[test]
fn test_job_with_unicode_description() {
    let config = create_test_training_config();
    let description = "Test job with unicode: 你好世界 🚀 ñoño".to_string();

    let job = TrainingJob::new(
        "test_model".to_string(),
        config,
        description.clone(),
        HashMap::new(),
    );

    assert_eq!(job.description, description);
}
