//! M5.2b — Suppression Metadata Compat Rules: integration tests.
//!
//! Each test installs a global trace sink and exercises a synthetic XFA
//! PDF that drives one of the two rules migrated in this commit:
//!
//! - `IgnoreInvisibleServerMetadataBindingsForDataBoundSignal`
//! - `ExcludeNonDataWidgetsFromPageSuppression`
//!
//! Tests serialise on a global sink lock to avoid cross-thread races
//! with the worker thread inside `flatten_xfa_to_pdf`.

use std::sync::{Arc, Mutex};

use lopdf::{dictionary, Document, Object, Stream};
use pdf_xfa::flatten_xfa_to_pdf;
use xfa_layout_engine::trace::{with_global_sink, RecordingSink, TraceEvent};

fn global_sink_lock() -> &'static Mutex<()> {
    static LOCK: std::sync::OnceLock<Mutex<()>> = std::sync::OnceLock::new();
    LOCK.get_or_init(|| Mutex::new(()))
}

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),
    }));
    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 flatten_with_sink(pdf: &[u8]) -> (Vec<u8>, Vec<TraceEvent>) {
    let sink: Arc<RecordingSink> = Arc::new(RecordingSink::new());
    let out = with_global_sink(sink.clone() as _, || flatten_xfa_to_pdf(pdf))
        .expect("flatten should succeed");
    let events = sink.events();
    (out, events)
}

fn has_reason(events: &[TraceEvent], reason: &str) -> bool {
    events.iter().any(|e| e.reason.tag() == reason)
}

// ── Rule 1: IgnoreInvisibleServerMetadataBindingsForDataBoundSignal ────────
//
// Single-page template with a visible `CustomerName` field (bound,
// triggers `AcceptedVisible`) and an invisible `frmid` field (also
// bound, triggers `IgnoredInvisible`). The rule's once-per-merge
// summary must fire because at least one invisible binding was
// observed.
const XDP_VISIBLE_AND_INVISIBLE_BOUND: &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="section1" 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>
    <xfa:data>
      <form1>
        <CustomerName>Alice</CustomerName>
        <frmid>https://server.example.com/form?id=abc</frmid>
      </form1>
    </xfa:data>
  </xfa:datasets>
</xdp:xdp>"#;

#[test]
fn invisible_binding_summary_fires_when_invisible_field_bound() {
    let _guard = global_sink_lock().lock().unwrap_or_else(|e| e.into_inner());
    let pdf = build_xfa_pdf(XDP_VISIBLE_AND_INVISIBLE_BOUND);
    let (_out, events) = flatten_with_sink(&pdf);

    assert!(
        has_reason(&events, "invisible_field_binding_ignored"),
        "invisible-binding summary must fire when an invisible field has a dataset binding"
    );
    let summary = events
        .iter()
        .find(|e| e.reason.tag() == "invisible_field_binding_ignored")
        .expect("summary event present");
    assert_eq!(summary.phase.tag(), "bind");
    let decision = summary.decision.as_deref().unwrap_or("");
    assert!(
        decision.contains("ignored_count="),
        "summary must report ignored_count: {decision}"
    );
    // Exactly one invisible field was bound — count must be 1.
    assert!(
        decision.contains("ignored_count=1"),
        "summary must report ignored_count=1, got {decision}"
    );
}

/// Counterexample template: a visible CustomerName binding fires, and
/// any_data_bound becomes true. The invisible-binding rule's summary
/// must NOT fire (no field had presence=invisible/hidden/inactive).
const XDP_VISIBLE_BOUND_ONLY: &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="section1" layout="tb" w="540pt">
        <field name="CustomerName" w="300pt" h="18pt">
          <ui><textEdit/></ui>
        </field>
      </subform>
    </subform>
  </template>
  <xfa:datasets>
    <xfa:data>
      <form1>
        <CustomerName>Alice</CustomerName>
      </form1>
    </xfa:data>
  </xfa:datasets>
</xdp:xdp>"#;

#[test]
fn invisible_binding_summary_silent_when_only_visible_bound() {
    let _guard = global_sink_lock().lock().unwrap_or_else(|e| e.into_inner());
    let pdf = build_xfa_pdf(XDP_VISIBLE_BOUND_ONLY);
    let (_out, events) = flatten_with_sink(&pdf);

    assert!(
        !has_reason(&events, "invisible_field_binding_ignored"),
        "summary must stay silent when no invisible/hidden/inactive field is bound"
    );
}

// ── Rule 2: ExcludeNonDataWidgetsFromPageSuppression ───────────────────────

/// Template with a signature field on page 1 plus a regular data
/// field on page 2 with a dataset binding. The widget rule must
/// classify the signature as excluded, the per-flatten summary must
/// fire (with excluded_count >= 1), and page-1 must NOT be dropped
/// (its only field is a non-data widget).
const XDP_SIGNATURE_PAGE_KEPT: &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="SignHere" w="300pt" h="60pt">
          <ui><signature/></ui>
        </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</CustomerName>
      </form1>
    </xfa:data>
  </xfa:datasets>
</xdp:xdp>"#;

#[test]
fn non_data_widget_summary_fires_for_signature_page() {
    let _guard = global_sink_lock().lock().unwrap_or_else(|e| e.into_inner());
    let pdf = build_xfa_pdf(XDP_SIGNATURE_PAGE_KEPT);
    let (out, events) = flatten_with_sink(&pdf);

    assert!(
        has_reason(&events, "non_data_widget_excluded_from_data_check"),
        "widget rule summary must fire when at least one signature/button/barcode is present"
    );
    let summary = events
        .iter()
        .find(|e| e.reason.tag() == "non_data_widget_excluded_from_data_check")
        .expect("summary event present");
    assert_eq!(summary.phase.tag(), "suppress");
    let decision = summary.decision.as_deref().unwrap_or("");
    assert!(
        decision.contains("excluded_count="),
        "widget summary must report excluded_count: {decision}"
    );

    // Both pages must survive (signature page is structural; data page
    // has the only data field).
    let pages = Document::load_mem(&out)
        .expect("reload flattened pdf")
        .get_pages()
        .len();
    assert_eq!(
        pages, 2,
        "signature-only page must not be dropped; data page must survive too"
    );
}

#[test]
fn non_data_widget_summary_silent_when_no_widget_present() {
    let _guard = global_sink_lock().lock().unwrap_or_else(|e| e.into_inner());
    let pdf = build_xfa_pdf(XDP_VISIBLE_BOUND_ONLY);
    let (_out, events) = flatten_with_sink(&pdf);

    assert!(
        !has_reason(&events, "non_data_widget_excluded_from_data_check"),
        "widget summary must stay silent when no signature/button/barcode is present"
    );
}
