use super::{profile_file_stem, render_markdown, ReportFormat, Reporter};
use crate::profile::types::{
ContributorProfile, Effort, Finding, LongitudinalFinding, TokenCostSummary, Trajectory,
TrendTag,
};
fn make_profile() -> ContributorProfile {
let mut p = ContributorProfile::new(
"alice@example.com",
"Alice Smith",
"2026-01-01",
"2026-06-30",
);
p.repositories = vec!["acme/api".to_string()];
p.improvement_trajectory = Trajectory::Improving;
p.quality_trend = vec![("2026-Q1".to_string(), 3.0), ("2026-Q2".to_string(), 3.8)];
p.strengths = vec!["Consistent ticket coverage".to_string()];
p.recurring_weaknesses = vec!["Missing error handling".to_string()];
p.all_findings = vec![LongitudinalFinding {
period_label: "2026-Q1".to_string(),
finding: Finding::new(
"src/lib.rs",
"error_handling",
"Missing propagation",
"Use ?",
0.8,
Effort::Medium,
),
trend_tag: Some(TrendTag::Recurring),
}];
p.narrative = "Alice shows strong improvement.".to_string();
p.token_cost = TokenCostSummary {
input_tokens: 500,
output_tokens: 200,
cost_usd: 0.005,
latency_ms: 1500,
};
p
}
#[test]
fn reporter_json_output() {
let tmp = tempfile::TempDir::new().expect("tempdir");
let reporter = Reporter::new(tmp.path(), ReportFormat::Json);
let profile = make_profile();
let paths = reporter.write_profile(&profile).expect("write_profile");
assert_eq!(paths.len(), 1);
assert!(paths[0].extension().is_some_and(|e| e == "json"));
let content = std::fs::read_to_string(&paths[0]).expect("read");
let back: ContributorProfile = serde_json::from_str(&content).expect("parse");
assert_eq!(back.canonical_email, "alice@example.com");
assert_eq!(back.all_findings.len(), 1);
assert_eq!(back.all_findings[0].finding.effort, Effort::Medium);
}
#[test]
fn reporter_markdown_contains_sections() {
let md = render_markdown(&make_profile());
assert!(md.contains("# Developer Profile: Alice Smith"), "header");
assert!(md.contains("## Quality Trend"), "quality trend section");
assert!(md.contains("2026-Q1"), "period label");
assert!(md.contains("## Strengths"), "strengths section");
assert!(
md.contains("Consistent ticket coverage"),
"strength content"
);
assert!(
md.contains("## Areas for Improvement"),
"weaknesses section"
);
assert!(md.contains("## Findings"), "findings section");
assert!(md.contains("error_handling"), "finding kind");
assert!(md.contains("Recurring"), "trend tag");
assert!(
md.contains("## Engineering Assessment"),
"narrative section"
);
assert!(
md.contains("Alice shows strong improvement"),
"narrative content"
);
assert!(md.contains("## Token & Cost Summary"), "cost section");
assert!(md.contains("500"), "input tokens");
assert!(md.contains("Generated by tga"), "footer names the producer");
}
#[test]
fn reporter_markdown_escapes_pipes_in_descriptions() {
let mut profile = make_profile();
profile.all_findings[0].finding.description = "matches a | b in the parser".to_string();
let md = render_markdown(&profile);
assert!(
md.contains("matches a \\| b in the parser"),
"a pipe in a description must be escaped: {md}"
);
}
#[test]
fn reporter_both_format_writes_two_files() {
let tmp = tempfile::TempDir::new().expect("tempdir");
let reporter = Reporter::new(tmp.path(), ReportFormat::Both);
let paths = reporter
.write_profile(&make_profile())
.expect("write_profile");
assert_eq!(paths.len(), 2, "should write 2 files in Both mode");
assert!(
paths
.iter()
.any(|p| p.extension().is_some_and(|e| e == "json")),
"should include JSON file"
);
assert!(
paths
.iter()
.any(|p| p.extension().is_some_and(|e| e == "md")),
"should include Markdown file"
);
}
#[test]
fn profile_file_stem_safe() {
let stem = profile_file_stem(&make_profile());
assert!(!stem.contains('@'), "stem must not contain @: {stem}");
assert!(
stem.starts_with("profile_"),
"stem must start with profile_: {stem}"
);
}
#[test]
fn report_format_from_str() {
use std::str::FromStr;
assert_eq!(
ReportFormat::from_str("json").expect("json"),
ReportFormat::Json
);
assert_eq!(
ReportFormat::from_str("markdown").expect("markdown"),
ReportFormat::Markdown
);
assert_eq!(
ReportFormat::from_str("both").expect("both"),
ReportFormat::Both
);
assert_eq!(
ReportFormat::from_str("md").expect("md"),
ReportFormat::Markdown
);
assert!(ReportFormat::from_str("xml").is_err());
}
#[test]
fn reporter_markdown_no_cost_section_when_zero() {
let mut profile = make_profile();
profile.token_cost = TokenCostSummary::default();
let md = render_markdown(&profile);
assert!(
!md.contains("## Token & Cost Summary"),
"zero cost must omit the cost section"
);
}
#[test]
fn reporter_markdown_carries_the_coverage_note() {
let profile = make_profile();
let plain = Reporter::new("/tmp/unused", ReportFormat::Markdown);
assert_eq!(
plain.render(&profile),
render_markdown(&profile),
"with no note, render must be exactly the plain markdown"
);
let noted = Reporter::new("/tmp/unused", ReportFormat::Markdown)
.with_coverage_note("## Coverage\n\n1 of 2 period(s) reviewed; `2026-Q2` skipped.\n");
let out = noted.render(&profile);
assert!(
out.contains("2026-Q2` skipped"),
"the coverage note must reach the rendered report: {out}"
);
let note_at = out.find("## Coverage").expect("note present");
let trend_at = out.find("## Quality Trend").expect("trend section present");
assert!(
note_at < trend_at,
"the note must sit above the trend it qualifies (note {note_at}, trend {trend_at})"
);
assert!(
out.contains("# Developer Profile: Alice Smith"),
"the header must survive the splice: {out}"
);
assert!(
out.contains("Consistent ticket coverage"),
"the body must survive the splice: {out}"
);
}