use std::cmp::Ordering;

use crate::{
    N_INF, N00,
    arena::ScratchArena,
    compute_margin,
    drift::Drift,
    factor::{Factor, trunc::TruncFactor},
    gaussian::Gaussian,
    rating::Rating,
    time::Time,
    tuple_gt, tuple_max,
};

#[derive(Clone, Copy, Debug)]
pub struct GameOptions {
    pub p_draw: f64,
    pub convergence: crate::ConvergenceOptions,
}

impl Default for GameOptions {
    fn default() -> Self {
        Self {
            p_draw: crate::P_DRAW,
            convergence: crate::ConvergenceOptions::default(),
        }
    }
}

/// Owned variant of `Game` returned by public constructors.
///
/// Unlike `Game<'a, T, D>` (which borrows its result/weights slices from
/// History's internal state), `OwnedGame<T, D>` owns its inputs so it can
/// be returned freely from public constructors.
#[derive(Debug)]
#[allow(dead_code)]
pub struct OwnedGame<T: Time, D: Drift<T>> {
    teams: Vec<Vec<Rating<T, D>>>,
    result: Vec<f64>,
    weights: Vec<Vec<f64>>,
    p_draw: f64,
    pub(crate) likelihoods: Vec<Vec<Gaussian>>,
    pub(crate) evidence: f64,
}

impl<T: Time, D: Drift<T>> OwnedGame<T, D> {
    pub(crate) fn new(
        teams: Vec<Vec<Rating<T, D>>>,
        result: Vec<f64>,
        weights: Vec<Vec<f64>>,
        p_draw: f64,
    ) -> Self {
        let mut arena = ScratchArena::new();
        let g = Game::ranked_with_arena(teams.clone(), &result, &weights, p_draw, &mut arena);
        let likelihoods = g.likelihoods;
        let evidence = g.evidence;
        Self {
            teams,
            result,
            weights,
            p_draw,
            likelihoods,
            evidence,
        }
    }

    pub fn posteriors(&self) -> Vec<Vec<Gaussian>> {
        self.likelihoods
            .iter()
            .zip(self.teams.iter())
            .map(|(l, t)| l.iter().zip(t.iter()).map(|(&l, r)| l * r.prior).collect())
            .collect()
    }

    pub fn log_evidence(&self) -> f64 {
        self.evidence.ln()
    }
}

#[derive(Debug)]
pub struct Game<'a, T: Time = i64, D: Drift<T> = crate::drift::ConstantDrift> {
    teams: Vec<Vec<Rating<T, D>>>,
    result: &'a [f64],
    weights: &'a [Vec<f64>],
    p_draw: f64,
    pub(crate) likelihoods: Vec<Vec<Gaussian>>,
    pub(crate) evidence: f64,
}

impl<'a, T: Time, D: Drift<T>> Game<'a, T, D> {
    pub(crate) fn ranked_with_arena(
        teams: Vec<Vec<Rating<T, D>>>,
        result: &'a [f64],
        weights: &'a [Vec<f64>],
        p_draw: f64,
        arena: &mut ScratchArena,
    ) -> Self {
        debug_assert!(
            result.len() == teams.len(),
            "result must have the same length as teams"
        );
        debug_assert!(
            weights
                .iter()
                .zip(teams.iter())
                .all(|(w, t)| w.len() == t.len()),
            "weights must have the same dimensions as teams"
        );
        debug_assert!(
            (0.0..1.0).contains(&p_draw),
            "draw probability must be >= 0.0 and < 1.0"
        );
        debug_assert!(
            p_draw > 0.0 || {
                let mut r = result.to_vec();
                r.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap());
                r.windows(2).all(|w| w[0] != w[1])
            },
            "draw must be > 0.0 if there are teams with draw"
        );

        let mut this = Self {
            teams,
            result,
            weights,
            p_draw,
            likelihoods: Vec::new(),
            evidence: 0.0,
        };

