//! Suite-level rollup of named per-test metrics.
//!
//! Each test reports its derived metrics (a name and a value). This aggregates
//! them across the whole run into one mean and count per metric name, so a
//! report can show "factuality: 0.82 (mean over 14 tests)" the way promptfoo's
//! summary metric column does. The per-test values are unchanged; this is a
//! read-only view over [`Report::tests`].

use crate::report::Report;

/// One metric's suite-level rollup: its mean value and how many tests reported
/// it. A metric named in only some tests is averaged over the tests that
/// reported it, and `count` reflects that denominator.
#[derive(Debug, Clone, PartialEq)]
pub struct MetricRollup {
    /// Metric name, shared across the tests that reported it.
    pub name: String,
    /// Mean of the reported values.
    pub mean: f64,
    /// Number of tests that reported this metric.
    pub count: usize,
}

impl Report {
    /// Aggregate the per-test derived metrics into one [`MetricRollup`] per
    /// metric name, in name order. Returns an empty vector when no test
    /// reported a derived metric, so a plain run adds nothing to the summary.
    pub fn metric_rollups(&self) -> Vec<MetricRollup> {
        use std::collections::BTreeMap;
        let mut sums: BTreeMap<&str, (f64, usize)> = BTreeMap::new();
        for test in &self.tests {
            for metric in &test.derived_metrics {
                let entry = sums.entry(metric.name.as_str()).or_insert((0.0, 0));
                entry.0 += metric.value;
                entry.1 += 1;
            }
        }
        sums.into_iter()
            .map(|(name, (sum, count))| MetricRollup {
                name: name.to_string(),
                mean: if count == 0 { 0.0 } else { sum / count as f64 },
                count,
            })
            .collect()
    }
}

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

    fn dm(name: &str, value: f64) -> DerivedMetricReport {
        DerivedMetricReport {
            name: name.into(),
            value,
            passed: None,
        }
    }

    #[test]
    fn rollup_means_over_reporting_tests_only() {
        let mut r = Report::sample();
        // `factuality` on tests 0 and 2, `relevance` on test 0 only.
        r.tests[0].derived_metrics = vec![dm("factuality", 0.8), dm("relevance", 0.6)];
        r.tests[2].derived_metrics = vec![dm("factuality", 0.4)];
        let rollups = r.metric_rollups();

        let fact = rollups.iter().find(|x| x.name == "factuality").unwrap();
        assert_eq!(
            fact.count, 2,
            "averaged over the two tests that reported it"
        );
        assert!((fact.mean - 0.6).abs() < f64::EPSILON);

        let rel = rollups.iter().find(|x| x.name == "relevance").unwrap();
        assert_eq!(rel.count, 1);
        assert!((rel.mean - 0.6).abs() < f64::EPSILON);
    }

    #[test]
    fn no_derived_metrics_yields_no_rollups() {
        assert!(Report::sample().metric_rollups().is_empty());
    }

    #[test]
    fn rollups_are_in_name_order() {
        let mut r = Report::sample();
        r.tests[0].derived_metrics = vec![dm("zeta", 1.0), dm("alpha", 0.5)];
        let names: Vec<String> = r.metric_rollups().into_iter().map(|m| m.name).collect();
        assert_eq!(names, vec!["alpha".to_string(), "zeta".to_string()]);
    }
}
