//! Bootstrap statistical methods for prediction market trading.
//!
//! Non-parametric confidence intervals via IID, block, circular, and stationary
//! bootstrap. Also includes jackknife bias estimation and hypothesis testing.

use pyo3::prelude::*;

// ---------------------------------------------------------------------------
// Xoshiro256++ PRNG (copied from simulation.rs to avoid coupling)
// ---------------------------------------------------------------------------

struct Xoshiro256PlusPlus {
    s: [u64; 4],
}

impl Xoshiro256PlusPlus {
    fn new(seed: u64) -> Self {
        let mut state = seed;
        let mut s = [0u64; 4];
        for slot in &mut s {
            state = state.wrapping_add(0x9E3779B97F4A7C15);
            let mut z = state;
            z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
            z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
            z ^= z >> 31;
            *slot = z;
        }
        Self { s }
    }

    #[inline]
    fn next_u64(&mut self) -> u64 {
        let result = (self.s[0].wrapping_add(self.s[3]))
            .rotate_left(23)
            .wrapping_add(self.s[0]);
        let t = self.s[1] << 17;
        self.s[2] ^= self.s[0];
        self.s[3] ^= self.s[1];
        self.s[1] ^= self.s[2];
        self.s[0] ^= self.s[3];
        self.s[2] ^= t;
        self.s[3] = self.s[3].rotate_left(45);
        result
    }

    #[inline]
    fn next_f64(&mut self) -> f64 {
        let bits = self.next_u64() >> 11;
        bits as f64 * (1.0 / (1u64 << 53) as f64)
    }

    #[inline]
    fn next_usize(&mut self, n: usize) -> usize {
        (self.next_f64() * n as f64) as usize % n
    }
}

// ---------------------------------------------------------------------------
// Result types
// ---------------------------------------------------------------------------

#[pyclass(frozen, get_all)]
#[derive(Debug, Clone)]
pub struct BootstrapResult {
    pub point_estimate: f64,
    pub ci_lower: f64,
    pub ci_upper: f64,
    pub std_error: f64,
    pub n_resamples: usize,
    pub bias: f64,
}

#[pymethods]
impl BootstrapResult {
    fn __repr__(&self) -> String {
        format!(
            "BootstrapResult(est={:.6}, ci=[{:.6}, {:.6}], se={:.6}, n={}, bias={:.6})",
            self.point_estimate, self.ci_lower, self.ci_upper,
            self.std_error, self.n_resamples, self.bias,
        )
    }
}

#[pyclass(frozen, get_all)]
#[derive(Debug, Clone)]
pub struct BootstrapHypothesisResult {
    pub test_statistic: f64,
    pub p_value: f64,
    pub ci_lower: f64,
    pub ci_upper: f64,
    pub reject: bool,
}

#[pymethods]
impl BootstrapHypothesisResult {
    fn __repr__(&self) -> String {
        format!(
            "BootstrapHypothesisResult(stat={:.6}, p={:.6}, reject={})",
            self.test_statistic, self.p_value, self.reject,
        )
    }
}

#[pyclass(frozen, get_all)]
#[derive(Debug, Clone)]
pub struct JackknifeResult {
    pub estimate: f64,
    pub bias: f64,
    pub std_error: f64,
    pub pseudovalues: Vec<f64>,
}

#[pymethods]
impl JackknifeResult {
    fn __repr__(&self) -> String {
        format!(
            "JackknifeResult(est={:.6}, bias={:.6}, se={:.6}, n={})",
            self.estimate, self.bias, self.std_error, self.pseudovalues.len(),
        )
    }
}

// ---------------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------------

#[inline]
fn finite_or_zero(v: f64) -> f64 {
    if v.is_finite() { v } else { 0.0 }
}

#[inline]
fn mean(data: &[f64]) -> f64 {
    if data.is_empty() { return 0.0; }
    data.iter().sum::<f64>() / data.len() as f64
}

