//! Part of the `transform` test suite; split off so no file exceeds
//! 1000 lines. See the parent `tests/mod.rs` for the shared helpers
//! (`doc_with_text`, `doc_with_table`, `apply_all`, `table_grid`).

use super::*;

#[test]
fn set_table_props_undo_restores_previous_values() {
    let (mut doc, table_id) = doc_with_table(2, 2);
    doc.get_block_mut(&table_id)
        .unwrap()
        .props
        .insert("backgroundColor".into(), Value::String("#000000".into()));
    let sel = Selection::collapsed(table_id.clone(), 0);
    let mut id_gen = SequentialIdGenerator::new("t");

    let mut props = Map::new();
    props.insert("backgroundColor".into(), Value::String("#fef3c7".into()));
    // A key that doesn't yet exist — prev should be Null.
    props.insert("borderColor".into(), Value::String("#f59e0b".into()));

    let ops = transform(
        &doc,
        &sel,
        &Command::SetTableProps {
            table_id: table_id.clone(),
            props,
        },
        &mut id_gen,
        None,
    )
    .unwrap();
    assert_eq!(ops.len(), 1);
    match &ops[0] {
        Operation::SetBlockProps { prev_props, .. } => {
            assert_eq!(
                prev_props.get("backgroundColor"),
                Some(&Value::String("#000000".into()))
            );
            assert_eq!(prev_props.get("borderColor"), Some(&Value::Null));
        }
        other => panic!("expected SetBlockProps, got {other:?}"),
    }
}

#[test]
fn set_table_props_rejects_non_table_block() {
    let (doc, table_id) = doc_with_table(1, 1);
    let grid = table_grid(&doc, &table_id);
    let cell_id = grid[0][0].clone();
    let sel = Selection::collapsed(cell_id.clone(), 0);
    let mut id_gen = SequentialIdGenerator::new("t");

    let mut props = Map::new();
    props.insert("backgroundColor".into(), Value::String("#fef3c7".into()));
    let err = transform(
        &doc,
        &sel,
        &Command::SetTableProps {
            // Point at the cell, not the table.
            table_id: cell_id,
            props,
        },
        &mut id_gen,
        None,
    )
    .unwrap_err();
    match err {
        EditorError::InvalidOperation(msg) => {
            assert!(msg.contains("not a table"), "unexpected error: {msg}");
        }
        other => panic!("expected InvalidOperation, got {other:?}"),
    }

    // And a block id that doesn't exist should bubble BlockNotFound.
    let bogus = BlockId::new("does-not-exist");
    let mut props = Map::new();
    props.insert("backgroundColor".into(), Value::String("#fef3c7".into()));
    let err = transform(
        &doc,
        &sel,
        &Command::SetTableProps {
            table_id: bogus.clone(),
            props,
        },
        &mut id_gen,
        None,
    )
    .unwrap_err();
    assert!(matches!(err, EditorError::BlockNotFound(id) if id == bogus));
}

#[test]
fn set_row_props_applies_to_table_row() {
    let (mut doc, table_id) = doc_with_table(2, 2);
    let row_id = doc.get_block(&table_id).unwrap().children[0].clone();
    let sel = Selection::collapsed(row_id.clone(), 0);
    let mut id_gen = SequentialIdGenerator::new("t");

    let mut props = Map::new();
    props.insert("height".into(), Value::Number(48.into()));
    let ops = transform(
        &doc,
        &sel,
        &Command::SetRowProps {
            row_id: row_id.clone(),
            props,
        },
        &mut id_gen,
        None,
    )
    .unwrap();
    let mut sel = Some(sel);
    apply_all(&mut doc, &mut sel, &ops);

    let row = doc.get_block(&row_id).unwrap();
    assert_eq!(row.props.get("height"), Some(&Value::Number(48.into())));
}

#[test]
fn set_row_props_rejects_non_row_block() {
    let (doc, table_id) = doc_with_table(1, 1);
    let sel = Selection::collapsed(table_id.clone(), 0);
    let mut id_gen = SequentialIdGenerator::new("t");

    let mut props = Map::new();
    props.insert("height".into(), Value::Number(48.into()));
    // Point at the table itself — not a row.
    let err = transform(
        &doc,
        &sel,
        &Command::SetRowProps {
            row_id: table_id,
            props,
        },
        &mut id_gen,
        None,
    )
    .unwrap_err();
    match err {
        EditorError::InvalidOperation(msg) => {
            assert!(msg.contains("not a table_row"), "unexpected error: {msg}");
        }
        other => panic!("expected InvalidOperation, got {other:?}"),
    }
}

