//! VDOM Torture Tests
//!
//! Comprehensive stress tests for the VDOM diff algorithm targeting:
//! - Deep nesting
//! - Wide sibling lists
//! - Keyed diffing edge cases (duplicate keys, full reversal, shuffle)
//! - Replace mode under stress
//! - Patch application correctness (diff → apply → verify)
//! - Mixed content types
//! - Text node edge cases
//! - Attribute thrashing
//! - Real HTML parsing round-trips

use djust_vdom::diff::diff_nodes;
use djust_vdom::patch::{apply_patch, apply_patches};
use djust_vdom::{parse_html, reset_id_counter, Patch, VNode};

// ============================================================================
// Helper: diff + apply all patches + verify result matches new tree
// ============================================================================

/// Diff old vs new, apply patches to old, verify result equals new.
/// This is the gold standard correctness check.
///
/// Uses `apply_patches()` which correctly handles `MoveChild` by resolving
/// children via `djust_id` instead of raw indices.
fn diff_apply_verify(old: &VNode, new: &VNode) {
    let patches = diff_nodes(old, new, &[]);
    let mut result = old.clone();

    apply_patches(&mut result, &patches);

    assert_eq_vnode(&result, new, &[]);
}

/// Deep comparison of two VNodes, ignoring djust_id (which may differ between
/// old-with-patches-applied and the new tree).
fn assert_eq_vnode(actual: &VNode, expected: &VNode, path: &[usize]) {
    assert_eq!(
        actual.tag, expected.tag,
        "Tag mismatch at path {:?}: actual={}, expected={}",
        path, actual.tag, expected.tag
    );
    assert_eq!(
        actual.text, expected.text,
        "Text mismatch at path {:?}: actual={:?}, expected={:?}",
        path, actual.text, expected.text
    );

    // Compare attributes ignoring dj-id
    let actual_attrs: std::collections::HashMap<&str, &str> = actual
        .attrs
        .iter()
        .filter(|(k, _)| k.as_str() != "dj-id")
        .map(|(k, v)| (k.as_str(), v.as_str()))
        .collect();
    let expected_attrs: std::collections::HashMap<&str, &str> = expected
        .attrs
        .iter()
        .filter(|(k, _)| k.as_str() != "dj-id")
        .map(|(k, v)| (k.as_str(), v.as_str()))
        .collect();
    assert_eq!(
        actual_attrs, expected_attrs,
        "Attrs mismatch at path {:?}",
        path
    );

    assert_eq!(
        actual.children.len(),
        expected.children.len(),
        "Children count mismatch at path {:?}: actual={}, expected={}. \
         Actual children: {:?}, Expected children: {:?}",
        path,
        actual.children.len(),
        expected.children.len(),
        actual.children.iter().map(|c| &c.tag).collect::<Vec<_>>(),
        expected.children.iter().map(|c| &c.tag).collect::<Vec<_>>(),
    );

    for (i, (a, e)) in actual
        .children
        .iter()
        .zip(expected.children.iter())
        .enumerate()
    {
        let mut child_path = path.to_vec();
        child_path.push(i);
        assert_eq_vnode(a, e, &child_path);
    }
}

// ============================================================================
// 1. DEEP NESTING TESTS
// ============================================================================

#[test]
fn torture_deeply_nested_30_levels() {
    // Build a 30-level deep tree, change the innermost text
    fn build_deep(depth: usize, text: &str) -> VNode {
        if depth == 0 {
            return VNode::text(text);
        }
        VNode::element("div")
            .with_djust_id(format!("d{}", depth))
            .with_child(build_deep(depth - 1, text))
    }

    let old = build_deep(30, "old-leaf");
    let new = build_deep(30, "new-leaf");

    let patches = diff_nodes(&old, &new, &[]);

    // Should produce exactly one SetText patch at the deepest level
    assert_eq!(
        patches.len(),
        1,
        "Deep nesting: expected 1 patch, got {}",
        patches.len()
    );
    assert!(matches!(&patches[0], Patch::SetText { text, .. } if text == "new-leaf"));

    // Verify path is 30 zeros (each level has one child at index 0)
    if let Patch::SetText { path, .. } = &patches[0] {
        assert_eq!(path.len(), 30);
        assert!(path.iter().all(|&i| i == 0));
    }
}

#[test]
fn torture_deeply_nested_50_levels_apply() {
    fn build_deep(depth: usize, text: &str, id_prefix: &str) -> VNode {
        if depth == 0 {
            return VNode::text(text);
        }
        VNode::element("div")
            .with_djust_id(format!("{}{}", id_prefix, depth))
            .with_child(build_deep(depth - 1, text, id_prefix))
    }

    let old = build_deep(50, "leaf-old", "o");
    let new = build_deep(50, "leaf-new", "n");
    diff_apply_verify(&old, &new);
}

