//! M3-B Phase D κ — template page-drop regression tests.
//!
//! Verifies that:
//! (κ) pages whose only fields are non-data-bearing widgets (signature, button,
//!     barcode) are never dropped by the `page_has_fields` heuristic.
//! (Bucket A) forms whose datasets contain only server-infrastructure metadata
//!     (no visible field bindings) never trigger page suppression.

use lopdf::{dictionary, Document, Object, Stream};
use pdf_xfa::flatten_xfa_to_pdf;

// ---------------------------------------------------------------------------
// Helper — build a minimal XFA PDF from an XDP string
// ---------------------------------------------------------------------------

fn build_xfa_pdf(xdp: &str) -> Vec<u8> {
    let mut doc = Document::with_version("1.4");
    let xdp_bytes = xdp.as_bytes().to_vec();
    let xfa_stream = Stream::new(
        dictionary! { "Length" => Object::Integer(xdp_bytes.len() as i64) },
        xdp_bytes,
    );
    let xfa_id = doc.add_object(Object::Stream(xfa_stream));
    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),
        "Fields" => Object::Array(vec![])
    }));
    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 xfa pdf");
    out
}

fn flatten_page_count(xdp: &str) -> usize {
    let pdf = build_xfa_pdf(xdp);
    let output = flatten_xfa_to_pdf(&pdf).expect("flatten_xfa_to_pdf failed");
    Document::load_mem(&output)
        .expect("reload flattened PDF")
        .get_pages()
        .len()
}

fn build_xfa_pdf_with_n_existing(xdp: &str, n_existing: usize) -> Vec<u8> {
    let mut doc = Document::with_version("1.4");
    let xdp_bytes = xdp.as_bytes().to_vec();
    let xfa_stream = Stream::new(
        dictionary! { "Length" => Object::Integer(xdp_bytes.len() as i64) },
        xdp_bytes,
    );
    let xfa_id = doc.add_object(Object::Stream(xfa_stream));
    let pages_id = doc.new_object_id();
    let mut kids = Vec::new();
    for _ in 0..n_existing {
        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)
        }));
        kids.push(Object::Reference(page_id));
    }
    doc.objects.insert(
        pages_id,
        Object::Dictionary(dictionary! {
            "Type"  => Object::Name(b"Pages".to_vec()),
            "Kids"  => Object::Array(kids),
            "Count" => Object::Integer(n_existing as i64)
        }),
    );
    let acroform_id = doc.add_object(Object::Dictionary(dictionary! {
        "XFA"    => Object::Reference(xfa_id),
        "Fields" => Object::Array(vec![])
    }));
    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 multi-page xfa pdf");
    out
}

fn flatten_page_count_with_n_existing(xdp: &str, n_existing: usize) -> usize {
    let pdf = build_xfa_pdf_with_n_existing(xdp, n_existing);
    let output = flatten_xfa_to_pdf(&pdf).expect("flatten_xfa_to_pdf failed");
    Document::load_mem(&output)
        .expect("reload flattened PDF")
        .get_pages()
        .len()
}

// ---------------------------------------------------------------------------
// XDP fixtures
// ---------------------------------------------------------------------------

/// Two-page form: page 1 has text fields with default values; page 2 has only
/// a signature field.  Models the 778a1138 (Finance Purchase Order) pattern.
const XDP_TWO_PAGE_SIGNATURE: &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/">
    <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>
        <pageArea name="Page2">
          <contentArea x="36pt" y="36pt" w="540pt" h="720pt"/>
          <medium stock="default" short="612pt" long="792pt"/>
        </pageArea>
      </pageSet>
      <subform name="page1Section" layout="tb" w="540pt">
        <field name="CompanyName" w="300pt" h="18pt">
          <ui><textEdit/></ui>
          <value><text>ACME Corporation</text></value>
        </field>
        <field name="Phone" w="200pt" h="18pt">
          <ui><textEdit/></ui>
          <value><text>0000000000</text></value>
        </field>
      </subform>
      <subform name="page2Section" layout="tb" w="540pt">
        <breakBefore targetType="pageArea"/>
        <field name="SignatureField1" w="300pt" h="72pt">
          <ui><signature/></ui>
        </field>
      </subform>
    </subform>
  </template>