#[test]
fn set_row_props_filters_unchanged_keys() {
    let (mut doc, table_id) = doc_with_table(2, 2);
    let row_id = doc.get_block(&table_id).unwrap().children[0].clone();
    // Pre-populate the row with an existing height.
    doc.get_block_mut(&row_id)
        .unwrap()
        .props
        .insert("height".into(), Value::Number(36.into()));
    let sel = Selection::collapsed(row_id.clone(), 0);
    let mut id_gen = SequentialIdGenerator::new("t");

    let mut same = Map::new();
    same.insert("height".into(), Value::Number(36.into()));
    let ops = transform(
        &doc,
        &sel,
        &Command::SetRowProps {
            row_id,
            props: same,
        },
        &mut id_gen,
        None,
    )
    .unwrap();
    assert!(ops.is_empty(), "expected no-op, got {ops:?}");
}

#[test]
fn toggle_mark_applies_to_all_selected_table_cells() {
    let (mut doc, table_id) = doc_with_table(2, 2);
    let grid = table_grid(&doc, &table_id);
    doc.get_block_mut(&grid[0][0]).unwrap().content = vec![TextSpan::plain("a")];
    doc.get_block_mut(&grid[0][1]).unwrap().content = vec![TextSpan::plain("b")];
    doc.get_block_mut(&grid[1][0]).unwrap().content = vec![TextSpan::plain("c")];
    let sel = Selection::with_cell_selection(
        Position::new(grid[0][0].clone(), 0),
        Position::new(grid[1][1].clone(), 0),
        CellSelection::new(table_id.clone(), CellCoord::new(0, 0), CellCoord::new(1, 1)),
    );
    let mut id_gen = SequentialIdGenerator::new("t");

    let ops = transform(
        &doc,
        &sel,
        &Command::ToggleMark {
            mark_type: "bold".into(),
        },
        &mut id_gen,
        None,
    )
    .unwrap();
    let mut sel = Some(sel);
    apply_all(&mut doc, &mut sel, &ops);

    for cell_id in [&grid[0][0], &grid[0][1], &grid[1][0]] {
        let marks = &doc.get_block(cell_id).unwrap().content[0].marks;
        assert!(marks.iter().any(|mark| mark.ty == "bold"));
    }
    let empty_cell = doc.get_block(&grid[1][1]).unwrap();
    assert!(empty_cell.content.is_empty());
    assert_eq!(
        empty_cell.props.get("pendingMarks"),
        Some(&Value::Array(vec![Value::String("bold".into())]))
    );
}

#[test]
fn toggle_mark_on_empty_selected_table_cells_sets_pending_marks() {
    let (mut doc, table_id) = doc_with_table(2, 2);
    let grid = table_grid(&doc, &table_id);
    let sel = Selection::with_cell_selection(
        Position::new(grid[0][0].clone(), 0),
        Position::new(grid[1][1].clone(), 0),
        CellSelection::new(table_id, CellCoord::new(0, 0), CellCoord::new(1, 1)),
    );
    let mut id_gen = SequentialIdGenerator::new("t");

    let ops = transform(
        &doc,
        &sel,
        &Command::ToggleCellMark {
            mark_type: "bold".into(),
            cell_ids: vec![],
        },
        &mut id_gen,
        None,
    )
    .unwrap();
    let mut sel = Some(sel);
    apply_all(&mut doc, &mut sel, &ops);

    for row in &grid {
        for cell_id in row {
            assert_eq!(
                doc.get_block(cell_id).unwrap().props.get("pendingMarks"),
                Some(&Value::Array(vec![Value::String("bold".into())]))
            );
        }
    }
}