#[test]
fn torture_deep_nesting_with_attr_changes_at_every_level() {
    fn build_deep(depth: usize, class_suffix: &str) -> VNode {
        if depth == 0 {
            return VNode::text("leaf");
        }
        VNode::element("div")
            .with_djust_id(format!("d{}", depth))
            .with_attr("class", format!("level-{}-{}", depth, class_suffix))
            .with_child(build_deep(depth - 1, class_suffix))
    }

    let old = build_deep(20, "old");
    let new = build_deep(20, "new");

    let patches = diff_nodes(&old, &new, &[]);
    // Should have 20 SetAttr patches (one per level)
    let attr_patches = patches
        .iter()
        .filter(|p| matches!(p, Patch::SetAttr { .. }))
        .count();
    assert_eq!(attr_patches, 20, "Should change attr at every level");
}

// ============================================================================
// 2. WIDE SIBLING LISTS
// ============================================================================

#[test]
fn torture_100_siblings_append_one() {
    let old_children: Vec<VNode> = (0..100)
        .map(|i| {
            VNode::element("li")
                .with_djust_id(format!("li{}", i))
                .with_child(VNode::text(format!("Item {}", i)))
        })
        .collect();

    let mut new_children = old_children.clone();
    new_children.push(
        VNode::element("li")
            .with_djust_id("li100")
            .with_child(VNode::text("Item 100")),
    );

    let old = VNode::element("ul")
        .with_djust_id("list")
        .with_children(old_children);
    let new = VNode::element("ul")
        .with_djust_id("list")
        .with_children(new_children);

    let patches = diff_nodes(&old, &new, &[]);

    // Should be exactly 1 InsertChild
    assert_eq!(patches.len(), 1);
    assert!(matches!(&patches[0], Patch::InsertChild { index: 100, .. }));
}

#[test]
fn torture_100_siblings_remove_first() {
    let children: Vec<VNode> = (0..100)
        .map(|i| {
            VNode::element("li")
                .with_djust_id(format!("li{}", i))
                .with_child(VNode::text(format!("Item {}", i)))
        })
        .collect();

    let old = VNode::element("ul")
        .with_djust_id("list")
        .with_children(children.clone());

    // Remove first element — indexed diff will morph each child
    let new = VNode::element("ul")
        .with_djust_id("list")
        .with_children(children[1..].to_vec());

    let patches = diff_nodes(&old, &new, &[]);

    // Indexed diff: 99 text changes + 1 RemoveChild
    let text_patches = patches
        .iter()
        .filter(|p| matches!(p, Patch::SetText { .. }))
        .count();
    let remove_patches = patches
        .iter()
        .filter(|p| matches!(p, Patch::RemoveChild { .. }))
        .count();
    assert_eq!(text_patches, 99, "Should morph 99 items");
    assert_eq!(remove_patches, 1, "Should remove 1 extra child");
}

#[test]
fn torture_100_keyed_siblings_remove_first() {
    let children: Vec<VNode> = (0..100)
        .map(|i| {
            VNode::element("li")
                .with_key(format!("k{}", i))
                .with_djust_id(format!("li{}", i))
                .with_child(VNode::text(format!("Item {}", i)))
        })
        .collect();

    let old = VNode::element("ul")
        .with_djust_id("list")
        .with_children(children.clone());

    let new = VNode::element("ul")
        .with_djust_id("list")
        .with_children(children[1..].to_vec());

    let patches = diff_nodes(&old, &new, &[]);

    // Keyed diff should produce 1 RemoveChild + moves (much more efficient)
    let remove_count = patches
        .iter()
        .filter(|p| matches!(p, Patch::RemoveChild { .. }))
        .count();
    assert_eq!(remove_count, 1, "Keyed: should remove exactly 1 child");

    // Should NOT have 99 text changes like indexed diff
    let text_count = patches
        .iter()
        .filter(|p| matches!(p, Patch::SetText { .. }))
        .count();
    assert_eq!(text_count, 0, "Keyed: should have 0 text changes");
}

#[test]
fn torture_100_keyed_siblings_full_reverse() {
    let children: Vec<VNode> = (0..100)
        .map(|i| {
            VNode::element("li")
                .with_key(format!("k{}", i))
                .with_djust_id(format!("li{}", i))
                .with_child(VNode::text(format!("Item {}", i)))
        })
        .collect();

    let mut reversed = children.clone();
    reversed.reverse();

    let old = VNode::element("ul")
        .with_djust_id("list")
        .with_children(children);
    let new = VNode::element("ul")
        .with_djust_id("list")
        .with_children(reversed);

    let patches = diff_nodes(&old, &new, &[]);

    // Should generate MoveChild patches, no inserts/removes
    let move_count = patches
        .iter()
        .filter(|p| matches!(p, Patch::MoveChild { .. }))
        .count();
    let insert_count = patches
        .iter()
        .filter(|p| matches!(p, Patch::InsertChild { .. }))
        .count();
    let remove_count = patches
        .iter()
        .filter(|p| matches!(p, Patch::RemoveChild { .. }))
        .count();

    assert!(move_count > 0, "Reverse should generate moves");
    assert_eq!(insert_count, 0, "Reverse should not insert");
    assert_eq!(remove_count, 0, "Reverse should not remove");
}

