//! SP20 Phase 3 Task 3.2 (2026-05-10) — `sp20_hold_baseline.cuh` GPU oracle tests.
//!
//! Verifies the two device helpers in `sp20_hold_baseline.cuh`
//! (Component 2 / Hold opportunity-cost dual emission per spec §4.2):
//!
//!   - `sp20_compute_per_bar_aux_conf_k2(logits)`:
//!     Per-row K=2 peak-confidence above the K=2 uniform mean.
//!     `aux_conf = max(softmax(logits)) - 0.5` ∈ [0, 0.5].
//!
//!   - `sp20_sum_hold_baseline_over_trade(buf, env, size, current_t,
//!     segment_hold_time)`:
//!     Sum the per-env circular buffer over the most recent
//!     `min(size, segment_hold_time)` slots BEFORE `current_t`.
//!
//!   - The full dual-emission shape (mirrors the production per-bar
//!     emission site at `experience_env_step` per-bar branches): Path 1
//!     centered Hold reward AND Path 2 uncentered buffer write, with
//!     the Hold-only / always action gating contract.
//!
//! Tests assert invariants per
//! `pearl_tests_must_prove_not_lock_observations` (boundedness,
//! formula correctness, fixed points), not observed-from-a-regression-
//! run values.
//!
//! Run on a GPU host:
//!
//!     SQLX_OFFLINE=true CUDA_COMPUTE_CAP=86 \
//!       cargo test -p ml --test sp20_hold_baseline_test --features cuda \
//!         -- --ignored --nocapture

#![allow(clippy::tests_outside_test_module)]

#[cfg(feature = "cuda")]
#[allow(unsafe_code)]
mod gpu {
    use std::sync::Arc;

    use cudarc::driver::{CudaContext, CudaFunction, CudaStream, PushKernelArg};
    use ml::cuda_pipeline::mapped_pinned::MappedF32Buffer;

    const TEST_CUBIN: &[u8] = include_bytes!(concat!(
        env!("OUT_DIR"),
        "/sp20_hold_baseline_test_kernel.cubin"
    ));

    fn make_stream() -> Arc<CudaStream> {
        let ctx = CudaContext::new(0).expect("CUDA context — is a GPU available?");
        ctx.default_stream()
    }

    fn load_fn(stream: &Arc<CudaStream>, name: &str) -> CudaFunction {
        let m = stream
            .context()
            .load_cubin(TEST_CUBIN.to_vec())
            .expect("load sp20_hold_baseline_test_kernel cubin");
        m.load_function(name)
            .unwrap_or_else(|e| panic!("load {name}: {e}"))
    }

    /// Helper: run `sp20_per_bar_aux_conf_k2_test_kernel` once with the
    /// given logits and return the resulting aux_conf.
    fn run_per_bar_aux_conf_k2(logits: [f32; 2]) -> f32 {
        let stream = make_stream();
        let kernel = load_fn(&stream, "sp20_per_bar_aux_conf_k2_test_kernel");

        let logits_buf = unsafe { MappedF32Buffer::new(2) }.expect("alloc logits");
        logits_buf.write_from_slice(&logits);
        let out_buf = unsafe { MappedF32Buffer::new(1) }.expect("alloc out");
        out_buf.write_from_slice(&[0.0_f32]);

        unsafe {
            stream
                .launch_builder(&kernel)
                .arg(&logits_buf.dev_ptr)
                .arg(&out_buf.dev_ptr)
                .launch(cudarc::driver::LaunchConfig {
                    grid_dim: (1, 1, 1),
                    block_dim: (1, 1, 1),
                    shared_mem_bytes: 0,
                })
                .expect("launch sp20_per_bar_aux_conf_k2_test_kernel");
        }
        stream
            .synchronize()
            .expect("sync after sp20_per_bar_aux_conf_k2_test_kernel");
        out_buf.read_all()[0]
    }

