use std::sync::{Arc, Mutex, OnceLock};
use lopdf::{dictionary, Document, Object, Stream};
use pdf_xfa::flatten_xfa_to_pdf;
use pdf_xfa::layout::trace::{with_global_sink, RecordingSink};
fn global_sink_serializer() -> &'static Mutex<()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
}
const XDP_INVISIBLE_BINDING: &str = r#"<?xml version="1.0"?>
<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/" xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="form1" layout="paginate" w="8.5in" h="11in">
<pageSet><pageArea name="Page1"><contentArea x="36pt" y="36pt" w="540pt" h="720pt"/><medium stock="default" short="612pt" long="792pt"/></pageArea></pageSet>
<subform name="s1" layout="tb" w="540pt">
<field name="CustomerName" w="300pt" h="18pt"><ui><textEdit/></ui></field>
<field name="frmid" presence="invisible" w="100pt" h="10pt"><ui><textEdit/></ui></field>
</subform>
</subform>
</template>
<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
<xfa:data><form1><CustomerName>Alice</CustomerName><frmid>https://s.example/x</frmid></form1></xfa:data>
</xfa:datasets>
</xdp:xdp>"#;
const XDP_CORRUPT_MINIMAL: &str = r#"<?xml version="1.0"?>
<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/">
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/"/>
</xdp:xdp>"#;
fn build_pdf(xdp: &str) -> Vec<u8> {
let mut doc = Document::with_version("1.7");
let xfa_id = doc.add_object(Object::Stream(Stream::new(
dictionary! {},
xdp.as_bytes().to_vec(),
)));
let pages_id = doc.new_object_id();
let content_id = doc.add_object(Object::Stream(Stream::new(
dictionary! { "Length" => Object::Integer(0_i64) },
vec![],
)));
let page_id = doc.add_object(Object::Dictionary(dictionary! {
"Type" => Object::Name(b"Page".to_vec()),
"Parent" => Object::Reference(pages_id),
"MediaBox" => Object::Array(vec![
Object::Integer(0), Object::Integer(0),
Object::Integer(612), Object::Integer(792),
]),
"Contents" => Object::Reference(content_id),
}));
doc.objects.insert(
pages_id,
Object::Dictionary(dictionary! {
"Type" => Object::Name(b"Pages".to_vec()),
"Kids" => Object::Array(vec![Object::Reference(page_id)]),
"Count" => Object::Integer(1),
}),
);
let acroform_id = doc.add_object(Object::Dictionary(dictionary! {
"XFA" => Object::Reference(xfa_id),
}));
let catalog_id = doc.add_object(Object::Dictionary(dictionary! {
"Type" => Object::Name(b"Catalog".to_vec()),
"Pages" => Object::Reference(pages_id),
"AcroForm" => Object::Reference(acroform_id),
}));
doc.trailer.set("Root", Object::Reference(catalog_id));
let mut out = Vec::new();
doc.save_to(&mut out).expect("save lopdf");
out
}
fn captured(pdf: &[u8]) -> (Vec<String>, Option<Vec<u8>>) {
let _guard = global_sink_serializer()
.lock()
.unwrap_or_else(|e| e.into_inner());
let sink: Arc<RecordingSink> = Arc::new(RecordingSink::new());
let out = with_global_sink(sink.clone() as _, || flatten_xfa_to_pdf(pdf)).ok();
let tags = sink
.events()
.iter()
.map(|e| format!("{}/{}", e.phase.tag(), e.reason.tag()))
.collect();
(tags, out)
}
#[test]
fn invisible_binding_xdp_flattens_without_static_fallback() {
let pdf = build_pdf(XDP_INVISIBLE_BINDING);
let (tags, _) = captured(&pdf);
assert!(
tags.iter()
.any(|t| t == "bind/invisible_field_binding_ignored"),
"pre-existing invisible-binding rule must still fire; got {:?}",
tags
);
assert!(
!tags.iter().any(|t| t == "fallback/static_fallback_taken"),
"Tier A static-template XDP must NOT take the static fallback; got {:?}",
tags
);
}
#[test]
fn invisible_binding_xdp_layout_produces_one_page() {
let pdf = build_pdf(XDP_INVISIBLE_BINDING);
let (_, out) = captured(&pdf);
let bytes = out.expect("flatten should succeed");
let doc = Document::load_mem(&bytes).expect("output PDF parses");
assert_eq!(
doc.get_pages().len(),
1,
"static-template XDP must render to exactly one XFA page"
);
}
#[test]
fn corrupt_minimal_template_still_takes_fallback() {
let pdf = build_pdf(XDP_CORRUPT_MINIMAL);
let (tags, _) = captured(&pdf);
assert!(
tags.iter().any(|t| t == "fallback/static_fallback_taken"),
"corrupt template must still emit fallback/static_fallback_taken; got {:?}",
tags
);
}