//! M5.3d — FormDomDrivenRepeatInstanceReplication: integration tests.
//!
//! When the saved form DOM records more repeating-subform instances
//! than the template's initial expansion produced, the engine clones
//! the template instance to match the form DOM's count. The trace
//! anchor `(occur, subform_materialised_from_data)` fires per group
//! when clones are added.

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()
}

/// Count events from the M5.3d rule's trace anchor
/// (`Phase::Occur` + `Reason::SubformMaterialisedFromData`). UX1's
/// bind summary uses the same Reason tag under `Phase::Bind`; the
/// phase filter discriminates between them.
fn count_form_dom_rule_fires(events: &[TraceEvent]) -> usize {
    events
        .iter()
        .filter(|e| e.phase.tag() == "occur" && e.reason.tag() == "subform_materialised_from_data")
        .count()
}

// Positive: template default produces 1 instance of `Row`; form DOM
// records 3 `<Row>` siblings. The rule must clone 2 more instances
// and emit `(occur, subform_materialised_from_data)` for the
// expanded group.
const XDP_FORM_DOM_THREE_INSTANCES: &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="Container" layout="tb" w="540pt">
        <subform name="Row" layout="tb" w="540pt">
          <occur min="1" 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>
        <Container>
          <Row><Cell>only-data-record</Cell></Row>
        </Container>
      </form1>
    </xfa:data>
  </xfa:datasets>
  <form xmlns="http://www.xfa.org/schema/xfa-form/3.3/">
    <subform name="form1">
      <subform name="Container">
        <subform name="Row"><field name="Cell"><value><text>r1</text></value></field></subform>
        <subform name="Row"><field name="Cell"><value><text>r2</text></value></field></subform>
        <subform name="Row"><field name="Cell"><value><text>r3</text></value></field></subform>
      </subform>
    </subform>
  </form>
</xdp:xdp>"#;

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

    let fires = count_form_dom_rule_fires(&events);
    assert!(
        fires >= 1,
        "rule must fire at least once when form DOM declares 3 instances vs template-default 1; events={events:?}"
    );
}

// Counterexample: same template, but form DOM records the same count
// as the template default. The rule fires (decision function consulted)
// but adds 0 clones, so the trace anchor stays silent.
const XDP_FORM_DOM_SAME_AS_TEMPLATE: &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="Container" layout="tb" w="540pt">
        <subform name="Row" layout="tb" w="540pt">
          <occur min="1" 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>
        <Container>
          <Row><Cell>only-row</Cell></Row>
        </Container>
      </form1>
    </xfa:data>
  </xfa:datasets>
  <form xmlns="http://www.xfa.org/schema/xfa-form/3.3/">
    <subform name="form1">
      <subform name="Container">
        <subform name="Row"><field name="Cell"><value><text>r1</text></value></field></subform>
      </subform>
    </subform>
  </form>
</xdp:xdp>"#;

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

    let fires = count_form_dom_rule_fires(&events);
    assert_eq!(
        fires, 0,
        "rule must stay silent when form DOM matches template default"
    );
}

// Counterexample: no form DOM at all. The rule path is never reached.
const XDP_NO_FORM_DOM: &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="Row" layout="tb" w="540pt">
        <occur min="0" max="-1"/>
        <field name="Cell" w="200pt" h="18pt">
          <ui><textEdit/></ui>
        </field>
      </subform>
    </subform>
  </template>
  <xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
    <xfa:data>
      <form1>
        <Row><Cell>a</Cell></Row>
        <Row><Cell>b</Cell></Row>
      </form1>
    </xfa:data>
  </xfa:datasets>
</xdp:xdp>"#;

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

    let fires = count_form_dom_rule_fires(&events);
    assert_eq!(
        fires, 0,
        "rule must stay silent when there is no form DOM packet"
    );
}

// ---------------------------------------------------------------------------
// Form-DOM clone value propagation regression (fidelity/form-dom-clone-value-fix)
//
// Before the fix, `apply_form_dom_presence` deep-cloned the template
// `Row` to match the form-DOM count but the cloned fields kept the
// template's `<xfa:datasets>`-bound value. The form-DOM apply walk's
// `value.is_empty()` override guard then silently no-op'd, so every
// rendered Row carried the single datasets value ("only-data-record")
// instead of the per-form-DOM values (r1, r2, r3).
//
// The two assertions below cover the two halves of the fix:
//
// 1. Each form-DOM clone (positions [existing_count..xml_count)) must
//    bind to its form-DOM record's value.
// 2. The existing template instances (positions [0..existing_count)) in
//    the same group must also pick up the form-DOM's value, because the
//    form DOM is the runtime source-of-truth for the entire group when
//    M5.3d clones are added (XFA §3.1).
//
// Both assertions inspect the rendered text via `lopdf::extract_text`
// (the same primitive the Tier A wave-4 content gate uses) and assert
// on extracted strings, mirroring the wave-3 binding regression pattern.
// ---------------------------------------------------------------------------