        this.likelihoods(arena);
        this
    }

    fn likelihoods(&mut self, arena: &mut ScratchArena) {
        arena.reset();

        let n_teams = self.teams.len();

        // Sort teams by result descending; reuse arena.sort_buf to avoid allocation.
        arena.sort_buf.extend(0..n_teams);
        arena.sort_buf.sort_by(|&i, &j| {
            self.result[j]
                .partial_cmp(&self.result[i])
                .unwrap_or(Ordering::Equal)
        });

        // Team performance priors written into arena buffer (capacity reused across games).
        arena.team_prior.extend(arena.sort_buf.iter().map(|&t| {
            self.teams[t]
                .iter()
                .zip(self.weights[t].iter())
                .fold(N00, |p, (player, &w)| p + (player.performance() * w))
        }));

        let n_diffs = n_teams.saturating_sub(1);

        // One TruncFactor per adjacent sorted-team pair; each owns a diff VarId.
        // trunc stays local (fresh state per game; Vec capacity is typically small).
        let mut trunc: Vec<TruncFactor> = (0..n_diffs)
            .map(|i| {
                let tie = self.result[arena.sort_buf[i]] == self.result[arena.sort_buf[i + 1]];
                let margin = if self.p_draw == 0.0 {
                    0.0
                } else {
                    let a: f64 = self.teams[arena.sort_buf[i]]
                        .iter()
                        .map(|p| p.beta.powi(2))
                        .sum();
                    let b: f64 = self.teams[arena.sort_buf[i + 1]]
                        .iter()
                        .map(|p| p.beta.powi(2))
                        .sum();
                    compute_margin(self.p_draw, (a + b).sqrt())
                };
                let vid = arena.vars.alloc(N_INF);
                TruncFactor::new(vid, margin, tie)
            })
            .collect();

        // Per-team messages from neighbouring RankDiff factors (replaces TeamMessage).
        arena.lhood_lose.resize(n_teams, N_INF);
        arena.lhood_win.resize(n_teams, N_INF);

        let mut step = (f64::INFINITY, f64::INFINITY);
        let mut iter = 0;

        while tuple_gt(step, 1e-6) && iter < 10 {
            step = (0.0_f64, 0.0_f64);

            // Forward sweep: diffs 0 .. n_diffs-2 (all but the last).
            for (e, tf) in trunc[..n_diffs.saturating_sub(1)].iter_mut().enumerate() {
                let pw = arena.team_prior[e] * arena.lhood_lose[e];
                let pl = arena.team_prior[e + 1] * arena.lhood_win[e + 1];
                let raw = pw - pl;
                arena.vars.set(tf.diff, raw * tf.msg);
                let d = tf.propagate(&mut arena.vars);
                step = tuple_max(step, d);

                let new_ll = pw - tf.msg;
                step = tuple_max(step, arena.lhood_lose[e + 1].delta(new_ll));
                arena.lhood_lose[e + 1] = new_ll;
            }

            // Backward sweep: diffs n_diffs-1 .. 1 (reverse, all but the first).
            for (rev_i, tf) in trunc[1..].iter_mut().rev().enumerate() {
                let e = n_diffs - 1 - rev_i;
                let pw = arena.team_prior[e] * arena.lhood_lose[e];
                let pl = arena.team_prior[e + 1] * arena.lhood_win[e + 1];
                let raw = pw - pl;
                arena.vars.set(tf.diff, raw * tf.msg);
                let d = tf.propagate(&mut arena.vars);
                step = tuple_max(step, d);

                let new_lw = pl + tf.msg;
                step = tuple_max(step, arena.lhood_win[e].delta(new_lw));
                arena.lhood_win[e] = new_lw;
            }

            iter += 1;
        }

        // Special case: exactly 1 diff (2-team game); loop body was empty.
        if n_diffs == 1 {
            let raw = (arena.team_prior[0] * arena.lhood_lose[0])
                - (arena.team_prior[1] * arena.lhood_win[1]);
            arena.vars.set(trunc[0].diff, raw * trunc[0].msg);
            trunc[0].propagate(&mut arena.vars);
        }

        // Boundary updates: close the chain at both ends.
        if n_diffs > 0 {
            let pl1 = arena.team_prior[1] * arena.lhood_win[1];
            arena.lhood_win[0] = pl1 + trunc[0].msg;
            let pw_last = arena.team_prior[n_teams - 2] * arena.lhood_lose[n_teams - 2];
            arena.lhood_lose[n_teams - 1] = pw_last - trunc[n_diffs - 1].msg;
        }

        // Evidence = product of per-diff evidences (each cached on first propagation).
        self.evidence = trunc
            .iter()
            .map(|t| t.evidence_cached.unwrap_or(1.0))
            .product();

        // Inverse permutation: inv_buf[orig_i] = sorted_i.
        arena.inv_buf.resize(n_teams, 0);
        for (si, &orig_i) in arena.sort_buf.iter().enumerate() {
            arena.inv_buf[orig_i] = si;
        }

        self.likelihoods = self
            .teams
            .iter()
            .zip(self.weights.iter())
            .enumerate()
            .map(|(orig_i, (players, weights))| {
                let si = arena.inv_buf[orig_i];
                let m = arena.lhood_win[si] * arena.lhood_lose[si];
                let performance = players
                    .iter()
                    .zip(weights.iter())
                    .fold(N00, |p, (player, &w)| p + (player.performance() * w));
                players
                    .iter()
                    .zip(weights.iter())
                    .map(|(player, &w)| {
                        ((m - performance.exclude(player.performance() * w)) * (1.0 / w))
                            .forget(player.beta.powi(2))
                    })
                    .collect::<Vec<_>>()
            })
            .collect::<Vec<_>>();
    }

    pub fn posteriors(&self) -> Vec<Vec<Gaussian>> {
        self.likelihoods
            .iter()
            .zip(self.teams.iter())
            .map(|(l, t)| {
                l.iter()
                    .zip(t.iter())
                    .map(|(&l, p)| l * p.prior)
                    .collect::<Vec<_>>()
            })
            .collect::<Vec<_>>()
    }

    pub fn log_evidence(&self) -> f64 {
        self.evidence.ln()
    }
}