#[test]
fn torture_500_indexed_siblings_change_middle() {
    // 500 siblings, change text of the one in the middle
    let mut children: Vec<VNode> = (0..500)
        .map(|i| {
            VNode::element("li")
                .with_djust_id(format!("li{}", i))
                .with_child(VNode::text(format!("Item {}", i)))
        })
        .collect();

    let old = VNode::element("ul")
        .with_djust_id("list")
        .with_children(children.clone());

    children[250] = VNode::element("li")
        .with_djust_id("li250")
        .with_child(VNode::text("CHANGED"));

    let new = VNode::element("ul")
        .with_djust_id("list")
        .with_children(children);

    let patches = diff_nodes(&old, &new, &[]);
    assert_eq!(patches.len(), 1, "Should only change 1 text node");
    assert!(matches!(&patches[0], Patch::SetText { text, .. } if text == "CHANGED"));
}

// ============================================================================
// 3. KEYED DIFFING EDGE CASES
// ============================================================================

#[test]
fn torture_duplicate_keys() {
    // Duplicate keys: the algorithm uses HashMap so last-wins.
    // This tests that it doesn't panic or produce invalid patches.
    // With DJUST_VDOM_TRACE=1, a warning is emitted for each duplicate key.
    let old = VNode::element("div")
        .with_djust_id("parent")
        .with_children(vec![
            VNode::element("div")
                .with_key("dup")
                .with_djust_id("d1")
                .with_child(VNode::text("First")),
            VNode::element("div")
                .with_key("dup")
                .with_djust_id("d2")
                .with_child(VNode::text("Second")),
            VNode::element("div")
                .with_key("unique")
                .with_djust_id("d3")
                .with_child(VNode::text("Third")),
        ]);

    let new = VNode::element("div")
        .with_djust_id("parent")
        .with_children(vec![
            VNode::element("div")
                .with_key("unique")
                .with_djust_id("d4")
                .with_child(VNode::text("Third")),
            VNode::element("div")
                .with_key("dup")
                .with_djust_id("d5")
                .with_child(VNode::text("Only one dup now")),
        ]);

    // Should not panic
    let patches = diff_nodes(&old, &new, &[]);
    // We don't assert exact behavior for duplicate keys — just no panics
    assert!(
        !patches.is_empty(),
        "Should generate some patches for duplicate key scenario"
    );
}

#[test]
fn torture_all_keys_removed_and_replaced() {
    // Remove all old keyed children, insert all new ones
    let old = VNode::element("div")
        .with_djust_id("parent")
        .with_children(vec![
            VNode::element("div").with_key("a").with_djust_id("a"),
            VNode::element("div").with_key("b").with_djust_id("b"),
            VNode::element("div").with_key("c").with_djust_id("c"),
        ]);

    let new = VNode::element("div")
        .with_djust_id("parent")
        .with_children(vec![
            VNode::element("div").with_key("x").with_djust_id("x"),
            VNode::element("div").with_key("y").with_djust_id("y"),
        ]);

    let patches = diff_nodes(&old, &new, &[]);

    let remove_count = patches
        .iter()
        .filter(|p| matches!(p, Patch::RemoveChild { .. }))
        .count();
    let insert_count = patches
        .iter()
        .filter(|p| matches!(p, Patch::InsertChild { .. }))
        .count();

    assert_eq!(remove_count, 3, "Should remove all 3 old keyed children");
    assert_eq!(insert_count, 2, "Should insert 2 new keyed children");
}

#[test]
fn torture_keyed_shuffle_20_items() {
    // Deterministic shuffle of 20 keyed items
    let order: Vec<usize> = vec![
        15, 3, 18, 7, 0, 12, 5, 19, 9, 1, 16, 4, 11, 8, 2, 14, 6, 17, 10, 13,
    ];

    let old_children: Vec<VNode> = (0..20)
        .map(|i| {
            VNode::element("div")
                .with_key(format!("k{}", i))
                .with_djust_id(format!("d{}", i))
                .with_child(VNode::text(format!("Item {}", i)))
        })
        .collect();

    let new_children: Vec<VNode> = order
        .iter()
        .map(|&i| {
            VNode::element("div")
                .with_key(format!("k{}", i))
                .with_djust_id(format!("n{}", i))
                .with_child(VNode::text(format!("Item {}", i)))
        })
        .collect();

    let old = VNode::element("div")
        .with_djust_id("list")
        .with_children(old_children);
    let new = VNode::element("div")
        .with_djust_id("list")
        .with_children(new_children);

    let patches = diff_nodes(&old, &new, &[]);

    // Should have only moves, no inserts/removes
    let insert_count = patches
        .iter()
        .filter(|p| matches!(p, Patch::InsertChild { .. }))
        .count();
    let remove_count = patches
        .iter()
        .filter(|p| matches!(p, Patch::RemoveChild { .. }))
        .count();
    assert_eq!(insert_count, 0, "Shuffle should not insert");
    assert_eq!(remove_count, 0, "Shuffle should not remove");
}