</xdp:xdp>"#;

/// Static XFAF form (baseProfile="interactiveForms") declaring 1 pageArea.
/// Phase 2 Wave 9: without a saved form DOM, the host PDF's page count is
/// authoritative — surplus host pages must be preserved, not trimmed.
const XDP_STATIC_XFAF_ONE_PAGE: &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/"
            baseProfile="interactiveForms">
    <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="content" layout="tb" w="540pt">
        <field name="FullName" w="300pt" h="18pt">
          <ui><textEdit/></ui>
          <value><text>Jane Doe</text></value>
        </field>
      </subform>
    </subform>
  </template>
</xdp:xdp>"#;

/// Two-page form: page 2 has only a button field.
const XDP_TWO_PAGE_BUTTON: &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/">
    <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>
        <pageArea name="Page2">
          <contentArea x="36pt" y="36pt" w="540pt" h="720pt"/>
          <medium stock="default" short="612pt" long="792pt"/>
        </pageArea>
      </pageSet>
      <subform name="page1Section" layout="tb" w="540pt">
        <field name="OrderNumber" w="200pt" h="18pt">
          <ui><numericEdit/></ui>
          <value><integer>12345</integer></value>
        </field>
      </subform>
      <subform name="page2Section" layout="tb" w="540pt">
        <breakBefore targetType="pageArea"/>
        <field name="SubmitButton" w="100pt" h="24pt">
          <ui><button/></ui>
        </field>
      </subform>
    </subform>
  </template>
</xdp:xdp>"#;

/// Single-page form: one text field with default value.  Used to confirm
/// the fix does not introduce over-pagination.
const XDP_ONE_PAGE_TEXT: &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/">
    <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="section" layout="tb" w="540pt">
        <field name="Name" w="300pt" h="18pt">
          <ui><textEdit/></ui>
          <value><text>John Smith</text></value>
        </field>
      </subform>
    </subform>
  </template>
</xdp:xdp>"#;

/// Two-page form where BOTH pages have data-bearing fields with values.
/// Both pages must be retained.
const XDP_TWO_PAGE_BOTH_DATA: &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/">
    <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>
        <pageArea name="Page2">
          <contentArea x="36pt" y="36pt" w="540pt" h="720pt"/>
          <medium stock="default" short="612pt" long="792pt"/>
        </pageArea>
      </pageSet>
      <subform name="page1Section" layout="tb" w="540pt">
        <field name="Field1" w="300pt" h="18pt">
          <ui><textEdit/></ui>
          <value><text>Page one content</text></value>
        </field>
      </subform>
      <subform name="page2Section" layout="tb" w="540pt">
        <breakBefore targetType="pageArea"/>
        <field name="Field2" w="300pt" h="18pt">
          <ui><textEdit/></ui>
          <value><text>Page two content</text></value>
        </field>
      </subform>
    </subform>
  </template>
</xdp:xdp>"#;

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[test]
fn signature_only_page_is_not_dropped() {
    // Page 1 has data fields → any_keep=true → suppression activates.
    // Page 2 has ONLY a signature field → must be treated as static and kept.
    // Bug: before fix, page_has_fields(page2)=true (signature IS a Field) and
    // page_has_field_data(page2)=false → page 2 was dropped.
    assert_eq!(
        flatten_page_count(XDP_TWO_PAGE_SIGNATURE),
        2,
        "page with only a signature field must not be dropped"
    );
}

#[test]
fn button_only_page_is_not_dropped() {
    // Button fields carry no data value — a page with only a button field
    // must be treated as static (always kept), just like a signature page.
    assert_eq!(
        flatten_page_count(XDP_TWO_PAGE_BUTTON),
        2,
        "page with only a button field must not be dropped"
    );
}

#[test]
fn single_page_text_form_stays_at_one_page() {
    // Regression guard: the fix must not cause over-pagination for a
    // standard 1-page text form.
    assert_eq!(
        flatten_page_count(XDP_ONE_PAGE_TEXT),
        1,
        "1-page text form must not gain extra pages after the fix"
    );
}