#[test]
fn insert_text_consumes_pending_marks_from_empty_table_cell() {
    let (mut doc, table_id) = doc_with_table(2, 2);
    let grid = table_grid(&doc, &table_id);
    let selection = Selection::with_cell_selection(
        Position::new(grid[0][0].clone(), 0),
        Position::new(grid[1][1].clone(), 0),
        CellSelection::new(table_id, CellCoord::new(0, 0), CellCoord::new(1, 1)),
    );
    let mut id_gen = SequentialIdGenerator::new("t");

    let mark_ops = transform(
        &doc,
        &selection,
        &Command::ToggleCellMark {
            mark_type: "bold".into(),
            cell_ids: vec![],
        },
        &mut id_gen,
        None,
    )
    .unwrap();
    let mut current_selection = Some(selection);
    apply_all(&mut doc, &mut current_selection, &mark_ops);

    let empty_cell_id = grid[1][1].clone();
    let insert_selection = Selection::collapsed(empty_cell_id.clone(), 0);
    let insert_ops = transform(
        &doc,
        &insert_selection,
        &Command::InsertText { text: "x".into() },
        &mut id_gen,
        None,
    )
    .unwrap();
    let mut insert_sel = Some(insert_selection);
    apply_all(&mut doc, &mut insert_sel, &insert_ops);

    let cell = doc.get_block(&empty_cell_id).unwrap();
    assert_eq!(cell.plain_text(), "x");
    assert!(cell.content.iter().all(|span| span.has_mark("bold")));
    assert_eq!(cell.props.get("pendingMarks"), Some(&Value::Null));
}

#[test]
fn clear_cells_clears_multiple_cells() {
    let (mut doc, table_id) = doc_with_table(2, 2);
    let grid = table_grid(&doc, &table_id);
    let mut id_gen = SequentialIdGenerator::new("cmd");

    let ops_a = transform(
        &doc,
        &Selection::collapsed(grid[0][0].clone(), 0),
        &Command::InsertText {
            text: "alpha".into(),
        },
        &mut id_gen,
        None,
    )
    .unwrap();
    let mut sel_a = Some(Selection::collapsed(grid[0][0].clone(), 0));
    apply_all(&mut doc, &mut sel_a, &ops_a);

    let ops_b = transform(
        &doc,
        &Selection::collapsed(grid[0][1].clone(), 0),
        &Command::InsertText {
            text: "beta".into(),
        },
        &mut id_gen,
        None,
    )
    .unwrap();
    let mut sel_b = Some(Selection::collapsed(grid[0][1].clone(), 0));
    apply_all(&mut doc, &mut sel_b, &ops_b);

    let sel = Selection::with_cell_selection(
        Position::new(grid[0][0].clone(), 0),
        Position::new(grid[0][1].clone(), 0),
        CellSelection::new(table_id.clone(), CellCoord::new(0, 0), CellCoord::new(0, 1)),
    );
    let ops = transform(
        &doc,
        &sel,
        &Command::ClearCells { cell_ids: vec![] },
        &mut id_gen,
        None,
    )
    .unwrap();
    let mut sel = Some(sel);
    apply_all(&mut doc, &mut sel, &ops);

    assert_eq!(doc.plain_text(&grid[0][0]), "");
    assert_eq!(doc.plain_text(&grid[0][1]), "");
    assert_eq!(doc.plain_text(&grid[1][0]), "");
}

// --- Table navigation boundary tests ---

#[test]
fn table_navigate_prev_cell() {
    let (mut doc, table_id) = doc_with_table(2, 2);
    let grid = table_grid(&doc, &table_id);
    // Start at (0,1) → PrevCell → (0,0)
    let sel = Selection::collapsed(grid[0][1].clone(), 0);
    let mut id_gen = SequentialIdGenerator::new("cmd");
    let ops = transform(
        &doc,
        &sel,
        &Command::TableNavigate {
            direction: TableNavDirection::PrevCell,
        },
        &mut id_gen,
        None,
    )
    .unwrap();
    let mut sel = Some(sel);
    apply_all(&mut doc, &mut sel, &ops);
    assert_eq!(sel.unwrap().anchor.id, grid[0][0]);
}

