#![allow(
    clippy::assertions_on_constants,
    clippy::assertions_on_result_states,
    clippy::clone_on_copy,
    clippy::decimal_literal_representation,
    clippy::doc_markdown,
    clippy::empty_line_after_doc_comments,
    clippy::field_reassign_with_default,
    clippy::get_unwrap,
    clippy::identity_op,
    clippy::inconsistent_digit_grouping,
    clippy::indexing_slicing,
    clippy::integer_division,
    clippy::len_zero,
    clippy::let_underscore_must_use,
    clippy::manual_div_ceil,
    clippy::manual_let_else,
    clippy::manual_range_contains,
    clippy::modulo_arithmetic,
    clippy::needless_range_loop,
    clippy::non_ascii_literal,
    clippy::redundant_clone,
    clippy::shadow_reuse,
    clippy::shadow_same,
    clippy::shadow_unrelated,
    clippy::single_match_else,
    clippy::str_to_string,
    clippy::string_slice,
    clippy::tests_outside_test_module,
    clippy::too_many_lines,
    clippy::unnecessary_wraps,
    clippy::unseparated_literal_suffix,
    clippy::use_debug,
    clippy::useless_vec,
    clippy::wildcard_enum_match_arm,
    clippy::else_if_without_else,
    clippy::expect_used,
    clippy::missing_const_for_fn,
    clippy::similar_names,
    clippy::type_complexity,
    clippy::collapsible_else_if,
    clippy::doc_lazy_continuation,
    clippy::items_after_test_module,
    clippy::map_clone,
    clippy::multiple_unsafe_ops_per_block,
    clippy::unwrap_or_default,
    clippy::assign_op_pattern,
    clippy::needless_borrow,
    clippy::println_empty_string,
    clippy::unnecessary_cast,
    clippy::used_underscore_binding,
    clippy::create_dir,
    clippy::implicit_saturating_sub,
    clippy::exit,
    clippy::expect_fun_call,
    clippy::too_many_arguments,
    clippy::unnecessary_map_or,
    clippy::unwrap_used,
    dead_code,
    unused_imports,
    unused_variables,
    clippy::cloned_ref_to_slice_refs,
    clippy::neg_multiply,
    clippy::while_let_loop,
    clippy::bool_assert_comparison,
    clippy::excessive_precision,
    clippy::trivially_copy_pass_by_ref,
    clippy::op_ref,
    clippy::redundant_closure,
    clippy::unnecessary_lazy_evaluations,
    clippy::if_then_some_else_none,
    clippy::unnecessary_to_owned,
    clippy::single_component_path_imports,
)]
//! Production Training Integration Smoke Test
//!
//! Validates that all production training pipeline components added in the
//! production-training worktree work together correctly:
//!
//! 2. 26D parameter space with round-trip preservation
//! 3. EarlyStoppingConfig with SuccessiveHalving
//! 4. EarlyStoppingConfig with Hyperband (rung-based pruning)
//! 5. ObjectiveMode equality comparison
//! 6. QR-DQN training on real data (graceful skip if unavailable)

#![allow(unused_crate_dependencies)]

use anyhow::{Context, Result};
use ml::hyperopt::adapters::dqn::{DQNParams, ObjectiveMode};
use ml::hyperopt::early_stopping::{
    EarlyStoppingConfig, EarlyStoppingObserver, EarlyStoppingStrategy, EpochMetrics,
    ObserverDecision, TrialObserver,
};
use ml::hyperopt::traits::ParameterSpace;
use tracing::{info, warn};

/// Test 1: Verify DQNParams::default() has IQN disabled and CQL alpha=0.1
/// (IQN disabled to fix train/inference network mismatch, CQL reduced from 1.0)
#[test]
fn test_qr_dqn_defaults() -> Result<()> {
    let params = DQNParams::default();

    // IQN is always enabled by default
    assert!(params.iqn_lambda > 0.0, "IQN should be enabled by default");
    assert_eq!(
        params.num_quantiles, 32,
        "DQNParams::default() should have num_quantiles: 32, got {}",
        params.num_quantiles
    );
    let cql_diff = (params.cql_alpha - 0.1).abs();
    assert!(
        cql_diff < 1e-10,
        "DQNParams::default() should have cql_alpha: 0.1, got {}",
        params.cql_alpha
    );

    info!(
        cql_alpha = params.cql_alpha,
        num_quantiles = params.num_quantiles,
        "Test 1 PASSED: DQN defaults verified"
    );

    Ok(())
}