#[test]
fn two_data_pages_both_kept() {
    // Both pages have data-bearing fields with non-empty values.
    // Both must be retained regardless of the fix.
    assert_eq!(
        flatten_page_count(XDP_TWO_PAGE_BOTH_DATA),
        2,
        "both data pages must be retained"
    );
}

// ---------------------------------------------------------------------------
// Corpus doc gate tests (skipped when test-workspace is absent)
// ---------------------------------------------------------------------------

#[test]
fn corpus_778a1138_two_pages() {
    let path = "test-workspace/issue-812/778a1138/input.pdf";
    if !std::path::Path::new(path).exists() {
        return;
    }
    let pdf = std::fs::read(path).expect("read 778a1138 input.pdf");
    let output = flatten_xfa_to_pdf(&pdf).expect("flatten 778a1138");
    let pages = Document::load_mem(&output)
        .expect("reload 778a1138 output")
        .get_pages()
        .len();
    assert_eq!(pages, 2, "778a1138: expected 2 pages (oracle), got {pages}");
}

#[test]
fn corpus_d9ec06f8_four_pages() {
    let path = "test-workspace/issue-812/d9ec06f8/input.pdf";
    if !std::path::Path::new(path).exists() {
        return;
    }
    let pdf = std::fs::read(path).expect("read d9ec06f8 input.pdf");
    let output = flatten_xfa_to_pdf(&pdf).expect("flatten d9ec06f8");
    let pages = Document::load_mem(&output)
        .expect("reload d9ec06f8 output")
        .get_pages()
        .len();
    assert_eq!(pages, 4, "d9ec06f8: expected 4 pages (oracle), got {pages}");
}

#[test]
fn corpus_927d2419_two_pages() {
    let path = "test-workspace/issue-812/927d2419/input.pdf";
    if !std::path::Path::new(path).exists() {
        return;
    }
    let pdf = std::fs::read(path).expect("read 927d2419 input.pdf");
    let output = flatten_xfa_to_pdf(&pdf).expect("flatten 927d2419");
    let pages = Document::load_mem(&output)
        .expect("reload 927d2419 output")
        .get_pages()
        .len();
    assert_eq!(
        pages, 2,
        "927d2419: expected 2 pages (consensus oracle), got {pages}"
    );
}

#[test]
fn corpus_ce382c3d_two_pages() {
    let path = "test-workspace/issue-812/ce382c3d/input.pdf";
    if !std::path::Path::new(path).exists() {
        return;
    }
    let pdf = std::fs::read(path).expect("read ce382c3d input.pdf");
    let output = flatten_xfa_to_pdf(&pdf).expect("flatten ce382c3d");
    let pages = Document::load_mem(&output)
        .expect("reload ce382c3d output")
        .get_pages()
        .len();
    assert_eq!(
        pages, 2,
        "ce382c3d: expected 2 pages (verify oracle), got {pages}"
    );
}

#[test]
fn corpus_03af199e_twentyone_pages() {
    let path = "test-workspace/issue-812/03af199e/input.pdf";
    if !std::path::Path::new(path).exists() {
        return;
    }
    let pdf = std::fs::read(path).expect("read 03af199e input.pdf");
    let output = flatten_xfa_to_pdf(&pdf).expect("flatten 03af199e");
    let pages = Document::load_mem(&output)
        .expect("reload 03af199e output")
        .get_pages()
        .len();
    assert_eq!(
        pages, 21,
        "03af199e: expected 21 pages (oracle), got {pages}"
    );
}

#[test]
fn corpus_3963b9b6_stays_at_three_pages() {
    let path = "test-workspace/issue-812/3963b9b6/input.pdf";
    if !std::path::Path::new(path).exists() {
        return;
    }
    let pdf = std::fs::read(path).expect("read 3963b9b6 input.pdf");
    let output = flatten_xfa_to_pdf(&pdf).expect("flatten 3963b9b6");
    let pages = Document::load_mem(&output)
        .expect("reload 3963b9b6 output")
        .get_pages()
        .len();
    assert_eq!(
        pages, 3,
        "3963b9b6 regression guard: must stay at 3 pages, got {pages}"
    );
}