impl<T: Time, D: Drift<T>> Game<'_, T, D> {
    pub fn ranked(
        teams: &[&[Rating<T, D>]],
        outcome: crate::Outcome,
        options: &GameOptions,
    ) -> Result<OwnedGame<T, D>, crate::InferenceError> {
        if !(0.0..1.0).contains(&options.p_draw) {
            return Err(crate::InferenceError::InvalidProbability {
                value: options.p_draw,
            });
        }
        if outcome.team_count() != teams.len() {
            return Err(crate::InferenceError::MismatchedShape {
                kind: "outcome ranks vs teams",
                expected: teams.len(),
                got: outcome.team_count(),
            });
        }

        let ranks = outcome.as_ranks();
        let max_rank = ranks.iter().copied().max().unwrap_or(0) as f64;
        let result: Vec<f64> = ranks.iter().map(|&r| max_rank - r as f64).collect();
        let teams_owned: Vec<Vec<Rating<T, D>>> = teams.iter().map(|t| t.to_vec()).collect();
        let weights: Vec<Vec<f64>> = teams.iter().map(|t| vec![1.0; t.len()]).collect();

        Ok(OwnedGame::new(teams_owned, result, weights, options.p_draw))
    }

    pub fn one_v_one(
        a: &Rating<T, D>,
        b: &Rating<T, D>,
        outcome: crate::Outcome,
    ) -> Result<(Gaussian, Gaussian), crate::InferenceError> {
        let game = Self::ranked(&[&[*a], &[*b]], outcome, &GameOptions::default())?;
        let post = game.posteriors();
        Ok((post[0][0], post[1][0]))
    }

    pub fn free_for_all(
        players: &[&Rating<T, D>],
        outcome: crate::Outcome,
        options: &GameOptions,
    ) -> Result<OwnedGame<T, D>, crate::InferenceError> {
        let teams: Vec<Vec<Rating<T, D>>> = players.iter().map(|p| vec![**p]).collect();
        let team_refs: Vec<&[Rating<T, D>]> = teams.iter().map(|t| t.as_slice()).collect();
        Self::ranked(&team_refs, outcome, options)
    }

    #[doc(hidden)]
    pub fn custom<S: crate::factors::Schedule>(
        factors: &mut [crate::factors::BuiltinFactor],
        vars: &mut crate::factors::VarStore,
        schedule: &S,
    ) -> crate::factors::ScheduleReport {
        schedule.run(factors, vars)
    }
}