#[test]
fn table_navigate_prev_cell_wraps_to_previous_row() {
    let (mut doc, table_id) = doc_with_table(2, 3);
    let grid = table_grid(&doc, &table_id);
    // Start at (1,0) → PrevCell → (0,2) (last col of previous row)
    let sel = Selection::collapsed(grid[1][0].clone(), 0);
    let mut id_gen = SequentialIdGenerator::new("cmd");
    let ops = transform(
        &doc,
        &sel,
        &Command::TableNavigate {
            direction: TableNavDirection::PrevCell,
        },
        &mut id_gen,
        None,
    )
    .unwrap();
    let mut sel = Some(sel);
    apply_all(&mut doc, &mut sel, &ops);
    assert_eq!(sel.unwrap().anchor.id, grid[0][2]);
}

#[test]
fn table_navigate_down_stays_same_column() {
    let (mut doc, table_id) = doc_with_table(3, 3);
    let grid = table_grid(&doc, &table_id);
    // Start at (0,1) → Down → (1,1) — same column, next row
    let sel = Selection::collapsed(grid[0][1].clone(), 0);
    let mut id_gen = SequentialIdGenerator::new("cmd");
    let ops = transform(
        &doc,
        &sel,
        &Command::TableNavigate {
            direction: TableNavDirection::Down,
        },
        &mut id_gen,
        None,
    )
    .unwrap();
    let mut sel = Some(sel);
    apply_all(&mut doc, &mut sel, &ops);
    assert_eq!(sel.unwrap().anchor.id, grid[1][1]);
}

#[test]
fn table_navigate_up_stays_same_column() {
    let (mut doc, table_id) = doc_with_table(3, 3);
    let grid = table_grid(&doc, &table_id);
    // Start at (2,2) → Up → (1,2)
    let sel = Selection::collapsed(grid[2][2].clone(), 0);
    let mut id_gen = SequentialIdGenerator::new("cmd");
    let ops = transform(
        &doc,
        &sel,
        &Command::TableNavigate {
            direction: TableNavDirection::Up,
        },
        &mut id_gen,
        None,
    )
    .unwrap();
    let mut sel = Some(sel);
    apply_all(&mut doc, &mut sel, &ops);
    assert_eq!(sel.unwrap().anchor.id, grid[1][2]);
}

#[test]
fn table_navigate_down_at_last_row_is_noop() {
    let (mut doc, table_id) = doc_with_table(2, 2);
    let grid = table_grid(&doc, &table_id);
    // Start at (1,0) → Down → no-op (already last row)
    let sel = Selection::collapsed(grid[1][0].clone(), 0);
    let mut id_gen = SequentialIdGenerator::new("cmd");
    let ops = transform(
        &doc,
        &sel,
        &Command::TableNavigate {
            direction: TableNavDirection::Down,
        },
        &mut id_gen,
        None,
    )
    .unwrap();
    let mut sel = Some(sel);
    apply_all(&mut doc, &mut sel, &ops);
    assert_eq!(sel.unwrap().anchor.id, grid[1][0]);
}

#[test]
fn table_navigate_up_at_first_row_is_noop() {
    let (mut doc, table_id) = doc_with_table(2, 2);
    let grid = table_grid(&doc, &table_id);
    // Start at (0,1) → Up → no-op
    let sel = Selection::collapsed(grid[0][1].clone(), 0);
    let mut id_gen = SequentialIdGenerator::new("cmd");
    let ops = transform(
        &doc,
        &sel,
        &Command::TableNavigate {
            direction: TableNavDirection::Up,
        },
        &mut id_gen,
        None,
    )
    .unwrap();
    let mut sel = Some(sel);
    apply_all(&mut doc, &mut sel, &ops);
    assert_eq!(sel.unwrap().anchor.id, grid[0][1]);
}

#[test]
fn table_navigate_left_stays_same_row() {
    let (mut doc, table_id) = doc_with_table(2, 3);
    let grid = table_grid(&doc, &table_id);
    // Start at (1,2) → Left → (1,1)
    let sel = Selection::collapsed(grid[1][2].clone(), 0);
    let mut id_gen = SequentialIdGenerator::new("cmd");
    let ops = transform(
        &doc,
        &sel,
        &Command::TableNavigate {
            direction: TableNavDirection::Left,
        },
        &mut id_gen,
        None,
    )
    .unwrap();
    let mut sel = Some(sel);
    apply_all(&mut doc, &mut sel, &ops);
    assert_eq!(sel.unwrap().anchor.id, grid[1][1]);
}