#[test]
fn corpus_08ea2c58_two_pages() {
    let path = "test-workspace/issue-812/08ea2c58/input.pdf";
    if !std::path::Path::new(path).exists() {
        return;
    }
    let pdf = std::fs::read(path).expect("read 08ea2c58 input.pdf");
    let output = flatten_xfa_to_pdf(&pdf).expect("flatten 08ea2c58");
    let pages = Document::load_mem(&output)
        .expect("reload 08ea2c58 output")
        .get_pages()
        .len();
    assert_eq!(pages, 2, "08ea2c58: expected 2 pages (oracle), got {pages}");
}

// ---------------------------------------------------------------------------
// Bucket A: invisible field binding must not activate page suppression
// ---------------------------------------------------------------------------

/// Two-page form: page 1 has a bind=none field (static); page 2 has a regular
/// field bound from datasets.
///
/// any_data_bound=true because CustomerName is visible and bound from datasets.
/// Page 1's StaticLabel has bind match="none" → data_bind_none=true → must NOT
/// count as a data field → page 1 must be retained.  Expected: 2 pages.
const XDP_BIND_NONE_PAGE_IS_NOT_DROPPED: &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>
        <pageArea name="Page2">
          <contentArea x="36pt" y="36pt" w="540pt" h="720pt"/>
          <medium stock="default" short="612pt" long="792pt"/>
        </pageArea>
      </pageSet>
      <subform name="page1Content" layout="tb" w="540pt">
        <field name="StaticLabel" w="300pt" h="18pt">
          <ui><textEdit/></ui>
          <value><text>Static Label</text></value>
          <bind match="none"/>
        </field>
      </subform>
      <subform name="page2Content" layout="tb" w="540pt">
        <breakBefore targetType="pageArea"/>
        <field name="CustomerName" w="300pt" h="18pt">
          <ui><textEdit/></ui>
        </field>
      </subform>
    </subform>
  </template>
  <xfa:datasets>
    <xfa:data>
      <form1>
        <CustomerName>Alice Johnson</CustomerName>
      </form1>
    </xfa:data>
  </xfa:datasets>
</xdp:xdp>"#;

/// Two-page form with visible data binding: page 2 has an empty visible field
/// and page 1 has a visible field bound from datasets.
///
/// Here any_data_bound=true (visible binding), so suppression fires and the
/// empty page 2 is correctly dropped → 1 page.
const XDP_VISIBLE_BOUND_DROPS_EMPTY_PAGE: &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>
        <pageArea name="Page2">
          <contentArea x="36pt" y="36pt" w="540pt" h="720pt"/>
          <medium stock="default" short="612pt" long="792pt"/>
        </pageArea>
      </pageSet>
      <subform name="section1" layout="tb" w="540pt">
        <field name="CustomerName" w="300pt" h="18pt">
          <ui><textEdit/></ui>
        </field>
      </subform>
      <subform name="section2" layout="tb" w="540pt">
        <breakBefore targetType="pageArea"/>
        <field name="EmptyOptional" w="300pt" h="18pt">
          <ui><textEdit/></ui>
        </field>
      </subform>
    </subform>
  </template>
  <xfa:datasets>
    <xfa:data>
      <form1>
        <CustomerName>Alice Johnson</CustomerName>
      </form1>
    </xfa:data>
  </xfa:datasets>
</xdp:xdp>"#;