    /// Helper: run `sp20_sum_hold_baseline_over_trade_test_kernel` once
    /// with the given buffer + indexing args and return the resulting sum.
    fn run_sum_hold_baseline(
        buf: &[f32],
        env: i32,
        hold_buffer_size: i32,
        current_t: i32,
        segment_hold_time: f32,
    ) -> f32 {
        let stream = make_stream();
        let kernel = load_fn(
            &stream,
            "sp20_sum_hold_baseline_over_trade_test_kernel",
        );

        let buf_dev = unsafe { MappedF32Buffer::new(buf.len()) }.expect("alloc buf");
        buf_dev.write_from_slice(buf);
        let out_buf = unsafe { MappedF32Buffer::new(1) }.expect("alloc out");
        out_buf.write_from_slice(&[0.0_f32]);

        unsafe {
            stream
                .launch_builder(&kernel)
                .arg(&buf_dev.dev_ptr)
                .arg(&env)
                .arg(&hold_buffer_size)
                .arg(&current_t)
                .arg(&segment_hold_time)
                .arg(&out_buf.dev_ptr)
                .launch(cudarc::driver::LaunchConfig {
                    grid_dim: (1, 1, 1),
                    block_dim: (1, 1, 1),
                    shared_mem_bytes: 0,
                })
                .expect("launch sp20_sum_hold_baseline_over_trade_test_kernel");
        }
        stream
            .synchronize()
            .expect("sync after sum_hold_baseline_over_trade");
        out_buf.read_all()[0]
    }

    /// ── Test 1: aux_conf bounds + uniform/peak fixed points ──────────
    ///
    /// `aux_conf = max(softmax(logits)) - 0.5` ∈ [0, 0.5].
    ///
    /// Fixed points:
    ///   - Equal logits  ⇒ peak_softmax = 0.5 ⇒ aux_conf = 0.0 (uniform)
    ///   - Δlogit → ∞    ⇒ peak_softmax → 1.0 ⇒ aux_conf → 0.5 (saturated)
    ///   - Δlogit ≈ 0.20067 (= ln(0.55/0.45)) ⇒ aux_conf = 0.05
    ///   - Δlogit ≈ 2.19722 (= ln(0.90/0.10)) ⇒ aux_conf = 0.40
    ///
    /// These match the spec §4.2 self-stabilizing-property operating
    /// points exercised in
    /// `sp20_phase1_4_wireup_test::target_hold_pct_behavioral_spot_checks`.
    #[test]
    #[ignore = "requires GPU"]
    fn aux_conf_k2_bounds_and_fixed_points() {
        let tol = 1e-4_f32;

        // Uniform logits ⇒ aux_conf = 0.0.
        let aux_conf = run_per_bar_aux_conf_k2([1.0, 1.0]);
        assert!(aux_conf.abs() < tol,
            "uniform logits ⇒ aux_conf = 0.0; got {aux_conf}");

        // Saturated (Δ = 20 ⇒ peak_softmax ≈ 1.0).
        let aux_conf = run_per_bar_aux_conf_k2([0.0, 20.0]);
        assert!((aux_conf - 0.5).abs() < tol,
            "saturated logits ⇒ aux_conf = 0.5; got {aux_conf}");

        // Spec-warmup operating point.
        let delta_low = (0.55_f32 / 0.45_f32).ln();
        let aux_conf = run_per_bar_aux_conf_k2([0.0, delta_low]);
        assert!((aux_conf - 0.05).abs() < 1e-3,
            "Δlogit=ln(0.55/0.45) ⇒ aux_conf = 0.05; got {aux_conf}");

        // Spec-confident operating point.
        let delta_high = (0.90_f32 / 0.10_f32).ln();
        let aux_conf = run_per_bar_aux_conf_k2([0.0, delta_high]);
        assert!((aux_conf - 0.40).abs() < 1e-3,
            "Δlogit=ln(0.90/0.10) ⇒ aux_conf = 0.40; got {aux_conf}");

        // Symmetry: order-reversed logits give the same aux_conf.
        let a1 = run_per_bar_aux_conf_k2([0.0, delta_high]);
        let a2 = run_per_bar_aux_conf_k2([delta_high, 0.0]);
        assert!((a1 - a2).abs() < tol,
            "symmetric logits ⇒ same aux_conf; got {a1} vs {a2}");

        // Non-stable inputs: large abs values with Δ=0 still uniform.
        let aux_conf = run_per_bar_aux_conf_k2([1000.0, 1000.0]);
        assert!(aux_conf.abs() < tol,
            "large equal logits ⇒ aux_conf = 0.0; got {aux_conf}");
    }