/// Test 2: Verify 28D parameter space and round-trip preservation of CQL alpha
#[test]
fn test_28d_parameter_space_round_trip() -> Result<()> {
    // Verify dimensionality (28D search space including sharpe_weight)
    let bounds = DQNParams::continuous_bounds();
    assert_eq!(
        bounds.len(),
        28,
        "DQNParams::continuous_bounds() should return 28 dimensions, got {}",
        bounds.len()
    );

    // Verify all bounds are valid (min < max)
    for (i, (lo, hi)) in bounds.iter().enumerate() {
        assert!(
            lo < hi,
            "Bound {} has invalid range: [{}, {}]",
            i, lo, hi
        );
    }

    // Round-trip: default -> continuous -> from_continuous
    let original = DQNParams::default();
    let continuous = original.to_continuous();
    assert_eq!(
        continuous.len(),
        28,
        "to_continuous() should return 28 values, got {}",
        continuous.len()
    );

    let reconstructed = DQNParams::from_continuous(&continuous)
        .map_err(|e| anyhow::anyhow!("from_continuous failed: {}", e))?;

    // Verify CQL alpha survives round trip
    let cql_diff = (reconstructed.cql_alpha - original.cql_alpha).abs();
    assert!(
        cql_diff < 0.01,
        "Round-trip should preserve cql_alpha: expected {}, got {} (diff={})",
        original.cql_alpha, reconstructed.cql_alpha, cql_diff
    );

    // Verify other key params survive the round trip
    let lr_diff = (reconstructed.learning_rate - original.learning_rate).abs();
    assert!(
        lr_diff < 1e-8,
        "Round-trip should preserve learning_rate: expected {}, got {}",
        original.learning_rate, reconstructed.learning_rate
    );

    info!(
        dimensions = bounds.len(),
        cql_alpha = reconstructed.cql_alpha,
        "Test 2 PASSED: 26D parameter space round-trip verified"
    );

    Ok(())
}

/// Test 3: EarlyStoppingConfig with SuccessiveHalving
///
/// Verifies that EarlyStoppingObserver with SHA strategy can be constructed
/// and returns Continue for a good initial trial (no history to prune against).
#[test]
fn test_early_stopping_successive_halving() -> Result<()> {
    let config = EarlyStoppingConfig {
        patience_epochs: 10,
        min_delta: 1e-4,
        min_epochs: 0, // Allow early decisions for testing
        strategy: EarlyStoppingStrategy::SuccessiveHalving {
            reduction_factor: 3,
        },
        ..Default::default()
    };

    let mut observer = EarlyStoppingObserver::new(config);

    // Start a trial with no prior history -- should not prune
    observer.on_trial_start(0, "sha_test_trial_0");
    let metrics = EpochMetrics {
        epoch: 1,
        train_loss: 0.3,
        val_loss: 0.3,
        timestamp: 1.0,
    };
    let decision = observer.on_epoch_complete(0, 1, &metrics);
    assert_eq!(
        decision,
        ObserverDecision::Continue,
        "SHA should not prune first trial with no history (got {:?})",
        decision
    );

    // Complete a few trials to build history, then verify pruning works
    for trial in 0..6 {
        observer.on_trial_start(trial, &format!("sha_trial_{}", trial));
        let loss = (trial as f64 + 1.0) * 0.1; // 0.1, 0.2, ..., 0.6
        observer.on_trial_complete(trial, loss);
    }

    // New trial with bad loss should be pruned (6 trials, keep top 1/3 = 2, threshold ~ 0.2)
    observer.on_trial_start(7, "sha_bad_trial");
    let bad_metrics = EpochMetrics {
        epoch: 1,
        train_loss: 0.9,
        val_loss: 0.9,
        timestamp: 2.0,
    };
    let bad_decision = observer.on_epoch_complete(7, 1, &bad_metrics);
    assert_eq!(
        bad_decision,
        ObserverDecision::StopTrial,
        "SHA should prune trial with val_loss=0.9 when threshold is ~0.2"
    );

    info!("Test 3 PASSED: SuccessiveHalving early stopping verified (good trial: Continue, bad trial: StopTrial)");

    Ok(())
}

/// Test 4: EarlyStoppingConfig with Hyperband
///
/// Verifies that Hyperband does NOT prune between rungs (e.g. epoch 5)
/// but DOES prune at rung epochs (e.g. epoch 27) for a bad trial.
#[test]
fn test_early_stopping_hyperband_rung_behavior() -> Result<()> {
    // max_resource=81, reduction_factor=3
    // Rung epochs: 81/3=27, 81/9=9, 81/27=3, 81/81=1
    let config = EarlyStoppingConfig {
        patience_epochs: 100, // High patience so only Hyperband logic matters
        min_delta: 1e-4,
        min_epochs: 0, // Allow early decisions
        strategy: EarlyStoppingStrategy::Hyperband {
            max_resource: 81,
            reduction_factor: 3,
        },
        ..Default::default()
    };

    let mut observer = EarlyStoppingObserver::new(config);

    // Seed with completed trials so pruning has history
    for trial in 0..9 {
        observer.on_trial_start(trial, &format!("hb_trial_{}", trial));
        observer.on_trial_complete(trial, (trial as f64 + 1.0) * 0.1);
    }

    // Test: epoch 5 is NOT a rung -- should NOT prune even with bad loss
    observer.on_trial_start(10, "hb_non_rung_trial");
    let non_rung_metrics = EpochMetrics {
        epoch: 5,
        train_loss: 0.95,
        val_loss: 0.95,
        timestamp: 5.0,
    };
    let non_rung_decision = observer.on_epoch_complete(10, 5, &non_rung_metrics);
    assert_eq!(
        non_rung_decision,
        ObserverDecision::Continue,
        "Hyperband should NOT prune at non-rung epoch 5 (got {:?})",
        non_rung_decision
    );

    // Test: epoch 27 IS a rung (81/3=27) -- should prune bad trial
    observer.on_trial_start(11, "hb_rung_trial");
    let rung_metrics = EpochMetrics {
        epoch: 27,
        train_loss: 0.95,
        val_loss: 0.95,
        timestamp: 27.0,
    };
    let rung_decision = observer.on_epoch_complete(11, 27, &rung_metrics);
    assert_eq!(
        rung_decision,
        ObserverDecision::StopTrial,
        "Hyperband should prune bad trial at rung epoch 27 (got {:?})",
        rung_decision
    );

    info!("Test 4 PASSED: Hyperband rung-based pruning verified (non-rung epoch 5: Continue, rung epoch 27: StopTrial)");

    Ok(())
}

