use crate::trainers::dqn::{DQNHyperparameters, DQNTrainer};
use ml_core::device::MlDevice;

/// Initialize tracing subscriber once for all smoke tests.
/// Uses `RUST_LOG` env var (defaults to `info`). `try_init` is idempotent.
static TRACING_INIT: std::sync::Once = std::sync::Once::new();

fn init_tracing() {
    TRACING_INIT.call_once(|| {
        let filter = tracing_subscriber::EnvFilter::try_from_default_env()
            .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info"));
        tracing_subscriber::fmt()
            .with_env_filter(filter)
            .with_test_writer() // captures output per-test in `cargo test`
            .try_init()
            .ok(); // ignore if another subscriber is already set
    });
}

/// Production-config smoke test params — all features on, small network for fast tests.
///
/// Matches production exactly: Rainbow DQN + branching + IQN + curiosity + CQL +
/// Kelly + action masking + circuit breaker + n-step + noisy nets.
/// Sizing values (hidden_dim_base=32, batch_size=16, buffer=1024, epochs=1, etc.)
/// are loaded from the `dqn-smoketest` TOML profile.
pub(super) fn smoke_params() -> DQNHyperparameters {
    let mut p = DQNHyperparameters::conservative();

    // ── Smoke test sizing — loaded from config/training/dqn-smoketest.toml ──
    // Sets: epochs, batch_size, warmup_steps, hidden_dim_base, max_steps_per_epoch,
    //       buffer_size, min_replay_size, early_stopping_enabled, gpu_n_episodes,
    //       gpu_timesteps_per_episode.
    let profile = crate::training_profile::DqnTrainingProfile::load("dqn-smoketest");
    profile.apply_to(&mut p);

    // ── Production features (always on — not in the TOML profile) ──
    p.curiosity_weight = 0.1;        // Curiosity — always on
    p.cql_alpha = 0.1;
    p.enable_kelly_sizing = true;    // Kelly criterion — always on
    p.kelly_fractional = 0.5;
    p.kelly_max_fraction = 0.25;
    p.enable_action_masking = true;  // Action masking — always on
    p.max_position_absolute = 2.0;
    p.enable_circuit_breaker = true; // Circuit breaker — always on

    // ── Testing flags (not training config) ──
    p.enable_stress_testing = false;
    p.enable_compliance = false;
    p.enable_regime_qnetwork = false;
    p.replay_buffer_vram_fraction = 0.0; // Explicit buffer_size, no AutoReplaySizer
    p.checkpoint_frequency = 100;

    p
}

/// Create a fresh CUDA device for each test.
///
/// NOT cached in a static — each test gets its own CudaContext + CudaStream.
/// A static OnceLock<MlDevice> caused sequential test failures: CudaSlice Drop
/// records errors on the shared context's error_state, poisoning it for the next test.
pub(super) fn cuda_device() -> MlDevice {
    init_tracing();
    MlDevice::new_cuda(0)
        .expect("CUDA device required — no CPU fallback in smoke tests")
}

/// Create trainer on CUDA device (no CPU fallback).
pub(super) fn smoke_trainer() -> anyhow::Result<DQNTrainer> {
    DQNTrainer::new_with_device(smoke_params(), cuda_device())
}

/// Create trainer with custom params on CUDA device (no CPU fallback).
pub(super) fn smoke_trainer_with(params: DQNHyperparameters) -> anyhow::Result<DQNTrainer> {
    DQNTrainer::new_with_device(params, cuda_device())
}

/// Assert a value is finite (not NaN or Inf)
pub(super) fn assert_finite(val: f64, name: &str) {
    assert!(val.is_finite(), "{name} is not finite: {val}");
}

/// Assert a value is finite f32
pub(super) fn assert_finite_f32(val: f32, name: &str) {
    assert!(val.is_finite(), "{name} is not finite: {val}");
}

/// Resolve single-symbol test data directory.
///
/// Uses `FOXHUNT_TEST_DATA` env var if set, otherwise auto-detects
/// `test_data/ES.FUT` relative to the workspace root.
/// Returns `None` if the directory doesn't exist.
pub(super) fn test_data_dir() -> Option<String> {
    // Check env vars: FOXHUNT_TEST_DATA (explicit), TEST_DATA_DIR (CI PVC root)
    for var in &["FOXHUNT_TEST_DATA", "TEST_DATA_DIR"] {
        if let Ok(dir) = std::env::var(var) {
            let path = std::path::Path::new(&dir);
            // Search for ES.FUT in common layouts:
            // 1. $DIR/ES.FUT (direct)
            // 2. $DIR/ohlcv/ES.FUT (CI PVC layout)
            for sub in &["ES.FUT", "ohlcv/ES.FUT"] {
                let candidate = path.join(sub);
                if candidate.exists() {
                    return Some(candidate.to_string_lossy().into_owned());
                }
            }
        }
    }
    // Auto-detect from workspace root: test_data/ES.FUT or test_data/ohlcv/ES.FUT
    let manifest = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_default();
    let workspace = std::path::Path::new(&manifest)
        .parent() // crates/
        .and_then(|p| p.parent()) // repo root
        .unwrap_or(std::path::Path::new("../.."));
    let test_data = workspace.join("test_data");
    for sub in &["ES.FUT", "ohlcv/ES.FUT"] {
        let candidate = test_data.join(sub);
        if candidate.exists() {
            return Some(candidate.to_string_lossy().into_owned());
        }
    }
    None
}
