use crate::core::db::Database;
use crate::report::period_trends::query::query_author_period_trends;
use chrono::{Datelike, NaiveDate};
use rusqlite::params;
use super::query::weeks_in_range;
fn seed_author(db: &Database, name: &str, email: &str) -> i64 {
db.connection()
.execute(
"INSERT INTO authors (canonical_name, canonical_email, aliases) \
VALUES (?1, ?2, '[]')",
params![name, email],
)
.expect("insert author");
db.connection().last_insert_rowid()
}
fn seed_commit(
db: &Database,
sha: &str,
author_id: i64,
timestamp: &str,
ticketed: i64,
category: Option<&str>,
) {
let cls_id: Option<i64> = if let Some(cat) = category {
db.connection()
.execute(
"INSERT OR IGNORE INTO classifications \
(id, category, confidence, method) \
VALUES (NULL, ?1, 0.9, 'exact_rule')",
params![cat],
)
.expect("insert classification");
let id: i64 = db
.connection()
.query_row(
"SELECT id FROM classifications WHERE category = ?1 LIMIT 1",
params![cat],
|r| r.get(0),
)
.expect("get cls id");
Some(id)
} else {
None
};
db.connection()
.execute(
"INSERT INTO commits \
(sha, author_id, author_name, author_email, timestamp, message, \
repository, insertions, deletions, ticketed, classification_id) \
VALUES (?1, ?2, 'n', 'e', ?3, 'm', 'repo-a', 5, 2, ?4, ?5)",
params![sha, author_id, timestamp, ticketed, cls_id],
)
.expect("insert commit");
}
#[test]
fn period_trends_basic_windowing() {
let db = Database::open_in_memory().expect("open");
let aid = seed_author(&db, "Alice", "alice@example.com");
let weeks = [
"2024-01-01T00:00:00Z", "2024-01-08T00:00:00Z", "2024-01-15T00:00:00Z", "2024-01-22T00:00:00Z", "2024-01-29T00:00:00Z", "2024-02-05T00:00:00Z", "2024-02-12T00:00:00Z", "2024-02-19T00:00:00Z", ];
for (i, ts) in weeks.iter().enumerate() {
seed_commit(&db, &format!("sha{i}"), aid, ts, 0, None);
}
let trends =
query_author_period_trends(&db, "alice@example.com", 4, None, None).expect("query");
assert_eq!(trends.len(), 2, "expected 2 period windows for 8 weeks");
assert_eq!(
trends[0].commit_count, 4,
"first window: 4 commits, got {}",
trends[0].commit_count
);
assert_eq!(
trends[1].commit_count, 4,
"second window: 4 commits, got {}",
trends[1].commit_count
);
}
#[test]
fn period_trends_label_and_date_format() {
let db = Database::open_in_memory().expect("open");
let aid = seed_author(&db, "Bob", "bob@example.com");
seed_commit(&db, "sha1", aid, "2024-01-01T00:00:00Z", 0, None);
seed_commit(&db, "sha2", aid, "2024-01-08T00:00:00Z", 0, None);
let trends = query_author_period_trends(&db, "bob@example.com", 4, None, None).expect("query");
assert!(!trends.is_empty());
let p = &trends[0];
assert!(
p.period_label.contains("-W"),
"period_label must contain '-W': {}",
p.period_label
);
assert_eq!(p.since.len(), 10, "since must be YYYY-MM-DD: {}", p.since);
assert_eq!(p.until.len(), 10, "until must be YYYY-MM-DD: {}", p.until);
}
#[test]
fn period_trends_ticketed_pct() {
let db = Database::open_in_memory().expect("open");
let aid = seed_author(&db, "Carol", "carol@example.com");
seed_commit(&db, "s1", aid, "2024-01-01T00:00:00Z", 1, None);
seed_commit(&db, "s2", aid, "2024-01-02T00:00:00Z", 1, None);
seed_commit(&db, "s3", aid, "2024-01-03T00:00:00Z", 0, None);
seed_commit(&db, "s4", aid, "2024-01-04T00:00:00Z", 0, None);
let trends =
query_author_period_trends(&db, "carol@example.com", 4, None, None).expect("query");
assert!(!trends.is_empty());
let pct = trends[0].ticketed_pct;
assert!(
(pct - 0.5).abs() < 1e-9,
"ticketed_pct should be 0.5, got {pct}"
);
}
#[test]
fn period_trends_empty_for_no_commits() {
let db = Database::open_in_memory().expect("open");
seed_author(&db, "Dave", "dave@example.com");
let trends = query_author_period_trends(&db, "dave@example.com", 4, None, None).expect("query");
assert!(trends.is_empty(), "no commits → empty Vec, got: {trends:?}");
}
#[test]
fn period_trends_empty_for_unknown_email() {
let db = Database::open_in_memory().expect("open");
let trends =
query_author_period_trends(&db, "nobody@example.com", 4, None, None).expect("query");
assert!(trends.is_empty(), "unknown email → empty Vec");
}
#[test]
fn period_trends_category_aggregation() {
let db = Database::open_in_memory().expect("open");
let aid = seed_author(&db, "Eve", "eve@example.com");
seed_commit(&db, "e1", aid, "2024-01-01T00:00:00Z", 0, Some("feature"));
seed_commit(&db, "e2", aid, "2024-01-02T00:00:00Z", 0, Some("feature"));
seed_commit(&db, "e3", aid, "2024-01-03T00:00:00Z", 0, Some("feature"));
seed_commit(&db, "e4", aid, "2024-01-04T00:00:00Z", 0, Some("bugfix"));
let trends = query_author_period_trends(&db, "eve@example.com", 4, None, None).expect("query");
assert!(!trends.is_empty());
let cats = &trends[0].categories;
assert_eq!(
cats.get("feature").copied(),
Some(3),
"expected 3 feature commits"
);
assert_eq!(
cats.get("bugfix").copied(),
Some(1),
"expected 1 bugfix commit"
);
}
#[test]
fn period_trends_date_filter() {
let db = Database::open_in_memory().expect("open");
let aid = seed_author(&db, "Frank", "frank@example.com");
seed_commit(&db, "f1", aid, "2024-01-10T00:00:00Z", 0, None);
seed_commit(&db, "f2", aid, "2024-01-20T00:00:00Z", 0, None);
seed_commit(&db, "f3", aid, "2024-02-10T00:00:00Z", 0, None);
let trends = query_author_period_trends(
&db,
"frank@example.com",
4,
Some("2024-01-01"),
Some("2024-01-31"),
)
.expect("query");
let total: u64 = trends.iter().map(|p| p.commit_count).sum();
assert_eq!(
total, 2,
"filter to Jan 2024 should yield 2 commits, got {total}"
);
}
#[test]
fn weeks_in_range_produces_correct_mondays() {
let start = NaiveDate::from_ymd_opt(2024, 1, 1).expect("date");
let end = NaiveDate::from_ymd_opt(2024, 1, 28).expect("date");
let weeks = weeks_in_range(start, end);
assert_eq!(weeks.len(), 4, "should be 4 Mondays in the range");
for w in &weeks {
assert_eq!(w.weekday(), chrono::Weekday::Mon, "{w} must be a Monday");
}
}