mod common;
use common::*;
use btctax_core::{Form8949Part, Form8949Row};
use btctax_forms::testonly::*;
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
fn big_fixture() -> Vec<Form8949Row> {
let mut rows = Vec::new();
for i in 0..26u32 {
rows.push(row(
Form8949Part::ShortTerm,
&format!("{i}.00000000 BTC"),
dec!(100) * Decimal::from(i + 1),
dec!(50),
false,
));
}
for i in 0..13u32 {
rows.push(row(
Form8949Part::LongTerm,
&format!("{i}.00000000 BTC"),
dec!(200),
dec!(50),
false,
));
}
rows
}
fn values_ending(doc: &lopdf::Document, fields: &[Field], suffix: &str) -> Vec<String> {
fields
.iter()
.filter(|f| f.fqn.ends_with(suffix))
.filter_map(|f| text_value(doc, f.id))
.collect()
}
#[test]
fn eleven_rows_per_page() {
let bytes = btctax_forms::fill_form_8949(&big_fixture(), 2025).unwrap();
let doc = load(&bytes).unwrap();
assert_eq!(doc.get_pages().len(), 6, "3 copies × 2 pages");
let fields = collect_fields(&doc).unwrap();
let row11_a = values_ending(&doc, &fields, "Row11[0].f1_83[0]"); assert_eq!(
row11_a.len(),
2,
"row 11 is filled on the two FULL Part I copies only (26 = 11+11+4)"
);
}
#[test]
fn overflow_renames_fields_per_copy_no_shared_value() {
let bytes = btctax_forms::fill_form_8949(&big_fixture(), 2025).unwrap();
let doc = load(&bytes).unwrap();
let fields = collect_fields(&doc).unwrap();
let mut r1 = values_ending(&doc, &fields, "Row1[0].f1_03[0]");
r1.sort();
r1.dedup();
assert_eq!(
r1,
vec![
"0.00000000 BTC".to_string(),
"11.00000000 BTC".to_string(),
"22.00000000 BTC".to_string()
],
"each copy's Row 1 shows its own first row"
);
let mut fqns: Vec<&str> = fields.iter().map(|f| f.fqn.as_str()).collect();
let total = fqns.len();
fqns.sort_unstable();
fqns.dedup();
assert_eq!(
fqns.len(),
total,
"all field names are unique after renaming"
);
}
#[test]
fn each_copy_has_its_own_totals() {
let bytes = btctax_forms::fill_form_8949(&big_fixture(), 2025).unwrap();
let doc = load(&bytes).unwrap();
let fields = collect_fields(&doc).unwrap();
let mut totals = values_ending(&doc, &fields, ".f1_91[0]");
totals.sort();
assert_eq!(totals, vec!["18700", "6600", "9800"]);
assert_eq!(
totals
.iter()
.collect::<std::collections::HashSet<_>>()
.len(),
3
);
}
fn contains(haystack: &[u8], needle: &[u8]) -> bool {
haystack.windows(needle.len()).any(|w| w == needle)
}
#[test]
fn pseudo_fill_is_watermarked() {
let clean = btctax_forms::fill_form_8949(&mixed_rows(), 2025).unwrap();
let stamped = btctax_forms::stamp_draft_watermark(&clean).unwrap();
assert!(
contains(&stamped, b"ESTIMATE, NOT FOR FILING"),
"stamped output must carry the DRAFT watermark text"
);
let doc = load(&stamped).unwrap();
assert!(!pdf_has_xfa(&doc).unwrap());
assert_eq!(
doc.get_pages().len(),
load(&clean).unwrap().get_pages().len()
);
}
#[test]
fn real_fill_is_clean() {
let clean = btctax_forms::fill_form_8949(&mixed_rows(), 2025).unwrap();
assert!(
!contains(&clean, b"ESTIMATE, NOT FOR FILING"),
"a real-ledger fill must NOT be watermarked"
);
}