use crate::domain::model::issue::{Issue, IssueView};
use crate::domain::model::status::StatusesConfig;
use crate::domain::model::temporal::iso_date::IsoDate;
use crate::domain::usecases::issue::filter::IssueFilter;
use super::by_month::compute_by_month;
use super::cadence::compute_cadence;
use super::cycle_time::compute_cycle_time;
use super::distributions::compute_distributions;
use super::flow_efficiency::compute_flow_efficiency;
use super::flow_load::compute_flow_load;
use super::lead_time::compute_lead_time;
use super::open_age::{compute_age_buckets, compute_open_age};
use super::overview::compute_overview;
use super::queue_time::compute_queue_time;
use super::throughput::compute_throughput;
use super::types::{AgeBuckets, IssueStats};
pub fn compute_issue_stats(
issues: &[Issue],
statuses: &StatusesConfig,
filter: &IssueFilter<'_>,
today: &IsoDate,
detailed: bool,
weeks: u32,
stale_after: u32,
) -> IssueStats {
let view = IssueView::from_slice(issues).matching(filter);
let ov = compute_overview(&view);
let age = compute_open_age(&view, today);
let lead_time = compute_lead_time(&view, statuses);
let flow_load = compute_flow_load(
&view,
statuses,
today,
lead_time.as_ref().map(|lt| lt.p85),
stale_after,
detailed,
);
let (throughput, throughput_stability_pct) = compute_throughput(&view, statuses, today, weeks);
let (
cycle_time,
queue_time,
flow_efficiency_pct,
age_buckets,
by_month,
cadence,
distributions,
) = if detailed {
(
compute_cycle_time(&view, statuses),
compute_queue_time(&view, statuses),
compute_flow_efficiency(&view, statuses),
compute_age_buckets(&age.open_ages_days),
compute_by_month(&view, today),
compute_cadence(&view, statuses, today, weeks),
compute_distributions(&view, statuses),
)
} else {
(None, None, None, AgeBuckets::default(), vec![], None, None)
};
IssueStats {
weeks,
total: ov.total,
by_status: ov.by_status,
avg_age_days: age.avg_age_days,
oldest_open_days: age.oldest_open_days,
newest_open_days: age.newest_open_days,
created_last_7: age.created_last_7,
created_last_30: age.created_last_30,
created_last_90: age.created_last_90,
flow_load,
lead_time,
throughput_stability_pct,
cycle_time,
queue_time,
flow_efficiency_pct,
throughput,
age_buckets,
by_month,
cadence,
distributions,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::model::issue::Issue;
use crate::domain::model::temporal::iso_date::IsoDate;
use crate::domain::usecases::issue::stats::types::AgeBuckets;
use crate::domain::usecases::issue::tests::{defect, feature, ir, IssueFixture};
#[test]
fn filters_by_status_before_computing() {
scenario()
.given(feature("Open").status("open"))
.given(defect("Closed").status("closed"))
.when_stats_with_status_filter("open")
.then_total(1);
}
#[test]
fn detailed_false_leaves_detail_fields_empty() {
scenario()
.given(
feature("T1")
.status("closed")
.with_timestamped_event("2026-03-01T00:00:00Z", "created", None, None)
.with_timestamped_event(
"2026-03-05T00:00:00Z",
"status_changed",
Some("open"),
Some("closed"),
),
)
.when_stats(false)
.then_detail_fields_empty();
}
#[test]
fn detailed_true_populates_detail_fields() {
scenario()
.given(
feature("T1")
.status("closed")
.with_timestamped_event("2026-03-01T00:00:00Z", "created", None, None)
.with_timestamped_event(
"2026-03-05T00:00:00Z",
"status_changed",
Some("open"),
Some("closed"),
),
)
.when_stats(true)
.then_detail_fields_populated();
}
struct Scenario {
issues: Vec<Issue>,
}
fn scenario() -> Scenario {
Scenario { issues: vec![] }
}
fn today() -> IsoDate {
IsoDate::new("2026-03-12").unwrap()
}
impl Scenario {
fn given(mut self, fixture: IssueFixture) -> Self {
let id = self.issues.len() as u64 + 1;
self.issues.push(fixture.build(ir(id)));
self
}
fn when_stats(self, detailed: bool) -> StatsOutcome {
let result = compute_issue_stats(
&self.issues,
&StatusesConfig::default_issue(),
&IssueFilter::default(),
&today(),
detailed,
8,
3,
);
StatsOutcome { result }
}
fn when_stats_with_status_filter(self, status: &str) -> StatsOutcome {
use crate::domain::model::status::Status;
let st = Status::new(status).unwrap();
let result = compute_issue_stats(
&self.issues,
&StatusesConfig::default_issue(),
&IssueFilter {
status: Some(&st),
..Default::default()
},
&today(),
false,
8,
3,
);
StatsOutcome { result }
}
}
struct StatsOutcome {
result: super::IssueStats,
}
impl StatsOutcome {
fn then_total(self, expected: u32) -> Self {
assert_eq!(self.result.total, expected, "total mismatch");
self
}
fn then_detail_fields_empty(self) -> Self {
assert!(self.result.cycle_time.is_none());
assert!(self.result.flow_efficiency_pct.is_none());
assert_eq!(self.result.age_buckets, AgeBuckets::default());
assert!(self.result.by_month.is_empty());
assert!(self.result.cadence.is_none());
assert!(self.result.distributions.is_none());
self
}
fn then_detail_fields_populated(self) -> Self {
assert!(self.result.cadence.is_some());
assert!(self.result.distributions.is_some());
self
}
}
}