#[test]
fn table_navigate_right_stays_same_row() {
    let (mut doc, table_id) = doc_with_table(2, 3);
    let grid = table_grid(&doc, &table_id);
    // Start at (0,0) → Right → (0,1)
    let sel = Selection::collapsed(grid[0][0].clone(), 0);
    let mut id_gen = SequentialIdGenerator::new("cmd");
    let ops = transform(
        &doc,
        &sel,
        &Command::TableNavigate {
            direction: TableNavDirection::Right,
        },
        &mut id_gen,
        None,
    )
    .unwrap();
    let mut sel = Some(sel);
    apply_all(&mut doc, &mut sel, &ops);
    assert_eq!(sel.unwrap().anchor.id, grid[0][1]);
}

#[test]
fn table_navigate_right_at_last_col_is_noop() {
    let (mut doc, table_id) = doc_with_table(2, 2);
    let grid = table_grid(&doc, &table_id);
    let sel = Selection::collapsed(grid[0][1].clone(), 0);
    let mut id_gen = SequentialIdGenerator::new("cmd");
    let ops = transform(
        &doc,
        &sel,
        &Command::TableNavigate {
            direction: TableNavDirection::Right,
        },
        &mut id_gen,
        None,
    )
    .unwrap();
    let mut sel = Some(sel);
    apply_all(&mut doc, &mut sel, &ops);
    assert_eq!(sel.unwrap().anchor.id, grid[0][1]);
}

#[test]
fn table_navigate_left_at_first_col_is_noop() {
    let (mut doc, table_id) = doc_with_table(2, 2);
    let grid = table_grid(&doc, &table_id);
    let sel = Selection::collapsed(grid[1][0].clone(), 0);
    let mut id_gen = SequentialIdGenerator::new("cmd");
    let ops = transform(
        &doc,
        &sel,
        &Command::TableNavigate {
            direction: TableNavDirection::Left,
        },
        &mut id_gen,
        None,
    )
    .unwrap();
    let mut sel = Some(sel);
    apply_all(&mut doc, &mut sel, &ops);
    assert_eq!(sel.unwrap().anchor.id, grid[1][0]);
}

// --- Cell editing boundary tests ---

#[test]
fn insert_text_in_table_cell() {
    let (mut doc, table_id) = doc_with_table(2, 2);
    let grid = table_grid(&doc, &table_id);
    let sel = Selection::collapsed(grid[0][0].clone(), 0);
    let mut id_gen = SequentialIdGenerator::new("cmd");
    let ops = transform(
        &doc,
        &sel,
        &Command::InsertText {
            text: "hello".into(),
        },
        &mut id_gen,
        None,
    )
    .unwrap();
    let mut sel = Some(sel);
    apply_all(&mut doc, &mut sel, &ops);
    assert_eq!(doc.plain_text(&grid[0][0]), "hello");
    assert_eq!(sel.unwrap().anchor.offset, 5);
}

#[test]
fn backspace_in_cell_deletes_char() {
    let (mut doc, table_id) = doc_with_table(2, 2);
    let grid = table_grid(&doc, &table_id);
    // Insert text first
    let sel = Selection::collapsed(grid[0][0].clone(), 0);
    let mut id_gen = SequentialIdGenerator::new("cmd");
    let ops = transform(
        &doc,
        &sel,
        &Command::InsertText { text: "abc".into() },
        &mut id_gen,
        None,
    )
    .unwrap();
    let mut sel = Some(sel);
    apply_all(&mut doc, &mut sel, &ops);
    assert_eq!(doc.plain_text(&grid[0][0]), "abc");

    // Backspace at offset 3
    let sel2 = Selection::collapsed(grid[0][0].clone(), 3);
    let ops2 = transform(&doc, &sel2, &Command::Backspace, &mut id_gen, None).unwrap();
    let mut sel2 = Some(sel2);
    apply_all(&mut doc, &mut sel2, &ops2);
    assert_eq!(doc.plain_text(&grid[0][0]), "ab");
}