fn flatten_and_extract_text(pdf: &[u8]) -> (String, Vec<TraceEvent>) {
    let sink: Arc<RecordingSink> = Arc::new(RecordingSink::new());
    let bytes = with_global_sink(sink.clone() as _, || flatten_xfa_to_pdf(pdf))
        .expect("flatten should succeed");
    let doc = Document::load_mem(&bytes).expect("load output");
    let page_ids: Vec<u32> = (1..=doc.get_pages().len() as u32).collect();
    let raw = doc.extract_text(&page_ids).unwrap_or_default();
    let normalized = raw.split_whitespace().collect::<Vec<_>>().join(" ");
    (normalized, sink.events())
}

#[test]
fn form_dom_clone_values_bind_to_form_dom_records_not_template_value() {
    let _guard = global_sink_lock().lock().unwrap_or_else(|e| e.into_inner());
    let pdf = build_xfa_pdf(XDP_FORM_DOM_THREE_INSTANCES);
    let (text, events) = flatten_and_extract_text(&pdf);

    // Rule must still fire — the fix preserves the M5.3d trace.
    let fires = count_form_dom_rule_fires(&events);
    assert!(
        fires >= 1,
        "M5.3d rule trace must still fire after the value-propagation fix; \
         events={events:?}"
    );

    // No fallback path must run for this Tier A static template.
    let fell_back = events
        .iter()
        .any(|e| e.phase.tag() == "fallback" && e.reason.tag() == "static_fallback_taken");
    assert!(
        !fell_back,
        "form-DOM clone fixture must not take the static fallback; events={events:?}"
    );

    // Each form-DOM record value must appear in the output exactly
    // once — the fix's primary acceptance signal.
    for expected in &["r1", "r2", "r3"] {
        assert!(
            text.contains(expected),
            "expected form-DOM record {expected:?} in rendered text; got {text:?}"
        );
    }

    // The single datasets-bound value must NOT appear: the form DOM is
    // authoritative for the whole group when clones are added, so even
    // the original template instance picks up the form-DOM record at
    // its index.
    assert!(
        !text.contains("only-data-record"),
        "datasets-bound value must not leak through any form-DOM-driven \
         clone; got {text:?}"
    );

    // Sequence assertion: r1, r2, r3 appear in form-DOM declared order.
    let r1 = text.find("r1");
    let r2 = text.find("r2");
    let r3 = text.find("r3");
    assert!(
        matches!((r1, r2, r3), (Some(a), Some(b), Some(c)) if a < b && b < c),
        "form-DOM records must render in declared order; got text={text:?}"
    );
}

#[test]
fn form_dom_same_count_path_unchanged_by_value_propagation_fix() {
    // Counterexample: form-DOM count matches the datasets count, so
    // the M5.3d clone path does NOT fire. The fix's clearing step is
    // therefore not reached, and the existing datasets-bound value
    // must still survive. Guards against regressing the
    // "no clones added" path.
    let _guard = global_sink_lock().lock().unwrap_or_else(|e| e.into_inner());
    let pdf = build_xfa_pdf(XDP_FORM_DOM_SAME_AS_TEMPLATE);
    let (text, events) = flatten_and_extract_text(&pdf);

    let fires = count_form_dom_rule_fires(&events);
    assert_eq!(
        fires, 0,
        "M5.3d rule must stay silent when form DOM matches template default"
    );
    // The form-DOM `<value><text>r1</text></value>` overrides the
    // datasets `only-row` via the existing is_empty() guard *only*
    // if the datasets value is empty. With a non-empty datasets value
    // the existing guard preserves it, which is pre-existing
    // behaviour. We assert either the datasets value OR the form-DOM
    // value is present (whichever the existing path elected); the
    // *combined* "only-row r1" or "r1 only-row" output that would
    // indicate over-eager clearing must NOT appear.
    let has_datasets_value = text.contains("only-row");
    let has_form_dom_value = text.contains("r1");
    assert!(
        has_datasets_value ^ has_form_dom_value,
        "exactly one of datasets/form-DOM value must render on the \
         no-clone path; got text={text:?}"
    );
}

#[test]
fn form_dom_no_packet_path_unchanged_by_value_propagation_fix() {
    // Counterexample: no form DOM packet at all. The M5.3d clone
    // path is never reached; the merger handles repeating expansion
    // entirely via datasets binding. Asserts the fix does not leak
    // into the no-form-DOM path.
    let _guard = global_sink_lock().lock().unwrap_or_else(|e| e.into_inner());
    let pdf = build_xfa_pdf(XDP_NO_FORM_DOM);
    let (text, events) = flatten_and_extract_text(&pdf);

    let fires = count_form_dom_rule_fires(&events);
    assert_eq!(
        fires, 0,
        "M5.3d rule must stay silent without a form DOM packet"
    );
    assert!(
        text.contains('a') && text.contains('b'),
        "datasets-driven instances must render their per-record values \
         (the wave-3 fix path); got text={text:?}"
    );
}