#[test]
fn torture_keyed_to_unkeyed_transition() {
    // All children go from keyed to unkeyed (different tag triggers full replace)
    let old = VNode::element("div")
        .with_djust_id("parent")
        .with_children(vec![
            VNode::element("div")
                .with_key("a")
                .with_djust_id("a")
                .with_child(VNode::text("A")),
            VNode::element("div")
                .with_key("b")
                .with_djust_id("b")
                .with_child(VNode::text("B")),
        ]);

    let new = VNode::element("div")
        .with_djust_id("parent")
        .with_children(vec![
            VNode::element("span")
                .with_djust_id("s1")
                .with_child(VNode::text("No keys")),
            VNode::element("span")
                .with_djust_id("s2")
                .with_child(VNode::text("Either")),
        ]);

    let patches = diff_nodes(&old, &new, &[]);

    // With no keys in new, uses indexed diffing. Tags differ → Replace patches.
    let replace_count = patches
        .iter()
        .filter(|p| matches!(p, Patch::Replace { .. }))
        .count();
    assert_eq!(
        replace_count, 2,
        "Should replace both children (tag mismatch)"
    );
}

// ============================================================================
// 4. REPLACE MODE TORTURE
// ============================================================================

#[test]
fn torture_replace_mode_100_to_100() {
    let old_children: Vec<VNode> = (0..100)
        .map(|i| {
            VNode::element("div")
                .with_djust_id(format!("o{}", i))
                .with_child(VNode::text(format!("Old {}", i)))
        })
        .collect();

    let new_children: Vec<VNode> = (0..100)
        .map(|i| {
            VNode::element("div")
                .with_djust_id(format!("n{}", i))
                .with_child(VNode::text(format!("New {}", i)))
        })
        .collect();

    let old = VNode::element("div")
        .with_djust_id("container")
        .with_attr("data-djust-replace", "")
        .with_children(old_children);
    let new = VNode::element("div")
        .with_djust_id("container")
        .with_attr("data-djust-replace", "")
        .with_children(new_children);

    let patches = diff_nodes(&old, &new, &[]);

    let remove_count = patches
        .iter()
        .filter(|p| matches!(p, Patch::RemoveChild { .. }))
        .count();
    let insert_count = patches
        .iter()
        .filter(|p| matches!(p, Patch::InsertChild { .. }))
        .count();

    assert_eq!(remove_count, 100);
    assert_eq!(insert_count, 100);

    // Verify ordering: all removes before all inserts
    let last_remove = patches
        .iter()
        .rposition(|p| matches!(p, Patch::RemoveChild { .. }))
        .unwrap();
    let first_insert = patches
        .iter()
        .position(|p| matches!(p, Patch::InsertChild { .. }))
        .unwrap();
    assert!(
        last_remove < first_insert,
        "All removes must precede all inserts"
    );

    // Verify remove indices are descending
    let remove_indices: Vec<usize> = patches
        .iter()
        .filter_map(|p| match p {
            Patch::RemoveChild { index, .. } => Some(*index),
            _ => None,
        })
        .collect();
    for i in 1..remove_indices.len() {
        assert!(
            remove_indices[i] < remove_indices[i - 1],
            "Remove indices must be descending"
        );
    }
}

#[test]
fn torture_replace_mode_0_to_50() {
    let old = VNode::element("div")
        .with_djust_id("c")
        .with_attr("data-djust-replace", "");

    let new_children: Vec<VNode> = (0..50)
        .map(|i| VNode::element("p").with_djust_id(format!("p{}", i)))
        .collect();

    let new = VNode::element("div")
        .with_djust_id("c")
        .with_attr("data-djust-replace", "")
        .with_children(new_children);

    let patches = diff_nodes(&old, &new, &[]);
    let insert_count = patches
        .iter()
        .filter(|p| matches!(p, Patch::InsertChild { .. }))
        .count();
    assert_eq!(insert_count, 50);
}

// ============================================================================
// 5. DIFF-APPLY-VERIFY CORRECTNESS TESTS
// ============================================================================

#[test]
fn torture_apply_verify_deep_nesting() {
    fn build(depth: usize, text: &str, id_prefix: &str) -> VNode {
        if depth == 0 {
            return VNode::text(text);
        }
        VNode::element("div")
            .with_djust_id(format!("{}{}", id_prefix, depth))
            .with_attr("class", format!("level-{}", depth))
            .with_child(build(depth - 1, text, id_prefix))
    }

    diff_apply_verify(&build(15, "old", "o"), &build(15, "new", "n"));
}

#[test]
fn torture_apply_verify_wide_insert() {
    let old = VNode::element("ul")
        .with_djust_id("list")
        .with_children(vec![
            VNode::element("li")
                .with_djust_id("a")
                .with_child(VNode::text("A")),
            VNode::element("li")
                .with_djust_id("b")
                .with_child(VNode::text("B")),
        ]);

    let new = VNode::element("ul")
        .with_djust_id("list")
        .with_children(vec![
            VNode::element("li")
                .with_djust_id("a")
                .with_child(VNode::text("A")),
            VNode::element("li")
                .with_djust_id("b")
                .with_child(VNode::text("B")),
            VNode::element("li")
                .with_djust_id("c")
                .with_child(VNode::text("C")),
            VNode::element("li")
                .with_djust_id("d")
                .with_child(VNode::text("D")),
        ]);

    diff_apply_verify(&old, &new);
}

