//! M5.3 — Repeating-subform occur-clamp rule: integration tests.
//!
//! Each test installs a process-global trace sink (Send+Sync; works
//! across the worker thread inside `flatten_xfa_to_pdf`) and exercises
//! a synthetic XFA PDF that drives one branch of the rule:
//!
//! - 5 data records + `<occur max="2">` → `data_count_clamped_by_occur_max`.
//! - 1 data record + `<occur min="3">` → `data_count_lifted_by_occur_min`.
//! - 3 data records + `<occur max="-1">` → rule silent (passthrough).
//! - `<bind match="none">` short-circuits expansion → rule never consulted.

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

fn count_reason(events: &[TraceEvent], reason: &str) -> usize {
    events.iter().filter(|e| e.reason.tag() == reason).count()
}

// 5 data records with occur max=2 → cap fires; count=2.
const XDP_CLAMPED_BY_MAX: &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="tb" 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="Orders" layout="tb" w="540pt">
        <subform name="Order" layout="tb" w="540pt">
          <occur min="0" max="2"/>
          <field name="Item" w="200pt" h="18pt">
            <ui><textEdit/></ui>
          </field>
        </subform>
      </subform>
    </subform>
  </template>
  <xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
    <xfa:data>
      <form1>
        <Order><Item>A</Item></Order>
        <Order><Item>B</Item></Order>
        <Order><Item>C</Item></Order>
        <Order><Item>D</Item></Order>
        <Order><Item>E</Item></Order>
      </form1>
    </xfa:data>
  </xfa:datasets>
</xdp:xdp>"#;

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

    assert!(
        count_reason(&events, "data_count_clamped_by_occur_max") >= 1,
        "rule must emit ClampedByMax on 5-records + occur.max=2 template; events={events:?}"
    );
    // Silent on the lift branch — data exceeds max, no lift fired.
    assert_eq!(
        count_reason(&events, "data_count_lifted_by_occur_min"),
        0,
        "lift branch must stay silent when data exceeds max"
    );
}

// 1 data record with occur min=3 → lift fires; count=3.
const XDP_LIFTED_BY_MIN: &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="tb" 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="Rows" layout="tb" w="540pt">
        <subform name="Row" layout="tb" w="540pt">
          <occur min="3" max="-1"/>
          <field name="Cell" w="200pt" h="18pt">
            <ui><textEdit/></ui>
          </field>
        </subform>
      </subform>
    </subform>
  </template>
  <xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
    <xfa:data>
      <form1>
        <Row><Cell>only-row</Cell></Row>
      </form1>
    </xfa:data>
  </xfa:datasets>
</xdp:xdp>"#;

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

    assert!(
        count_reason(&events, "data_count_lifted_by_occur_min") >= 1,
        "rule must emit LiftedByMin on 1-record + occur.min=3 template; events={events:?}"
    );
    assert_eq!(
        count_reason(&events, "data_count_clamped_by_occur_max"),
        0,
        "cap branch must stay silent when data is below min"
    );
}

// 3 data records with occur max=-1 (unbounded), min=0 → rule silent.
const XDP_PASSTHROUGH_UNBOUNDED: &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="tb" 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="Items" layout="tb" w="540pt">
        <subform name="Item" layout="tb" w="540pt">
          <occur min="0" max="-1"/>
          <field name="Name" w="200pt" h="18pt">
            <ui><textEdit/></ui>
          </field>
        </subform>
      </subform>
    </subform>
  </template>
  <xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
    <xfa:data>
      <form1>
        <Item><Name>x</Name></Item>
        <Item><Name>y</Name></Item>
        <Item><Name>z</Name></Item>
      </form1>
    </xfa:data>
  </xfa:datasets>
</xdp:xdp>"#;

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

    assert_eq!(
        count_reason(&events, "data_count_clamped_by_occur_max"),
        0,
        "clamp must be silent on unbounded passthrough"
    );
    assert_eq!(
        count_reason(&events, "data_count_lifted_by_occur_min"),
        0,
        "lift must be silent on unbounded passthrough"
    );
}

// <bind match="none"> short-circuits — clamp rule never consulted.
const XDP_BIND_NONE_SHORTCIRCUITS: &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="tb" 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="StaticGroup" layout="tb" w="540pt">
        <subform name="StaticRow" layout="tb" w="540pt">
          <occur min="0" max="5"/>
          <bind match="none"/>
          <field name="Label" w="200pt" h="18pt">
            <ui><textEdit/></ui>
            <value><text>static</text></value>
          </field>
        </subform>
      </subform>
    </subform>
  </template>
</xdp:xdp>"#;

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

    // bind=none caller-side short-circuit prevents the rule from being
    // consulted at all. Neither branch may fire.
    assert_eq!(
        count_reason(&events, "data_count_clamped_by_occur_max"),
        0,
        "clamp must stay silent when bind=none short-circuits expansion"
    );
    assert_eq!(
        count_reason(&events, "data_count_lifted_by_occur_min"),
        0,
        "lift must stay silent when bind=none short-circuits expansion"
    );
}