/// Test 5: ObjectiveMode equality comparison
#[test]
fn test_objective_mode_equality() -> Result<()> {
    let mode_a = ObjectiveMode::EpisodeReward;
    let mode_b = ObjectiveMode::Sharpe;
    let mode_c = ObjectiveMode::EpisodeReward;

    // Same variant should be equal
    assert_eq!(
        mode_a, mode_c,
        "EpisodeReward should equal EpisodeReward"
    );

    // Different variants should not be equal
    assert_ne!(
        mode_a, mode_b,
        "EpisodeReward should not equal Sharpe"
    );

    // Default should be Sharpe
    let default_mode = ObjectiveMode::default();
    assert_eq!(
        default_mode, mode_b,
        "ObjectiveMode::default() should be Sharpe"
    );

    info!("Test 5 PASSED: ObjectiveMode PartialEq verified");

    Ok(())
}

/// Test 6: QR-DQN training on real data
///
/// Gracefully skips if test data is not available.
#[tokio::test]
async fn test_qr_dqn_training_real_data() -> Result<()> {
    use ml::trainers::dqn::{DQNHyperparameters, DQNTrainer};
    use std::path::PathBuf;

    // Locate test data (same pattern as dqn_training_smoke_test.rs)
    let workspace_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .ancestors()
        .find(|p| p.join("test_data").exists())
        .context("Failed to find workspace root with test_data/")?
        .to_path_buf();
    let data_dir = workspace_root.join("test_data/real/databento");

    if !data_dir.exists() {
        warn!(path = %data_dir.display(), "Skipping test: data not found");
        return Ok(());
    }
    let data_dir_str = data_dir.to_string_lossy().to_string();

    // Configure hyperparameters with QR-DQN enabled
    let mut hyperparams = DQNHyperparameters::conservative();
    hyperparams.epochs = 5;
    hyperparams.batch_size = 64;
    hyperparams.learning_rate = 0.0001;
    hyperparams.epsilon_start = 1.0;
    hyperparams.epsilon_end = 0.05;
    hyperparams.epsilon_decay = 0.90;
    hyperparams.early_stopping_enabled = false; // No early stopping for 5 epochs
    hyperparams.num_quantiles = 32;
    hyperparams.qr_kappa = 1.0;

    // Train
    let checkpoint_dir = tempfile::tempdir()?;
    let mut trainer = DQNTrainer::new(hyperparams)?;

    let metrics = trainer
        .train(&data_dir_str, |epoch, checkpoint_data, is_best| {
            let name = if is_best {
                "qrdqn_best.safetensors".to_string()
            } else {
                format!("qrdqn_epoch_{}.safetensors", epoch)
            };
            let path = checkpoint_dir.path().join(&name);
            std::fs::write(&path, &checkpoint_data)?;
            Ok(path.to_string_lossy().to_string())
        })
        .await?;

    // Verify epochs completed
    assert!(
        metrics.epochs_trained >= 1,
        "Should complete at least 1 epoch, got {}",
        metrics.epochs_trained
    );

    // Verify losses are finite and non-zero
    let loss_history = trainer.loss_history();
    assert!(
        !loss_history.is_empty(),
        "Loss history should not be empty after training"
    );

    for (i, loss) in loss_history.iter().enumerate() {
        assert!(
            loss.is_finite(),
            "Loss at epoch {} should be finite, got {}",
            i, loss
        );
        assert!(
            *loss > 0.0,
            "Loss at epoch {} should be non-zero, got {}",
            i, loss
        );
    }

    info!(
        epochs_trained = metrics.epochs_trained,
        loss_history = ?loss_history,
        training_time_secs = metrics.training_time_seconds,
        "Test 6 PASSED: QR-DQN training on real data verified"
    );

    Ok(())
}