#[test]
fn torture_apply_verify_wide_remove() {
    let old = VNode::element("ul")
        .with_djust_id("list")
        .with_children(vec![
            VNode::element("li")
                .with_djust_id("a")
                .with_child(VNode::text("A")),
            VNode::element("li")
                .with_djust_id("b")
                .with_child(VNode::text("B")),
            VNode::element("li")
                .with_djust_id("c")
                .with_child(VNode::text("C")),
            VNode::element("li")
                .with_djust_id("d")
                .with_child(VNode::text("D")),
        ]);

    let new = VNode::element("ul")
        .with_djust_id("list")
        .with_children(vec![VNode::element("li")
            .with_djust_id("a")
            .with_child(VNode::text("A"))]);

    diff_apply_verify(&old, &new);
}

#[test]
fn torture_apply_verify_tag_replacement() {
    let old = VNode::element("div")
        .with_djust_id("root")
        .with_children(vec![
            VNode::element("div")
                .with_djust_id("c1")
                .with_child(VNode::text("child")),
            VNode::element("span")
                .with_djust_id("c2")
                .with_child(VNode::text("span")),
        ]);

    let new = VNode::element("div")
        .with_djust_id("root")
        .with_children(vec![
            VNode::element("section")
                .with_djust_id("c1n")
                .with_child(VNode::text("replaced")),
            VNode::element("article")
                .with_djust_id("c2n")
                .with_child(VNode::text("also replaced")),
        ]);

    diff_apply_verify(&old, &new);
}

#[test]
fn torture_apply_verify_attr_add_remove_change() {
    let old = VNode::element("div")
        .with_djust_id("root")
        .with_attr("class", "old")
        .with_attr("title", "remove-me")
        .with_attr("id", "keep");

    let new = VNode::element("div")
        .with_djust_id("root")
        .with_attr("class", "new")
        .with_attr("id", "keep")
        .with_attr("data-new", "added");

    diff_apply_verify(&old, &new);
}

#[test]
fn torture_apply_verify_empty_to_complex() {
    let old = VNode::element("div").with_djust_id("root");

    let new = VNode::element("div")
        .with_djust_id("root")
        .with_children(vec![
            VNode::element("header")
                .with_djust_id("h")
                .with_child(VNode::text("Title")),
            VNode::element("main")
                .with_djust_id("m")
                .with_children(vec![
                    VNode::element("p")
                        .with_djust_id("p1")
                        .with_child(VNode::text("Para 1")),
                    VNode::element("p")
                        .with_djust_id("p2")
                        .with_child(VNode::text("Para 2")),
                ]),
            VNode::element("footer")
                .with_djust_id("f")
                .with_child(VNode::text("Footer")),
        ]);

    diff_apply_verify(&old, &new);
}

#[test]
fn torture_apply_verify_complex_to_empty() {
    let old = VNode::element("div")
        .with_djust_id("root")
        .with_children(vec![
            VNode::element("header")
                .with_djust_id("h")
                .with_child(VNode::text("Title")),
            VNode::element("main")
                .with_djust_id("m")
                .with_children(vec![
                    VNode::element("p")
                        .with_djust_id("p1")
                        .with_child(VNode::text("Para 1")),
                    VNode::element("p")
                        .with_djust_id("p2")
                        .with_child(VNode::text("Para 2")),
                ]),
        ]);

    let new = VNode::element("div").with_djust_id("root");

    diff_apply_verify(&old, &new);
}

// ============================================================================
// 6. TEXT NODE EDGE CASES
// ============================================================================

#[test]
fn torture_text_empty_string() {
    let old = VNode::text("");
    let new = VNode::text("content");
    let patches = diff_nodes(&old, &new, &[]);
    assert_eq!(patches.len(), 1);
    assert!(matches!(&patches[0], Patch::SetText { text, .. } if text == "content"));
}

#[test]
fn torture_text_to_empty_string() {
    let old = VNode::text("content");
    let new = VNode::text("");
    let patches = diff_nodes(&old, &new, &[]);
    assert_eq!(patches.len(), 1);
    assert!(matches!(&patches[0], Patch::SetText { text, .. } if text.is_empty()));
}

#[test]
fn torture_text_unicode() {
    let old = VNode::text("Hello 🌍");
    let new = VNode::text("Привет мир 🚀 日本語");
    let patches = diff_nodes(&old, &new, &[]);
    assert_eq!(patches.len(), 1);
    assert!(matches!(&patches[0], Patch::SetText { text, .. } if text == "Привет мир 🚀 日本語"));
}

#[test]
fn torture_text_with_html_entities() {
    let old = VNode::text("a < b && c > d");
    let new = VNode::text("a < b & c > d \"quoted\"");
    let patches = diff_nodes(&old, &new, &[]);
    assert_eq!(patches.len(), 1);
}

#[test]
fn torture_many_adjacent_text_nodes() {
    // Multiple text nodes as siblings (unusual but valid in VDOM)
    let old = VNode::element("div").with_djust_id("d").with_children(vec![
        VNode::text("a"),
        VNode::text("b"),
        VNode::text("c"),
        VNode::text("d"),
        VNode::text("e"),
    ]);

    let new = VNode::element("div").with_djust_id("d").with_children(vec![
        VNode::text("a"),
        VNode::text("B"),
        VNode::text("c"),
        VNode::text("D"),
        VNode::text("e"),
    ]);

    let patches = diff_nodes(&old, &new, &[]);
    assert_eq!(patches.len(), 2, "Should change 2 text nodes");
}

