//! Smoke tests for OOS Performance + Generalization Enhancement components.
//!
//! Verifies that all 29 new components compile, initialize, and produce
//! finite metrics during a short training run (3 epochs, small batch).
//! Runs on local GPU (RTX 3050 compatible with fxcache data).

use super::helpers::*;
use tracing::info;

/// Full integration smoke: 3 epochs with all generalization components active.
/// Verifies: AdamW weight decay, L1 sparsity, cost curriculum, Sharpe reward,
/// gamma annealing, regime dropout, counterfactual augmentation, epistemic gate,
/// branch independence, temporal consistency, Q-anchoring, predictive coding,
/// CVaR objective, ensemble heads, Mamba2 temporal scan, adaptive atoms,
/// regime branch gating, multi-horizon, Q-mean drift reg, LR warmup.
///
/// Run: FOXHUNT_TEST_DATA=test_data/futures-baseline cargo test -p ml --lib -- test_generalization_components_smoke --ignored --nocapture
#[test]
#[ignore] // GPU + fxcache data — ~30s on RTX 3050
fn test_generalization_components_smoke() -> anyhow::Result<()> {
    use crate::fxcache;

    let cache_dir = feature_cache_dir();
    assert!(cache_dir.exists(), "feature-cache not found at {:?}", cache_dir);

    let entries: Vec<_> = std::fs::read_dir(&cache_dir)?
        .filter_map(|e| e.ok())
        .filter(|e| e.path().extension().and_then(|s| s.to_str()) == Some("fxcache"))
        .collect();
    assert!(!entries.is_empty(), "No .fxcache files in {:?}", cache_dir);

    let fxcache_data = fxcache::load_fxcache(&entries[0].path())?;
    let n = fxcache_data.bar_count.min(5000);
    let train_end = (n * 80) / 100;

    let mut params = smoke_params();
    params.epochs = 3;
    params.batch_size = 4096;  // Larger batch to catch OOB that only trigger at scale
    params.buffer_size = 8192;
    // SP4 Layer B: `weight_decay` removed from hyperparams. Per-group AdamW
    // rates flow from ISV[WD_RATE[group]] at runtime; smoke tests inherit
    // the bootstrap value (Pearl A sentinel = 0 → zero-decay until the
    // first real observation, identical to the historical "AdamW active"
    // contract once the EMA spins up).

    let mut trainer = smoke_trainer_with(params)?;
    let rt = tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()?;

    let features = &fxcache_data.features[..n];
    let targets = &fxcache_data.targets[..n];
    let ofi = &fxcache_data.ofi[..n.min(fxcache_data.ofi.len())];

    let train_feat = &features[..train_end];
    let train_targets = &targets[..train_end];
    let val_feat = &features[train_end..n];
    let val_targets = &targets[train_end..n];

    rt.block_on(trainer.init_from_fxcache(features, targets, ofi))?;
    trainer.set_training_range(0, train_end, train_end, n);
    trainer.set_val_data_from_slices(val_feat, val_targets, train_end);
    rt.block_on(trainer.reset_for_fold())?;

    let metrics = rt.block_on(trainer.train_fold_from_slices(
        train_feat, train_targets,
        |_epoch, _bytes, _best| Ok("skip".to_owned()),
    ))?;

    // ── Basic sanity ──
    assert_finite(metrics.loss, "loss");
    assert!(metrics.loss >= 0.0, "loss should be non-negative");
    assert!(metrics.epochs_trained > 0, "should complete at least 1 epoch");

    let grad_norm = metrics.additional_metrics.get("avg_gradient_norm").copied().unwrap_or(f64::NAN);
    assert_finite(grad_norm, "avg_gradient_norm");
    assert!(grad_norm > 0.0, "gradients must be flowing, got {grad_norm}");

    let avg_q = metrics.additional_metrics.get("avg_q_value").copied().unwrap_or(f64::NAN);
    assert_finite(avg_q, "avg_q_value");

    // ── Generalization component state verification ──
    info!("=== Generalization Components State ===");

    // G1: AdamW — training completed without collapse
    info!("  G1 AdamW: loss={:.4} (finite = decay not too aggressive)", metrics.loss);

    // G2: Cost curriculum — anneal should be > 0 after epoch 1
    info!("  G2 cost_anneal: {:.3}", trainer.cost_anneal_factor);
    assert!(trainer.cost_anneal_factor >= 0.0 && trainer.cost_anneal_factor <= 1.0,
        "cost_anneal should be in [0, 1], got {}", trainer.cost_anneal_factor);

    // Plan 1 Task 10: gamma is now GPU-computed via gamma_update kernel (ISV[43]).
    // Validate the config gamma_base is in a reasonable range.
    let gamma_base = trainer.hyperparams.gamma;
    info!("  G4 gamma_base: {:.3}", gamma_base);
    assert!(gamma_base >= 0.85 && gamma_base <= 0.999,
        "gamma_base should be in [0.85, 0.999], got {}", gamma_base);

    // Adaptive DSR — should be initialized
    info!("  DSR sharpe_ema: {:.3} (initialized={})",
        trainer.training_sharpe_ema, trainer.training_sharpe_ema_initialized);
    assert!(trainer.training_sharpe_ema_initialized,
        "Sharpe EMA should be initialized after training");

    // G3: Walk-forward state
    info!("  G3 wf_min_sharpe: {:.3}", trainer.wf_min_sharpe);
    assert_eq!(trainer.wf_num_windows, 6);
    assert_eq!(trainer.wf_purge_bars, 2000);

    // Epoch Sharpe from training metrics
    let epoch_sharpe = metrics.additional_metrics.get("epoch_sharpe").copied().unwrap_or(f64::NAN);
    info!("  training_sharpe: {:.3}", epoch_sharpe);

    // val_loss (negative Sharpe)
    let val_loss = metrics.additional_metrics.get("val_loss").copied().unwrap_or(f64::NAN);
    info!("  val_loss: {:.3}", val_loss);

    // Self-improving enrichment verification
    info!("  Enrichment cycles: {}", trainer.enrichment.cycle_count);
    info!("  Enrichment epsilon: {:.4}", trainer.enrichment.epsilon);
    info!("  Enrichment agreement_threshold: {:.3}", trainer.enrichment.agreement_threshold);
    assert!(trainer.enrichment.cycle_count > 0,
        "Enrichment should have run at least once");

    info!("=== All generalization components verified ===");
    Ok(())
}