#[test]
fn delete_range_in_cell() {
    let (mut doc, table_id) = doc_with_table(2, 2);
    let grid = table_grid(&doc, &table_id);
    // Insert text first
    let sel = Selection::collapsed(grid[0][0].clone(), 0);
    let mut id_gen = SequentialIdGenerator::new("cmd");
    let ops = transform(
        &doc,
        &sel,
        &Command::InsertText {
            text: "hello world".into(),
        },
        &mut id_gen,
        None,
    )
    .unwrap();
    let mut sel = Some(sel);
    apply_all(&mut doc, &mut sel, &ops);

    // Select all text (offset 0..11) and backspace
    let range_sel = Selection::new(
        Position::new(grid[0][0].clone(), 0),
        Position::new(grid[0][0].clone(), 11),
    );
    let ops2 = transform(&doc, &range_sel, &Command::Backspace, &mut id_gen, None).unwrap();
    let mut sel2 = Some(range_sel);
    apply_all(&mut doc, &mut sel2, &ops2);
    assert_eq!(doc.plain_text(&grid[0][0]), "");
}

#[test]
fn enter_in_cell_preserves_structure() {
    let (mut doc, table_id) = doc_with_table(2, 2);
    let grid = table_grid(&doc, &table_id);
    // Insert text, then press Enter mid-cell
    let sel = Selection::collapsed(grid[0][0].clone(), 0);
    let mut id_gen = SequentialIdGenerator::new("cmd");
    let ops = transform(
        &doc,
        &sel,
        &Command::InsertText { text: "ab".into() },
        &mut id_gen,
        None,
    )
    .unwrap();
    let mut sel = Some(sel);
    apply_all(&mut doc, &mut sel, &ops);

    // Enter at offset 1
    let sel2 = Selection::collapsed(grid[0][0].clone(), 1);
    let ops2 = transform(&doc, &sel2, &Command::Enter, &mut id_gen, None).unwrap();
    let mut sel2 = Some(sel2);
    apply_all(&mut doc, &mut sel2, &ops2);

    // Should insert \n, NOT split block. Table structure intact.
    assert_eq!(doc.plain_text(&grid[0][0]), "a\nb");
    // Table still has same number of rows
    let table = doc.get_block(&table_id).unwrap();
    assert_eq!(table.children.len(), 2);
}

// --- Undo/redo for table operations ---

#[test]
fn insert_table_undo_removes_table() {
    let mut doc = Document::new();
    doc.insert_block(Block::new_paragraph(BlockId::new("p1")), 0);
    assert_eq!(doc.root_block_count(), 1);

    let mut engine = EditorEngine::from_document(doc, Box::new(SequentialIdGenerator::new("e")));
    engine
        .execute(Command::InsertTable { rows: 2, cols: 2 })
        .unwrap();
    assert_eq!(engine.document().root_block_count(), 2);

    let _ = engine.undo();
    assert_eq!(engine.document().root_block_count(), 1);
    assert!(engine.document().get_block(&BlockId::new("p1")).is_some());

    let _ = engine.redo();
    assert_eq!(engine.document().root_block_count(), 2);
}

#[test]
fn insert_row_undo_removes_row() {
    let (doc, table_id) = doc_with_table(2, 2);
    let mut engine = EditorEngine::from_document(doc, Box::new(SequentialIdGenerator::new("e")));

    let grid_before = table_grid(engine.document(), &table_id);
    assert_eq!(grid_before.len(), 2);

    engine.set_selection(Some(Selection::collapsed(grid_before[0][0].clone(), 0)));
    engine
        .execute(Command::InsertTableRow {
            table_id: table_id.clone(),
            row_index: 0,
            above: false,
        })
        .unwrap();

    let grid_after = table_grid(engine.document(), &table_id);
    assert_eq!(grid_after.len(), 3);

    let _ = engine.undo();
    let grid_undone = table_grid(engine.document(), &table_id);
    assert_eq!(grid_undone.len(), 2);
}

#[test]
fn delete_col_undo_restores_col() {
    let (doc, table_id) = doc_with_table(2, 3);
    let mut engine = EditorEngine::from_document(doc, Box::new(SequentialIdGenerator::new("e")));

    let grid_before = table_grid(engine.document(), &table_id);
    assert_eq!(grid_before[0].len(), 3);

    engine.set_selection(Some(Selection::collapsed(grid_before[0][0].clone(), 0)));
    engine
        .execute(Command::DeleteTableCol {
            table_id: table_id.clone(),
            col_index: 1,
        })
        .unwrap();

    let grid_after = table_grid(engine.document(), &table_id);
    assert_eq!(grid_after[0].len(), 2);

    let _ = engine.undo();
    let grid_undone = table_grid(engine.document(), &table_id);
    assert_eq!(grid_undone[0].len(), 3);
}