#[inline]
fn sharpe(returns: &[f64]) -> f64 {
    let n = returns.len();
    if n < 2 { return 0.0; }
    let m = mean(returns);
    let var = returns.iter().map(|r| (r - m).powi(2)).sum::<f64>() / (n - 1) as f64;
    let std = var.sqrt();
    if std < 1e-15 { return 0.0; }
    finite_or_zero(m / std)
}

fn var_at_level(sorted_data: &[f64], level: f64) -> f64 {
    if sorted_data.is_empty() { return 0.0; }
    let idx = (level * sorted_data.len() as f64).floor() as usize;
    let idx = idx.min(sorted_data.len() - 1);
    sorted_data[idx]
}

fn pearson_correlation(x: &[f64], y: &[f64]) -> f64 {
    let n = x.len().min(y.len());
    if n < 2 { return 0.0; }
    let mx = x[..n].iter().sum::<f64>() / n as f64;
    let my = y[..n].iter().sum::<f64>() / n as f64;
    let mut cov = 0.0;
    let mut vx = 0.0;
    let mut vy = 0.0;
    for i in 0..n {
        let dx = x[i] - mx;
        let dy = y[i] - my;
        cov += dx * dy;
        vx += dx * dx;
        vy += dy * dy;
    }
    let denom = (vx * vy).sqrt();
    if denom < 1e-15 { return 0.0; }
    finite_or_zero(cov / denom)
}

fn resample_indices(rng: &mut Xoshiro256PlusPlus, n: usize) -> Vec<usize> {
    (0..n).map(|_| rng.next_usize(n)).collect()
}

fn percentile_ci(sorted: &mut Vec<f64>, confidence: f64) -> (f64, f64) {
    sorted.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
    let n = sorted.len();
    if n == 0 { return (0.0, 0.0); }
    let alpha = 1.0 - confidence;
    let lo_idx = ((alpha / 2.0) * n as f64).floor() as usize;
    let hi_idx = ((1.0 - alpha / 2.0) * n as f64).ceil() as usize;
    let lo_idx = lo_idx.min(n - 1);
    let hi_idx = hi_idx.min(n - 1);
    (sorted[lo_idx], sorted[hi_idx])
}

fn compute_statistic(data: &[f64], stat_name: &str) -> f64 {
    match stat_name {
        "sharpe" => sharpe(data),
        "var" => {
            let mut sorted = data.to_vec();
            sorted.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
            var_at_level(&sorted, 0.05)
        }
        _ => mean(data), // "mean" and fallback
    }
}

fn std_dev(vals: &[f64]) -> f64 {
    let n = vals.len();
    if n < 2 { return 0.0; }
    let m = mean(vals);
    let var = vals.iter().map(|v| (v - m).powi(2)).sum::<f64>() / (n - 1) as f64;
    var.sqrt()
}

// ---------------------------------------------------------------------------
// Bootstrap functions
// ---------------------------------------------------------------------------

fn bootstrap_mean_impl(
    data: &[f64], n_resamples: usize, confidence: f64, seed: u64,
) -> BootstrapResult {
    if data.is_empty() {
        return BootstrapResult { point_estimate: 0.0, ci_lower: 0.0, ci_upper: 0.0, std_error: 0.0, n_resamples, bias: 0.0 };
    }
    let point = mean(data);
    let mut rng = Xoshiro256PlusPlus::new(seed);
    let mut estimates = Vec::with_capacity(n_resamples);
    for _ in 0..n_resamples {
        let idx = resample_indices(&mut rng, data.len());
        let sample: Vec<f64> = idx.iter().map(|&i| data[i]).collect();
        estimates.push(mean(&sample));
    }
    let se = std_dev(&estimates);
    let bias = mean(&estimates) - point;
    let (lo, hi) = percentile_ci(&mut estimates, confidence);
    BootstrapResult { point_estimate: point, ci_lower: lo, ci_upper: hi, std_error: se, n_resamples, bias: finite_or_zero(bias) }
}

#[pyfunction]
#[pyo3(signature = (data, n_resamples=10000, confidence=0.95, seed=42))]
fn bootstrap_mean(data: Vec<f64>, n_resamples: usize, confidence: f64, seed: u64) -> BootstrapResult {
    bootstrap_mean_impl(&data, n_resamples, confidence, seed)
}

