#![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,
)]
//! DQN Checkpoint -> Inference Integration Test
//!
//! Proves the complete checkpoint -> load -> inference path:
//! 1. Train 5 epochs on real market data (fast, just to get a valid checkpoint)
//! 2. Save checkpoint to temp dir via the trainer's checkpoint callback
//! 3. Load checkpoint into a fresh DQN with matching architecture
//! 4. Run inference on 100 synthetic state vectors
//! 5. Assert: actions valid (0..45), Q-values finite, Q-values non-zero

#![allow(unused_crate_dependencies)]

use anyhow::{Context, Result};
// candle eliminated — test uses native cudarc APIs
use ml::dqn::{DQNConfig, DQN};
use ml::trainers::dqn::{DQNHyperparameters, DQNTrainer};
use ml_core::cuda_autograd::GpuTensor;
use std::path::PathBuf;
use tracing::info;
use tracing::warn;

/// Locate the small training data directory, returning an error if absent.
fn get_data_dir() -> Result<String> {
    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() {
        anyhow::bail!("Data not found: {}", data_dir.display());
    }
    Ok(data_dir.to_string_lossy().to_string())
}

/// Build a `DQNConfig` that matches the trainer's internal config exactly.
///
/// The trainer builds its DQNConfig from DQNHyperparameters::conservative() with
/// hardcoded architecture params: state_dim=54, num_actions=45, hidden_dims=[256,128,64].
/// NoisyLinear layers are always enabled (unconditional since Rainbow DQN is the
/// only supported architecture). Layer keys use "noisy_hidden_*" / "noisy_output" prefixes.
fn build_matching_config(checkpoint_tensors: &safetensors::SafeTensors<'_>) -> DQNConfig {
    // Discover state_dim from the first NoisyLinear layer weight.
    // NoisyLinear key format: "noisy_hidden_0.mu_w" shape [hidden, input]
    let state_dim = checkpoint_tensors
        .tensors()
        .iter()
        .find(|(name, _)| name.contains("noisy_hidden_0") && name.contains("mu_w"))
        .map(|(_, view)| {
            let d = view.shape();
            if d.len() == 2 { d[1] } else { 54 }
        })
        .unwrap_or(54);

    // Replicate the exact config the trainer constructs (see trainer.rs ~line 289).
    let mut config = DQNConfig::conservative();
    config.state_dim = state_dim;
    config.num_actions = 45;
    config.hidden_dims = vec![256, 128, 64];
    // Trainer hardcodes noisy_sigma_init = 0.5 via hyperparams
    config.noisy_sigma_init = 0.5;
    // Match trainer defaults: IQN disabled, CQL enabled with alpha=0.1
    config.use_iqn = false;
    config.cql_alpha = 0.1;
    config
}

