//! Determinism tests: identical posteriors across RAYON_NUM_THREADS
//! values. Only compiled with the `rayon` feature.

#![cfg(feature = "rayon")]

use smallvec::smallvec;
use trueskill_tt::{ConstantDrift, ConvergenceOptions, Event, History, Member, Outcome, Team};

/// Build a deterministic workload using a simple LCG (no external rand crate).
fn build_and_converge(seed: u64) -> Vec<(i64, trueskill_tt::Gaussian)> {
    let mut h = History::<i64, _, _, String>::builder_with_key()
        .mu(25.0)
        .sigma(25.0 / 3.0)
        .beta(25.0 / 6.0)
        .drift(ConstantDrift(25.0 / 300.0))
        .convergence(ConvergenceOptions {
            max_iter: 30,
            epsilon: 1e-6,
            alpha: 1.0,
        })
        .build();

    // LCG for deterministic pseudo-random ints.
    let mut rng = seed;
    let mut next = || {
        rng = rng
            .wrapping_mul(6364136223846793005)
            .wrapping_add(1442695040888963407);
        rng
    };

    let mut events: Vec<Event<i64, String>> = Vec::with_capacity(200);
    for ev_i in 0..200 {
        let a = (next() % 40) as usize;
        let mut b = (next() % 40) as usize;
        while b == a {
            b = (next() % 40) as usize;
        }
        // ~10 events per slice so color groups have material parallelism.
        events.push(Event {
            time: (ev_i as i64 / 10) + 1,
            teams: smallvec![
                Team::with_members([Member::new(format!("p{a}"))]),
                Team::with_members([Member::new(format!("p{b}"))]),
            ],
            outcome: Outcome::winner((next() % 2) as u32, 2),
        });
    }
    h.add_events(events).unwrap();
    h.converge().unwrap();
    // Sample one competitor's curve for the comparison.
    h.learning_curve("p0")
}

#[test]
fn posteriors_identical_across_thread_counts() {
    let sizes = [1usize, 2, 4, 8];
    let mut results: Vec<Vec<(i64, trueskill_tt::Gaussian)>> = Vec::new();
    for &n in &sizes {
        let pool = rayon::ThreadPoolBuilder::new()
            .num_threads(n)
            .build()
            .expect("rayon pool build");
        let curve = pool.install(|| build_and_converge(42));
        results.push(curve);
    }

    let reference = &results[0];
    for (i, curve) in results.iter().enumerate().skip(1) {
        assert_eq!(
            curve.len(),
            reference.len(),
            "curve length differs at {n} threads",
            n = sizes[i],
        );
        for (j, (&(t_ref, g_ref), &(t, g))) in reference.iter().zip(curve.iter()).enumerate() {
            assert_eq!(
                t_ref,
                t,
                "time point {j} differs at {n} threads: ref={t_ref} vs got={t}",
                n = sizes[i],
            );
            assert_eq!(
                g_ref.mu().to_bits(),
                g.mu().to_bits(),
                "mu bits differ at {n} threads, time {t}: ref={ref_mu} got={got_mu}",
                n = sizes[i],
                ref_mu = g_ref.mu(),
                got_mu = g.mu(),
            );
            assert_eq!(
                g_ref.sigma().to_bits(),
                g.sigma().to_bits(),
                "sigma bits differ at {n} threads, time {t}: ref={ref_sigma} got={got_sigma}",
                n = sizes[i],
                ref_sigma = g_ref.sigma(),
                got_sigma = g.sigma(),
            );
        }
    }
}