fn bootstrap_sharpe_impl(
    returns: &[f64], n_resamples: usize, confidence: f64, seed: u64,
) -> BootstrapResult {
    if returns.len() < 2 {
        return BootstrapResult { point_estimate: 0.0, ci_lower: 0.0, ci_upper: 0.0, std_error: 0.0, n_resamples, bias: 0.0 };
    }
    let point = sharpe(returns);
    let mut rng = Xoshiro256PlusPlus::new(seed);
    let mut estimates = Vec::with_capacity(n_resamples);
    for _ in 0..n_resamples {
        let idx = resample_indices(&mut rng, returns.len());
        let sample: Vec<f64> = idx.iter().map(|&i| returns[i]).collect();
        estimates.push(sharpe(&sample));
    }
    let se = std_dev(&estimates);
    let bias = mean(&estimates) - point;
    let (lo, hi) = percentile_ci(&mut estimates, confidence);
    BootstrapResult { point_estimate: point, ci_lower: lo, ci_upper: hi, std_error: se, n_resamples, bias: finite_or_zero(bias) }
}

#[pyfunction]
#[pyo3(signature = (returns, n_resamples=10000, confidence=0.95, seed=42))]
fn bootstrap_sharpe(returns: Vec<f64>, n_resamples: usize, confidence: f64, seed: u64) -> BootstrapResult {
    bootstrap_sharpe_impl(&returns, n_resamples, confidence, seed)
}

fn bootstrap_var_impl(
    returns: &[f64], var_level: f64, n_resamples: usize, confidence: f64, seed: u64,
) -> BootstrapResult {
    if returns.is_empty() {
        return BootstrapResult { point_estimate: 0.0, ci_lower: 0.0, ci_upper: 0.0, std_error: 0.0, n_resamples, bias: 0.0 };
    }
    let mut sorted = returns.to_vec();
    sorted.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
    let point = var_at_level(&sorted, var_level);
    let mut rng = Xoshiro256PlusPlus::new(seed);
    let mut estimates = Vec::with_capacity(n_resamples);
    for _ in 0..n_resamples {
        let idx = resample_indices(&mut rng, returns.len());
        let mut sample: Vec<f64> = idx.iter().map(|&i| returns[i]).collect();
        sample.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
        estimates.push(var_at_level(&sample, var_level));
    }
    let se = std_dev(&estimates);
    let bias = mean(&estimates) - point;
    let (lo, hi) = percentile_ci(&mut estimates, confidence);
    BootstrapResult { point_estimate: point, ci_lower: lo, ci_upper: hi, std_error: se, n_resamples, bias: finite_or_zero(bias) }
}

#[pyfunction]
#[pyo3(signature = (returns, var_level=0.05, n_resamples=10000, confidence=0.95, seed=42))]
fn bootstrap_var(returns: Vec<f64>, var_level: f64, n_resamples: usize, confidence: f64, seed: u64) -> BootstrapResult {
    bootstrap_var_impl(&returns, var_level, n_resamples, confidence, seed)
}

fn bootstrap_correlation_impl(
    x: &[f64], y: &[f64], n_resamples: usize, confidence: f64, seed: u64,
) -> BootstrapResult {
    let n = x.len().min(y.len());
    if n < 2 {
        return BootstrapResult { point_estimate: 0.0, ci_lower: 0.0, ci_upper: 0.0, std_error: 0.0, n_resamples, bias: 0.0 };
    }
    let point = pearson_correlation(x, y);
    let mut rng = Xoshiro256PlusPlus::new(seed);
    let mut estimates = Vec::with_capacity(n_resamples);
    for _ in 0..n_resamples {
        let idx = resample_indices(&mut rng, n);
        let sx: Vec<f64> = idx.iter().map(|&i| x[i]).collect();
        let sy: Vec<f64> = idx.iter().map(|&i| y[i]).collect();
        estimates.push(pearson_correlation(&sx, &sy));
    }
    let se = std_dev(&estimates);
    let bias = mean(&estimates) - point;
    let (lo, hi) = percentile_ci(&mut estimates, confidence);
    BootstrapResult { point_estimate: point, ci_lower: lo, ci_upper: hi, std_error: se, n_resamples, bias: finite_or_zero(bias) }
}

