use std::cmp::Ordering;
use std::fmt;

/// A finite floating-point score used for ranking.
///
/// Rejects NaN and infinities at construction, which makes `Eq` and `Ord`
/// safe to implement — all stored values are totally ordered.
#[derive(Clone, Copy)]
pub struct Score(f64);

impl Score {
    pub const ZERO: Self = Self(0.0);

    /// Creates a score if the value is finite (not NaN, not infinite).
    #[must_use]
    pub const fn new(value: f64) -> Option<Self> {
        if value.is_finite() {
            Some(Self(value))
        } else {
            None
        }
    }

    #[must_use]
    pub const fn as_f64(self) -> f64 {
        self.0
    }
}

impl PartialEq for Score {
    fn eq(&self, other: &Self) -> bool {
        self.0.total_cmp(&other.0) == Ordering::Equal
    }
}

impl Eq for Score {}

impl Ord for Score {
    fn cmp(&self, other: &Self) -> Ordering {
        self.0.total_cmp(&other.0)
    }
}

impl PartialOrd for Score {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl fmt::Display for Score {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{:.6}", self.0)
    }
}

impl fmt::Debug for Score {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "Score({:.6})", self.0)
    }
}

#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::float_cmp)]
mod tests {
    use super::*;

    #[test]
    fn rejects_nan() {
        assert!(Score::new(f64::NAN).is_none());
    }

    #[test]
    fn rejects_infinity() {
        assert!(Score::new(f64::INFINITY).is_none());
    }

    #[test]
    fn rejects_neg_infinity() {
        assert!(Score::new(f64::NEG_INFINITY).is_none());
    }

    #[test]
    fn accepts_zero() {
        let s = Score::new(0.0).unwrap();
        assert_eq!(s.as_f64(), 0.0);
    }

    #[test]
    fn accepts_negative() {
        let s = Score::new(-1.5).unwrap();
        assert_eq!(s.as_f64(), -1.5);
    }

    #[test]
    fn accepts_positive() {
        let s = Score::new(100.0).unwrap();
        assert_eq!(s.as_f64(), 100.0);
    }

    #[test]
    fn zero_constant() {
        assert_eq!(Score::ZERO.as_f64(), 0.0);
    }

    #[test]
    fn display_format() {
        let s = Score::new(std::f64::consts::PI).unwrap();
        assert_eq!(s.to_string(), "3.141593");
    }

    #[test]
    fn debug_format() {
        let s = Score::new(std::f64::consts::PI).unwrap();
        assert_eq!(format!("{s:?}"), "Score(3.141593)");
    }

    #[test]
    fn ordering_works() {
        let a = Score::new(-1.0).unwrap();
        let b = Score::ZERO;
        let c = Score::new(1.0).unwrap();
        assert!(a < b);
        assert!(b < c);
        assert!(a < c);
    }

    mod proptests {
        use super::*;
        use proptest::prelude::*;

        proptest! {
            #[test]
            fn total_ordering_consistency(
                a in any::<f64>().prop_filter("finite", |x| x.is_finite()),
                b in any::<f64>().prop_filter("finite", |x| x.is_finite()),
            ) {
                let sa = Score::new(a).unwrap();
                let sb = Score::new(b).unwrap();
                // Ord and PartialOrd agree
                prop_assert_eq!(sa.partial_cmp(&sb), Some(sa.cmp(&sb)));
                // Antisymmetry
                if sa.cmp(&sb) == Ordering::Equal {
                    prop_assert_eq!(sb.cmp(&sa), Ordering::Equal);
                }
                // Consistency with Eq
                if sa == sb {
                    prop_assert_eq!(sa.cmp(&sb), Ordering::Equal);
                }
            }
        }
    }
}