#[test]
fn invisible_field_binding_does_not_set_any_data_bound() {
    // Direct merger-level test: binding an invisible field must NOT set
    // any_data_bound on the FormTree — doing so would wrongly activate
    // page suppression on forms with only server-metadata datasets.
    use pdf_xfa::dom_resolver::data_dom::DataDom;
    use pdf_xfa::merger::FormMerger;

    let template_xml = r#"<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
      <subform name="form1">
        <field name="UserName" w="300pt" h="18pt">
          <ui><textEdit/></ui>
        </field>
        <field name="frmid" w="100pt" h="10pt" presence="invisible">
          <ui><textEdit/></ui>
        </field>
      </subform>
    </template>"#;
    let datasets_xml = r#"<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
      <xfa:data>
        <form1>
          <frmid>https://server.example.com/form?id=abc123</frmid>
        </form1>
      </xfa:data>
    </xfa:datasets>"#;

    let data_dom = DataDom::from_xml(datasets_xml).unwrap();
    let merger = FormMerger::new(&data_dom);
    let (tree, _) = merger.merge(template_xml).unwrap();
    assert!(
        !tree.any_data_bound,
        "invisible field binding must not set any_data_bound (suppression gate must stay false)"
    );
}

#[test]
fn visible_field_binding_sets_any_data_bound() {
    // Regression guard: binding a VISIBLE field must set any_data_bound=true
    // so that the page suppression heuristic still fires for data-bound forms.
    use pdf_xfa::dom_resolver::data_dom::DataDom;
    use pdf_xfa::merger::FormMerger;

    let template_xml = r#"<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
      <subform name="form1">
        <field name="CustomerName" w="300pt" h="18pt">
          <ui><textEdit/></ui>
        </field>
      </subform>
    </template>"#;
    let datasets_xml = r#"<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
      <xfa:data>
        <form1>
          <CustomerName>Alice Johnson</CustomerName>
        </form1>
      </xfa:data>
    </xfa:datasets>"#;

    let data_dom = DataDom::from_xml(datasets_xml).unwrap();
    let merger = FormMerger::new(&data_dom);
    let (tree, _) = merger.merge(template_xml).unwrap();
    assert!(
        tree.any_data_bound,
        "visible field binding must set any_data_bound=true"
    );
}

#[test]
fn static_xfaf_host_pages_preserved_without_form_dom() {
    // Phase 2 Wave 9: a static XFAF form (baseProfile="interactiveForms")
    // with 1 template pageArea but a 2-page host PDF and **no saved form
    // DOM** must preserve the host pages. The host PDF's page count is
    // authoritative when no form DOM hint is available; trimming surplus
    // host pages would discard real content (the pdfRest oracle on the
    // Cluster T n_layout=1 corpus confirms host pages are kept).
    assert_eq!(
        flatten_page_count_with_n_existing(XDP_STATIC_XFAF_ONE_PAGE, 2),
        2,
        "static XFAF with no form DOM must preserve all host pages"
    );
}

#[test]
fn bind_none_page_is_not_dropped() {
    // A page whose only field has bind match="none" must be treated as static
    // and must never be dropped by the page suppression heuristic, even when
    // any_data_bound=true (another page has real data bindings).
    assert_eq!(
        flatten_page_count(XDP_BIND_NONE_PAGE_IS_NOT_DROPPED),
        2,
        "page with only bind=none fields must not be dropped by page suppression"
    );
}

#[test]
fn visible_data_binding_still_drops_empty_page() {
    // Regression guard: when real (visible) data is bound from datasets, the
    // suppression heuristic must still fire and drop pages with empty fields.
    // Page 1: CustomerName bound from datasets (non-empty) → kept
    // Page 2: EmptyOptional not in datasets (empty) → dropped
    assert_eq!(
        flatten_page_count(XDP_VISIBLE_BOUND_DROPS_EMPTY_PAGE),
        1,
        "page with empty visible field must still be dropped when real data binding exists"
    );
}

// ---------------------------------------------------------------------------
// Corpus gate tests: Bucket A target docs (silently skip when absent)
// ---------------------------------------------------------------------------

macro_rules! corpus_page_gate {
    ($fn_name:ident, $path:expr, $expected:expr) => {
        #[test]
        fn $fn_name() {
            let path = $path;
            if !std::path::Path::new(path).exists() {
                return;
            }
            let pdf = std::fs::read(path).expect(concat!("read ", $path));
            let output = flatten_xfa_to_pdf(&pdf).expect(concat!("flatten ", $path));
            let pages = Document::load_mem(&output)
                .expect(concat!("reload ", $path))
                .get_pages()
                .len();
            assert_eq!(
                pages, $expected,
                "{}: expected {} pages (oracle), got {}",
                $path, $expected, pages
            );
        }
    };
}