#[pyfunction]
#[pyo3(signature = (x, y, n_resamples=10000, confidence=0.95, seed=42))]
fn bootstrap_correlation(x: Vec<f64>, y: Vec<f64>, n_resamples: usize, confidence: f64, seed: u64) -> BootstrapResult {
    bootstrap_correlation_impl(&x, &y, n_resamples, confidence, seed)
}

fn bootstrap_edge_impl(
    predictions: &[f64], outcomes: &[f64], n_resamples: usize, confidence: f64, seed: u64,
) -> BootstrapResult {
    let n = predictions.len().min(outcomes.len());
    if n == 0 {
        return BootstrapResult { point_estimate: 0.0, ci_lower: 0.0, ci_upper: 0.0, std_error: 0.0, n_resamples, bias: 0.0 };
    }
    let edges: Vec<f64> = (0..n).map(|i| outcomes[i] - predictions[i]).collect();
    let point = mean(&edges);
    let mut rng = Xoshiro256PlusPlus::new(seed);
    let mut estimates = Vec::with_capacity(n_resamples);
    for _ in 0..n_resamples {
        let idx = resample_indices(&mut rng, n);
        let sample: Vec<f64> = idx.iter().map(|&i| edges[i]).collect();
        estimates.push(mean(&sample));
    }
    let se = std_dev(&estimates);
    let bias = mean(&estimates) - point;
    let (lo, hi) = percentile_ci(&mut estimates, confidence);
    BootstrapResult { point_estimate: point, ci_lower: lo, ci_upper: hi, std_error: se, n_resamples, bias: finite_or_zero(bias) }
}

#[pyfunction]
#[pyo3(signature = (predictions, outcomes, n_resamples=10000, confidence=0.95, seed=42))]
fn bootstrap_edge(predictions: Vec<f64>, outcomes: Vec<f64>, n_resamples: usize, confidence: f64, seed: u64) -> BootstrapResult {
    bootstrap_edge_impl(&predictions, &outcomes, n_resamples, confidence, seed)
}

fn block_bootstrap_impl(
    data: &[f64], block_size: usize, n_resamples: usize, confidence: f64, seed: u64,
) -> BootstrapResult {
    let n = data.len();
    if n == 0 || block_size == 0 {
        return BootstrapResult { point_estimate: 0.0, ci_lower: 0.0, ci_upper: 0.0, std_error: 0.0, n_resamples, bias: 0.0 };
    }
    let point = mean(data);
    let mut rng = Xoshiro256PlusPlus::new(seed);
    let mut estimates = Vec::with_capacity(n_resamples);
    let n_blocks = (n + block_size - 1) / block_size;
    let max_start = if n > block_size { n - block_size } else { 0 };

    for _ in 0..n_resamples {
        let mut sample = Vec::with_capacity(n);
        for _ in 0..n_blocks {
            let start = if max_start > 0 { rng.next_usize(max_start + 1) } else { 0 };
            for j in 0..block_size {
                if sample.len() >= n { break; }
                sample.push(data[(start + j).min(n - 1)]);
            }
        }
        estimates.push(mean(&sample));
    }
    let se = std_dev(&estimates);
    let bias = mean(&estimates) - point;
    let (lo, hi) = percentile_ci(&mut estimates, confidence);
    BootstrapResult { point_estimate: point, ci_lower: lo, ci_upper: hi, std_error: se, n_resamples, bias: finite_or_zero(bias) }
}

#[pyfunction]
#[pyo3(signature = (data, block_size, n_resamples=10000, confidence=0.95, seed=42))]
fn block_bootstrap(data: Vec<f64>, block_size: usize, n_resamples: usize, confidence: f64, seed: u64) -> BootstrapResult {
    block_bootstrap_impl(&data, block_size, n_resamples, confidence, seed)
}