#[test]
fn clear_cells_undo_restores_multiple_cells_in_one_step() {
    let (doc, table_id) = doc_with_table(2, 2);
    let mut engine = EditorEngine::from_document(doc, Box::new(SequentialIdGenerator::new("e")));

    let grid = table_grid(engine.document(), &table_id);

    engine.set_selection(Some(Selection::collapsed(grid[0][0].clone(), 0)));
    engine
        .execute(Command::InsertText {
            text: "alpha".into(),
        })
        .unwrap();
    engine.set_selection(Some(Selection::collapsed(grid[0][1].clone(), 0)));
    engine
        .execute(Command::InsertText {
            text: "beta".into(),
        })
        .unwrap();

    engine.set_selection(Some(Selection::with_cell_selection(
        Position::new(grid[0][0].clone(), 0),
        Position::new(grid[0][1].clone(), 0),
        CellSelection::new(table_id.clone(), CellCoord::new(0, 0), CellCoord::new(0, 1)),
    )));
    engine
        .execute(Command::ClearCells { cell_ids: vec![] })
        .unwrap();

    assert_eq!(engine.document().plain_text(&grid[0][0]), "");
    assert_eq!(engine.document().plain_text(&grid[0][1]), "");

    let undone = engine.undo();
    assert!(matches!(undone, Ok(true)));
    assert_eq!(engine.document().plain_text(&grid[0][0]), "alpha");
    assert_eq!(engine.document().plain_text(&grid[0][1]), "beta");
}

// ─── EnsureTrailingParagraph ─────────────────────────────────

#[test]
fn ensure_trailing_paragraph_at_doc_end_inserts_empty_paragraph() {
    // Cursor on the only block in the document → no block exists after
    // it, so the command must seed a fresh empty paragraph at root[1].
    // Caret is intentionally NOT moved (composition contract).
    let mut doc = Document::new();
    doc.insert_block(Block::new_paragraph(BlockId::new("p1")), 0);
    let sel = Selection::collapsed("p1", 0);
    let mut id_gen = SequentialIdGenerator::new("cmd");

    let ops = transform(
        &doc,
        &sel,
        &Command::EnsureTrailingParagraph,
        &mut id_gen,
        None,
    )
    .unwrap();
    let mut sel = Some(sel);
    apply_all(&mut doc, &mut sel, &ops);

    assert_eq!(doc.root_block_count(), 2);
    let trailing_id = &doc.root_block_ids()[1];
    let trailing = doc.get_block(trailing_id).unwrap();
    assert!(trailing.is_paragraph());
    assert!(trailing.content.is_empty());
    // Caret stays on the original block (no SetSelection op).
    let sel = sel.unwrap();
    assert_eq!(sel.anchor.id, BlockId::new("p1"));
    assert_eq!(sel.anchor.offset, 0);
}

#[test]
fn ensure_trailing_paragraph_no_op_when_next_block_is_empty_paragraph() {
    // Slot below the cursor's root is already an empty paragraph →
    // the command MUST emit zero operations so re-dispatching it
    // never bloats the doc or the undo history (idempotent guard).
    let mut doc = Document::new();
    doc.insert_block(Block::new_paragraph(BlockId::new("p1")), 0);
    doc.insert_block(Block::new_paragraph(BlockId::new("p2")), 1);
    let sel = Selection::collapsed("p1", 0);
    let mut id_gen = SequentialIdGenerator::new("cmd");

    let ops = transform(
        &doc,
        &sel,
        &Command::EnsureTrailingParagraph,
        &mut id_gen,
        None,
    )
    .unwrap();
    assert!(ops.is_empty(), "expected no-op, got: {ops:?}");
}

