//! Headless DOM tests for the `unmount_island` removal path.
//!
//! These exercise the full `unmount_island(element)` surface against a real DOM
//! `Element`: the mount → unmount round-trip clears both client-state attributes,
//! unmount drops the island's `Scope` from `SCOPE_REGISTRY` exactly once (a
//! second call is a no-op), a never-mounted element is a no-op, and a
//! mount/unmount loop returns `SCOPE_REGISTRY` to its baseline size (the leak
//! canary, AC-V14).
//!
//! `SCOPE_REGISTRY` is the sole strong owner of each `Scope`, so a scope leaving
//! the registry is dropped — running its LIFO cleanups exactly once. That
//! cleanup-closure execution + LIFO ordering is asserted deterministically by
//! the native `#[cfg(test)]` module in `src/registry.rs`; here we assert the
//! DOM-facing contract (attributes + registry membership), using registry size
//! as the deterministic "cleanup ran exactly once" signal. (A `Drop`-counter on
//! a `#[wasm_bindgen]` value is NOT a reliable signal: dropping a JsValue handle
//! to an exported struct does not synchronously run the struct's Rust `Drop` —
//! that is left to JS GC — so registry size is used instead.)
//!
//! `wasm-bindgen-test` runs every test in one shared document, so each test
//! first calls [`clear_islands`] to remove markers a prior test left in `<body>`
//! (otherwise `mount_all`, which scans the whole document, would re-mount them).

#![cfg(target_arch = "wasm32")]

use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use wasm_bindgen_test::*;

use islands_runtime::{mount_all, register_island, scope_registry_len, unmount_island};

wasm_bindgen_test_configure!(run_in_browser);

/// The current document.
fn document() -> web_sys::Document {
    web_sys::window().expect("no window").document().expect("no document")
}

/// Build a detached `<div data-island="Name" data-island-props='...'>` and append
/// it to `<body>` so `mount_all`'s `document.query_selector_all` finds it.
fn append_island_marker(name: &str, props_json: &str) -> web_sys::Element {
    let element = document().create_element("div").expect("create_element");
    element
        .set_attribute("data-island", name)
        .expect("set data-island");
    element
        .set_attribute("data-island-props", props_json)
        .expect("set data-island-props");
    document()
        .body()
        .expect("no body")
        .append_child(&element)
        .expect("append_child");
    element
}

/// Remove every island marker from the document so a test starts from a clean
/// DOM. `wasm-bindgen-test` shares one document across tests and `mount_all`
/// scans the whole document, so a prior test's leftover (now unmounted) markers
/// would otherwise be re-mounted here and skew the registry counts.
fn clear_islands() {
    let markers = document()
        .query_selector_all("[data-island]")
        .expect("query_selector_all");
    for index in 0..markers.length() {
        if let Some(node) = markers.get(index) {
            if let Some(element) = node.dyn_ref::<web_sys::Element>() {
                element.remove();
            }
        }
    }
}

#[wasm_bindgen_test]
fn unmount_clears_both_client_state_attributes() {
    clear_islands();
    register_island("AttributeProbe", &noop_mount()).expect("register_island");
    let element = append_island_marker("AttributeProbe", "{}");

    mount_all().expect("mount_all");
    assert_eq!(
        element.get_attribute("data-island-mounted").as_deref(),
        Some("true"),
        "mount_all should mark the element mounted",
    );
    assert!(
        element.get_attribute("data-island-scope-id").is_some(),
        "mount_all should mint a scope id",
    );

    unmount_island(&element).expect("unmount_island");
    assert!(
        element.get_attribute("data-island-mounted").is_none(),
        "unmount_island must clear data-island-mounted",
    );
    assert!(
        element.get_attribute("data-island-scope-id").is_none(),
        "unmount_island must clear data-island-scope-id so no stale id survives",
    );
}

#[wasm_bindgen_test]
fn unmount_drops_the_scope_exactly_once() {
    clear_islands();
    register_island("CleanupOnce", &noop_mount()).expect("register_island");
    let element = append_island_marker("CleanupOnce", "{}");

    let baseline = scope_registry_len();
    mount_all().expect("mount_all");
    assert_eq!(
        scope_registry_len(),
        baseline + 1,
        "mount registers exactly one scope for the island",
    );

    unmount_island(&element).expect("first unmount_island");
    assert_eq!(
        scope_registry_len(),
        baseline,
        "unmount drops the departing scope exactly once (it left the registry)",
    );

    // A second unmount finds no scope id (it was cleared) and must be a no-op:
    // it must not drop another scope or otherwise disturb the registry.
    unmount_island(&element).expect("second unmount_island");
    assert_eq!(
        scope_registry_len(),
        baseline,
        "second unmount must not change the registry",
    );
}

#[wasm_bindgen_test]
fn unmount_of_never_mounted_element_is_a_noop() {
    clear_islands();
    let element = document().create_element("div").unwrap();
    // No data-island-scope-id: the element was never mounted.
    let baseline = scope_registry_len();
    unmount_island(&element).expect("unmount of unmounted element");
    assert_eq!(
        scope_registry_len(),
        baseline,
        "unmounting a never-mounted element must not touch the registry",
    );
    assert!(element.get_attribute("data-island-scope-id").is_none());
    assert!(element.get_attribute("data-island-mounted").is_none());
}

#[wasm_bindgen_test]
fn mount_unmount_loop_returns_registry_to_baseline() {
    clear_islands();
    register_island("LeakCanary", &noop_mount()).expect("register_island");
    let baseline = scope_registry_len();

    let round_trips = 25;
    for _ in 0..round_trips {
        let element = append_island_marker("LeakCanary", "{}");
        mount_all().expect("mount_all");
        assert_eq!(
            scope_registry_len(),
            baseline + 1,
            "exactly one scope is registered while the island is mounted",
        );
        unmount_island(&element).expect("unmount_island");
        assert_eq!(
            scope_registry_len(),
            baseline,
            "registry returns to baseline after each unmount — no orphaned scopes",
        );
        element.remove();
    }

    assert_eq!(scope_registry_len(), baseline, "no leaked SCOPE_REGISTRY entries");
}

/// A mount fn that does nothing — for tests that only assert attribute/registry
/// bookkeeping and don't need a cleanup probe.
fn noop_mount() -> js_sys::Function {
    let closure = Closure::<dyn FnMut(JsValue, JsValue) -> Result<(), JsValue>>::new(
        move |_element: JsValue, _props: JsValue| -> Result<(), JsValue> { Ok(()) },
    );
    let function = closure.as_ref().unchecked_ref::<js_sys::Function>().clone();
    closure.forget();
    function
}
