#![cfg(feature = "xfa-js-sandboxed")]

//! W3-D RETRY regression tests — `<variables>` `<text>` data items.
//!
//! After W2-B reduced the `implicit_function` cluster from 79 → 13 (−83 %),
//! the residual cluster (5 docs, 13 occurrences) decomposes into:
//!
//! - `globValidatePressed.value = "true";` on the IMM5709/IMM5257/IMM5710
//!   family (Canadian IRCC forms). These reference a sibling of
//!   `<variables>` `<script>`: a `<variables>` `<text name="X">…</text>`
//!   **data item** (XFA 3.3 §5.5.2). Adobe Reader exposes these as
//!   form-level mutable string containers; our W2-B pass only registered
//!   `<script>` blocks, so every read or write of the data item surfaced
//!   as a `js_resolve_failure`.
//! - `this.getDeltas()` on the same templates — a field-handle Acrobat
//!   method (out of W3-D scope; documented in `W3D_RETRY_DECISION_MEMO.md`).
//! - `this.closeDoc(true)` on `dd416428_dol4n.pdf` — bare `closeDoc()` is
//!   already stubbed on `app.*` / `xfa.host.*`; the field-handle method
//!   is out of W3-D scope (would require host-stub extension).
//! - `_ListInitiales` on `60df78fe_pdf_0012.pdf` — a `resolveNode(...)`
//!   path that correctly returns `null`. **Cluster C contract** demands
//!   the surface stay null; the +1 resolve-failure count is intentional.
//! - `glob` on `bda32eb1_pdf_0008.pdf` — already a `<variables>` `<script>`
//!   (registered post-W2-B). The single residual is benign edge noise.
//!
//! W3-D RETRY lands the data-item fix only — bounded, no activity-policy
//! change, no broad rquickjs refactor. The other residual categories are
//! either Cluster-C-preserving (intentional) or outside the W3-D file
//! scope (`crates/pdf-xfa/src/js_runtime/*`).
//!
//! Spec basis: XFA 3.3 §5.5.2 (Variable Data Items): `<text name="X">…`
//! children of `<variables>` are mutable string containers accessible
//! from any event script in the same form as `X.value`.

use pdf_xfa::dynamic::apply_dynamic_scripts_with_runtime;
use pdf_xfa::js_runtime::QuickJsRuntime;
use pdf_xfa::{DynamicScriptOutcome, JsExecutionMode};
use xfa_layout_engine::form::{
    EventScript, FormNode, FormNodeId, FormNodeType, FormTree, Occur, ScriptLanguage,
};
use xfa_layout_engine::text::FontMetrics;
use xfa_layout_engine::types::{BoxModel, LayoutStrategy};

fn add_node(tree: &mut FormTree, name: &str, node_type: FormNodeType) -> FormNodeId {
    tree.add_node(FormNode {
        name: name.to_string(),
        node_type,
        box_model: BoxModel::default(),
        layout: LayoutStrategy::TopToBottom,
        children: Vec::new(),
        occur: Occur::once(),
        font: FontMetrics::default(),
        calculate: None,
        validate: None,
        column_widths: Vec::new(),
        col_span: 1,
    })
}

fn add_field(tree: &mut FormTree, parent: FormNodeId, name: &str, value: &str) -> FormNodeId {
    let child = add_node(
        tree,
        name,
        FormNodeType::Field {
            value: value.to_string(),
        },
    );
    tree.get_mut(parent).children.push(child);
    child
}

fn add_js_script(tree: &mut FormTree, node_id: FormNodeId, activity: &str, script: &str) {
    tree.meta_mut(node_id).event_scripts = vec![EventScript::new(
        script.to_string(),
        ScriptLanguage::JavaScript,
        Some(activity.to_string()),
        None,
        None,
    )];
}