#[tokio::test]
async fn test_checkpoint_to_inference() -> Result<()> {
    // -- Skip gracefully when real data is absent (CI environments) --------
    let data_dir = match get_data_dir() {
        Ok(dir) => dir,
        Err(e) => {
            warn!(reason = %e, "Skipping test: data not available");
            return Ok(());
        }
    };

    let checkpoint_dir = tempfile::tempdir()?;

    // =====================================================================
    // Phase 1: Train for 5 epochs to produce a valid checkpoint
    // =====================================================================
    let mut hyperparams = DQNHyperparameters::conservative();
    hyperparams.replay_buffer_vram_fraction = 0.0; // Disable AutoReplaySizer for test determinism
    hyperparams.epochs = 5;
    hyperparams.batch_size = 32;
    hyperparams.learning_rate = 0.0001;
    hyperparams.early_stopping_enabled = false;
    hyperparams.checkpoint_frequency = 5; // checkpoint on last epoch

    let mut trainer = DQNTrainer::new(hyperparams)?;
    let mut best_checkpoint_path: Option<PathBuf> = None;

    let _metrics = trainer
        .train(&data_dir, |epoch, checkpoint_data, is_best| {
            let name = if is_best {
                "inference_best.safetensors".to_string()
            } else {
                format!("inference_epoch_{epoch}.safetensors")
            };
            let path = checkpoint_dir.path().join(&name);
            std::fs::write(&path, &checkpoint_data)?;
            if is_best {
                best_checkpoint_path = Some(path.clone());
            }
            Ok(path.to_string_lossy().to_string())
        })
        .await?;

    // If no "best" was saved, fall back to any checkpoint in the directory.
    let checkpoint_path = match best_checkpoint_path {
        Some(p) => p,
        None => {
            // Find first .safetensors file in the temp dir
            let mut entries: Vec<_> = std::fs::read_dir(checkpoint_dir.path())?
                .filter_map(|e| e.ok())
                .filter(|e| {
                    e.path()
                        .extension()
                        .map_or(false, |ext| ext == "safetensors")
                })
                .collect();
            anyhow::ensure!(
                !entries.is_empty(),
                "No checkpoint files were saved during training"
            );
            entries.sort_by_key(|e| e.path());
            entries.pop().context("No checkpoint file")?.path()
        }
    };

    assert!(
        checkpoint_path.exists(),
        "Checkpoint file does not exist: {}",
        checkpoint_path.display()
    );
    info!(
        checkpoint = %checkpoint_path.display(),
        bytes = std::fs::metadata(&checkpoint_path)?.len(),
        "Phase 1 complete: checkpoint saved"
    );

    // =====================================================================
    // Phase 2: Load checkpoint into a fresh DQN
    // =====================================================================
    // Load the checkpoint's tensor metadata to discover architecture params
    // (state_dim, noisy vs standard layers) so we can construct a DQN whose
    // VarMap key names and tensor shapes match exactly.
    let checkpoint_path_str = checkpoint_path
        .to_str()
        .context("Non-UTF8 checkpoint path")?;

    let checkpoint_bytes = std::fs::read(checkpoint_path_str)?;
    let checkpoint_tensors = safetensors::SafeTensors::deserialize(&checkpoint_bytes)?;

    info!(
        tensor_count = checkpoint_tensors.len(),
        keys = ?checkpoint_tensors.names(),
        "Checkpoint tensor inventory"
    );

    let config = build_matching_config(&checkpoint_tensors);
    let state_dim = config.state_dim;
    info!(
        state_dim = config.state_dim,
        num_actions = config.num_actions,
        "Built matching config (noisy nets always enabled)"
    );

    let mut fresh_dqn = DQN::new(config)?;
    fresh_dqn.load_from_safetensors(checkpoint_path_str)?;

    info!("Phase 2 complete: fresh DQN loaded from checkpoint");

    // =====================================================================
    // Phase 3: Inference on 100 synthetic state vectors
    // =====================================================================
    let num_inference_samples: usize = 100;
    let num_actions: usize = 45;
    let stream = fresh_dqn.cuda_stream();

    let mut all_actions_valid = true;
    let mut all_q_finite = true;
    let mut any_q_nonzero = false;
    let mut action_counts = vec![0usize; num_actions];

    for i in 0..num_inference_samples {
        // Deterministic synthetic state: slight variation per sample
        let state_vec: Vec<f32> = (0..state_dim)
            .map(|j| ((i * state_dim + j) as f32 * 0.01).sin())
            .collect();

        let state_tensor = GpuTensor::from_host(&state_vec, vec![1, state_dim], stream)?;

        let q_values = fresh_dqn.forward(&state_tensor)?;

        // Shape check: [1, 45]
        let dims = q_values.shape();
        assert_eq!(
            dims,
            &[1, num_actions],
            "Q-value tensor shape mismatch: expected [1, {}], got {:?}",
            num_actions,
            dims
        );

        let q_vec: Vec<f32> = q_values.to_host(stream)?;

        // Check finite
        for &q in &q_vec {
            if !q.is_finite() {
                all_q_finite = false;
            }
            if q.abs() > 1e-9 {
                any_q_nonzero = true;
            }
        }

        // Argmax to get action index
        let best_action_idx = q_vec
            .iter()
            .enumerate()
            .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
            .map(|(idx, _)| idx)
            .unwrap_or(0);

        if best_action_idx >= num_actions {
            all_actions_valid = false;
        } else if let Some(count) = action_counts.get_mut(best_action_idx) {
            *count += 1;
        }
    }

    // =====================================================================
    // Phase 4: Assertions
    // =====================================================================
    assert!(all_q_finite, "Some Q-values were NaN or Inf");
    assert!(
        any_q_nonzero,
        "All Q-values were zero -- model likely failed to load weights"
    );
    assert!(
        all_actions_valid,
        "Some action indices were out of range (expected 0..45)"
    );

    // Report action distribution
    let unique_actions = action_counts.iter().filter(|&&c| c > 0).count();
    info!(
        inference_passes = num_inference_samples,
        unique_actions,
        "Phase 3 complete: inference passes done"
    );

    // Sanity: at least 1 unique action (degenerate model is still valid for this test)
    assert!(
        unique_actions >= 1,
        "No actions were selected -- inference loop failure"
    );

    info!("All assertions passed: checkpoint -> load -> inference pipeline verified");

    Ok(())
}