    /// ── Test 2: sum_hold_baseline_over_trade indexing ────────────────
    ///
    /// Buffer = [N=1 env × hold_buffer_size = 5] for clarity. Slots
    /// pre-loaded with values matching their slot index for easy
    /// arithmetic verification.
    ///
    /// At current_t = 5, segment_hold_time = 3:
    ///   - Walks backwards from idx (current_t - 1) % 5 = 4.
    ///   - Reads slots [4, 3, 2] ⇒ sum = 4 + 3 + 2 = 9.
    ///
    /// At current_t = 5, segment_hold_time = 10 (> hold_buffer_size):
    ///   - Walks backwards but bounded by hold_buffer_size = 5.
    ///   - Reads slots [4, 3, 2, 1, 0] ⇒ sum = 0 + 1 + 2 + 3 + 4 = 10.
    ///
    /// At segment_hold_time = 0:
    ///   - Returns 0.0f (defensive guard for empty trade).
    ///
    /// At hold_buffer_size = 0:
    ///   - Returns 0.0f (defensive guard for empty buffer).
    ///
    /// Wrap-around: current_t = 7, hold_buffer_size = 5 ⇒ start idx =
    /// (7 - 1) % 5 = 1; segment_hold_time = 4 walks 1, 0, 4, 3 (wrap).
    /// Sum = 1 + 0 + 4 + 3 = 8.
    #[test]
    #[ignore = "requires GPU"]
    fn sum_hold_baseline_over_trade_indexing() {
        let tol = 1e-5_f32;

        // 1 env × 5-slot buffer. Slot[k] = k.
        let buf: Vec<f32> = (0..5).map(|k| k as f32).collect();

        // Trade of 3 bars closing at t=5.
        let s = run_sum_hold_baseline(&buf, 0, 5, 5, 3.0);
        assert!((s - 9.0).abs() < tol,
            "current_t=5, segment_hold_time=3 ⇒ sum slots [4,3,2] = 9; got {s}");

        // Trade of 10 bars (clamped by buffer size).
        let s = run_sum_hold_baseline(&buf, 0, 5, 5, 10.0);
        assert!((s - 10.0).abs() < tol,
            "current_t=5, segment_hold_time=10 ⇒ all 5 slots = 0+1+2+3+4=10; got {s}");

        // Empty trade.
        let s = run_sum_hold_baseline(&buf, 0, 5, 5, 0.0);
        assert!(s.abs() < tol,
            "segment_hold_time=0 ⇒ defensive guard returns 0; got {s}");

        // Negative trade (defensive).
        let s = run_sum_hold_baseline(&buf, 0, 5, 5, -1.0);
        assert!(s.abs() < tol,
            "segment_hold_time<0 ⇒ defensive guard returns 0; got {s}");

        // Empty buffer.
        let s = run_sum_hold_baseline(&buf, 0, 0, 5, 3.0);
        assert!(s.abs() < tol,
            "hold_buffer_size=0 ⇒ defensive guard returns 0; got {s}");

        // Wrap-around test: current_t=7, size=5, hold_time=4.
        // Start idx = (7-1) % 5 = 1; walks 1, 0, 4, 3.
        let s = run_sum_hold_baseline(&buf, 0, 5, 7, 4.0);
        assert!((s - (1.0 + 0.0 + 4.0 + 3.0)).abs() < tol,
            "current_t=7, hold_time=4 ⇒ sum slots [1,0,4,3] = 8; got {s}");
    }