// ============================================================================
// 7. ATTRIBUTE THRASHING
// ============================================================================

#[test]
fn torture_many_attributes_changed() {
    let mut old = VNode::element("div").with_djust_id("d");
    let mut new = VNode::element("div").with_djust_id("d");

    for i in 0..50 {
        old = old.with_attr(format!("data-attr-{}", i), format!("old-{}", i));
        new = new.with_attr(format!("data-attr-{}", i), format!("new-{}", i));
    }

    let patches = diff_nodes(&old, &new, &[]);
    let attr_count = patches
        .iter()
        .filter(|p| matches!(p, Patch::SetAttr { .. }))
        .count();
    assert_eq!(attr_count, 50, "Should change all 50 attributes");
}

#[test]
fn torture_all_attributes_removed() {
    let mut old = VNode::element("div").with_djust_id("d");
    for i in 0..20 {
        old = old.with_attr(format!("data-attr-{}", i), format!("val-{}", i));
    }

    let new = VNode::element("div").with_djust_id("d");

    let patches = diff_nodes(&old, &new, &[]);
    let remove_count = patches
        .iter()
        .filter(|p| matches!(p, Patch::RemoveAttr { .. }))
        .count();
    assert_eq!(remove_count, 20, "Should remove all 20 attributes");
}

#[test]
fn torture_all_attributes_added() {
    let old = VNode::element("div").with_djust_id("d");

    let mut new = VNode::element("div").with_djust_id("d");
    for i in 0..20 {
        new = new.with_attr(format!("data-attr-{}", i), format!("val-{}", i));
    }

    let patches = diff_nodes(&old, &new, &[]);
    let add_count = patches
        .iter()
        .filter(|p| matches!(p, Patch::SetAttr { .. }))
        .count();
    assert_eq!(add_count, 20, "Should add all 20 attributes");
}

#[test]
fn torture_dj_event_attrs_never_removed_mass() {
    // 10 dj-* attributes — none should be removed
    let mut old = VNode::element("div").with_djust_id("d");
    for i in 0..10 {
        old = old.with_attr(format!("dj-event-{}", i), format!("handler_{}", i));
    }

    let new = VNode::element("div").with_djust_id("d");

    let patches = diff_nodes(&old, &new, &[]);

    for patch in &patches {
        if let Patch::RemoveAttr { key, .. } = patch {
            assert!(
                !key.starts_with("dj-"),
                "dj-* attr should never be removed: {}",
                key
            );
        }
    }
}

// ============================================================================
// 8. REAL HTML PARSING ROUND-TRIP TESTS
// ============================================================================

#[test]
fn torture_parse_diff_complex_html() {
    reset_id_counter();
    let old_html = r#"<div class="container">
        <header><h1>Title</h1></header>
        <main>
            <article><p>Paragraph 1</p><p>Paragraph 2</p></article>
            <aside><ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul></aside>
        </main>
        <footer><p>Footer text</p></footer>
    </div>"#;

    let old = parse_html(old_html).unwrap();

    reset_id_counter();
    let new_html = r#"<div class="container">
        <header><h1>New Title</h1></header>
        <main>
            <article><p>Paragraph 1</p><p>Modified Paragraph 2</p><p>New Paragraph 3</p></article>
            <aside><ul><li>Item 1</li><li>Item 3</li></ul></aside>
        </main>
        <footer><p>Updated footer</p></footer>
    </div>"#;

    let new = parse_html(new_html).unwrap();

    let patches = diff_nodes(&old, &new, &[]);

    // Should have patches for:
    // - Title text change
    // - Paragraph 2 text change
    // - New paragraph 3 inserted
    // - Item 2 → Item 3 (text change), Item 3 removed
    // - Footer text change
    assert!(
        patches.len() >= 4,
        "Should generate multiple patches, got {}",
        patches.len()
    );
}

#[test]
fn torture_parse_diff_table() {
    reset_id_counter();
    let old_html = r#"<table>
        <thead><tr><th>Name</th><th>Age</th></tr></thead>
        <tbody>
            <tr><td>Alice</td><td>30</td></tr>
            <tr><td>Bob</td><td>25</td></tr>
        </tbody>
    </table>"#;

    let old = parse_html(old_html).unwrap();

    reset_id_counter();
    let new_html = r#"<table>
        <thead><tr><th>Name</th><th>Age</th><th>City</th></tr></thead>
        <tbody>
            <tr><td>Alice</td><td>31</td><td>NYC</td></tr>
            <tr><td>Bob</td><td>26</td><td>LA</td></tr>
            <tr><td>Carol</td><td>28</td><td>SF</td></tr>
        </tbody>
    </table>"#;

    let new = parse_html(new_html).unwrap();

    let patches = diff_nodes(&old, &new, &[]);
    assert!(!patches.is_empty(), "Table diff should produce patches");
}