#[cfg(test)]
mod tests {
    use ::approx::assert_ulps_eq;

    use super::*;
    use crate::{ConstantDrift, GAMMA, Gaussian, N_INF, Rating, arena::ScratchArena};

    type R = Rating<i64, ConstantDrift>;

    #[test]
    fn test_1vs1() {
        let t_a = R::new(
            Gaussian::from_ms(25.0, 25.0 / 3.0),
            25.0 / 6.0,
            ConstantDrift(25.0 / 300.0),
        );
        let t_b = R::new(
            Gaussian::from_ms(25.0, 25.0 / 3.0),
            25.0 / 6.0,
            ConstantDrift(25.0 / 300.0),
        );

        let w = [vec![1.0], vec![1.0]];
        let g = Game::ranked_with_arena(
            vec![vec![t_a], vec![t_b]],
            &[0.0, 1.0],
            &w,
            0.0,
            &mut ScratchArena::new(),
        );
        let p = g.posteriors();

        let a = p[0][0];
        let b = p[1][0];

        assert_ulps_eq!(a, Gaussian::from_ms(20.794779, 7.194481), epsilon = 1e-6);
        assert_ulps_eq!(b, Gaussian::from_ms(29.205220, 7.194481), epsilon = 1e-6);

        let t_a = R::new(
            Gaussian::from_ms(29.0, 1.0),
            25.0 / 6.0,
            ConstantDrift(GAMMA),
        );
        let t_b = R::new(
            Gaussian::from_ms(25.0, 25.0 / 3.0),
            25.0 / 6.0,
            ConstantDrift(GAMMA),
        );

        let w = [vec![1.0], vec![1.0]];
        let g = Game::ranked_with_arena(
            vec![vec![t_a], vec![t_b]],
            &[0.0, 1.0],
            &w,
            0.0,
            &mut ScratchArena::new(),
        );
        let p = g.posteriors();

        let a = p[0][0];
        let b = p[1][0];

        assert_ulps_eq!(a, Gaussian::from_ms(28.896475, 0.996604), epsilon = 1e-6);
        assert_ulps_eq!(b, Gaussian::from_ms(32.189211, 6.062063), epsilon = 1e-6);

        let t_a = R::new(Gaussian::from_ms(1.139, 0.531), 1.0, ConstantDrift(0.2125));
        let t_b = R::new(Gaussian::from_ms(15.568, 0.51), 1.0, ConstantDrift(0.2125));

        let w = [vec![1.0], vec![1.0]];
        let g = Game::ranked_with_arena(
            vec![vec![t_a], vec![t_b]],
            &[0.0, 1.0],
            &w,
            0.0,
            &mut ScratchArena::new(),
        );

        assert_eq!(g.likelihoods[0][0], N_INF);
        assert_eq!(g.likelihoods[1][0], N_INF);
    }

    #[test]
    fn test_1vs1vs1() {
        let teams = vec![
            vec![R::new(
                Gaussian::from_ms(25.0, 25.0 / 3.0),
                25.0 / 6.0,
                ConstantDrift(25.0 / 300.0),
            )],
            vec![R::new(
                Gaussian::from_ms(25.0, 25.0 / 3.0),
                25.0 / 6.0,
                ConstantDrift(25.0 / 300.0),
            )],
            vec![R::new(
                Gaussian::from_ms(25.0, 25.0 / 3.0),
                25.0 / 6.0,
                ConstantDrift(25.0 / 300.0),
            )],
        ];

        let w = [vec![1.0], vec![1.0], vec![1.0]];
        let g = Game::ranked_with_arena(
            teams.clone(),
            &[1.0, 2.0, 0.0],
            &w,
            0.0,
            &mut ScratchArena::new(),
        );
        let p = g.posteriors();

        let a = p[0][0];
        let b = p[1][0];

        assert_ulps_eq!(a, Gaussian::from_ms(25.000000, 6.238469), epsilon = 1e-6);
        assert_ulps_eq!(b, Gaussian::from_ms(31.311358, 6.698818), epsilon = 1e-6);

        let w = [vec![1.0], vec![1.0], vec![1.0]];
        let g = Game::ranked_with_arena(
            teams.clone(),
            &[2.0, 1.0, 0.0],
            &w,
            0.0,
            &mut ScratchArena::new(),
        );
        let p = g.posteriors();

        let a = p[0][0];
        let b = p[1][0];

        assert_ulps_eq!(a, Gaussian::from_ms(31.311358, 6.698818), epsilon = 1e-6);
        assert_ulps_eq!(b, Gaussian::from_ms(25.000000, 6.238469), epsilon = 1e-6);

        let w = [vec![1.0], vec![1.0], vec![1.0]];
        let g = Game::ranked_with_arena(teams, &[1.0, 2.0, 0.0], &w, 0.5, &mut ScratchArena::new());
        let p = g.posteriors();

        let a = p[0][0];
        let b = p[1][0];
        let c = p[2][0];

        // T1 ULP shift: mu rounds to 25.0 (was 24.999999) under natural-parameter storage.
        assert_ulps_eq!(a, Gaussian::from_ms(25.0, 6.092561), epsilon = 1e-6);
        assert_ulps_eq!(b, Gaussian::from_ms(33.379314, 6.483575), epsilon = 1e-6);
        assert_ulps_eq!(c, Gaussian::from_ms(16.620685, 6.483575), epsilon = 1e-6);
    }