fn circular_block_bootstrap_impl(
    data: &[f64], block_size: usize, n_resamples: usize, confidence: f64, seed: u64,
) -> BootstrapResult {
    let n = data.len();
    if n == 0 || block_size == 0 {
        return BootstrapResult { point_estimate: 0.0, ci_lower: 0.0, ci_upper: 0.0, std_error: 0.0, n_resamples, bias: 0.0 };
    }
    let point = mean(data);
    let mut rng = Xoshiro256PlusPlus::new(seed);
    let mut estimates = Vec::with_capacity(n_resamples);
    let n_blocks = (n + block_size - 1) / block_size;

    for _ in 0..n_resamples {
        let mut sample = Vec::with_capacity(n);
        for _ in 0..n_blocks {
            let start = rng.next_usize(n);
            for j in 0..block_size {
                if sample.len() >= n { break; }
                sample.push(data[(start + j) % n]);
            }
        }
        estimates.push(mean(&sample));
    }
    let se = std_dev(&estimates);
    let bias = mean(&estimates) - point;
    let (lo, hi) = percentile_ci(&mut estimates, confidence);
    BootstrapResult { point_estimate: point, ci_lower: lo, ci_upper: hi, std_error: se, n_resamples, bias: finite_or_zero(bias) }
}

#[pyfunction]
#[pyo3(signature = (data, block_size, n_resamples=10000, confidence=0.95, seed=42))]
fn circular_block_bootstrap(data: Vec<f64>, block_size: usize, n_resamples: usize, confidence: f64, seed: u64) -> BootstrapResult {
    circular_block_bootstrap_impl(&data, block_size, n_resamples, confidence, seed)
}

fn stationary_bootstrap_impl(
    data: &[f64], expected_block_size: f64, n_resamples: usize, confidence: f64, seed: u64,
) -> BootstrapResult {
    let n = data.len();
    if n == 0 || expected_block_size <= 0.0 {
        return BootstrapResult { point_estimate: 0.0, ci_lower: 0.0, ci_upper: 0.0, std_error: 0.0, n_resamples, bias: 0.0 };
    }
    let point = mean(data);
    let p = 1.0 / expected_block_size; // geometric probability of starting new block
    let mut rng = Xoshiro256PlusPlus::new(seed);
    let mut estimates = Vec::with_capacity(n_resamples);

    for _ in 0..n_resamples {
        let mut sample = Vec::with_capacity(n);
        let mut idx = rng.next_usize(n);
        for _ in 0..n {
            sample.push(data[idx]);
            if rng.next_f64() < p {
                idx = rng.next_usize(n);
            } else {
                idx = (idx + 1) % n;
            }
        }
        estimates.push(mean(&sample));
    }
    let se = std_dev(&estimates);
    let bias = mean(&estimates) - point;
    let (lo, hi) = percentile_ci(&mut estimates, confidence);
    BootstrapResult { point_estimate: point, ci_lower: lo, ci_upper: hi, std_error: se, n_resamples, bias: finite_or_zero(bias) }
}

#[pyfunction]
#[pyo3(signature = (data, expected_block_size, n_resamples=10000, confidence=0.95, seed=42))]
fn stationary_bootstrap(data: Vec<f64>, expected_block_size: f64, n_resamples: usize, confidence: f64, seed: u64) -> BootstrapResult {
    stationary_bootstrap_impl(&data, expected_block_size, n_resamples, confidence, seed)
}