corpus_page_gate!(
    corpus_27c0da72_twelve_pages,
    "test-workspace/gate28-low/27c0da72/input.pdf",
    12
);
corpus_page_gate!(
    corpus_a511b1ae_twelve_pages,
    "test-workspace/gate28-low/a511b1ae/input.pdf",
    12
);
corpus_page_gate!(
    corpus_f556168a_four_pages,
    "test-workspace/gate28-low/f556168a/input.pdf",
    4
);
corpus_page_gate!(
    corpus_aec38229_three_pages,
    "test-workspace/gate28-low/aec38229/input.pdf",
    3
);
corpus_page_gate!(
    corpus_1da8a16a_three_pages,
    "test-workspace/gate28-low/1da8a16a/input.pdf",
    3
);
corpus_page_gate!(
    corpus_adde1473_four_pages,
    "test-workspace/gate28-low/adde1473/input.pdf",
    4
);

// Wave 7: XFAF form with no scripts/FormCalc and resolved-break instances
// that legitimately collapse to one page in layout. Host PDF has 9 stale
// placeholder pages that flatten must trim to match Adobe's 1-page output.
// Verifies the static_can_trim relaxation in flatten.rs for static templates
// without dynamic logic.
corpus_page_gate!(
    corpus_fe5de953_one_page,
    "test-workspace/gate7-samples/fe5de953c3052756bee2bab4dacb1ba5c6a8780906e06b90faaba20b795d7035/input.pdf",
    1
);

// Phase D-κ: form DOM page-count cap (issue-812 doc 13275420).
// Oracle = 10. Layout over-creates 13 pages (JS-driven P2-P4 contact
// continuation pages not hidden). Suppression capped at form DOM's 10
// page-area count → drops exactly pages 2-4, keeps all structural pages.
corpus_page_gate!(
    corpus_13275420_ten_pages,
    "test-workspace/issue-812/13275420/input.pdf",
    10
);

// Wave 4 — preserve_static overflow guard (e0909cf9).
// Multi-record insurance card form: 2 AutoIDCardData records, layout=2.
// The original PDF has 1 placeholder page. Before this fix the
// `has_static_content && overlay_substantial` heuristic fired even when
// n_layout > n_existing, capping output at 1 page.  After the fix the guard
// correctly falls through to the dynamic write path → flat=2.
corpus_page_gate!(
    corpus_e0909cf9_two_pages,
    "test-workspace/gate7-samples/e0909cf9146ea98157e4ae83aab92ac52082ea2bc161875399e1bbe70b3eba61/input.pdf",
    2
);

// Wave 5 → Wave 9 realignment — static XFAF host pages preserved (7dbbe9d9).
// Static form (baseProfile="interactiveForms"), template declares 1 pageArea,
// host PDF has 2 pages, no saved form DOM. Wave 5 trimmed to 1 page on the
// (mistaken) assumption that the surplus page was a placeholder. Wave 9's
// corpus dossier shows the pdfRest oracle renders 2 pages here — the surplus
// host page carries real content. The hardened None-arm of
// `static_xfaf_excess_page_trim_with_form_dom_guard` now preserves both
// host pages, aligning the engine with the oracle truth.
corpus_page_gate!(
    corpus_7dbbe9d9_two_pages,
    "test-workspace/gate7-samples/7dbbe9d92ee7c3b93b7cf79dcba6ad2a5db18f55737f19d2adbf2849c993c2bb/input.pdf",
    2
);

// Wave 5 regression guard — static XFAF form with form-DOM guidance (322faac4).
// Also is_static_form=true, but form DOM declares 17 pageAreas (> layout=7).
// Excess trimming must NOT fire here — the form DOM is authoritative.
// Oracle=17: all 17 original pages must be preserved.
corpus_page_gate!(
    corpus_322faac4_seventeen_pages,
    "test-workspace/moderate-band-nearpass/322faac4e5c5b3de8992765a97deb195aef27b76fc240a9bd1782cb8ae385890/input.pdf",
    17
);