    /// ── Test 3: per-env stride (multiple envs in row-major buffer) ───
    ///
    /// Buffer = [N=3 envs × 4-slot]. Slot[env, k] = env * 100 + k.
    /// Layout (row-major):
    ///   env=0: [  0,   1,   2,   3]
    ///   env=1: [100, 101, 102, 103]
    ///   env=2: [200, 201, 202, 203]
    ///
    /// At env=1, current_t=4, segment_hold_time=2:
    ///   - Walks backwards from idx (4-1) % 4 = 3.
    ///   - Reads buf[1*4 + 3] = 103, buf[1*4 + 2] = 102 ⇒ sum = 205.
    ///
    /// Crucially: the buffer reader MUST use the env stride (`env *
    /// hold_buffer_size`) to land in env=1's row, not env=0's. A
    /// missing stride would return 1+2=3 (env=0 contents), failing
    /// this assertion clearly.
    #[test]
    #[ignore = "requires GPU"]
    fn sum_hold_baseline_per_env_stride() {
        let tol = 1e-5_f32;
        let n_envs = 3;
        let size = 4;
        let mut buf = vec![0.0_f32; n_envs * size];
        for env in 0..n_envs {
            for k in 0..size {
                buf[env * size + k] = (env * 100 + k) as f32;
            }
        }

        let s = run_sum_hold_baseline(&buf, 1, size as i32, 4, 2.0);
        assert!((s - 205.0).abs() < tol,
            "env=1, t=4, hold=2 ⇒ buf[1,3]+buf[1,2] = 103+102 = 205; got {s}");

        let s = run_sum_hold_baseline(&buf, 2, size as i32, 4, 2.0);
        assert!((s - 405.0).abs() < tol,
            "env=2, t=4, hold=2 ⇒ buf[2,3]+buf[2,2] = 203+202 = 405; got {s}");
    }