fn run_sandbox(tree: &mut FormTree, root: FormNodeId) -> DynamicScriptOutcome {
    let mut runtime = QuickJsRuntime::new().expect("quickjs runtime");
    apply_dynamic_scripts_with_runtime(tree, root, JsExecutionMode::SandboxedRuntime, &mut runtime)
        .expect("sandbox dispatch")
}

fn field_value(tree: &FormTree, node_id: FormNodeId) -> &str {
    match &tree.get(node_id).node_type {
        FormNodeType::Field { value } => value,
        _ => panic!("expected field"),
    }
}

/// W3-D RETRY canonical case: an empty `<variables>` `<text name="X"/>`
/// data item must be readable as `X.value` returning an empty string —
/// not surface as a `js_resolve_failure`.
#[test]
fn empty_text_data_item_is_readable_as_empty_string() {
    let mut tree = FormTree::new();
    let root = add_node(&mut tree, "root", FormNodeType::Root);

    // Empty `<text name="globValidatePressed"/>` (no body) — canonical
    // IMM5709 pattern.
    tree.variables_data_items
        .push((None, "globValidatePressed".into(), String::new()));

    let out = add_field(&mut tree, root, "Out", "");
    add_js_script(
        &mut tree,
        out,
        "calculate",
        "this.rawValue = (typeof globValidatePressed === \"undefined\") \
         ? \"undefined\" : \"value=\" + globValidatePressed.value;",
    );

    let outcome = run_sandbox(&mut tree, root);

    assert_eq!(
        field_value(&tree, out),
        "value=",
        "data item must be readable as empty string",
    );
    assert_eq!(outcome.js_executed, 1);
    assert_eq!(
        outcome.js_resolve_failures, 0,
        "data-item read must not surface as resolve_failure",
    );
    assert_eq!(outcome.js_runtime_errors, 0);
}

/// W3-D RETRY canonical case: writing `X.value = "true"` followed by a
/// later script reading it sees the new value. Mirrors the IMM5709 flow
/// where one event sets the flag and a subsequent validate script reads
/// it.
#[test]
fn text_data_item_is_writable_and_persists_across_scripts() {
    let mut tree = FormTree::new();
    let root = add_node(&mut tree, "root", FormNodeType::Root);

    tree.variables_data_items
        .push((None, "globValidatePressed".into(), String::new()));

    // First script: write the flag.
    let writer = add_field(&mut tree, root, "Writer", "");
    add_js_script(
        &mut tree,
        writer,
        "calculate",
        "globValidatePressed.value = \"true\"; this.rawValue = \"written\";",
    );

    // Second script: read it back.
    let reader = add_field(&mut tree, root, "Reader", "");
    add_js_script(
        &mut tree,
        reader,
        "calculate",
        "this.rawValue = globValidatePressed.value;",
    );

    let outcome = run_sandbox(&mut tree, root);

    assert_eq!(field_value(&tree, writer), "written");
    assert_eq!(
        field_value(&tree, reader),
        "true",
        "data-item write must persist across scripts",
    );
    assert_eq!(outcome.js_executed, 2);
    assert_eq!(outcome.js_resolve_failures, 0);
    assert_eq!(outcome.js_runtime_errors, 0);
}

/// W3-D RETRY: an initial-value text data item (`<text name="X">U</text>`)
/// retains its initial body when read. Mirrors `<text name="globInvoker">U</text>`
/// in the IMM5709 template.
#[test]
fn text_data_item_with_initial_value_is_read_correctly() {
    let mut tree = FormTree::new();
    let root = add_node(&mut tree, "root", FormNodeType::Root);

    tree.variables_data_items
        .push((None, "globInvoker".into(), "U".into()));

    let out = add_field(&mut tree, root, "Out", "");
    add_js_script(
        &mut tree,
        out,
        "calculate",
        "this.rawValue = globInvoker.value;",
    );

    let outcome = run_sandbox(&mut tree, root);

    assert_eq!(field_value(&tree, out), "U");
    assert_eq!(outcome.js_executed, 1);
    assert_eq!(outcome.js_resolve_failures, 0);
    assert_eq!(outcome.js_runtime_errors, 0);
}