#[test]
fn torture_parse_diff_form_with_validation() {
    reset_id_counter();
    let old_html = r#"<form>
        <div class="field">
            <label>Username</label>
            <input type="text" class="form-control is-invalid">
            <div class="invalid-feedback">Username required</div>
        </div>
        <div class="field">
            <label>Email</label>
            <input type="email" class="form-control is-invalid">
            <div class="invalid-feedback">Email required</div>
        </div>
        <div class="field">
            <label>Password</label>
            <input type="password" class="form-control is-invalid">
            <div class="invalid-feedback">Password too short</div>
        </div>
        <button type="submit">Submit</button>
    </form>"#;

    let old = parse_html(old_html).unwrap();

    reset_id_counter();
    let new_html = r#"<form>
        <div class="field">
            <label>Username</label>
            <input type="text" class="form-control">
        </div>
        <div class="field">
            <label>Email</label>
            <input type="email" class="form-control">
        </div>
        <div class="field">
            <label>Password</label>
            <input type="password" class="form-control">
        </div>
        <button type="submit">Submit</button>
    </form>"#;

    let new = parse_html(new_html).unwrap();

    let patches = diff_nodes(&old, &new, &[]);

    // Should have attr changes for 3 inputs + child removals for 3 error divs
    let attr_changes = patches
        .iter()
        .filter(|p| matches!(p, Patch::SetAttr { .. }))
        .count();
    let removals = patches
        .iter()
        .filter(|p| matches!(p, Patch::RemoveChild { .. }))
        .count();

    assert!(
        attr_changes >= 3,
        "Should change class on 3 inputs, got {}",
        attr_changes
    );
    assert!(
        removals >= 3,
        "Should remove 3 error divs, got {}",
        removals
    );
}

// ============================================================================
// 9. MIXED SCENARIOS & WEIRD CASES
// ============================================================================

#[test]
fn torture_identical_trees() {
    // No changes → no patches
    let tree = VNode::element("div")
        .with_djust_id("root")
        .with_attr("class", "same")
        .with_children(vec![
            VNode::element("span")
                .with_djust_id("s1")
                .with_child(VNode::text("text")),
            VNode::element("span")
                .with_djust_id("s2")
                .with_child(VNode::text("more")),
        ]);

    let patches = diff_nodes(&tree, &tree, &[]);
    assert!(
        patches.is_empty(),
        "Identical trees should produce 0 patches"
    );
}

#[test]
fn torture_completely_different_trees() {
    let old = VNode::element("div")
        .with_djust_id("root")
        .with_children(vec![
            VNode::element("h1")
                .with_djust_id("h")
                .with_child(VNode::text("Title")),
            VNode::element("p")
                .with_djust_id("p")
                .with_child(VNode::text("Para")),
        ]);

    let new = VNode::element("div")
        .with_djust_id("root")
        .with_children(vec![VNode::element("nav")
            .with_djust_id("nav")
            .with_children(vec![
                VNode::element("a")
                    .with_djust_id("a1")
                    .with_child(VNode::text("Link 1")),
                VNode::element("a")
                    .with_djust_id("a2")
                    .with_child(VNode::text("Link 2")),
                VNode::element("a")
                    .with_djust_id("a3")
                    .with_child(VNode::text("Link 3")),
            ])]);

    let patches = diff_nodes(&old, &new, &[]);
    assert!(
        !patches.is_empty(),
        "Completely different trees should produce patches"
    );
}

#[test]
fn torture_text_to_element() {
    // Parent has text child → element child (tag mismatch triggers replace)
    let old = VNode::element("div")
        .with_djust_id("d")
        .with_child(VNode::text("just text"));

    let new = VNode::element("div").with_djust_id("d").with_child(
        VNode::element("span")
            .with_djust_id("s")
            .with_child(VNode::text("wrapped")),
    );

    let patches = diff_nodes(&old, &new, &[]);
    // Text node (tag="#text") vs element (tag="span") → Replace
    assert!(patches.iter().any(|p| matches!(p, Patch::Replace { .. })));
}

#[test]
fn torture_element_to_text() {
    let old = VNode::element("div").with_djust_id("d").with_child(
        VNode::element("span")
            .with_djust_id("s")
            .with_child(VNode::text("wrapped")),
    );

    let new = VNode::element("div")
        .with_djust_id("d")
        .with_child(VNode::text("just text"));

    let patches = diff_nodes(&old, &new, &[]);
    assert!(patches.iter().any(|p| matches!(p, Patch::Replace { .. })));
}

#[test]
fn torture_sibling_count_oscillation() {
    // Rapidly changing child count (simulates show/hide toggle)
    for count in [0, 5, 0, 10, 3, 0, 7] {
        let old = VNode::element("div").with_djust_id("d");
        let children: Vec<VNode> = (0..count)
            .map(|i| {
                VNode::element("p")
                    .with_djust_id(format!("p{}", i))
                    .with_child(VNode::text(format!("{}", i)))
            })
            .collect();
        let new = VNode::element("div")
            .with_djust_id("d")
            .with_children(children);

        // Should not panic
        let _patches = diff_nodes(&old, &new, &[]);
    }
}