    #[test]
    fn test_1vs1_draw() {
        let t_a = R::new(
            Gaussian::from_ms(25.0, 25.0 / 3.0),
            25.0 / 6.0,
            ConstantDrift(25.0 / 300.0),
        );
        let t_b = R::new(
            Gaussian::from_ms(25.0, 25.0 / 3.0),
            25.0 / 6.0,
            ConstantDrift(25.0 / 300.0),
        );

        let w = [vec![1.0], vec![1.0]];
        let g = Game::ranked_with_arena(
            vec![vec![t_a], vec![t_b]],
            &[0.0, 0.0],
            &w,
            0.25,
            &mut ScratchArena::new(),
        );
        let p = g.posteriors();

        let a = p[0][0];
        let b = p[1][0];

        assert_ulps_eq!(a, Gaussian::from_ms(24.999999, 6.469480), epsilon = 1e-6);
        assert_ulps_eq!(b, Gaussian::from_ms(24.999999, 6.469480), epsilon = 1e-6);

        let t_a = R::new(
            Gaussian::from_ms(25.0, 3.0),
            25.0 / 6.0,
            ConstantDrift(25.0 / 300.0),
        );
        let t_b = R::new(
            Gaussian::from_ms(29.0, 2.0),
            25.0 / 6.0,
            ConstantDrift(25.0 / 300.0),
        );

        let w = [vec![1.0], vec![1.0]];
        let g = Game::ranked_with_arena(
            vec![vec![t_a], vec![t_b]],
            &[0.0, 0.0],
            &w,
            0.25,
            &mut ScratchArena::new(),
        );
        let p = g.posteriors();

        let a = p[0][0];
        let b = p[1][0];

        assert_ulps_eq!(a, Gaussian::from_ms(25.736001, 2.709956), epsilon = 1e-6);
        assert_ulps_eq!(b, Gaussian::from_ms(28.672888, 1.916471), epsilon = 1e-6);
    }

