//! Regression: a single time slice with many distinct competitors must converge to finite
//! skills. Before the `pi <= 0` guard in `Gaussian::mu()/sigma()`, EP message cancellation
//! produced a tiny-negative precision whose `sigma() = 1/sqrt(pi)` was NaN, which the
//! moment-space `Sub` in the game chain propagated into every skill once the slice grew past
//! ~75 competitors (e.g. a real ranking dataset with hundreds of players).
use trueskill_tt::{ConstantDrift, ConvergenceOptions, EPSILON, History, ITERATIONS, NullObserver};

/// Tiny deterministic LCG — avoids a dev-dependency on `rand`.
struct Lcg(u64);
impl Lcg {
    fn next(&mut self) -> u64 {
        self.0 = self
            .0
            .wrapping_mul(6364136223846793005)
            .wrapping_add(1442695040888963407);
        self.0
    }
    fn below(&mut self, n: usize) -> usize {
        (self.next() >> 33) as usize % n
    }
    fn coin(&mut self) -> bool {
        self.next() & 1 == 0
    }
}

fn nan_after_fit(players: usize) -> usize {
    let mut h: History<i64, ConstantDrift, NullObserver, String> = History::builder_with_key()
        .beta(1.0)
        .sigma(6.0)
        .drift(ConstantDrift(0.1))
        .convergence(ConvergenceOptions {
            max_iter: ITERATIONS,
            epsilon: EPSILON,
            ..Default::default()
        })
        .build();

    let ids: Vec<String> = (0..players).map(|i| format!("p{i:04}")).collect();
    let mut rng = Lcg(1);
    for _ in 0..(players * 4) {
        let a = rng.below(players);
        let mut b = rng.below(players - 1);
        if b >= a {
            b += 1;
        }
        let (w, l) = if rng.coin() { (a, b) } else { (b, a) };
        h.record_winner(&ids[w], &ids[l], 0).unwrap();
    }
    h.converge().unwrap();

    ids.iter()
        .filter(|id| {
            h.current_skill(id.as_str())
                .map(|g| !g.mu().is_finite() || !g.sigma().is_finite())
                .unwrap_or(true)
        })
        .count()
}

#[test]
fn many_competitors_converge_to_finite_skills() {
    // The NaN regression onset was between 70 and 80 competitors; 250 is comfortably past it
    // and in the range of a real ranking dataset.
    for players in [12usize, 75, 150, 250] {
        assert_eq!(
            nan_after_fit(players),
            0,
            "{players}-competitor history produced NaN skills"
        );
    }
}
