use crate::factor::{Factor, VarId, VarStore};

/// Maintains the constraint `diff = team_a - team_b` between three vars.
///
/// On each propagation:
/// - Reads marginals at `team_a` and `team_b` (which already incorporate any
///   incoming messages from neighboring factors).
/// - Computes `new_diff = team_a - team_b` (variance addition; see Gaussian::Sub).
/// - Writes the new marginal to `diff`.
/// - Returns the delta against the previous diff value.
///
/// This factor does NOT store an outgoing message; the diff variable is
/// effectively replaced on each propagation. The TruncFactor on the same diff
/// var holds the EP-divide message that produces the cavity.
#[derive(Debug)]
pub struct RankDiffFactor {
    pub team_a: VarId,
    pub team_b: VarId,
    pub diff: VarId,
}

impl Factor for RankDiffFactor {
    fn propagate(&mut self, vars: &mut VarStore) -> (f64, f64) {
        let a = vars.get(self.team_a);
        let b = vars.get(self.team_b);
        let new_diff = a - b;
        let old = vars.get(self.diff);
        vars.set(self.diff, new_diff);
        old.delta(new_diff)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{N_INF, gaussian::Gaussian};

    #[test]
    fn diff_of_two_known_gaussians() {
        let mut vars = VarStore::new();
        let team_a = vars.alloc(Gaussian::from_ms(25.0, 3.0));
        let team_b = vars.alloc(Gaussian::from_ms(20.0, 4.0));
        let diff = vars.alloc(N_INF);

        let mut f = RankDiffFactor {
            team_a,
            team_b,
            diff,
        };
        f.propagate(&mut vars);

        let result = vars.get(diff);
        // mu = 25 - 20 = 5; var = 9 + 16 = 25; sigma = 5
        assert!((result.mu() - 5.0).abs() < 1e-12);
        assert!((result.sigma() - 5.0).abs() < 1e-12);
    }

    #[test]
    fn delta_zero_on_repeat() {
        let mut vars = VarStore::new();
        let team_a = vars.alloc(Gaussian::from_ms(10.0, 2.0));
        let team_b = vars.alloc(Gaussian::from_ms(8.0, 1.0));
        let diff = vars.alloc(N_INF);

        let mut f = RankDiffFactor {
            team_a,
            team_b,
            diff,
        };
        f.propagate(&mut vars);
        let (dmu, dsig) = f.propagate(&mut vars);
        assert!(dmu < 1e-12);
        assert!(dsig < 1e-12);
    }

    #[test]
    fn delta_reflects_team_change() {
        let mut vars = VarStore::new();
        let team_a = vars.alloc(Gaussian::from_ms(10.0, 1.0));
        let team_b = vars.alloc(Gaussian::from_ms(0.0, 1.0));
        let diff = vars.alloc(N_INF);

        let mut f = RankDiffFactor {
            team_a,
            team_b,
            diff,
        };
        f.propagate(&mut vars);

        // change team_a, repropagate; delta should be positive
        vars.set(team_a, Gaussian::from_ms(15.0, 1.0));
        let (dmu, _dsig) = f.propagate(&mut vars);
        assert!(dmu > 4.0, "expected ~5 delta, got {}", dmu);
    }
}