    #[test]
    fn test_1vs1vs1_draw() {
        let t_a = R::new(
            Gaussian::from_ms(25.0, 25.0 / 3.0),
            25.0 / 6.0,
            ConstantDrift(25.0 / 300.0),
        );
        let t_b = R::new(
            Gaussian::from_ms(25.0, 25.0 / 3.0),
            25.0 / 6.0,
            ConstantDrift(25.0 / 300.0),
        );
        let t_c = R::new(
            Gaussian::from_ms(25.0, 25.0 / 3.0),
            25.0 / 6.0,
            ConstantDrift(25.0 / 300.0),
        );

        let w = [vec![1.0], vec![1.0], vec![1.0]];
        let g = Game::ranked_with_arena(
            vec![vec![t_a], vec![t_b], vec![t_c]],
            &[0.0, 0.0, 0.0],
            &w,
            0.25,
            &mut ScratchArena::new(),
        );
        let p = g.posteriors();

        let a = p[0][0];
        let b = p[1][0];
        let c = p[2][0];

        // Goldens updated for natural-parameter storage: mu rounds to 25.0 (was 24.999999),
        // sigma shifts by ~3e-7 ULPs (within 1e-6 of original). Both bounded differences.
        assert_ulps_eq!(a, Gaussian::from_ms(25.0, 5.729069), epsilon = 1e-6);
        assert_ulps_eq!(b, Gaussian::from_ms(25.0, 5.707424), epsilon = 1e-6);
        assert_ulps_eq!(c, Gaussian::from_ms(25.0, 5.729069), epsilon = 1e-6);

        let t_a = R::new(
            Gaussian::from_ms(25.0, 3.0),
            25.0 / 6.0,
            ConstantDrift(25.0 / 300.0),
        );
        let t_b = R::new(
            Gaussian::from_ms(25.0, 3.0),
            25.0 / 6.0,
            ConstantDrift(25.0 / 300.0),
        );
        let t_c = R::new(
            Gaussian::from_ms(29.0, 2.0),
            25.0 / 6.0,
            ConstantDrift(25.0 / 300.0),
        );

        let w = [vec![1.0], vec![1.0], vec![1.0]];
        let g = Game::ranked_with_arena(
            vec![vec![t_a], vec![t_b], vec![t_c]],
            &[0.0, 0.0, 0.0],
            &w,
            0.25,
            &mut ScratchArena::new(),
        );
        let p = g.posteriors();

        let a = p[0][0];
        let b = p[1][0];
        let c = p[2][0];

        assert_ulps_eq!(a, Gaussian::from_ms(25.488507, 2.638208), epsilon = 1e-6);
        assert_ulps_eq!(b, Gaussian::from_ms(25.510671, 2.628751), epsilon = 1e-6);
        assert_ulps_eq!(c, Gaussian::from_ms(28.555920, 1.885689), epsilon = 1e-6);
    }

    #[test]
    fn test_2vs1vs2_mixed() {
        let t_a = vec![
            R::new(
                Gaussian::from_ms(12.0, 3.0),
                25.0 / 6.0,
                ConstantDrift(25.0 / 300.0),
            ),
            R::new(
                Gaussian::from_ms(18.0, 3.0),
                25.0 / 6.0,
                ConstantDrift(25.0 / 300.0),
            ),
        ];
        let t_b = vec![R::new(
            Gaussian::from_ms(30.0, 3.0),
            25.0 / 6.0,
            ConstantDrift(25.0 / 300.0),
        )];
        let t_c = vec![
            R::new(
                Gaussian::from_ms(14.0, 3.0),
                25.0 / 6.0,
                ConstantDrift(25.0 / 300.0),
            ),
            R::new(
                Gaussian::from_ms(16., 3.0),
                25.0 / 6.0,
                ConstantDrift(25.0 / 300.0),
            ),
        ];

        let w = [vec![1.0, 1.0], vec![1.0], vec![1.0, 1.0]];
        let g = Game::ranked_with_arena(
            vec![t_a, t_b, t_c],
            &[1.0, 0.0, 0.0],
            &w,
            0.25,
            &mut ScratchArena::new(),
        );
        let p = g.posteriors();

        assert_ulps_eq!(p[0][0], Gaussian::from_ms(13.051, 2.864), epsilon = 1e-3);
        assert_ulps_eq!(p[0][1], Gaussian::from_ms(19.051, 2.864), epsilon = 1e-3);
        assert_ulps_eq!(p[1][0], Gaussian::from_ms(29.292, 2.764), epsilon = 1e-3);
        assert_ulps_eq!(p[2][0], Gaussian::from_ms(13.658, 2.813), epsilon = 1e-3);
        assert_ulps_eq!(p[2][1], Gaussian::from_ms(15.658, 2.813), epsilon = 1e-3);
    }