/// W3-D RETRY: writing a non-string value to `.value` coerces to string,
/// matching Adobe Reader's text-field semantics. A calculate script that
/// sets `X.value = true` must read back the string `"true"`.
#[test]
fn text_data_item_coerces_non_string_writes_to_string() {
    let mut tree = FormTree::new();
    let root = add_node(&mut tree, "root", FormNodeType::Root);

    tree.variables_data_items
        .push((None, "flag".into(), String::new()));

    let writer = add_field(&mut tree, root, "W", "");
    add_js_script(
        &mut tree,
        writer,
        "calculate",
        "flag.value = true; this.rawValue = typeof flag.value;",
    );

    let outcome = run_sandbox(&mut tree, root);

    assert_eq!(
        field_value(&tree, writer),
        "string",
        "data-item write must coerce to string",
    );
    assert_eq!(outcome.js_executed, 1);
    assert_eq!(outcome.js_resolve_failures, 0);
}

/// W3-D RETRY: data items must NOT outrank `<variables>` `<script>` of
/// the same name. Adobe Reader's template merge is last-wins on name
/// collisions; our host-side loop registers scripts first, so scripts
/// take precedence — matching the JS-side `get` trap order
/// (variablesScripts before variablesDataItems).
#[test]
fn script_outranks_data_item_on_name_collision() {
    let mut tree = FormTree::new();
    let root = add_node(&mut tree, "root", FormNodeType::Root);

    tree.variables_scripts.push((
        None,
        "shared".into(),
        "function ping() { return \"from_script\"; }".into(),
    ));
    // Same name as the script — should be shadowed.
    tree.variables_data_items
        .push((None, "shared".into(), "from_data_item".into()));

    let out = add_field(&mut tree, root, "Out", "");
    add_js_script(
        &mut tree,
        out,
        "calculate",
        "this.rawValue = (typeof shared.ping === \"function\") \
         ? shared.ping() : \"data_item_value=\" + (shared.value || \"\");",
    );

    let outcome = run_sandbox(&mut tree, root);

    assert_eq!(
        field_value(&tree, out),
        "from_script",
        "script precedence on name collision",
    );
    assert_eq!(outcome.js_executed, 1);
    assert_eq!(outcome.js_resolve_failures, 0);
}

/// W3-D RETRY + Cluster C contract: registering data items must not
/// break `resolveNode(missing_path) -> null`. The data item is unrelated
/// to the resolve_node path, but the registration loop sits next to the
/// variables-script loop and we explicitly assert the contract under the
/// new code path.
#[test]
fn data_item_registration_does_not_break_resolve_node_null() {
    let mut tree = FormTree::new();
    let root = add_node(&mut tree, "root", FormNodeType::Root);

    tree.variables_data_items
        .push((None, "globValidatePressed".into(), String::new()));

    let out = add_field(&mut tree, root, "Out", "");
    add_js_script(
        &mut tree,
        out,
        "calculate",
        "var n = xfa.resolveNode(\"definitely.missing.path\"); \
         this.rawValue = (n === null) ? \"null_contract_ok\" : \"contract_broken\";",
    );

    let outcome = run_sandbox(&mut tree, root);

    assert_eq!(
        field_value(&tree, out),
        "null_contract_ok",
        "Cluster C contract: resolveNode(missing) must return null",
    );
    assert_eq!(outcome.js_executed, 1);
}

