//! Outcome of a match.
//!
//! `Ranked(ranks)` for ordinal results; `Scored { scores, sigma }` for
//! continuous per-team scores (engages `MarginFactor` in the engine).

use smallvec::SmallVec;

/// Final outcome of a match.
///
/// `Ranked(ranks)`: lower rank = better. Equal ranks mean a tie between those
/// teams. `ranks.len()` must equal the number of teams in the event.
///
/// `Scored { scores, sigma }`: higher score = better. Adjacent (sorted) pairs
/// feed observed margins to `MarginFactor`. `scores.len()` must equal the
/// number of teams in the event. `sigma` overrides `HistoryBuilder::score_sigma`
/// when `Some`; `None` inherits the history default.
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub enum Outcome {
    Ranked(SmallVec<[u32; 4]>),
    Scored {
        scores: SmallVec<[f64; 4]>,
        /// Per-event noise override. `None` means inherit
        /// `HistoryBuilder::score_sigma`. Must be `> 0.0` if `Some`.
        sigma: Option<f64>,
    },
}

impl Outcome {
    /// `n`-team outcome where team `winner` won and everyone else tied for last.
    ///
    /// Panics if `winner >= n`.
    pub fn winner(winner: u32, n: u32) -> Self {
        assert!(winner < n, "winner index {winner} out of range 0..{n}");
        let ranks: SmallVec<[u32; 4]> = (0..n).map(|i| if i == winner { 0 } else { 1 }).collect();
        Self::Ranked(ranks)
    }

    /// All `n` teams tied.
    pub fn draw(n: u32) -> Self {
        Self::Ranked(SmallVec::from_vec(vec![0; n as usize]))
    }

    /// Explicit per-team ranking.
    pub fn ranking<I: IntoIterator<Item = u32>>(ranks: I) -> Self {
        Self::Ranked(ranks.into_iter().collect())
    }

    /// Explicit per-team continuous scores; higher = better.
    /// Inherits `HistoryBuilder::score_sigma` for the noise model.
    pub fn scores<I: IntoIterator<Item = f64>>(scores: I) -> Self {
        Self::Scored {
            scores: scores.into_iter().collect(),
            sigma: None,
        }
    }

    /// Explicit per-team continuous scores with a per-event noise override.
    ///
    /// `sigma` must be `> 0.0`; debug-asserts otherwise.
    pub fn scores_with_sigma<I: IntoIterator<Item = f64>>(scores: I, sigma: f64) -> Self {
        debug_assert!(sigma > 0.0, "score_sigma must be > 0.0 (got {sigma})");
        Self::Scored {
            scores: scores.into_iter().collect(),
            sigma: Some(sigma),
        }
    }

    pub fn team_count(&self) -> usize {
        match self {
            Self::Ranked(r) => r.len(),
            Self::Scored { scores, .. } => scores.len(),
        }
    }

    pub(crate) fn as_ranks(&self) -> Option<&[u32]> {
        match self {
            Self::Ranked(r) => Some(r),
            Self::Scored { .. } => None,
        }
    }

    pub(crate) fn as_scores(&self) -> Option<&[f64]> {
        match self {
            Self::Scored { scores, .. } => Some(scores),
            Self::Ranked(_) => None,
        }
    }
}

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

    #[test]
    fn winner_two_teams() {
        let o = Outcome::winner(0, 2);
        assert_eq!(o.as_ranks(), Some(&[0u32, 1][..]));
        assert_eq!(o.team_count(), 2);
    }

    #[test]
    fn winner_three_teams_second_wins() {
        let o = Outcome::winner(1, 3);
        assert_eq!(o.as_ranks(), Some(&[1u32, 0, 1][..]));
    }

    #[test]
    fn draw_three_teams() {
        let o = Outcome::draw(3);
        assert_eq!(o.as_ranks(), Some(&[0u32, 0, 0][..]));
    }

    #[test]
    fn ranking_from_iter() {
        let o = Outcome::ranking([2, 0, 1]);
        assert_eq!(o.as_ranks(), Some(&[2u32, 0, 1][..]));
    }

    #[test]
    #[should_panic(expected = "winner index 2 out of range")]
    fn winner_out_of_range_panics() {
        let _ = Outcome::winner(2, 2);
    }

    #[test]
    fn scored_two_teams() {
        let o = Outcome::scores([10.0, 4.0]);
        assert_eq!(o.team_count(), 2);
        assert_eq!(o.as_scores(), Some(&[10.0, 4.0][..]));
        assert_eq!(o.as_ranks(), None);
    }

    #[test]
    fn scored_team_count_matches_input() {
        let o = Outcome::scores([3.0, 1.0, 2.0, 0.0]);
        assert_eq!(o.team_count(), 4);
    }

    #[test]
    fn ranked_as_scores_returns_none() {
        let o = Outcome::winner(0, 2);
        assert!(o.as_scores().is_none());
        assert!(o.as_ranks().is_some());
    }

    #[test]
    fn scores_with_sigma_round_trips() {
        let o = Outcome::scores_with_sigma([10.0, 4.0], 0.5);
        assert_eq!(o.team_count(), 2);
        assert_eq!(o.as_scores(), Some(&[10.0, 4.0][..]));
    }

    #[test]
    fn scores_constructor_leaves_sigma_unset() {
        let o = Outcome::scores([3.0, 1.0]);
        match o {
            Outcome::Scored { scores: _, sigma } => assert!(sigma.is_none()),
            Outcome::Ranked(_) => panic!("expected Scored variant"),
        }
    }

    #[test]
    fn scores_with_sigma_sets_sigma_some() {
        let o = Outcome::scores_with_sigma([3.0, 1.0], 2.0);
        match o {
            Outcome::Scored { scores: _, sigma } => assert_eq!(sigma, Some(2.0)),
            Outcome::Ranked(_) => panic!("expected Scored variant"),
        }
    }

    #[test]
    #[should_panic(expected = "score_sigma must be > 0.0")]
    fn scores_with_sigma_rejects_zero() {
        let _ = Outcome::scores_with_sigma([3.0, 1.0], 0.0);
    }
}