    #[test]
    fn test_1vs1_weighted() {
        let w_a = vec![1.0];
        let w_b = vec![2.0];

        let t_a = vec![R::new(
            Gaussian::from_ms(25.0, 25.0 / 3.0),
            25.0 / 6.0,
            ConstantDrift(0.0),
        )];
        let t_b = vec![R::new(
            Gaussian::from_ms(25.0, 25.0 / 3.0),
            25.0 / 6.0,
            ConstantDrift(0.0),
        )];

        let w = [w_a, w_b];
        let g = Game::ranked_with_arena(
            vec![t_a.clone(), t_b.clone()],
            &[1.0, 0.0],
            &w,
            0.0,
            &mut ScratchArena::new(),
        );
        let p = g.posteriors();

        assert_ulps_eq!(
            p[0][0],
            Gaussian::from_ms(30.625173, 7.765472),
            epsilon = 1e-6
        );
        assert_ulps_eq!(
            p[1][0],
            Gaussian::from_ms(13.749653, 5.733840),
            epsilon = 1e-6
        );

        let w_a = vec![1.0];
        let w_b = vec![0.7];

        let w = [w_a, w_b];
        let g = Game::ranked_with_arena(
            vec![t_a.clone(), t_b.clone()],
            &[1.0, 0.0],
            &w,
            0.0,
            &mut ScratchArena::new(),
        );
        let p = g.posteriors();

        assert_ulps_eq!(
            p[0][0],
            Gaussian::from_ms(27.630080, 7.206676),
            epsilon = 1e-6
        );
        assert_ulps_eq!(
            p[1][0],
            Gaussian::from_ms(23.158943, 7.801628),
            epsilon = 1e-6
        );

        let w_a = vec![1.6];
        let w_b = vec![0.7];

        let w = [w_a, w_b];
        let g = Game::ranked_with_arena(
            vec![t_a, t_b],
            &[1.0, 0.0],
            &w,
            0.0,
            &mut ScratchArena::new(),
        );
        let p = g.posteriors();

        assert_ulps_eq!(
            p[0][0],
            Gaussian::from_ms(26.142438, 7.573088),
            epsilon = 1e-6
        );
        assert_ulps_eq!(
            p[1][0],
            Gaussian::from_ms(24.500183, 8.193278),
            epsilon = 1e-6
        );

        let w_a = vec![1.0];
        let w_b = vec![0.0];

        let t_a = vec![R::new(Gaussian::from_ms(2.0, 6.0), 1.0, ConstantDrift(0.0))];
        let t_b = vec![R::new(Gaussian::from_ms(2.0, 6.0), 1.0, ConstantDrift(0.0))];

        let w = [w_a, w_b];
        let g = Game::ranked_with_arena(
            vec![t_a, t_b],
            &[1.0, 0.0],
            &w,
            0.0,
            &mut ScratchArena::new(),
        );
        let p = g.posteriors();

        assert_ulps_eq!(
            p[0][0],
            Gaussian::from_ms(5.557067, 4.052826),
            epsilon = 1e-6
        );
        assert_ulps_eq!(
            p[1][0],
            Gaussian::from_ms(2.000000, 6.000000),
            epsilon = 1e-6
        );

        let w_a = vec![1.0];
        let w_b = vec![-1.0];

        let t_a = vec![R::new(Gaussian::from_ms(2.0, 6.0), 1.0, ConstantDrift(0.0))];
        let t_b = vec![R::new(Gaussian::from_ms(2.0, 6.0), 1.0, ConstantDrift(0.0))];

        let w = [w_a, w_b];
        let g = Game::ranked_with_arena(
            vec![t_a, t_b],
            &[1.0, 0.0],
            &w,
            0.0,
            &mut ScratchArena::new(),
        );
        let p = g.posteriors();

        assert_ulps_eq!(p[0][0], p[1][0], epsilon = 1e-6);
    }