    /// ── Test 4: full dual-emission shape (Hold action AND non-Hold) ──
    ///
    /// Mirrors the production per-bar emission site (positioned-non-
    /// event branch ~line 3650 of experience_kernels.cu).
    ///
    /// Per spec §4.2:
    ///   per_bar_opp_cost = -aux_conf × cost_scale
    ///   Path 1 (Hold-only):  centered = per_bar_opp_cost - hold_reward_ema
    ///   Path 2 (always):     buf[..] = per_bar_opp_cost (UNcentered)
    ///
    /// Plan §3.2 reference fixture (line ~1306):
    ///   Fix aux_conf=0.3, cost_scale=0.1, hold_reward_ema=-0.02
    ///   Bar with action=Hold:
    ///     per_bar_centered = -0.3*0.1 - (-0.02) = -0.03 + 0.02 = -0.01
    ///     hold_baseline_buffer = -0.3*0.1 = -0.03 (uncentered)
    ///   Bar with action=Long:
    ///     per_bar_centered = 0
    ///     hold_baseline_buffer = -0.3*0.1 = -0.03 (always written)
    ///
    /// To engineer aux_conf=0.3 ⇒ peak_softmax = 0.8 ⇒ Δlogit =
    /// ln(0.8/0.2) = ln(4) ≈ 1.3862944.
    #[test]
    #[ignore = "requires GPU"]
    fn dual_emission_hold_vs_non_hold() {
        let tol = 1e-4_f32;
        const HOLD_BUFFER_SIZE: i32 = 30;
        const HOLD_ACTION_ID: i32 = 1; // DIR_HOLD
        const LONG_ACTION_ID: i32 = 2; // DIR_LONG

        // Δlogit = ln(0.8/0.2) ⇒ aux_conf = 0.3.
        let delta = (0.8_f32 / 0.2_f32).ln();
        let logits = [0.0_f32, delta];
        let cost_scale = 0.1_f32;
        let hold_reward_ema = -0.02_f32;
        let current_t = 7_i32;

        // Expected:
        //   per_bar_opp_cost = -0.3 × 0.1 = -0.03
        //   Hold:    centered = -0.03 - (-0.02) = -0.01
        //            buf[7 % 30 = 7] = -0.03
        //   Long:    centered = 0.0 (Hold-only path)
        //            buf[7 % 30 = 7] = -0.03 (still written)
        let expected_per_bar = -0.03_f32;
        let expected_hold_centered = -0.01_f32;

        let stream = make_stream();
        let kernel = load_fn(&stream, "sp20_dual_emission_test_kernel");

        let logits_buf = unsafe { MappedF32Buffer::new(2) }.expect("alloc logits");
        logits_buf.write_from_slice(&logits);

        let centered_buf = unsafe { MappedF32Buffer::new(1) }.expect("alloc centered");
        let buf_buf = unsafe { MappedF32Buffer::new(HOLD_BUFFER_SIZE as usize) }
            .expect("alloc hold_baseline_buffer");

        // ── Sub-test A: action = Hold ─────────────────────────────────
        centered_buf.write_from_slice(&[0.0_f32]);
        buf_buf.write_from_slice(&vec![0.0_f32; HOLD_BUFFER_SIZE as usize]);
        unsafe {
            stream
                .launch_builder(&kernel)
                .arg(&logits_buf.dev_ptr)
                .arg(&cost_scale)
                .arg(&hold_reward_ema)
                .arg(&HOLD_ACTION_ID)
                .arg(&HOLD_ACTION_ID) // hold_action_id
                .arg(&current_t)
                .arg(&HOLD_BUFFER_SIZE)
                .arg(&centered_buf.dev_ptr)
                .arg(&buf_buf.dev_ptr)
                .launch(cudarc::driver::LaunchConfig {
                    grid_dim: (1, 1, 1),
                    block_dim: (1, 1, 1),
                    shared_mem_bytes: 0,
                })
                .expect("launch dual_emission Hold");
        }
        stream.synchronize().expect("sync");

        let centered = centered_buf.read_all()[0];
        let buf_full = buf_buf.read_all();
        let buf_slot = buf_full[(current_t % HOLD_BUFFER_SIZE) as usize];

        assert!((centered - expected_hold_centered).abs() < tol,
            "Hold-action centered: expected {expected_hold_centered}, got {centered}");
        assert!((buf_slot - expected_per_bar).abs() < tol,
            "Hold-action Path 2 buffer slot: expected {expected_per_bar}, got {buf_slot}");

        // Non-target slots untouched (still 0.0).
        for k in 0..(HOLD_BUFFER_SIZE as usize) {
            if k as i32 == current_t % HOLD_BUFFER_SIZE { continue; }
            assert!(buf_full[k].abs() < tol,
                "Hold: buffer slot {k} should be untouched 0.0, got {}", buf_full[k]);
        }

        // ── Sub-test B: action = Long ─────────────────────────────────
        centered_buf.write_from_slice(&[999.0_f32]); // sentinel != 0
        buf_buf.write_from_slice(&vec![0.0_f32; HOLD_BUFFER_SIZE as usize]);
        unsafe {
            stream
                .launch_builder(&kernel)
                .arg(&logits_buf.dev_ptr)
                .arg(&cost_scale)
                .arg(&hold_reward_ema)
                .arg(&LONG_ACTION_ID)
                .arg(&HOLD_ACTION_ID)
                .arg(&current_t)
                .arg(&HOLD_BUFFER_SIZE)
                .arg(&centered_buf.dev_ptr)
                .arg(&buf_buf.dev_ptr)
                .launch(cudarc::driver::LaunchConfig {
                    grid_dim: (1, 1, 1),
                    block_dim: (1, 1, 1),
                    shared_mem_bytes: 0,
                })
                .expect("launch dual_emission Long");
        }
        stream.synchronize().expect("sync");

        let centered = centered_buf.read_all()[0];
        let buf_full = buf_buf.read_all();
        let buf_slot = buf_full[(current_t % HOLD_BUFFER_SIZE) as usize];

        assert!(centered.abs() < tol,
            "Long-action Path 1 (Hold-only) ⇒ centered = 0; got {centered}");
        assert!((buf_slot - expected_per_bar).abs() < tol,
            "Long-action Path 2 ⇒ buffer ALWAYS written; expected {expected_per_bar}, got {buf_slot}");
    }
}
