use rustledger_core::Directive;
use rustledger_loader::{LoadOptions, load};
use std::io::Write;
const PADDED_SOURCE: &str = r#"option "operating_currency" "USD"
2026-01-01 open Assets:Wallet USD
2026-01-01 open Equity:Void USD
2026-01-01 open Expenses:Expense USD
2026-01-01 * "opening"
Assets:Wallet 1000 USD
Equity:Void
2026-06-01 * "expense"
Expenses:Expense 10 USD
Assets:Wallet
2026-06-01 pad Assets:Wallet Equity:Void
2026-06-02 balance Assets:Wallet 975 USD
2026-06-02 * "expense"
Expenses:Expense 10 USD
Assets:Wallet
"#;
fn write_fixture(content: &str) -> tempfile::NamedTempFile {
let mut f = tempfile::Builder::new()
.prefix("balance-view-")
.suffix(".beancount")
.tempfile()
.expect("create tempfile");
f.write_all(content.as_bytes()).expect("write fixture");
f
}
#[test]
fn directives_field_keeps_pads_as_pads() {
let fixture = write_fixture(PADDED_SOURCE);
let opts = LoadOptions {
validate: false,
..Default::default()
};
let ledger = load(fixture.path(), &opts).expect("load");
let pad_count = ledger
.directives
.iter()
.filter(|s| matches!(s.value, Directive::Pad(_)))
.count();
assert_eq!(
pad_count, 1,
"Ledger.directives must keep Pads as Pads (source-faithful). \
A loader-side expansion regression would make this 0."
);
let p_flag_count = ledger
.directives
.iter()
.filter(|s| matches!(&s.value, Directive::Transaction(t) if t.flag == 'P'))
.count();
assert_eq!(
p_flag_count, 0,
"Ledger.directives must NOT contain synth P-flag transactions \
(those are the derived view). A loader-side pre-expansion \
would put one here."
);
}
#[test]
fn balance_view_adds_synth_transaction_alongside_original_pad() {
let fixture = write_fixture(PADDED_SOURCE);
let opts = LoadOptions {
validate: false,
..Default::default()
};
let ledger = load(fixture.path(), &opts).expect("load");
let view = ledger.balance_view();
let pad_count = view
.iter()
.filter(|d| matches!(d, Directive::Pad(_)))
.count();
assert_eq!(
pad_count, 1,
"balance_view() must preserve the original Pad directive so \
BQL `WHERE type = 'pad'` queries continue to match it.",
);
let synth_count = view
.iter()
.filter(|d| matches!(d, Directive::Transaction(t) if t.flag == 'P'))
.count();
assert_eq!(
synth_count, 1,
"balance_view() must contain exactly one synthesized P-flag \
transaction representing the pad's effect."
);
}
#[test]
fn balance_view_does_not_mutate_source_directives() {
let fixture = write_fixture(PADDED_SOURCE);
let opts = LoadOptions {
validate: false,
..Default::default()
};
let ledger = load(fixture.path(), &opts).expect("load");
let len_before = ledger.directives.len();
let _ = ledger.balance_view();
let len_after = ledger.directives.len();
assert_eq!(
len_before, len_after,
"balance_view() must not change the length of self.directives"
);
let pads_after = ledger
.directives
.iter()
.filter(|s| matches!(s.value, Directive::Pad(_)))
.count();
assert_eq!(
pads_after, 1,
"balance_view() must not remove or transform Pads in self.directives"
);
}
#[test]
fn balance_view_on_pad_free_ledger_is_equivalent_to_directives() {
const PAD_FREE: &str = r#"option "operating_currency" "USD"
2026-01-01 open Assets:Wallet USD
2026-01-01 open Equity:Void USD
2026-01-01 * "opening"
Assets:Wallet 1000 USD
Equity:Void
"#;
let fixture = write_fixture(PAD_FREE);
let opts = LoadOptions {
validate: false,
..Default::default()
};
let ledger = load(fixture.path(), &opts).expect("load");
let view = ledger.balance_view();
assert_eq!(
view.len(),
ledger.directives.len(),
"balance_view() must have the same length as directives on \
pad-free input (no spurious synth, no drops)."
);
let synth_count = view
.iter()
.filter(|d| matches!(d, Directive::Transaction(t) if t.flag == 'P'))
.count();
assert_eq!(
synth_count, 0,
"balance_view() must not emit synth P-flag transactions on \
pad-free input."
);
}