// ============================================================================
// 10. KEYED MOVE CORRECTNESS (apply and verify)
// ============================================================================

#[test]
fn torture_keyed_move_apply_verify_simple_swap() {
    let old = VNode::element("ul")
        .with_djust_id("list")
        .with_children(vec![
            VNode::element("li")
                .with_key("a")
                .with_djust_id("a")
                .with_child(VNode::text("A")),
            VNode::element("li")
                .with_key("b")
                .with_djust_id("b")
                .with_child(VNode::text("B")),
        ]);

    let new = VNode::element("ul")
        .with_djust_id("list")
        .with_children(vec![
            VNode::element("li")
                .with_key("b")
                .with_djust_id("b2")
                .with_child(VNode::text("B")),
            VNode::element("li")
                .with_key("a")
                .with_djust_id("a2")
                .with_child(VNode::text("A")),
        ]);

    // Now that apply_patch resolves MoveChild by djust_id instead of
    // raw index, diff_apply_verify works for keyed moves.
    diff_apply_verify(&old, &new);
}

// ============================================================================
// 11. REPLACE CONTAINER WITH SIBLINGS STRESS TEST
// ============================================================================

#[test]
fn torture_replace_with_many_siblings() {
    // Replace container surrounded by 5 siblings on each side
    let mut children = Vec::new();
    for i in 0..5 {
        children.push(
            VNode::element("div")
                .with_djust_id(format!("before{}", i))
                .with_child(VNode::text(format!("Before {}", i))),
        );
    }
    children.push(
        VNode::element("div")
            .with_djust_id("replace-container")
            .with_attr("data-djust-replace", "")
            .with_children(vec![
                VNode::element("p").with_child(VNode::text("Old message 1")),
                VNode::element("p").with_child(VNode::text("Old message 2")),
            ]),
    );
    for i in 0..5 {
        children.push(
            VNode::element("div")
                .with_djust_id(format!("after{}", i))
                .with_child(VNode::text(format!("After {}", i))),
        );
    }

    let old = VNode::element("div")
        .with_djust_id("root")
        .with_children(children);

    // New: replace container has different children, siblings unchanged
    let mut new_children = Vec::new();
    for i in 0..5 {
        new_children.push(
            VNode::element("div")
                .with_djust_id(format!("before{}", i))
                .with_child(VNode::text(format!("Before {}", i))),
        );
    }
    new_children.push(
        VNode::element("div")
            .with_djust_id("replace-container")
            .with_attr("data-djust-replace", "")
            .with_children(vec![
                VNode::element("p").with_child(VNode::text("New message 1")),
                VNode::element("p").with_child(VNode::text("New message 2")),
                VNode::element("p").with_child(VNode::text("New message 3")),
            ]),
    );
    for i in 0..5 {
        new_children.push(
            VNode::element("div")
                .with_djust_id(format!("after{}", i))
                .with_child(VNode::text(format!("After {}", i))),
        );
    }

    let new = VNode::element("div")
        .with_djust_id("root")
        .with_children(new_children);

    let patches = diff_nodes(&old, &new, &[]);

    // All InsertChild/RemoveChild patches should target replace-container
    for patch in &patches {
        match patch {
            Patch::InsertChild { d, .. } | Patch::RemoveChild { d, .. } => {
                assert_eq!(
                    d.as_deref(),
                    Some("replace-container"),
                    "Child ops must target replace-container, got {:?}",
                    d
                );
            }
            _ => {}
        }
    }
}

// ============================================================================
// 12. RAPID SEQUENTIAL DIFFS (simulates rapid state updates)
// ============================================================================

#[test]
fn torture_rapid_sequential_diffs_counter() {
    // Simulate a counter incrementing 100 times
    let mut current = VNode::element("div")
        .with_djust_id("counter")
        .with_child(VNode::text("0"));

    for i in 1..=100 {
        let next = VNode::element("div")
            .with_djust_id("counter")
            .with_child(VNode::text(format!("{}", i)));

        let patches = diff_nodes(&current, &next, &[]);
        assert_eq!(patches.len(), 1, "Counter update should be 1 patch");
        assert!(matches!(&patches[0], Patch::SetText { .. }));

        // Apply the patch for next round
        for patch in &patches {
            apply_patch(&mut current, patch);
        }
    }
}

#[test]
fn torture_rapid_sequential_diffs_growing_list() {
    // Simulate a list growing from 0 to 50 items, one at a time
    let mut current = VNode::element("ul").with_djust_id("list");

    for i in 0..50 {
        let mut children: Vec<VNode> = current.children.clone();
        children.push(
            VNode::element("li")
                .with_djust_id(format!("li{}", i))
                .with_child(VNode::text(format!("Item {}", i))),
        );

        let next = VNode::element("ul")
            .with_djust_id("list")
            .with_children(children);

        let patches = diff_nodes(&current, &next, &[]);
        assert_eq!(
            patches.len(),
            1,
            "Growing list: should insert 1 item at step {}",
            i
        );
        assert!(matches!(&patches[0], Patch::InsertChild { index, .. } if *index == i));

        for patch in &patches {
            apply_patch(&mut current, patch);
        }
    }
}