fn bootstrap_hypothesis_test_impl(
    sample_a: &[f64], sample_b: &[f64], n_resamples: usize, seed: u64,
) -> BootstrapHypothesisResult {
    let na = sample_a.len();
    let nb = sample_b.len();
    if na == 0 || nb == 0 {
        return BootstrapHypothesisResult { test_statistic: 0.0, p_value: 1.0, ci_lower: 0.0, ci_upper: 0.0, reject: false };
    }
    let observed_diff = mean(sample_a) - mean(sample_b);

    // Pooled data for permutation test
    let mut pooled: Vec<f64> = Vec::with_capacity(na + nb);
    pooled.extend_from_slice(sample_a);
    pooled.extend_from_slice(sample_b);

    let mut rng = Xoshiro256PlusPlus::new(seed);
    let mut count_extreme = 0usize;
    let mut diffs = Vec::with_capacity(n_resamples);

    for _ in 0..n_resamples {
        // Fisher-Yates partial shuffle: pick na elements
        let mut perm = pooled.clone();
        for i in 0..na {
            let j = i + rng.next_usize(perm.len() - i);
            perm.swap(i, j);
        }
        let perm_a_mean = mean(&perm[..na]);
        let perm_b_mean = mean(&perm[na..]);
        let diff = perm_a_mean - perm_b_mean;
        diffs.push(diff);
        if diff.abs() >= observed_diff.abs() {
            count_extreme += 1;
        }
    }

    let p_value = count_extreme as f64 / n_resamples as f64;
    let (lo, hi) = percentile_ci(&mut diffs, 0.95);

    BootstrapHypothesisResult {
        test_statistic: observed_diff,
        p_value,
        ci_lower: lo,
        ci_upper: hi,
        reject: p_value < 0.05,
    }
}

#[pyfunction]
#[pyo3(signature = (sample_a, sample_b, n_resamples=10000, seed=42))]
fn bootstrap_hypothesis_test(sample_a: Vec<f64>, sample_b: Vec<f64>, n_resamples: usize, seed: u64) -> BootstrapHypothesisResult {
    bootstrap_hypothesis_test_impl(&sample_a, &sample_b, n_resamples, seed)
}

fn bootstrap_prediction_interval_impl(
    residuals: &[f64], point_forecast: f64, n_resamples: usize, confidence: f64, seed: u64,
) -> BootstrapResult {
    if residuals.is_empty() {
        return BootstrapResult { point_estimate: point_forecast, ci_lower: point_forecast, ci_upper: point_forecast, std_error: 0.0, n_resamples, bias: 0.0 };
    }
    let mut rng = Xoshiro256PlusPlus::new(seed);
    let mut forecasts = Vec::with_capacity(n_resamples);
    for _ in 0..n_resamples {
        let idx = rng.next_usize(residuals.len());
        forecasts.push(point_forecast + residuals[idx]);
    }
    let se = std_dev(&forecasts);
    let bias = mean(&forecasts) - point_forecast;
    let (lo, hi) = percentile_ci(&mut forecasts, confidence);
    BootstrapResult { point_estimate: point_forecast, ci_lower: lo, ci_upper: hi, std_error: se, n_resamples, bias: finite_or_zero(bias) }
}

#[pyfunction]
#[pyo3(signature = (residuals, point_forecast, n_resamples=10000, confidence=0.95, seed=42))]
fn bootstrap_prediction_interval(residuals: Vec<f64>, point_forecast: f64, n_resamples: usize, confidence: f64, seed: u64) -> BootstrapResult {
    bootstrap_prediction_interval_impl(&residuals, point_forecast, n_resamples, confidence, seed)
}

fn jackknife_bias_impl(data: &[f64], statistic: &str) -> JackknifeResult {
    let n = data.len();
    if n < 2 {
        let est = if n == 1 { compute_statistic(data, statistic) } else { 0.0 };
        return JackknifeResult { estimate: est, bias: 0.0, std_error: 0.0, pseudovalues: vec![] };
    }
    let full_stat = compute_statistic(data, statistic);
    let mut leave_one_out = Vec::with_capacity(n);
    let mut pseudovalues = Vec::with_capacity(n);
    let nf = n as f64;

    for i in 0..n {
        let subset: Vec<f64> = data.iter().enumerate()
            .filter(|&(j, _)| j != i)
            .map(|(_, &v)| v)
            .collect();
        let jack_stat = compute_statistic(&subset, statistic);
        leave_one_out.push(jack_stat);
        pseudovalues.push(nf * full_stat - (nf - 1.0) * jack_stat);
    }

    let jack_mean = mean(&leave_one_out);
    let bias = (nf - 1.0) * (jack_mean - full_stat);
    let jack_var = leave_one_out.iter()
        .map(|v| (v - jack_mean).powi(2))
        .sum::<f64>() * (nf - 1.0) / nf;
    let se = jack_var.sqrt();

    JackknifeResult {
        estimate: full_stat,
        bias: finite_or_zero(bias),
        std_error: finite_or_zero(se),
        pseudovalues,
    }
}

