// SPDX-License-Identifier: MIT OR Apache-2.0

#![forbid(unsafe_code)]

use crate::model::CostModel;
use cpd_core::{
    CachePolicy, CpdError, DTypeView, MemoryLayout, MissingSupport, ReproMode, TimeSeriesView,
    prefix_sum_squares, prefix_sum_squares_kahan, prefix_sums, prefix_sums_kahan,
};
use std::cell::RefCell;

const VAR_FLOOR: f64 = f64::EPSILON * 1e6;
const FULL_COV_RIDGE_RELATIVE: f64 = 1.0e-6;
const FULL_COV_MAX_JITTER_TRIALS: usize = 8;

thread_local! {
    static FULL_COV_WORKSPACE: RefCell<Vec<f64>> = RefCell::new(Vec::new());
}

/// Gaussian segment cost model with MLE mean and variance.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CostNormalMeanVar {
    pub repro_mode: ReproMode,
}

impl CostNormalMeanVar {
    pub const fn new(repro_mode: ReproMode) -> Self {
        Self { repro_mode }
    }
}

impl Default for CostNormalMeanVar {
    fn default() -> Self {
        Self::new(ReproMode::Balanced)
    }
}

/// Prefix-stat cache for O(1) Normal segment-cost queries.
#[derive(Clone, Debug, PartialEq)]
pub struct NormalCache {
    prefix_sum: Vec<f64>,
    prefix_sum_sq: Vec<f64>,
    n: usize,
    d: usize,
}

impl NormalCache {
    fn prefix_len_per_dim(&self) -> usize {
        self.n + 1
    }

    fn dim_offset(&self, dim: usize) -> usize {
        dim * self.prefix_len_per_dim()
    }
}

/// Gaussian segment cost model using a full covariance estimate for `d > 1`.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CostNormalFullCov {
    pub repro_mode: ReproMode,
}

impl CostNormalFullCov {
    pub const fn new(repro_mode: ReproMode) -> Self {
        Self { repro_mode }
    }
}

impl Default for CostNormalFullCov {
    fn default() -> Self {
        Self::new(ReproMode::Balanced)
    }
}

/// Prefix-stat cache for full-covariance Normal segment-cost queries.
#[derive(Clone, Debug, PartialEq)]
pub struct NormalFullCovCache {
    prefix_sum: Vec<f64>,
    prefix_cross: Vec<f64>,
    n: usize,
    d: usize,
}

impl NormalFullCovCache {
    fn prefix_len_per_dim(&self) -> usize {
        self.n + 1
    }

    fn dim_offset(&self, dim: usize) -> usize {
        dim * self.prefix_len_per_dim()
    }

    fn pair_offset(&self, i: usize, j: usize) -> usize {
        let pair = upper_triangular_index(self.d, i, j);
        pair * self.prefix_len_per_dim()
    }
}

fn strided_linear_index(
    t: usize,
    dim: usize,
    row_stride: isize,
    col_stride: isize,
    len: usize,
) -> Result<usize, CpdError> {
    let t_isize = isize::try_from(t).map_err(|_| {
        CpdError::invalid_input(format!(
            "strided index overflow: t={t} does not fit into isize"
        ))
    })?;
    let dim_isize = isize::try_from(dim).map_err(|_| {
        CpdError::invalid_input(format!(
            "strided index overflow: dim={dim} does not fit into isize"
        ))
    })?;

    let index = t_isize
        .checked_mul(row_stride)
        .and_then(|left| {
            dim_isize
                .checked_mul(col_stride)
                .and_then(|right| left.checked_add(right))
        })
        .ok_or_else(|| {
            CpdError::invalid_input(format!(
                "strided index overflow at t={t}, dim={dim}, row_stride={row_stride}, col_stride={col_stride}"
            ))
        })?;

    let index_usize = usize::try_from(index).map_err(|_| {
        CpdError::invalid_input(format!(
            "strided index negative at t={t}, dim={dim}: idx={index}"
        ))
    })?;

    if index_usize >= len {
        return Err(CpdError::invalid_input(format!(
            "strided index out of bounds at t={t}, dim={dim}: idx={index_usize}, len={len}"
        )));
    }

    Ok(index_usize)
}

fn read_value(x: &TimeSeriesView<'_>, t: usize, dim: usize) -> Result<f64, CpdError> {
    match (x.values, x.layout) {
        (DTypeView::F32(values), MemoryLayout::CContiguous) => {
            let idx = t
                .checked_mul(x.d)
                .and_then(|base| base.checked_add(dim))
                .ok_or_else(|| CpdError::invalid_input("C-contiguous index overflow"))?;
            values
                .get(idx)
                .map(|v| f64::from(*v))
                .ok_or_else(|| CpdError::invalid_input("C-contiguous index out of bounds"))
        }
        (DTypeView::F64(values), MemoryLayout::CContiguous) => {
            let idx = t
                .checked_mul(x.d)
                .and_then(|base| base.checked_add(dim))
                .ok_or_else(|| CpdError::invalid_input("C-contiguous index overflow"))?;
            values
                .get(idx)
                .copied()
                .ok_or_else(|| CpdError::invalid_input("C-contiguous index out of bounds"))
        }
        (DTypeView::F32(values), MemoryLayout::FContiguous) => {
            let idx = dim
                .checked_mul(x.n)
                .and_then(|base| base.checked_add(t))
                .ok_or_else(|| CpdError::invalid_input("F-contiguous index overflow"))?;
            values
                .get(idx)
                .map(|v| f64::from(*v))
                .ok_or_else(|| CpdError::invalid_input("F-contiguous index out of bounds"))
        }
        (DTypeView::F64(values), MemoryLayout::FContiguous) => {
            let idx = dim
                .checked_mul(x.n)
                .and_then(|base| base.checked_add(t))
                .ok_or_else(|| CpdError::invalid_input("F-contiguous index overflow"))?;
            values
                .get(idx)
                .copied()
                .ok_or_else(|| CpdError::invalid_input("F-contiguous index out of bounds"))
        }
        (
            DTypeView::F32(values),
            MemoryLayout::Strided {
                row_stride,
                col_stride,
            },
        ) => {
            let idx = strided_linear_index(t, dim, row_stride, col_stride, values.len())?;
            Ok(f64::from(values[idx]))
        }
        (
            DTypeView::F64(values),
            MemoryLayout::Strided {
                row_stride,
                col_stride,
            },
        ) => {
            let idx = strided_linear_index(t, dim, row_stride, col_stride, values.len())?;
            Ok(values[idx])
        }
    }
}

