//! Confidence bounds for proportions: the Wilson score interval.
//!
//! The tool-selection benchmark reports an accuracy (a fraction of intents
//! whose top tool was correct) computed over a finite intent set, so a point
//! estimate alone hides how much the number can move. The Wilson score
//! interval gives a two-sided confidence bound that stays inside `[0, 1]` and
//! behaves well for small samples and proportions near 0 or 1, where the naive
//! normal interval breaks down. It is deterministic and closed-form, so it
//! needs no sampling.

use serde::{Deserialize, Serialize};

/// z for a 95% two-sided interval (the standard normal 0.975 quantile).
pub const Z_95: f64 = 1.959_963_984_540_054;

/// A proportion estimate with a Wilson score confidence interval.
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Proportion {
    /// Number of successes.
    pub successes: u64,
    /// Sample size.
    pub total: u64,
    /// Point estimate `successes / total` (0.0 when `total` is zero).
    pub estimate: f64,
    /// Lower bound of the confidence interval, clamped to `[0, 1]`.
    pub low: f64,
    /// Upper bound of the confidence interval, clamped to `[0, 1]`.
    pub high: f64,
}

/// Build a [`Proportion`] with a Wilson score interval at the given `z`.
///
/// A zero `total` yields a zero estimate and a full `[0, 1]` interval, since no
/// observation constrains the rate. Use [`Z_95`] for the common 95% bound.
pub fn wilson(successes: u64, total: u64, z: f64) -> Proportion {
    if total == 0 {
        return Proportion {
            successes,
            total,
            estimate: 0.0,
            low: 0.0,
            high: 1.0,
        };
    }
    let n = total as f64;
    let p = successes as f64 / n;
    let z2 = z * z;
    let denom = 1.0 + z2 / n;
    let center = (p + z2 / (2.0 * n)) / denom;
    let margin = (z * ((p * (1.0 - p) / n) + z2 / (4.0 * n * n)).sqrt()) / denom;
    Proportion {
        successes,
        total,
        estimate: p,
        low: (center - margin).clamp(0.0, 1.0),
        high: (center + margin).clamp(0.0, 1.0),
    }
}

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

    #[test]
    fn brackets_the_point_estimate() {
        let p = wilson(8, 10, Z_95);
        assert!((p.estimate - 0.8).abs() < 1e-9);
        assert!(p.low < p.estimate && p.estimate < p.high, "{p:?}");
        assert!(p.low >= 0.0 && p.high <= 1.0);
    }

    #[test]
    fn a_perfect_rate_keeps_the_upper_bound_at_one_and_pulls_the_lower_in() {
        let p = wilson(10, 10, Z_95);
        assert_eq!(p.estimate, 1.0);
        assert!((p.high - 1.0).abs() < 1e-9, "{p:?}");
        // Wilson does not collapse to a point at the boundary the way the
        // normal interval does: the lower bound stays well below 1.
        assert!(p.low < 1.0 && p.low > 0.6, "{p:?}");
    }

    #[test]
    fn a_larger_sample_tightens_the_interval() {
        let small = wilson(8, 10, Z_95);
        let large = wilson(800, 1000, Z_95);
        assert!(
            (large.high - large.low) < (small.high - small.low),
            "small={small:?} large={large:?}"
        );
    }

    #[test]
    fn an_empty_sample_is_maximally_uncertain() {
        let p = wilson(0, 0, Z_95);
        assert_eq!((p.low, p.high), (0.0, 1.0));
    }
}