    #[test]
    fn test_2vs2_weighted() {
        let t_a = vec![
            R::new(
                Gaussian::from_ms(25.0, 25.0 / 3.0),
                25.0 / 6.0,
                ConstantDrift(0.0),
            ),
            R::new(
                Gaussian::from_ms(25.0, 25.0 / 3.0),
                25.0 / 6.0,
                ConstantDrift(0.0),
            ),
        ];
        let w_a = vec![0.4, 0.8];

        let t_b = vec![
            R::new(
                Gaussian::from_ms(25.0, 25.0 / 3.0),
                25.0 / 6.0,
                ConstantDrift(0.0),
            ),
            R::new(
                Gaussian::from_ms(25.0, 25.0 / 3.0),
                25.0 / 6.0,
                ConstantDrift(0.0),
            ),
        ];
        let w_b = vec![0.9, 0.6];

        let w = [w_a, w_b];
        let g = Game::ranked_with_arena(
            vec![t_a.clone(), t_b.clone()],
            &[1.0, 0.0],
            &w,
            0.0,
            &mut ScratchArena::new(),
        );
        let p = g.posteriors();

        assert_ulps_eq!(
            p[0][0],
            Gaussian::from_ms(27.539023, 8.129639),
            epsilon = 1e-6
        );
        assert_ulps_eq!(
            p[0][1],
            Gaussian::from_ms(30.078046, 7.485372),
            epsilon = 1e-6
        );
        assert_ulps_eq!(
            p[1][0],
            Gaussian::from_ms(19.287197, 7.243465),
            epsilon = 1e-6
        );
        assert_ulps_eq!(
            p[1][1],
            Gaussian::from_ms(21.191465, 7.867608),
            epsilon = 1e-6
        );

        let w_a = vec![1.3, 1.5];
        let w_b = vec![0.7, 0.4];

        let w = [w_a, w_b];
        let g = Game::ranked_with_arena(
            vec![t_a.clone(), t_b.clone()],
            &[1.0, 0.0],
            &w,
            0.0,
            &mut ScratchArena::new(),
        );
        let p = g.posteriors();

        assert_ulps_eq!(
            p[0][0],
            Gaussian::from_ms(25.190190, 8.220511),
            epsilon = 1e-6
        );
        assert_ulps_eq!(
            p[0][1],
            Gaussian::from_ms(25.219450, 8.182783),
            epsilon = 1e-6
        );
        assert_ulps_eq!(
            p[1][0],
            Gaussian::from_ms(24.897589, 8.300779),
            epsilon = 1e-6
        );
        assert_ulps_eq!(
            p[1][1],
            Gaussian::from_ms(24.941479, 8.322717),
            epsilon = 1e-6
        );

        let w_a = vec![1.6, 0.2];
        let w_b = vec![0.7, 2.4];

        let w = [w_a, w_b];
        let g = Game::ranked_with_arena(
            vec![t_a.clone(), t_b.clone()],
            &[1.0, 0.0],
            &w,
            0.0,
            &mut ScratchArena::new(),
        );
        let p = g.posteriors();

        assert_ulps_eq!(
            p[0][0],
            Gaussian::from_ms(31.674697, 7.501180),
            epsilon = 1e-6
        );
        assert_ulps_eq!(
            p[0][1],
            Gaussian::from_ms(25.834337, 8.320970),
            epsilon = 1e-6
        );
        assert_ulps_eq!(
            p[1][0],
            Gaussian::from_ms(22.079819, 8.180607),
            epsilon = 1e-6
        );
        assert_ulps_eq!(
            p[1][1],
            Gaussian::from_ms(14.987953, 6.308469),
            epsilon = 1e-6
        );

        let w = [vec![1.0, 1.0], vec![1.0]];
        let g = Game::ranked_with_arena(
            vec![
                t_a.clone(),
                vec![R::new(
                    Gaussian::from_ms(25.0, 25.0 / 3.0),
                    25.0 / 6.0,
                    ConstantDrift(0.0),
                )],
            ],
            &[1.0, 0.0],
            &w,
            0.0,
            &mut ScratchArena::new(),
        );
        let post_2vs1 = g.posteriors();

        let w_a = vec![1.0, 1.0];
        let w_b = vec![1.0, 0.0];

        let w = [w_a, w_b];
        let g = Game::ranked_with_arena(
            vec![t_a, t_b.clone()],
            &[1.0, 0.0],
            &w,
            0.0,
            &mut ScratchArena::new(),
        );
        let p = g.posteriors();

        assert_ulps_eq!(p[0][0], post_2vs1[0][0], epsilon = 1e-6);
        assert_ulps_eq!(p[0][1], post_2vs1[0][1], epsilon = 1e-6);
        assert_ulps_eq!(p[1][0], post_2vs1[1][0], epsilon = 1e-6);
        assert_ulps_eq!(p[1][1], t_b[1].prior, epsilon = 1e-6);
    }
}
