use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use crate::value_objects::{DurationMs, Specialty};
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Statistics {
total_deliberations: u64,
total_orchestrations: u64,
total_duration: DurationMs,
per_specialty: BTreeMap<Specialty, u64>,
}
impl Statistics {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn from_counters(
total_deliberations: u64,
total_orchestrations: u64,
total_duration: DurationMs,
per_specialty: BTreeMap<Specialty, u64>,
) -> Self {
Self {
total_deliberations,
total_orchestrations,
total_duration,
per_specialty,
}
}
pub fn record_deliberation(&mut self, specialty: &Specialty, duration: DurationMs) {
self.total_deliberations = self.total_deliberations.saturating_add(1);
self.total_duration = self.total_duration.saturating_add(duration);
let entry = self.per_specialty.entry(specialty.clone()).or_insert(0);
*entry = entry.saturating_add(1);
}
pub fn record_orchestration(&mut self, duration: DurationMs) {
self.total_orchestrations = self.total_orchestrations.saturating_add(1);
self.total_duration = self.total_duration.saturating_add(duration);
}
pub fn reset(&mut self) {
*self = Self::default();
}
#[must_use]
pub fn total_deliberations(&self) -> u64 {
self.total_deliberations
}
#[must_use]
pub fn total_orchestrations(&self) -> u64 {
self.total_orchestrations
}
#[must_use]
pub fn total_duration(&self) -> DurationMs {
self.total_duration
}
#[must_use]
pub fn per_specialty(&self) -> &BTreeMap<Specialty, u64> {
&self.per_specialty
}
#[must_use]
pub fn average_duration_ms(&self) -> f64 {
let ops = self.total_deliberations + self.total_orchestrations;
if ops == 0 {
0.0
} else {
self.total_duration.get() as f64 / ops as f64
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sp(s: &str) -> Specialty {
Specialty::new(s).unwrap()
}
#[test]
fn fresh_stats_are_zero() {
let s = Statistics::new();
assert_eq!(s.total_deliberations(), 0);
assert_eq!(s.total_orchestrations(), 0);
assert_eq!(s.total_duration(), DurationMs::ZERO);
assert_eq!(s.average_duration_ms(), 0.0);
assert!(s.per_specialty().is_empty());
}
#[test]
fn record_deliberation_advances_counters() {
let mut s = Statistics::new();
s.record_deliberation(&sp("triage"), DurationMs::from_millis(100));
s.record_deliberation(&sp("triage"), DurationMs::from_millis(50));
s.record_deliberation(&sp("reviewer"), DurationMs::from_millis(200));
assert_eq!(s.total_deliberations(), 3);
assert_eq!(s.total_duration(), DurationMs::from_millis(350));
assert_eq!(s.per_specialty().get(&sp("triage")).copied(), Some(2));
assert_eq!(s.per_specialty().get(&sp("reviewer")).copied(), Some(1));
}
#[test]
fn record_orchestration_advances_counters_but_not_specialty_map() {
let mut s = Statistics::new();
s.record_orchestration(DurationMs::from_millis(300));
assert_eq!(s.total_orchestrations(), 1);
assert_eq!(s.total_duration(), DurationMs::from_millis(300));
assert!(s.per_specialty().is_empty());
}
#[test]
fn average_duration_divides_over_all_ops() {
let mut s = Statistics::new();
s.record_deliberation(&sp("a"), DurationMs::from_millis(100));
s.record_orchestration(DurationMs::from_millis(200));
assert_eq!(s.average_duration_ms(), 150.0);
}
#[test]
fn reset_clears_state() {
let mut s = Statistics::new();
s.record_deliberation(&sp("x"), DurationMs::from_millis(10));
s.reset();
assert_eq!(s, Statistics::default());
}
#[test]
fn saturating_counters_do_not_overflow() {
let mut s = Statistics::new();
s.total_deliberations = u64::MAX;
s.total_duration = DurationMs::from_millis(u64::MAX);
s.record_deliberation(&sp("x"), DurationMs::from_millis(42));
assert_eq!(s.total_deliberations(), u64::MAX);
assert_eq!(s.total_duration().get(), u64::MAX);
}
}