fn cache_overflow_err(n: usize, d: usize) -> CpdError {
    CpdError::resource_limit(format!(
        "cache size overflow while planning NormalCache for n={n}, d={d}"
    ))
}

fn normalize_variance(raw_var: f64) -> f64 {
    if raw_var.is_nan() || raw_var <= VAR_FLOOR {
        VAR_FLOOR
    } else if raw_var == f64::INFINITY {
        f64::MAX
    } else {
        raw_var
    }
}

fn triangular_pair_count(d: usize) -> Option<usize> {
    d.checked_mul(d.checked_add(1)?)?.checked_div(2)
}

fn upper_triangular_index(d: usize, i: usize, j: usize) -> usize {
    debug_assert!(i < d);
    debug_assert!(j < d);
    debug_assert!(i <= j);
    let row_prefix = i
        .checked_mul(d)
        .and_then(|left| {
            i.checked_mul(i.saturating_sub(1))
                .and_then(|value| value.checked_div(2))
                .and_then(|tri| left.checked_sub(tri))
        })
        .expect("upper-triangle index should fit usize");
    row_prefix
        .checked_add(j - i)
        .expect("upper-triangle index should fit usize")
}

fn normalize_covariance_entry(raw: f64) -> f64 {
    if raw.is_nan() {
        0.0
    } else if raw == f64::INFINITY {
        f64::MAX
    } else if raw == f64::NEG_INFINITY {
        -f64::MAX
    } else {
        raw
    }
}

fn full_cov_ridge(trace: f64, d: usize, segment_len: f64) -> f64 {
    let mean_var = normalize_variance(trace / d as f64);
    let mut ridge = (mean_var * FULL_COV_RIDGE_RELATIVE).max(VAR_FLOOR);
    if segment_len < d as f64 {
        ridge *= 1.0 + (d as f64 - segment_len) / d as f64;
    }
    if ridge.is_finite() { ridge } else { f64::MAX }
}

fn cholesky_logdet_in_place(matrix: &mut [f64], d: usize) -> Option<f64> {
    for i in 0..d {
        for j in 0..=i {
            let mut sum = matrix[i * d + j];
            for k in 0..j {
                sum -= matrix[i * d + k] * matrix[j * d + k];
            }

            if i == j {
                if !sum.is_finite() || sum <= VAR_FLOOR {
                    return None;
                }
                matrix[i * d + i] = sum.sqrt();
            } else {
                let diag = matrix[j * d + j];
                if !diag.is_finite() || diag <= VAR_FLOOR {
                    return None;
                }
                let value = sum / diag;
                if !value.is_finite() {
                    return None;
                }
                matrix[i * d + j] = value;
            }
        }
    }

    let mut log_det = 0.0;
    for i in 0..d {
        let diag = matrix[i * d + i];
        if !diag.is_finite() || diag <= 0.0 {
            return None;
        }
        log_det += 2.0 * diag.ln();
    }

    if log_det.is_finite() {
        Some(log_det)
    } else {
        None
    }
}

fn regularized_logdet_with_scratch(
    covariance: &mut [f64],
    scratch: &mut [f64],
    d: usize,
    base_ridge: f64,
) -> f64 {
    debug_assert_eq!(covariance.len(), d * d);
    debug_assert_eq!(scratch.len(), d * d);

    let mut scale = base_ridge.max(VAR_FLOOR);
    for value in covariance.iter().copied() {
        let abs = value.abs();
        if abs.is_finite() && abs > scale {
            scale = abs;
        }
    }
    if !scale.is_finite() || scale <= 0.0 {
        scale = 1.0;
    }

    let inv_scale = 1.0 / scale;
    for (idx, value) in covariance.iter().copied().enumerate() {
        let normalized = normalize_covariance_entry(value * inv_scale);
        scratch[idx] = if normalized.is_finite() {
            normalized
        } else if normalized.is_sign_positive() {
            f64::MAX
        } else {
            -f64::MAX
        };
    }

    let mut jitter = base_ridge.max(VAR_FLOOR);
    for _ in 0..FULL_COV_MAX_JITTER_TRIALS {
        covariance.copy_from_slice(scratch);
        let scaled_jitter = (jitter * inv_scale).max(VAR_FLOOR);
        for i in 0..d {
            covariance[i * d + i] =
                normalize_covariance_entry(covariance[i * d + i] + scaled_jitter);
        }
        if let Some(log_det_scaled) = cholesky_logdet_in_place(covariance, d) {
            return d as f64 * scale.ln() + log_det_scaled;
        }
        jitter *= 10.0;
    }

    d as f64 * normalize_variance(jitter).ln()
}

impl CostModel for CostNormalMeanVar {
    type Cache = NormalCache;