/// W3-D RETRY: deletion of `data_item.value` must NOT succeed (the
/// property is configurable:false). Defence-in-depth so a malicious
/// or poorly-written event script cannot destroy the form-level
/// container.
#[test]
fn data_item_value_property_cannot_be_deleted() {
    let mut tree = FormTree::new();
    let root = add_node(&mut tree, "root", FormNodeType::Root);

    tree.variables_data_items
        .push((None, "flag".into(), "initial".into()));

    let out = add_field(&mut tree, root, "Out", "");
    add_js_script(
        &mut tree,
        out,
        "calculate",
        // delete throws in strict mode; in sloppy (our wrapper) it returns false.
        "var deleted = delete flag.value; \
         this.rawValue = \"deleted=\" + deleted + \",value=\" + flag.value;",
    );

    let outcome = run_sandbox(&mut tree, root);

    assert_eq!(
        field_value(&tree, out),
        "deleted=false,value=initial",
        "data-item value property must remain after delete attempt",
    );
    assert_eq!(outcome.js_executed, 1);
}

/// W3-D RETRY: cross-document isolation. After a document boundary the
/// data items from the previous document must NOT leak into the next.
/// The `reset_for_new_document` hook calls `clearVariablesScripts`,
/// which also drops the data-item maps (per the JS-side teardown).
#[test]
fn data_items_do_not_leak_across_documents() {
    use pdf_xfa::js_runtime::XfaJsRuntime;

    let mut runtime = QuickJsRuntime::new().expect("quickjs runtime");

    // Document 1.
    {
        let mut tree = FormTree::new();
        let root = add_node(&mut tree, "root", FormNodeType::Root);
        tree.variables_data_items
            .push((None, "leakProbe".into(), "doc1".into()));
        let out = add_field(&mut tree, root, "Out", "");
        add_js_script(
            &mut tree,
            out,
            "calculate",
            "this.rawValue = leakProbe.value;",
        );
        apply_dynamic_scripts_with_runtime(
            &mut tree,
            root,
            JsExecutionMode::SandboxedRuntime,
            &mut runtime,
        )
        .expect("doc1 dispatch");
        assert_eq!(
            field_value(&tree, out),
            "doc1",
            "doc1 must see its own data item",
        );
    }

    // Document 2 — no data item declared.
    {
        runtime.reset_for_new_document().expect("reset");
        let mut tree = FormTree::new();
        let root = add_node(&mut tree, "root", FormNodeType::Root);
        let out = add_field(&mut tree, root, "Out", "");
        add_js_script(
            &mut tree,
            out,
            "calculate",
            "this.rawValue = (typeof leakProbe === \"undefined\") \
             ? \"no_leak\" : \"leaked=\" + leakProbe.value;",
        );
        apply_dynamic_scripts_with_runtime(
            &mut tree,
            root,
            JsExecutionMode::SandboxedRuntime,
            &mut runtime,
        )
        .expect("doc2 dispatch");
        assert_eq!(
            field_value(&tree, out),
            "no_leak",
            "data items must not leak across documents",
        );
    }
}

/// W3-D RETRY: a `<text>` data item with whitespace-only or empty name
/// is silently skipped — defence-in-depth so a malformed template cannot
/// pollute the data-item namespace with anonymous entries.
#[test]
fn empty_name_data_items_are_silently_skipped() {
    let mut tree = FormTree::new();
    let root = add_node(&mut tree, "root", FormNodeType::Root);

    // Push an empty-name entry. The host-side bridge rejects it; no JS
    // global is created.
    tree.variables_data_items
        .push((None, String::new(), "ignored".into()));
    // And a valid one alongside, to assert the registration loop survives
    // the skip without aborting subsequent entries.
    tree.variables_data_items
        .push((None, "validName".into(), "ok".into()));

    let out = add_field(&mut tree, root, "Out", "");
    add_js_script(
        &mut tree,
        out,
        "calculate",
        "this.rawValue = (typeof validName === \"undefined\") \
         ? \"absent\" : validName.value;",
    );

    let outcome = run_sandbox(&mut tree, root);

    assert_eq!(field_value(&tree, out), "ok");
    assert_eq!(outcome.js_executed, 1);
    assert_eq!(outcome.js_runtime_errors, 0);
}