#[test]
fn ensure_trailing_paragraph_inserts_when_next_block_is_non_empty() {
    // Slot below the cursor's root exists but is NOT an empty
    // paragraph (it has text) → the command inserts a fresh empty
    // paragraph between them, shifting the existing content down.
    let mut doc = Document::new();
    doc.insert_block(Block::new_paragraph(BlockId::new("p1")), 0);
    let mut p2 = Block::new_paragraph(BlockId::new("p2"));
    p2.content = vec![TextSpan::plain("not empty")];
    doc.insert_block(p2, 1);
    let sel = Selection::collapsed("p1", 0);
    let mut id_gen = SequentialIdGenerator::new("cmd");

    let ops = transform(
        &doc,
        &sel,
        &Command::EnsureTrailingParagraph,
        &mut id_gen,
        None,
    )
    .unwrap();
    let mut sel = Some(sel);
    apply_all(&mut doc, &mut sel, &ops);

    assert_eq!(doc.root_block_count(), 3);
    // The new empty paragraph lands at root[1], shifting "not empty"
    // to root[2].
    let trailing = doc.get_block(&doc.root_block_ids()[1]).unwrap();
    assert!(trailing.is_paragraph());
    assert!(trailing.content.is_empty());
    assert_eq!(doc.root_block_ids()[2], BlockId::new("p2"));
}

#[test]
fn ensure_trailing_paragraph_in_table_cell_walks_up_to_table_root() {
    // Cursor inside a table cell → the command must walk up the
    // parent chain (cell → row → table) and insert the trailing
    // paragraph AFTER the table block, not as a sibling of the cell
    // (which would corrupt the table structure).
    let (mut doc, table_id) = doc_with_table(2, 2);
    let cell_id = table_grid(&doc, &table_id)[0][0].clone();
    let sel = Selection::collapsed(cell_id.clone(), 0);
    let mut id_gen = SequentialIdGenerator::new("cmd");

    let ops = transform(
        &doc,
        &sel,
        &Command::EnsureTrailingParagraph,
        &mut id_gen,
        None,
    )
    .unwrap();
    let mut sel = Some(sel);
    apply_all(&mut doc, &mut sel, &ops);

    // doc_with_table seeds [p1, table]. After EnsureTrailingParagraph
    // root becomes [p1, table, trailing_p].
    assert_eq!(doc.root_block_count(), 3);
    assert_eq!(doc.root_block_ids()[1], table_id);
    let trailing = doc.get_block(&doc.root_block_ids()[2]).unwrap();
    assert!(trailing.is_paragraph());
    assert!(trailing.content.is_empty());
    // Cursor is untouched — still on the cell where the user was.
    let sel = sel.unwrap();
    assert_eq!(sel.anchor.id, cell_id);
}

#[test]
fn insert_table_then_ensure_trailing_paragraph_composition() {
    // End-to-end composition test for the slash menu / toolbar grid
    // picker path: dispatching `[InsertTable, EnsureTrailingParagraph]`
    // must produce [p1, table, trailing_p] with the caret in the
    // FIRST CELL (not the trailing paragraph). This is the contract
    // that lets us drop the trailing-paragraph logic out of the
    // `transform_insert_table` function while still meeting the
    // "테이블 입력 후 테이블 아래에 무조건 새 블록" requirement.
    let mut doc = Document::new();
    doc.insert_block(Block::new_paragraph(BlockId::new("p1")), 0);

    let mut engine = EditorEngine::from_document(doc, Box::new(SequentialIdGenerator::new("e")));
    engine
        .execute(Command::InsertTable { rows: 2, cols: 2 })
        .unwrap();
    engine.execute(Command::EnsureTrailingParagraph).unwrap();

    let doc = engine.document();
    assert_eq!(doc.root_block_count(), 3);
    let table_id = &doc.root_block_ids()[1];
    assert!(doc.get_block(table_id).unwrap().is_table());
    let trailing = doc.get_block(&doc.root_block_ids()[2]).unwrap();
    assert!(trailing.is_paragraph());
    assert!(trailing.content.is_empty());

    // The decisive assertion: caret is in the first cell, NOT on the
    // trailing paragraph. If EnsureTrailingParagraph regressed to
    // moving the caret, this would fire.
    let sel = engine.selection().as_ref().unwrap();
    let first_row = &doc.get_block(table_id).unwrap().children[0];
    let first_cell = &doc.get_block(first_row).unwrap().children[0];
    assert_eq!(sel.anchor.id, *first_cell);
    assert_eq!(sel.anchor.offset, 0);
}