    fn name(&self) -> &'static str {
        "normal_mean_var"
    }

    fn penalty_params_per_segment(&self) -> usize {
        3
    }

    fn validate(&self, x: &TimeSeriesView<'_>) -> Result<(), CpdError> {
        if x.n == 0 {
            return Err(CpdError::invalid_input(
                "CostNormalMeanVar requires n >= 1; got n=0",
            ));
        }
        if x.d == 0 {
            return Err(CpdError::invalid_input(
                "CostNormalMeanVar requires d >= 1; got d=0",
            ));
        }

        if x.has_missing() {
            return Err(CpdError::invalid_input(format!(
                "CostNormalMeanVar does not support missing values: effective_missing_count={}",
                x.n_missing()
            )));
        }

        match x.values {
            DTypeView::F32(_) | DTypeView::F64(_) => Ok(()),
        }
    }

    fn missing_support(&self) -> MissingSupport {
        MissingSupport::Reject
    }

    fn precompute(
        &self,
        x: &TimeSeriesView<'_>,
        policy: &CachePolicy,
    ) -> Result<Self::Cache, CpdError> {
        let required_bytes = self.worst_case_cache_bytes(x);

        if matches!(policy, CachePolicy::Approximate { .. }) {
            return Err(CpdError::not_supported(
                "CostNormalMeanVar does not support CachePolicy::Approximate",
            ));
        }

        if required_bytes == usize::MAX {
            return Err(cache_overflow_err(x.n, x.d));
        }

        if let CachePolicy::Budgeted { max_bytes } = policy
            && required_bytes > *max_bytes
        {
            return Err(CpdError::resource_limit(format!(
                "CostNormalMeanVar cache requires {} bytes, exceeds budget {} bytes",
                required_bytes, max_bytes
            )));
        }

        let prefix_len_per_dim =
            x.n.checked_add(1)
                .ok_or_else(|| cache_overflow_err(x.n, x.d))?;
        let total_prefix_len = prefix_len_per_dim
            .checked_mul(x.d)
            .ok_or_else(|| cache_overflow_err(x.n, x.d))?;

        let mut prefix_sum = Vec::with_capacity(total_prefix_len);
        let mut prefix_sum_sq = Vec::with_capacity(total_prefix_len);

        for dim in 0..x.d {
            let mut series = Vec::with_capacity(x.n);
            for t in 0..x.n {
                series.push(read_value(x, t, dim)?);
            }

            let dim_prefix_sum = if matches!(self.repro_mode, ReproMode::Strict) {
                prefix_sums_kahan(&series)
            } else {
                prefix_sums(&series)
            };

            let dim_prefix_sum_sq = if matches!(self.repro_mode, ReproMode::Strict) {
                prefix_sum_squares_kahan(&series)
            } else {
                prefix_sum_squares(&series)
            };

            debug_assert_eq!(dim_prefix_sum.len(), prefix_len_per_dim);
            debug_assert_eq!(dim_prefix_sum_sq.len(), prefix_len_per_dim);

            prefix_sum.extend_from_slice(&dim_prefix_sum);
            prefix_sum_sq.extend_from_slice(&dim_prefix_sum_sq);
        }

        Ok(NormalCache {
            prefix_sum,
            prefix_sum_sq,
            n: x.n,
            d: x.d,
        })
    }

    fn worst_case_cache_bytes(&self, x: &TimeSeriesView<'_>) -> usize {
        let prefix_len_per_dim = match x.n.checked_add(1) {
            Some(v) => v,
            None => return usize::MAX,
        };
        let total_prefix_len = match prefix_len_per_dim.checked_mul(x.d) {
            Some(v) => v,
            None => return usize::MAX,
        };
        let bytes_per_array = match total_prefix_len.checked_mul(std::mem::size_of::<f64>()) {
            Some(v) => v,
            None => return usize::MAX,
        };

        match bytes_per_array.checked_mul(2) {
            Some(v) => v,
            None => usize::MAX,
        }
    }

    fn supports_approx_cache(&self) -> bool {
        false
    }

    fn segment_cost(&self, cache: &Self::Cache, start: usize, end: usize) -> f64 {
        assert!(
            start < end,
            "segment_cost requires start < end; got start={start}, end={end}"
        );
        assert!(
            end <= cache.n,
            "segment_cost end out of bounds: end={end}, n={} ",
            cache.n
        );

        let m = (end - start) as f64;
        let mut total = 0.0;

        for dim in 0..cache.d {
            let base = cache.dim_offset(dim);
            let sum = cache.prefix_sum[base + end] - cache.prefix_sum[base + start];
            let sum_sq = cache.prefix_sum_sq[base + end] - cache.prefix_sum_sq[base + start];
            let mean = sum / m;
            let raw_var = sum_sq / m - mean * mean;
            let var = normalize_variance(raw_var);
            total += m * var.ln();
        }

        total
    }
}

impl CostModel for CostNormalFullCov {
    type Cache = NormalFullCovCache;

