use std::fs;
use chrono::NaiveDate;
use datasynth_core::models::journal_entry::JournalEntryHeader;
use datasynth_core::models::{IcPairId, JournalEntry};
use datasynth_group::aggregate::ic_matcher::{IcMatchResult, IcMatchedPair, UnmatchedSide};
use datasynth_group::manifest::builder::GroupManifest;
use datasynth_group::shard::{
derive_ic_pair_plans, inject_ic_journal_entries, IcRole, InjectionCtx,
};
use datasynth_group::{
build_coverage_report, build_manifest, match_ic_pairs, write_coverage_report, GroupConfig,
IcRelationshipConfig, UnmatchedReason, COVERAGE_REPORT_FILENAME, COVERAGE_REPORT_SUBDIR,
UNMATCHED_SAMPLE_CAP,
};
fn load_two_entity_manifest() -> GroupManifest {
let yaml = include_str!("fixtures/mini_acme.yaml");
let mut cfg: GroupConfig =
serde_yaml::from_str(yaml).expect("mini_acme.yaml must parse into GroupConfig");
cfg.ownership
.entities
.retain(|e| e.code == "ACME_SA" || e.code == "ACME_USA");
cfg.intercompany.relationships.retain(|rel| match rel {
IcRelationshipConfig::Explicit(e) => {
(e.seller == "ACME_SA" || e.seller == "ACME_USA")
&& (e.buyer == "ACME_SA" || e.buyer == "ACME_USA")
}
IcRelationshipConfig::Pattern(_) => true,
});
if let Some(p2) = cfg.tax.pillar_two.as_mut() {
p2.jurisdictions.retain(|j| j == "CH" || j == "US");
}
if let Some(tp) = cfg.tax.transfer_pricing.as_mut() {
tp.local_files_for.retain(|j| j == "CH" || j == "US");
}
build_manifest(&cfg).expect("trimmed mini_acme must still build a manifest")
}
fn load_two_entity_manifest_no_ic() -> GroupManifest {
let yaml = include_str!("fixtures/mini_acme.yaml");
let mut cfg: GroupConfig =
serde_yaml::from_str(yaml).expect("mini_acme.yaml must parse into GroupConfig");
cfg.ownership
.entities
.retain(|e| e.code == "ACME_SA" || e.code == "ACME_USA");
cfg.intercompany.relationships.clear();
if let Some(p2) = cfg.tax.pillar_two.as_mut() {
p2.jurisdictions.retain(|j| j == "CH" || j == "US");
}
if let Some(tp) = cfg.tax.transfer_pricing.as_mut() {
tp.local_files_for.retain(|j| j == "CH" || j == "US");
}
build_manifest(&cfg).expect("manifest with no IC relationships must still build")
}
fn sa_jes(manifest: &GroupManifest) -> Vec<JournalEntry> {
let plans = derive_ic_pair_plans(manifest, "ACME_SA");
inject_ic_journal_entries(
&plans,
&InjectionCtx {
entity_code: "ACME_SA".to_string(),
},
)
}
fn usa_jes(manifest: &GroupManifest) -> Vec<JournalEntry> {
let plans = derive_ic_pair_plans(manifest, "ACME_USA");
inject_ic_journal_entries(
&plans,
&InjectionCtx {
entity_code: "ACME_USA".to_string(),
},
)
}
fn dummy_je(entity: &str) -> JournalEntry {
let header = JournalEntryHeader::new(
entity.to_string(),
NaiveDate::from_ymd_opt(2024, 6, 15).unwrap(),
);
JournalEntry::new(header)
}
#[test]
fn happy_path_full_coverage_zero_unmatched() {
let manifest = load_two_entity_manifest();
let result = match_ic_pairs(
&manifest,
&[
("ACME_SA".to_string(), sa_jes(&manifest)),
("ACME_USA".to_string(), usa_jes(&manifest)),
],
)
.expect("match");
let report = build_coverage_report(&result);
assert!(
(report.coverage - 1.0).abs() < f64::EPSILON,
"happy path: coverage = 1.0; got {}",
report.coverage
);
assert!(report.matched > 0, "happy path: at least one match");
assert_eq!(report.matched, result.matched.len());
assert_eq!(report.total_pairs_planned, result.total_planned);
assert!(
report.unmatched_sample.is_empty(),
"happy path: empty unmatched sample"
);
assert_eq!(report.unmatched_by_reason.len(), 3);
for r in [
UnmatchedReason::MissingBuyerSide,
UnmatchedReason::MissingSellerSide,
UnmatchedReason::AmountDriftAboveTolerance,
] {
assert_eq!(
report.unmatched_by_reason.get(&r),
Some(&0),
"reason {r:?} must be present at 0"
);
}
}
#[test]
fn partial_match_missing_buyer_side() {
let manifest = load_two_entity_manifest();
let sa_plans = derive_ic_pair_plans(&manifest, "ACME_SA");
let sa_seller_count = sa_plans.iter().filter(|p| p.role == IcRole::Seller).count();
assert!(
sa_seller_count >= 1,
"fixture sanity: need at least one SA seller plan"
);
let result =
match_ic_pairs(&manifest, &[("ACME_SA".to_string(), sa_jes(&manifest))]).expect("match");
let report = build_coverage_report(&result);
assert!(
report.coverage < 1.0,
"partial: coverage < 1.0; got {}",
report.coverage
);
assert!(
!report.unmatched_sample.is_empty(),
"partial: sample must be populated"
);
assert!(
report
.unmatched_by_reason
.get(&UnmatchedReason::MissingBuyerSide)
.copied()
.unwrap_or(0)
>= sa_seller_count,
"MissingBuyerSide must reflect every SA-seller-side plan",
);
assert_eq!(
report
.unmatched_by_reason
.get(&UnmatchedReason::AmountDriftAboveTolerance),
Some(&0),
"v5.0 must never emit AmountDriftAboveTolerance"
);
}
#[test]
fn empty_manifest_zero_coverage() {
let manifest = load_two_entity_manifest_no_ic();
assert!(manifest.ic_relationships.is_empty(), "fixture sanity");
let result = match_ic_pairs(
&manifest,
&[
("ACME_SA".to_string(), Vec::new()),
("ACME_USA".to_string(), Vec::new()),
],
)
.expect("match");
let report = build_coverage_report(&result);
assert_eq!(report.total_pairs_planned, 0);
assert_eq!(report.matched, 0);
assert_eq!(report.coverage, 0.0);
assert!(report.unmatched_sample.is_empty());
assert_eq!(report.unmatched_by_reason.len(), 3);
for r in [
UnmatchedReason::MissingBuyerSide,
UnmatchedReason::MissingSellerSide,
UnmatchedReason::AmountDriftAboveTolerance,
] {
assert_eq!(report.unmatched_by_reason.get(&r), Some(&0));
}
}
#[test]
fn unmatched_sample_caps_at_100() {
let je = dummy_je("ENT");
let unmatched: Vec<UnmatchedSide> = (0..150u8)
.map(|i| UnmatchedSide {
pair_id: IcPairId::from_bytes([i; 32]),
present_role: IcRole::Seller,
present_entity: "ENT".to_string(),
present_je: je.clone(),
reason: UnmatchedReason::MissingBuyerSide,
})
.collect();
let result = IcMatchResult {
matched: Vec::new(),
unmatched,
total_planned: 150,
coverage: 0.0,
};
let report = build_coverage_report(&result);
assert_eq!(
report.unmatched_sample.len(),
UNMATCHED_SAMPLE_CAP,
"sample must cap at {UNMATCHED_SAMPLE_CAP} per spec §5.4"
);
assert_eq!(
UNMATCHED_SAMPLE_CAP, 100,
"spec §5.4 mandates first-100 cap"
);
assert_eq!(
report
.unmatched_by_reason
.get(&UnmatchedReason::MissingBuyerSide),
Some(&150),
"histogram counts every unmatched side, not just the sample"
);
}
#[test]
fn writer_emits_at_spec_path_and_roundtrips() {
let manifest = load_two_entity_manifest();
let result = match_ic_pairs(
&manifest,
&[
("ACME_SA".to_string(), sa_jes(&manifest)),
("ACME_USA".to_string(), usa_jes(&manifest)),
],
)
.expect("match");
let report = build_coverage_report(&result);
let tmp = tempfile::tempdir().expect("tempdir");
let written = write_coverage_report(&report, tmp.path()).expect("write");
let expected = tmp
.path()
.join(COVERAGE_REPORT_SUBDIR)
.join(COVERAGE_REPORT_FILENAME);
assert_eq!(
written, expected,
"writer must return the actual path it wrote to"
);
assert!(expected.exists(), "file must exist on disk");
let bytes = fs::read(&expected).expect("read");
let on_disk: serde_json::Value = serde_json::from_slice(&bytes).expect("parse");
let in_memory: serde_json::Value =
serde_json::to_value(&report).expect("serialise in-memory report");
assert_eq!(
on_disk, in_memory,
"on-disk JSON must round-trip equal to the in-memory report"
);
}
#[test]
fn writer_returns_the_written_path() {
let report = build_coverage_report(&IcMatchResult {
matched: Vec::new(),
unmatched: Vec::new(),
total_planned: 0,
coverage: 0.0,
});
let tmp = tempfile::tempdir().expect("tempdir");
let returned = write_coverage_report(&report, tmp.path()).expect("write");
assert_eq!(
returned.file_name().and_then(|n| n.to_str()),
Some(COVERAGE_REPORT_FILENAME),
);
assert_eq!(
returned
.parent()
.and_then(|p| p.file_name())
.and_then(|n| n.to_str()),
Some(COVERAGE_REPORT_SUBDIR),
"returned path's parent dir must be `{COVERAGE_REPORT_SUBDIR}`"
);
assert!(
returned.is_file(),
"returned path must point at a real file"
);
}
#[test]
fn deterministic_on_disk_output() {
let je = dummy_je("ENT");
let unmatched: Vec<UnmatchedSide> = (0..5u8)
.map(|i| UnmatchedSide {
pair_id: IcPairId::from_bytes([i; 32]),
present_role: if i % 2 == 0 {
IcRole::Seller
} else {
IcRole::Buyer
},
present_entity: format!("E{i}"),
present_je: je.clone(),
reason: if i < 3 {
UnmatchedReason::MissingBuyerSide
} else {
UnmatchedReason::MissingSellerSide
},
})
.collect();
let pair = IcMatchedPair {
pair_id: IcPairId::from_bytes([99; 32]),
seller_entity: "S".to_string(),
buyer_entity: "B".to_string(),
seller_je: je.clone(),
buyer_je: je,
};
let result = IcMatchResult {
matched: vec![pair],
unmatched,
total_planned: 6,
coverage: 1.0 / 6.0,
};
let report = build_coverage_report(&result);
let tmp_a = tempfile::tempdir().expect("tempdir a");
let tmp_b = tempfile::tempdir().expect("tempdir b");
let path_a = write_coverage_report(&report, tmp_a.path()).expect("write a");
let path_b = write_coverage_report(&report, tmp_b.path()).expect("write b");
let bytes_a = fs::read(&path_a).expect("read a");
let bytes_b = fs::read(&path_b).expect("read b");
assert_eq!(
bytes_a, bytes_b,
"two writes of the same report must produce byte-identical files"
);
}