/// Quick kernel initialization test — no training data needed.
/// Verifies all new CUDA kernels load from cubins without error.
///
/// Run: cargo test -p ml --lib -- test_generalization_kernels_load
#[test]
fn test_generalization_kernels_load() -> anyhow::Result<()> {
    use crate::cuda_pipeline::gpu_dqn_trainer::{GpuDqnTrainConfig, GpuDqnTrainer};

    let dev = cuda_device();
    let stream = std::sync::Arc::clone(dev.cuda_stream().expect("cuda stream"));
    let cfg = GpuDqnTrainConfig {
        state_dim: 48,
        shared_h1: 64,
        shared_h2: 64,
        value_h: 32,
        adv_h: 32,
        num_atoms: 11,
        v_min: -5.0,
        v_max: 5.0,
        branch_0_size: 4,
        branch_1_size: 3,
        branch_2_size: 3,
        branch_3_size: 3,
        batch_size: 32,
        market_dim: 40,
        ..GpuDqnTrainConfig::default()
    };

    // Constructor loads ALL kernels: regime_branch_gate, adaptive_atom_positions,
    // mamba2_update_history, mamba2_scan_projected_fwd/bwd, q_anchor_to_flat,
    // regime_dropout, epistemic_gate_magnitude, branch_independence_penalty,
    // temporal_consistency_penalty, predictive_coding_loss, atom_position_gradient
    let trainer = GpuDqnTrainer::new(stream, cfg);
    assert!(trainer.is_ok(), "GpuDqnTrainer should load all kernels: {:?}", trainer.err());

    info!("All 29 generalization + performance CUDA kernels loaded successfully");
    Ok(())
}