    fn name(&self) -> &'static str {
        "normal_full_cov"
    }

    fn penalty_params_per_segment(&self) -> usize {
        3
    }

    fn penalty_effective_params(&self, d: usize) -> Option<usize> {
        let mean_params = d;
        let covariance_params = triangular_pair_count(d)?;
        mean_params.checked_add(covariance_params)?.checked_add(1)
    }

    fn validate(&self, x: &TimeSeriesView<'_>) -> Result<(), CpdError> {
        if x.n == 0 {
            return Err(CpdError::invalid_input(
                "CostNormalFullCov requires n >= 1; got n=0",
            ));
        }
        if x.d == 0 {
            return Err(CpdError::invalid_input(
                "CostNormalFullCov requires d >= 1; got d=0",
            ));
        }
        if x.has_missing() {
            return Err(CpdError::invalid_input(format!(
                "CostNormalFullCov does not support missing values: effective_missing_count={}",
                x.n_missing()
            )));
        }

        match x.values {
            DTypeView::F32(_) | DTypeView::F64(_) => Ok(()),
        }
    }

    fn missing_support(&self) -> MissingSupport {
        MissingSupport::Reject
    }

    fn precompute(
        &self,
        x: &TimeSeriesView<'_>,
        policy: &CachePolicy,
    ) -> Result<Self::Cache, CpdError> {
        let required_bytes = self.worst_case_cache_bytes(x);

        if matches!(policy, CachePolicy::Approximate { .. }) {
            return Err(CpdError::not_supported(
                "CostNormalFullCov does not support CachePolicy::Approximate",
            ));
        }
        if required_bytes == usize::MAX {
            return Err(CpdError::resource_limit(format!(
                "cache size overflow while planning NormalFullCovCache for n={}, d={}",
                x.n, x.d
            )));
        }
        if let CachePolicy::Budgeted { max_bytes } = policy
            && required_bytes > *max_bytes
        {
            return Err(CpdError::resource_limit(format!(
                "CostNormalFullCov cache requires {} bytes, exceeds budget {} bytes",
                required_bytes, max_bytes
            )));
        }

        let prefix_len_per_dim =
            x.n.checked_add(1)
                .ok_or_else(|| CpdError::resource_limit("n+1 overflow for full-cov cache"))?;
        let sum_prefix_len = prefix_len_per_dim
            .checked_mul(x.d)
            .ok_or_else(|| CpdError::resource_limit("sum-prefix overflow for full-cov cache"))?;
        let pair_count = triangular_pair_count(x.d)
            .ok_or_else(|| CpdError::resource_limit("pair-count overflow for full-cov cache"))?;
        let cross_prefix_len = prefix_len_per_dim
            .checked_mul(pair_count)
            .ok_or_else(|| CpdError::resource_limit("cross-prefix overflow for full-cov cache"))?;

        let mut values_by_dim = Vec::with_capacity(
            x.n.checked_mul(x.d)
                .ok_or_else(|| CpdError::resource_limit("n*d overflow for full-cov values"))?,
        );
        for dim in 0..x.d {
            for t in 0..x.n {
                values_by_dim.push(read_value(x, t, dim)?);
            }
        }

        let mut prefix_sum = Vec::with_capacity(sum_prefix_len);
        for dim in 0..x.d {
            let start = dim * x.n;
            let end = start + x.n;
            let series = &values_by_dim[start..end];
            let prefix = if matches!(self.repro_mode, ReproMode::Strict) {
                prefix_sums_kahan(series)
            } else {
                prefix_sums(series)
            };
            debug_assert_eq!(prefix.len(), prefix_len_per_dim);
            prefix_sum.extend_from_slice(&prefix);
        }

        let mut prefix_cross = Vec::with_capacity(cross_prefix_len);
        let mut cross_series = Vec::with_capacity(x.n);
        for i in 0..x.d {
            let i_base = i * x.n;
            for j in i..x.d {
                let j_base = j * x.n;
                cross_series.clear();
                for t in 0..x.n {
                    cross_series.push(values_by_dim[i_base + t] * values_by_dim[j_base + t]);
                }
                let prefix = if matches!(self.repro_mode, ReproMode::Strict) {
                    prefix_sums_kahan(&cross_series)
                } else {
                    prefix_sums(&cross_series)
                };
                debug_assert_eq!(prefix.len(), prefix_len_per_dim);
                prefix_cross.extend_from_slice(&prefix);
            }
        }

        Ok(NormalFullCovCache {
            prefix_sum,
            prefix_cross,
            n: x.n,
            d: x.d,
        })
    }

    fn worst_case_cache_bytes(&self, x: &TimeSeriesView<'_>) -> usize {
        let prefix_len_per_dim = match x.n.checked_add(1) {
            Some(value) => value,
            None => return usize::MAX,
        };
        let pair_count = match triangular_pair_count(x.d) {
            Some(value) => value,
            None => return usize::MAX,
        };
        let sum_len = match prefix_len_per_dim.checked_mul(x.d) {
            Some(value) => value,
            None => return usize::MAX,
        };
        let cross_len = match prefix_len_per_dim.checked_mul(pair_count) {
            Some(value) => value,
            None => return usize::MAX,
        };
        let total_len = match sum_len.checked_add(cross_len) {
            Some(value) => value,
            None => return usize::MAX,
        };
        total_len
            .checked_mul(std::mem::size_of::<f64>())
            .unwrap_or(usize::MAX)
    }

    fn supports_approx_cache(&self) -> bool {
        false
    }

    fn segment_cost(&self, cache: &Self::Cache, start: usize, end: usize) -> f64 {
        assert!(
            start < end,
            "segment_cost requires start < end; got start={start}, end={end}"
        );
        assert!(
            end <= cache.n,
            "segment_cost end out of bounds: end={end}, n={}",
            cache.n
        );

        let m = (end - start) as f64;

        if cache.d == 1 {
            let sum = cache.prefix_sum[end] - cache.prefix_sum[start];
            let sum_sq = cache.prefix_cross[end] - cache.prefix_cross[start];
            let mean = sum / m;
            let raw_var = sum_sq / m - mean * mean;
            return m * normalize_variance(raw_var).ln();
        }

        FULL_COV_WORKSPACE.with(|workspace| {
            let matrix_len = cache
                .d
                .checked_mul(cache.d)
                .expect("full-cov workspace length overflow");
            let required_len = matrix_len
                .checked_mul(2)
                .expect("full-cov workspace length overflow");
            let mut buffer = workspace.borrow_mut();
            if buffer.len() < required_len {
                buffer.resize(required_len, 0.0);
            }
            let (covariance, scratch_rest) = buffer.split_at_mut(matrix_len);
            let scratch = &mut scratch_rest[..matrix_len];

            let mut trace = 0.0;
            for i in 0..cache.d {
                let i_offset = cache.dim_offset(i);
                let mean_i =
                    (cache.prefix_sum[i_offset + end] - cache.prefix_sum[i_offset + start]) / m;
                for j in i..cache.d {
                    let j_offset = cache.dim_offset(j);
                    let mean_j =
                        (cache.prefix_sum[j_offset + end] - cache.prefix_sum[j_offset + start]) / m;
                    let pair_offset = cache.pair_offset(i, j);
                    let cross = cache.prefix_cross[pair_offset + end]
                        - cache.prefix_cross[pair_offset + start];
                    let centered = normalize_covariance_entry(cross / m - mean_i * mean_j);
                    covariance[i * cache.d + j] = centered;
                    covariance[j * cache.d + i] = centered;
                    if i == j {
                        trace += normalize_variance(centered);
                    }
                }
            }

            let ridge = full_cov_ridge(trace, cache.d, m);
            let log_det = regularized_logdet_with_scratch(covariance, scratch, cache.d, ridge);
            m * log_det
        })
    }
}

#[cfg(test)]
mod tests {
    use super::{
        CostNormalFullCov, CostNormalMeanVar, NormalCache, VAR_FLOOR, cache_overflow_err,
        normalize_variance, read_value, strided_linear_index, triangular_pair_count,
    };
    use crate::l2::CostL2Mean;
    use crate::model::CostModel;
    use cpd_core::{
        CachePolicy, CpdError, DTypeView, MemoryLayout, MissingPolicy, MissingSupport, ReproMode,
        TimeIndex, TimeSeriesView,
    };

    fn assert_close(actual: f64, expected: f64, tol: f64) {
        let diff = (actual - expected).abs();
        assert!(
            diff <= tol,
            "expected {expected}, got {actual}, |diff|={diff}, tol={tol}"
        );
    }