#[pyfunction]
#[pyo3(signature = (data, statistic="mean"))]
fn jackknife_bias(data: Vec<f64>, statistic: &str) -> JackknifeResult {
    jackknife_bias_impl(&data, statistic)
}

// ---------------------------------------------------------------------------
// Registration
// ---------------------------------------------------------------------------

pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
    m.add_class::<BootstrapResult>()?;
    m.add_class::<BootstrapHypothesisResult>()?;
    m.add_class::<JackknifeResult>()?;
    m.add_function(wrap_pyfunction!(bootstrap_mean, m)?)?;
    m.add_function(wrap_pyfunction!(bootstrap_sharpe, m)?)?;
    m.add_function(wrap_pyfunction!(bootstrap_var, m)?)?;
    m.add_function(wrap_pyfunction!(bootstrap_correlation, m)?)?;
    m.add_function(wrap_pyfunction!(bootstrap_edge, m)?)?;
    m.add_function(wrap_pyfunction!(block_bootstrap, m)?)?;
    m.add_function(wrap_pyfunction!(circular_block_bootstrap, m)?)?;
    m.add_function(wrap_pyfunction!(stationary_bootstrap, m)?)?;
    m.add_function(wrap_pyfunction!(bootstrap_hypothesis_test, m)?)?;
    m.add_function(wrap_pyfunction!(bootstrap_prediction_interval, m)?)?;
    m.add_function(wrap_pyfunction!(jackknife_bias, m)?)?;
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn bootstrap_mean_known() {
        let data: Vec<f64> = (0..100).map(|i| i as f64).collect();
        let r = bootstrap_mean_impl(&data, 1000, 0.95, 42);
        assert!((r.point_estimate - 49.5).abs() < 0.01);
        assert!(r.ci_lower <= r.point_estimate);
        assert!(r.ci_upper >= r.point_estimate);
    }

    #[test]
    fn bootstrap_sharpe_positive() {
        let returns: Vec<f64> = (0..100).map(|i| 0.01 + 0.001 * (i as f64 % 5.0)).collect();
        let r = bootstrap_sharpe_impl(&returns, 1000, 0.95, 42);
        assert!(r.point_estimate > 0.0);
    }

    #[test]
    fn hypothesis_test_same_dist() {
        let a: Vec<f64> = (0..50).map(|i| i as f64).collect();
        let b: Vec<f64> = (0..50).map(|i| i as f64).collect();
        let r = bootstrap_hypothesis_test_impl(&a, &b, 1000, 42);
        assert!(r.p_value > 0.05);
        assert!(!r.reject);
    }

    #[test]
    fn jackknife_mean_bias() {
        let data: Vec<f64> = (1..=10).map(|i| i as f64).collect();
        let r = jackknife_bias_impl(&data, "mean");
        assert!((r.estimate - 5.5).abs() < 0.01);
        assert!(r.bias.abs() < 0.01); // mean is unbiased
        assert_eq!(r.pseudovalues.len(), 10);
    }

    #[test]
    fn block_bootstrap_runs() {
        let data: Vec<f64> = (0..50).map(|i| (i as f64).sin()).collect();
        let r = block_bootstrap_impl(&data, 5, 500, 0.95, 42);
        assert!(r.ci_lower <= r.ci_upper);
    }

    #[test]
    fn stationary_bootstrap_runs() {
        let data: Vec<f64> = (0..50).map(|i| (i as f64).sin()).collect();
        let r = stationary_bootstrap_impl(&data, 5.0, 500, 0.95, 42);
        assert!(r.ci_lower <= r.ci_upper);
    }
}