    fn make_f64_view<'a>(
        values: &'a [f64],
        n: usize,
        d: usize,
        layout: MemoryLayout,
        missing: MissingPolicy,
    ) -> TimeSeriesView<'a> {
        TimeSeriesView::new(
            DTypeView::F64(values),
            n,
            d,
            layout,
            None,
            TimeIndex::None,
            missing,
        )
        .expect("test view should be valid")
    }

    fn make_f32_view<'a>(
        values: &'a [f32],
        n: usize,
        d: usize,
        layout: MemoryLayout,
        missing: MissingPolicy,
    ) -> TimeSeriesView<'a> {
        TimeSeriesView::new(
            DTypeView::F32(values),
            n,
            d,
            layout,
            None,
            TimeIndex::None,
            missing,
        )
        .expect("test view should be valid")
    }

    fn normalize_variance_for_test(raw_var: f64) -> f64 {
        if raw_var.is_nan() || raw_var <= VAR_FLOOR {
            VAR_FLOOR
        } else if raw_var == f64::INFINITY {
            f64::MAX
        } else {
            raw_var
        }
    }

    fn naive_normal_univariate(values: &[f64], start: usize, end: usize) -> f64 {
        let segment = &values[start..end];
        let m = segment.len() as f64;
        let sum: f64 = segment.iter().sum();
        let sum_sq: f64 = segment.iter().map(|v| v * v).sum();
        let mean = sum / m;
        let raw_var = sum_sq / m - mean * mean;
        let var = normalize_variance_for_test(raw_var);
        m * var.ln()
    }

    fn synthetic_multivariate_values(n: usize, d: usize) -> Vec<f64> {
        let mut values = Vec::with_capacity(n * d);
        for t in 0..n {
            for dim in 0..d {
                let x = t as f64 + 1.0;
                let y = dim as f64 + 1.0;
                values.push((x * y) + (0.04 * x).sin() + (0.11 * y).cos());
            }
        }
        values
    }

    fn dim_series(values: &[f64], n: usize, d: usize, dim: usize) -> Vec<f64> {
        (0..n).map(|t| values[t * d + dim]).collect()
    }

    fn lcg_next(state: &mut u64) -> u64 {
        *state = state
            .wrapping_mul(6364136223846793005)
            .wrapping_add(1442695040888963407);
        *state
    }

    #[test]
    fn strided_linear_index_reports_overflow_and_negative_paths() {
        let err_t = strided_linear_index(usize::MAX, 0, 1, 1, 8).expect_err("t overflow expected");
        assert!(matches!(err_t, CpdError::InvalidInput(_)));
        assert!(err_t.to_string().contains("t="));

        let err_dim =
            strided_linear_index(0, usize::MAX, 1, 1, 8).expect_err("dim overflow expected");
        assert!(matches!(err_dim, CpdError::InvalidInput(_)));
        assert!(err_dim.to_string().contains("dim="));

        let err_mul = strided_linear_index(isize::MAX as usize, 0, 2, 0, 8)
            .expect_err("checked_mul overflow expected");
        assert!(matches!(err_mul, CpdError::InvalidInput(_)));
        assert!(err_mul.to_string().contains("overflow"));

        let err_neg = strided_linear_index(1, 0, -1, 0, 8).expect_err("negative index expected");
        assert!(matches!(err_neg, CpdError::InvalidInput(_)));
        assert!(err_neg.to_string().contains("negative"));
    }

    #[test]
    fn read_value_f32_layout_paths_and_errors() {
        let c_values = [1.5_f32, 10.5, 2.5, 20.5];
        let c_view = make_f32_view(
            &c_values,
            2,
            2,
            MemoryLayout::CContiguous,
            MissingPolicy::Error,
        );
        assert_close(
            read_value(&c_view, 1, 0).expect("C f32 read should succeed"),
            2.5,
            1e-12,
        );
        let c_oob = read_value(&c_view, 1, 2).expect_err("C f32 oob expected");
        assert!(c_oob.to_string().contains("out of bounds"));
        let c_overflow = read_value(&c_view, usize::MAX, 0).expect_err("C f32 overflow expected");
        assert!(c_overflow.to_string().contains("overflow"));

        let f_values = [1.5_f32, 2.5, 10.5, 20.5];
        let f_view = make_f32_view(
            &f_values,
            2,
            2,
            MemoryLayout::FContiguous,
            MissingPolicy::Error,
        );
        assert_close(
            read_value(&f_view, 1, 0).expect("F f32 read should succeed"),
            2.5,
            1e-12,
        );
        let f_oob = read_value(&f_view, 1, 2).expect_err("F f32 oob expected");
        assert!(f_oob.to_string().contains("out of bounds"));
        let f_overflow = read_value(&f_view, 0, usize::MAX).expect_err("F f32 overflow expected");
        assert!(f_overflow.to_string().contains("overflow"));

        let s_values = [1.5_f32, 10.5, 2.5, 20.5];
        let s_view = make_f32_view(
            &s_values,
            2,
            2,
            MemoryLayout::Strided {
                row_stride: 2,
                col_stride: 1,
            },
            MissingPolicy::Error,
        );
        assert_close(
            read_value(&s_view, 1, 1).expect("strided f32 read should succeed"),
            20.5,
            1e-12,
        );
    }

    #[test]
    fn cache_overflow_error_message_is_stable() {
        let err = cache_overflow_err(7, 11);
        assert!(matches!(err, CpdError::ResourceLimit(_)));
        assert!(err.to_string().contains("n=7, d=11"));
    }

    #[test]
    fn normalize_variance_clamps_floor_and_infinity() {
        assert_eq!(normalize_variance(f64::NAN), VAR_FLOOR);
        assert_eq!(normalize_variance(-1.0), VAR_FLOOR);
        assert_eq!(normalize_variance(VAR_FLOOR / 2.0), VAR_FLOOR);
        assert_eq!(normalize_variance(f64::INFINITY), f64::MAX);
        assert_eq!(normalize_variance(1.25), 1.25);
    }

    #[test]
    fn trait_contract_and_defaults() {
        let model = CostNormalMeanVar::default();
        assert_eq!(model.name(), "normal_mean_var");
        assert_eq!(model.repro_mode, ReproMode::Balanced);
        assert_eq!(model.missing_support(), MissingSupport::Reject);
        assert!(!model.supports_approx_cache());
    }

    #[test]
    fn validate_rejects_missing_effective_values() {
        let values = [1.0, f64::NAN, 3.0];
        let view = make_f64_view(
            &values,
            3,
            1,
            MemoryLayout::CContiguous,
            MissingPolicy::Ignore,
        );
        let err = CostNormalMeanVar::default()
            .validate(&view)
            .expect_err("missing values should be rejected");
        assert!(matches!(err, CpdError::InvalidInput(_)));
        assert!(err.to_string().contains("missing values"));
    }

    #[test]
    fn validate_accepts_clean_f32_and_f64_views() {
        let model = CostNormalMeanVar::default();

        let f32_values = [1.0_f32, 2.0, 3.0, 4.0];
        let f32_view = TimeSeriesView::new(
            DTypeView::F32(&f32_values),
            4,
            1,
            MemoryLayout::CContiguous,
            None,
            TimeIndex::None,
            MissingPolicy::Error,
        )
        .expect("f32 view should be valid");
        model.validate(&f32_view).expect("f32 should validate");

        let f64_values = [1.0_f64, 2.0, 3.0, 4.0];
        let f64_view = make_f64_view(
            &f64_values,
            4,
            1,
            MemoryLayout::CContiguous,
            MissingPolicy::Error,
        );
        model.validate(&f64_view).expect("f64 should validate");
    }

    #[test]
    fn known_answer_univariate_and_constant_segment() {
        let model = CostNormalMeanVar::default();
        let values = [1.0, 2.0, 3.0, 4.0];
        let view = make_f64_view(
            &values,
            4,
            1,
            MemoryLayout::CContiguous,
            MissingPolicy::Error,
        );
        let cache = model
            .precompute(&view, &CachePolicy::Full)
            .expect("precompute should succeed");

        let full = model.segment_cost(&cache, 0, 4);
        assert_close(full, 4.0 * 1.25_f64.ln(), 1e-12);

        let sub = model.segment_cost(&cache, 1, 3);
        assert_close(sub, 2.0 * 0.25_f64.ln(), 1e-12);

        let const_values = [2.0, 2.0, 2.0, 2.0];
        let const_view = make_f64_view(
            &const_values,
            4,
            1,
            MemoryLayout::CContiguous,
            MissingPolicy::Error,
        );
        let const_cache = model
            .precompute(&const_view, &CachePolicy::Full)
            .expect("precompute should succeed");
        let const_cost = model.segment_cost(&const_cache, 0, 4);
        assert_close(const_cost, 4.0 * VAR_FLOOR.ln(), 1e-12);
        assert!(const_cost.is_finite());
    }

    #[test]
    fn segment_cost_matches_naive_on_deterministic_queries() {
        let model = CostNormalMeanVar::default();
        let n = 256;
        let values: Vec<f64> = (0..n)
            .map(|i| {
                let x = i as f64;
                x.sin() * 0.3 + x.cos() * 0.7 + (i % 11) as f64 * 1e-3
            })
            .collect();
        let view = make_f64_view(
            &values,
            n,
            1,
            MemoryLayout::CContiguous,
            MissingPolicy::Error,
        );
        let cache = model
            .precompute(&view, &CachePolicy::Full)
            .expect("precompute should succeed");

        let mut state = 0x1234_5678_9abc_def0_u64;
        let start = 8;
        let mut end = 8;
        if start == end {
            end = (start + 1).min(n);
        }
        let fast = model.segment_cost(&cache, start, end);
        let naive = naive_normal_univariate(&values, start, end);
        assert_close(fast, naive, 1e-9);
        for _ in 0..600 {
            let a = (lcg_next(&mut state) as usize) % n;
            let b = (lcg_next(&mut state) as usize) % n;
            let start = a.min(b);
            let mut end = a.max(b) + 1;
            if start == end {
                end = (start + 1).min(n);
            }

            let fast = model.segment_cost(&cache, start, end);
            let naive = naive_normal_univariate(&values, start, end);
            assert_close(fast, naive, 1e-9);
        }
    }

    #[test]
    fn multivariate_matches_univariate_sum_for_d1_d4_d16() {
        let model = CostNormalMeanVar::default();
        let n = 9;
        let start = 2;
        let end = 8;

        for d in [1_usize, 4, 16] {
            let values = synthetic_multivariate_values(n, d);
            let view = make_f64_view(
                &values,
                n,
                d,
                MemoryLayout::CContiguous,
                MissingPolicy::Error,
            );
            let cache = model
                .precompute(&view, &CachePolicy::Full)
                .expect("precompute should succeed");
            let multivariate = model.segment_cost(&cache, start, end);

            let mut per_dimension_sum = 0.0;
            for dim in 0..d {
                let series = dim_series(&values, n, d, dim);
                let dim_view = make_f64_view(
                    &series,
                    n,
                    1,
                    MemoryLayout::CContiguous,
                    MissingPolicy::Error,
                );
                let dim_cache = model
                    .precompute(&dim_view, &CachePolicy::Full)
                    .expect("univariate precompute should succeed");
                per_dimension_sum += model.segment_cost(&dim_cache, start, end);
            }

            assert_close(multivariate, per_dimension_sum, 1e-10);
        }
    }

    #[test]
    fn layout_coverage_c_f_and_strided() {
        let model = CostNormalMeanVar::default();

        let c_values = vec![1.0, 10.0, 2.0, 20.0, 3.0, 30.0, 4.0, 40.0];
        let c_view = make_f64_view(
            &c_values,
            4,
            2,
            MemoryLayout::CContiguous,
            MissingPolicy::Error,
        );
        let c_cache = model
            .precompute(&c_view, &CachePolicy::Full)
            .expect("C precompute should succeed");

        let f_values = vec![1.0, 2.0, 3.0, 4.0, 10.0, 20.0, 30.0, 40.0];
        let f_view = make_f64_view(
            &f_values,
            4,
            2,
            MemoryLayout::FContiguous,
            MissingPolicy::Error,
        );
        let f_cache = model
            .precompute(&f_view, &CachePolicy::Full)
            .expect("F precompute should succeed");

        let c_cost = model.segment_cost(&c_cache, 1, 4);
        let f_cost = model.segment_cost(&f_cache, 1, 4);
        assert_close(c_cost, f_cost, 1e-12);

        let strided_view = make_f64_view(
            &c_values,
            4,
            2,
            MemoryLayout::Strided {
                row_stride: 2,
                col_stride: 1,
            },
            MissingPolicy::Error,
        );
        let strided_cache = model
            .precompute(&strided_view, &CachePolicy::Full)
            .expect("strided precompute should succeed");
        let strided_cost = model.segment_cost(&strided_cache, 1, 4);
        assert_close(c_cost, strided_cost, 1e-12);
    }

    #[test]
    fn strided_invalid_index_returns_invalid_input() {
        let model = CostNormalMeanVar::default();
        let values = vec![1.0, 10.0, 2.0, 20.0, 3.0, 30.0];
        let view = make_f64_view(
            &values,
            3,
            2,
            MemoryLayout::Strided {
                row_stride: 3,
                col_stride: 1,
            },
            MissingPolicy::Error,
        );

        let err = model
            .precompute(&view, &CachePolicy::Full)
            .expect_err("invalid stride should fail during precompute");
        assert!(matches!(err, CpdError::InvalidInput(_)));
        assert!(err.to_string().contains("out of bounds"));
    }

    #[test]
    fn cache_policy_behavior_budgeted_and_approximate() {
        let model = CostNormalMeanVar::default();
        let values = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
        let view = make_f64_view(
            &values,
            3,
            2,
            MemoryLayout::CContiguous,
            MissingPolicy::Error,
        );

        let required = model.worst_case_cache_bytes(&view);
        assert!(required > 0);

        model
            .precompute(
                &view,
                &CachePolicy::Budgeted {
                    max_bytes: required,
                },
            )
            .expect("budget equal to required should succeed");

        let err = model
            .precompute(
                &view,
                &CachePolicy::Budgeted {
                    max_bytes: required - 1,
                },
            )
            .expect_err("budget below required should fail");
        assert!(matches!(err, CpdError::ResourceLimit(_)));

        let err = model
            .precompute(
                &view,
                &CachePolicy::Approximate {
                    max_bytes: required,
                    error_tolerance: 0.1,
                },
            )
            .expect_err("approximate policy should be unsupported");
        assert!(matches!(err, CpdError::NotSupported(_)));
    }

    #[test]
    fn worst_case_cache_bytes_matches_multivariate_formula() {
        let model = CostNormalMeanVar::default();
        let values = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
        let view = make_f64_view(
            &values,
            3,
            2,
            MemoryLayout::CContiguous,
            MissingPolicy::Error,
        );

        let prefix_len_per_dim = view.n + 1;
        let total_prefix_len = prefix_len_per_dim * view.d;
        let expected_small = 2 * total_prefix_len * std::mem::size_of::<f64>();
        assert_eq!(model.worst_case_cache_bytes(&view), expected_small);

        let n_large = 1_000_000usize;
        let d_large = 16usize;
        let expected_large = n_large
            .checked_add(1)
            .and_then(|v| v.checked_mul(d_large))
            .and_then(|v| v.checked_mul(2 * std::mem::size_of::<f64>()))
            .expect("formula should not overflow");
        if std::mem::size_of::<f64>() == 8 {
            assert_eq!(expected_large, 256_000_256);
        }
    }

    #[test]
    fn differential_test_detects_variance_shift_that_l2_misses() {
        let n_half = 16;
        let mut values = Vec::with_capacity(n_half * 2);
        for _ in 0..(n_half / 2) {
            values.push(-1.0);
            values.push(1.0);
        }
        for _ in 0..(n_half / 2) {
            values.push(-10.0);
            values.push(10.0);
        }

        let view = make_f64_view(
            &values,
            values.len(),
            1,
            MemoryLayout::CContiguous,
            MissingPolicy::Error,
        );

        let normal = CostNormalMeanVar::default();
        let normal_cache = normal
            .precompute(&view, &CachePolicy::Full)
            .expect("normal precompute should succeed");

        let l2 = CostL2Mean::default();
        let l2_cache = l2
            .precompute(&view, &CachePolicy::Full)
            .expect("l2 precompute should succeed");

        let split = n_half;
        let normal_full = normal.segment_cost(&normal_cache, 0, values.len());
        let normal_split = normal.segment_cost(&normal_cache, 0, split)
            + normal.segment_cost(&normal_cache, split, values.len());
        let normal_gain = normal_full - normal_split;
        assert!(
            normal_gain > 1.0,
            "expected strong normal gain on variance shift, got {normal_gain}"
        );

        let l2_full = l2.segment_cost(&l2_cache, 0, values.len());
        let l2_split =
            l2.segment_cost(&l2_cache, 0, split) + l2.segment_cost(&l2_cache, split, values.len());
        let l2_gain = l2_full - l2_split;
        assert!(
            l2_gain.abs() <= 1e-10,
            "expected near-zero l2 gain when mean is unchanged, got {l2_gain}"
        );
    }

    #[test]
    fn numeric_stress_near_constant_and_extreme_magnitudes() {
        let model = CostNormalMeanVar::default();

        let near_constant = vec![
            1.0e15,
            1.0e15 + 1.0e-3,
            1.0e15 + 2.0e-3,
            1.0e15 + 3.0e-3,
            1.0e15 + 4.0e-3,
        ];
        let near_constant_view = make_f64_view(
            &near_constant,
            near_constant.len(),
            1,
            MemoryLayout::CContiguous,
            MissingPolicy::Error,
        );
        let near_constant_cache = model
            .precompute(&near_constant_view, &CachePolicy::Full)
            .expect("near-constant precompute should succeed");
        let near_constant_cost = model.segment_cost(&near_constant_cache, 0, near_constant.len());
        assert!(near_constant_cost.is_finite());

        let extreme = vec![1.0e150, -1.0e150, 2.0e150, -2.0e150];
        let extreme_view = make_f64_view(
            &extreme,
            extreme.len(),
            1,
            MemoryLayout::CContiguous,
            MissingPolicy::Error,
        );
        let extreme_cache = model
            .precompute(&extreme_view, &CachePolicy::Full)
            .expect("extreme precompute should succeed");
        let extreme_cost = model.segment_cost(&extreme_cache, 0, extreme.len());
        assert!(extreme_cost.is_finite());
    }

    #[test]
    fn raw_var_pos_inf_is_capped_to_f64_max() {
        let model = CostNormalMeanVar::default();
        let values = vec![1.0e308, -1.0e308];
        let view = make_f64_view(
            &values,
            values.len(),
            1,
            MemoryLayout::CContiguous,
            MissingPolicy::Error,
        );
        let cache = model
            .precompute(&view, &CachePolicy::Full)
            .expect("precompute should succeed");

        let cost = model.segment_cost(&cache, 0, values.len());
        assert_close(cost, values.len() as f64 * f64::MAX.ln(), 1e-9);
        assert!(cost.is_finite());
    }

    #[test]
    fn full_cov_trait_contract_and_defaults() {
        let model = CostNormalFullCov::default();
        assert_eq!(model.name(), "normal_full_cov");
        assert_eq!(model.repro_mode, ReproMode::Balanced);
        assert_eq!(model.missing_support(), MissingSupport::Reject);
        assert!(!model.supports_approx_cache());
        assert_eq!(model.penalty_effective_params(1), Some(3));
        assert_eq!(model.penalty_effective_params(4), Some(15));
    }

    #[test]
    fn full_cov_matches_diagonal_for_d1() {
        let values: Vec<f64> = (0..64)
            .map(|idx| {
                let x = idx as f64;
                (0.4 * x).sin() + (0.07 * x).cos() + x * 0.01
            })
            .collect();
        let view = make_f64_view(
            &values,
            values.len(),
            1,
            MemoryLayout::CContiguous,
            MissingPolicy::Error,
        );

        let diagonal = CostNormalMeanVar::default();
        let diagonal_cache = diagonal
            .precompute(&view, &CachePolicy::Full)
            .expect("diagonal precompute should succeed");
        let full_cov = CostNormalFullCov::default();
        let full_cov_cache = full_cov
            .precompute(&view, &CachePolicy::Full)
            .expect("full-cov precompute should succeed");

        let start = 7;
        let end = 53;
        let diagonal_cost = diagonal.segment_cost(&diagonal_cache, start, end);
        let full_cov_cost = full_cov.segment_cost(&full_cov_cache, start, end);
        assert_close(full_cov_cost, diagonal_cost, 1e-10);
    }

    #[test]
    fn full_cov_prefers_correlated_segments_over_diagonal() {
        let n = 80;
        let d = 2;
        let mut values = Vec::with_capacity(n * d);
        for t in 0..n {
            let base = t as f64 * 0.25 + (0.03 * t as f64).sin();
            values.push(base);
            values.push(base * 1.2 + 0.002 * (t as f64).cos());
        }
        let view = make_f64_view(
            &values,
            n,
            d,
            MemoryLayout::CContiguous,
            MissingPolicy::Error,
        );

        let diagonal = CostNormalMeanVar::default();
        let diagonal_cache = diagonal
            .precompute(&view, &CachePolicy::Full)
            .expect("diagonal precompute should succeed");
        let full_cov = CostNormalFullCov::default();
        let full_cov_cache = full_cov
            .precompute(&view, &CachePolicy::Full)
            .expect("full-cov precompute should succeed");

        let diagonal_cost = diagonal.segment_cost(&diagonal_cache, 0, n);
        let full_cov_cost = full_cov.segment_cost(&full_cov_cache, 0, n);
        assert!(
            full_cov_cost < diagonal_cost,
            "expected full-cov cost to be lower for strongly correlated dimensions: full_cov={full_cov_cost}, diagonal={diagonal_cost}"
        );
    }

    #[test]
    fn full_cov_regularizes_rank_deficient_segments() {
        let n = 3;
        let d = 5;
        let mut values = Vec::with_capacity(n * d);
        for t in 0..n {
            let x = t as f64 + 1.0;
            values.extend([x, 2.0 * x, -x, 3.0 * x, 0.5 * x]);
        }
        let view = make_f64_view(
            &values,
            n,
            d,
            MemoryLayout::CContiguous,
            MissingPolicy::Error,
        );

        let model = CostNormalFullCov::default();
        let cache = model
            .precompute(&view, &CachePolicy::Full)
            .expect("full-cov precompute should succeed");
        let cost = model.segment_cost(&cache, 0, n);
        assert!(
            cost.is_finite(),
            "regularized full-cov cost should be finite on rank-deficient segments"
        );
    }

    #[test]
    fn full_cov_layout_coverage_c_f_and_strided() {
        let model = CostNormalFullCov::default();
        let c_values = vec![1.0, 10.0, 100.0, 2.0, 20.0, 200.0, 3.0, 30.0, 300.0];
        let c_view = make_f64_view(
            &c_values,
            3,
            3,
            MemoryLayout::CContiguous,
            MissingPolicy::Error,
        );
        let c_cache = model
            .precompute(&c_view, &CachePolicy::Full)
            .expect("C layout precompute should succeed");

        let f_values = vec![1.0, 2.0, 3.0, 10.0, 20.0, 30.0, 100.0, 200.0, 300.0];
        let f_view = make_f64_view(
            &f_values,
            3,
            3,
            MemoryLayout::FContiguous,
            MissingPolicy::Error,
        );
        let f_cache = model
            .precompute(&f_view, &CachePolicy::Full)
            .expect("F layout precompute should succeed");

        let s_view = make_f64_view(
            &c_values,
            3,
            3,
            MemoryLayout::Strided {
                row_stride: 3,
                col_stride: 1,
            },
            MissingPolicy::Error,
        );
        let s_cache = model
            .precompute(&s_view, &CachePolicy::Full)
            .expect("strided precompute should succeed");

        let c_cost = model.segment_cost(&c_cache, 0, 3);
        let f_cost = model.segment_cost(&f_cache, 0, 3);
        let s_cost = model.segment_cost(&s_cache, 0, 3);
        assert_close(c_cost, f_cost, 1e-12);
        assert_close(c_cost, s_cost, 1e-12);
    }

    #[test]
    fn full_cov_worst_case_cache_bytes_matches_formula() {
        let model = CostNormalFullCov::default();
        let values = vec![0.0; 12];
        let view = make_f64_view(
            &values,
            3,
            4,
            MemoryLayout::CContiguous,
            MissingPolicy::Error,
        );
        let pair_count = triangular_pair_count(view.d).expect("pair count should fit");
        let expected = (view.n + 1)
            .checked_mul(view.d + pair_count)
            .and_then(|len| len.checked_mul(std::mem::size_of::<f64>()))
            .expect("formula should not overflow");
        assert_eq!(model.worst_case_cache_bytes(&view), expected);
    }

    #[test]
    fn full_cov_rejects_approximate_cache_policy() {
        let model = CostNormalFullCov::default();
        let values = vec![1.0, 2.0, 3.0, 4.0];
        let view = make_f64_view(
            &values,
            2,
            2,
            MemoryLayout::CContiguous,
            MissingPolicy::Error,
        );
        let err = model
            .precompute(
                &view,
                &CachePolicy::Approximate {
                    max_bytes: 1024,
                    error_tolerance: 0.1,
                },
            )
            .expect_err("approximate cache policy should fail");
        assert!(matches!(err, CpdError::NotSupported(_)));
    }

    #[test]
    fn strict_mode_uses_compensated_prefixes() {
        let values = vec![1.0e16, 1.0, -1.0e16];
        let view = make_f64_view(
            &values,
            values.len(),
            1,
            MemoryLayout::CContiguous,
            MissingPolicy::Error,
        );

        let balanced = CostNormalMeanVar::new(ReproMode::Balanced)
            .precompute(&view, &CachePolicy::Full)
            .expect("balanced precompute should succeed");
        let strict = CostNormalMeanVar::new(ReproMode::Strict)
            .precompute(&view, &CachePolicy::Full)
            .expect("strict precompute should succeed");

        let balanced_final = balanced.prefix_sum[balanced.prefix_len_per_dim() - 1];
        let strict_final = strict.prefix_sum[strict.prefix_len_per_dim() - 1];
        assert_eq!(balanced_final, 0.0);
        assert_eq!(strict_final, 1.0);
    }

    #[test]
    #[should_panic(expected = "start < end")]
    fn segment_cost_panics_when_start_ge_end() {
        let model = CostNormalMeanVar::default();
        let values = vec![1.0, 2.0, 3.0, 4.0];
        let view = make_f64_view(
            &values,
            4,
            1,
            MemoryLayout::CContiguous,
            MissingPolicy::Error,
        );
        let cache: NormalCache = model
            .precompute(&view, &CachePolicy::Full)
            .expect("precompute should succeed");

        let _ = model.segment_cost(&cache, 2, 2);
    }

    #[test]
    #[should_panic(expected = "out of bounds")]
    fn segment_cost_panics_when_end_exceeds_n() {
        let model = CostNormalMeanVar::default();
        let values = vec![1.0, 2.0, 3.0, 4.0];
        let view = make_f64_view(
            &values,
            4,
            1,
            MemoryLayout::CContiguous,
            MissingPolicy::Error,
        );
        let cache = model
            .precompute(&view, &CachePolicy::Full)
            .expect("precompute should succeed");

        let _ = model.segment_cost(&cache, 0, 5);
    }
}
