use std::time::Instant;

use crate::cmd::effect::Effect;
use crate::model::app_state::AppState;
use crate::model::browse::query_execution::{DeleteRefreshTarget, PostDeleteRowSelection};
use crate::model::shared::input_mode::InputMode;
use crate::policy::json::json_diff::compute_json_diff;
use crate::policy::write::write_guardrails::{
    ColumnDiff, RiskLevel, WriteOperation, WritePreview, evaluate_guardrails,
};
use crate::policy::write::write_update::{
    build_pk_pairs, escape_preview_value, normalize_for_diff,
};
use crate::services::AppServices;
use crate::update::action::Action;
use crate::update::browse::query::preview_effect_for_current_table;
use crate::update::dispatch_result::DispatchResult;
use crate::update::helpers::{
    EditGuardrailError, build_bulk_delete_preview, editable_preview_base,
};

fn build_update_preview(
    state: &AppState,
    services: &AppServices,
) -> Result<WritePreview, EditGuardrailError> {
    if !state.result_interaction.cell_edit().is_active() {
        return Err(EditGuardrailError::NoActiveCellEditSession);
    }

    let (result, pk_cols) = editable_preview_base(state)?;

    let row_idx = state
        .result_interaction
        .cell_edit()
        .row
        .ok_or(EditGuardrailError::NoRowSelectedForEdit)?;
    let col_idx = state
        .result_interaction
        .cell_edit()
        .col
        .ok_or(EditGuardrailError::NoColumnSelectedForEdit)?;

    let row = result
        .rows
        .get(row_idx)
        .ok_or(EditGuardrailError::RowIndexOutOfBounds)?;
    let column_name = result
        .columns
        .get(col_idx)
        .ok_or(EditGuardrailError::ColumnIndexOutOfBounds)?
        .clone();

    if pk_cols.iter().any(|pk| pk == &column_name) {
        return Err(EditGuardrailError::PrimaryKeyColumnsReadOnly);
    }

    let pk_pairs = build_pk_pairs(&result.columns, row, pk_cols);
    let target = crate::policy::write::write_guardrails::TargetSummary {
        schema: state.query.pagination.schema.clone(),
        table: state.query.pagination.table.clone(),
        key_values: pk_pairs.clone().unwrap_or_default(),
    };
    let has_where = pk_pairs.as_ref().is_some_and(|pairs| !pairs.is_empty());
    let has_stable_row_identity = pk_pairs.is_some();
    let guardrail = evaluate_guardrails(has_where, has_stable_row_identity, Some(target.clone()));
    if guardrail.blocked {
        let reason = guardrail
            .reason
            .unwrap_or_else(|| "Write blocked by guardrails".to_string());
        return Err(EditGuardrailError::GuardrailBlocked(reason));
    }

    let sql = services.sql_dialect.build_update_sql(
        &target.schema,
        &target.table,
        &column_name,
        state.result_interaction.cell_edit().draft_value(),
        &target.key_values,
    );
    let preview = WritePreview {
        operation: WriteOperation::Update,
        sql,
        target_summary: target,
        diff: {
            let before = normalize_for_diff(&state.result_interaction.cell_edit().original_value);
            let after = normalize_for_diff(state.result_interaction.cell_edit().draft_value());
            let is_jsonb = state
                .session
                .table_detail()
                .and_then(|td| td.columns.get(col_idx))
                .is_some_and(|c| c.data_type == "jsonb");
            let json_diff = is_jsonb
                .then(|| compute_json_diff(&before, &after, 1))
                .flatten();
            vec![ColumnDiff {
                column: column_name,
                before,
                after,
                json_diff,
            }]
        },
        guardrail,
    };
    Ok(preview)
}

fn build_write_preview_fallback_message(preview: &WritePreview) -> String {
    let mut lines = Vec::new();
    if preview.guardrail.risk_level != RiskLevel::Low {
        lines.push(format!("Risk: {}", preview.guardrail.risk_level.as_str()));
    }
    match preview.operation {
        WriteOperation::Update => {
            lines.push(preview.diff.first().map_or_else(
                || "(no changes)".to_string(),
                |d| {
                    format!(
                        "{}: \"{}\" -> \"{}\"",
                        d.column,
                        escape_preview_value(&d.before),
                        escape_preview_value(&d.after)
                    )
                },
            ));
        }
        WriteOperation::Delete => {
            lines.push(format!(
                "Target: {}",
                preview.target_summary.format_compact()
            ));
        }
    }
    lines.join("\n")
}

pub fn reduce_write(
    state: &mut AppState,
    action: &Action,
    now: Instant,
    services: &AppServices,
) -> DispatchResult {
    match action {
        Action::SubmitCellEditWrite => {
            if !state.result_interaction.staged_delete_rows().is_empty() {
                match build_bulk_delete_preview(state, services) {
                    Ok(result) => {
                        let staged_count = state.result_interaction.staged_delete_rows().len();
                        state.query.set_delete_refresh_target(
                            result.target_page,
                            result.target_row,
                            staged_count,
                        );
                        return DispatchResult::handled_with(vec![Effect::DispatchActions(vec![
                            Action::OpenWritePreviewConfirm(Box::new(result.preview)),
                        ])]);
                    }
                    Err(err) => {
                        state.messages.set_error_at(err.to_string(), now);
                        return DispatchResult::handled();
                    }
                }
            }

            if state.query.is_running() {
                state.messages.set_error_at(
                    EditGuardrailError::WriteUnavailableWhileQueryRunning.to_string(),
                    now,
                );
                return DispatchResult::handled();
            }

            match build_update_preview(state, services) {
                Ok(preview) => DispatchResult::handled_with(vec![Effect::DispatchActions(vec![
                    Action::OpenWritePreviewConfirm(Box::new(preview)),
                ])]),
                Err(err) => {
                    state.messages.set_error_at(err.to_string(), now);
                    DispatchResult::handled()
                }
            }
        }

        Action::OpenWritePreviewConfirm(preview) => {
            if state.session.read_only {
                state.messages.set_error_at(
                    "Read-only mode: write operations are disabled".to_string(),
                    now,
                );
                return DispatchResult::handled();
            }
            state
                .result_interaction
                .set_write_preview((**preview).clone());
            let operation = preview.operation;
            let title = match operation {
                WriteOperation::Update => {
                    state.query.clear_delete_refresh_target();
                    format!("Confirm UPDATE: {}", preview.target_summary.table)
                }
                WriteOperation::Delete => {
                    let n = state
                        .query
                        .pending_delete_refresh_target()
                        .map_or(1, |target| target.expected_delete_count);
                    format!(
                        "Confirm DELETE: {} {} from {}",
                        n,
                        if n == 1 { "row" } else { "rows" },
                        preview.target_summary.table
                    )
                }
            };

            state.confirm_dialog.open(
                title,
                build_write_preview_fallback_message(preview),
                crate::model::shared::confirm_dialog::ConfirmIntent::ExecuteWrite {
                    sql: preview.sql.clone(),
                    blocked: preview.guardrail.blocked,
                },
            );
            if matches!(operation, WriteOperation::Delete) {
                state.modal.set_mode(InputMode::Normal);
            }
            state.modal.push_mode(InputMode::ConfirmDialog);

            DispatchResult::handled()
        }

        Action::ExecuteWrite(query) => {
            if state.session.read_only {
                state.messages.set_error_at(
                    "Read-only mode: write operations are disabled".to_string(),
                    now,
                );
                return DispatchResult::handled();
            }
            if let Some(dsn) = &state.session.dsn {
                let run_id = state.query.begin_running(now);
                DispatchResult::handled_with(vec![Effect::ExecuteWrite {
                    dsn: dsn.clone(),
                    run_id,
                    query: query.clone(),
                    read_only: state.session.read_only,
                }])
            } else {
                state
                    .messages
                    .set_error_at("No active connection".to_string(), now);
                DispatchResult::handled()
            }
        }

        Action::ExecuteWriteSucceeded {
            dsn,
            run_id,
            affected_rows,
        } => {
            if state.is_stale_query_run(dsn, *run_id) {
                return DispatchResult::handled();
            }

            state.query.mark_idle();
            let operation = state
                .result_interaction
                .pending_write_preview()
                .map_or(WriteOperation::Update, |p| p.operation);
            state.result_interaction.clear_write_preview();
            match operation {
                WriteOperation::Update => {
                    if *affected_rows != 1 {
                        state.messages.set_error_at(
                            format!("UPDATE expected 1 row, but affected {affected_rows} rows"),
                            now,
                        );
                        state.modal.set_mode(InputMode::CellEdit);
                        return DispatchResult::handled();
                    }

                    state
                        .messages
                        .set_success_at("Updated 1 row".to_string(), now);
                    state.result_interaction.clear_cell_edit();
                    state.modal.set_mode(InputMode::Normal);

                    let page = state.query.pagination.current_page;
                    let generation = state.session.selection_generation();
                    match preview_effect_for_current_table(state, now, page, generation) {
                        Some(effect) => DispatchResult::handled_with(vec![effect]),
                        None => DispatchResult::handled(),
                    }
                }
                WriteOperation::Delete => {
                    let DeleteRefreshTarget {
                        target_page,
                        target_row,
                        expected_delete_count: expected,
                    } = state
                        .query
                        .take_delete_refresh_target()
                        .unwrap_or(DeleteRefreshTarget {
                            target_page: state.query.pagination.current_page,
                            target_row: None,
                            expected_delete_count: 1,
                        });

                    let row_word = |n: usize| if n == 1 { "row" } else { "rows" };
                    if *affected_rows == expected {
                        state.messages.set_success_at(
                            format!("Deleted {} {}", expected, row_word(expected)),
                            now,
                        );
                    } else {
                        state.messages.set_error_at(
                            format!(
                                "DELETE expected {} {}, but affected {} {}",
                                expected,
                                row_word(expected),
                                affected_rows,
                                row_word(*affected_rows),
                            ),
                            now,
                        );
                    }
                    state.result_interaction.clear_cell_edit();
                    state.result_interaction.clear_staged_deletes();
                    state.modal.set_mode(InputMode::Normal);

                    state.query.set_post_delete_selection(target_row.map_or(
                        PostDeleteRowSelection::Clear,
                        PostDeleteRowSelection::Select,
                    ));

                    let generation = state.session.selection_generation();
                    match preview_effect_for_current_table(state, now, target_page, generation) {
                        Some(effect) => {
                            state.query.pagination.reached_end = false;
                            DispatchResult::handled_with(vec![effect])
                        }
                        None => DispatchResult::handled(),
                    }
                }
            }
        }

        Action::ExecuteWriteFailed { dsn, run_id, error } => {
            if state.is_stale_query_run(dsn, *run_id) {
                return DispatchResult::handled();
            }

            state.query.mark_idle();
            let operation = state
                .result_interaction
                .pending_write_preview()
                .map_or(WriteOperation::Update, |p| p.operation);
            state.result_interaction.clear_write_preview();
            state.query.clear_delete_refresh_target();
            state.messages.set_error_at(error.user_message(), now);
            state.modal.set_mode(match operation {
                WriteOperation::Update => InputMode::CellEdit,
                WriteOperation::Delete => InputMode::Normal,
            });
            DispatchResult::handled()
        }

        _ => DispatchResult::pass(),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    use crate::model::browse::query_execution::{
        DeleteRefreshTarget, PREVIEW_PAGE_SIZE, PostDeleteRowSelection, QueryStatus,
    };
    use crate::policy::write::write_guardrails::{
        GuardrailDecision, RiskLevel, TargetSummary, WriteOperation, WritePreview,
    };
    use crate::ports::outbound::DbOperationError;
    use crate::update::browse::query::dispatch_query;
    use crate::update::browse::query::tests::*;

    fn write_succeeded_action(state: &mut AppState, affected_rows: usize) -> Action {
        let run_id = begin_query_run(state);
        Action::ExecuteWriteSucceeded {
            dsn: "postgres://localhost/test".to_string(),
            run_id,
            affected_rows,
        }
    }

    fn write_failed_action(state: &mut AppState, error: DbOperationError) -> Action {
        let run_id = begin_query_run(state);
        Action::ExecuteWriteFailed {
            dsn: "postgres://localhost/test".to_string(),
            run_id,
            error,
        }
    }

    mod write_flow {
        use super::*;

        fn editable_state() -> AppState {
            let mut state = AppState::new("test_project".to_string());
            state.session.dsn = Some("postgres://localhost/test".to_string());
            state.query.set_current_result(editable_preview_result());
            state
                .session
                .set_table_detail_raw(Some(users_table_detail()));
            state.query.pagination.schema = "public".to_string();
            state.query.pagination.table = "users".to_string();
            state.modal.set_mode(InputMode::CellEdit);
            state
                .result_interaction
                .begin_cell_edit(0, 1, "Alice".to_string());
            state
                .result_interaction
                .cell_edit_input_mut()
                .set_content("Bob".to_string());
            state
        }

        #[test]
        fn write_requires_cell_edit_mode() {
            let mut state = create_test_state();
            state.modal.set_mode(InputMode::Normal);

            let effects = dispatch_query(
                &mut state,
                &Action::SubmitCellEditWrite,
                Instant::now(),
                &AppServices::stub(),
            );
            assert!(
                effects
                    .into_effects()
                    .expect("reducer should handle action")
                    .is_empty()
            );
            assert_eq!(
                state.messages.last_error.as_deref(),
                Some("No active cell edit session")
            );
        }

        #[test]
        fn write_requires_idle_query_status() {
            let mut state = editable_state();
            let _ = state.query.begin_running(Instant::now());

            let effects = dispatch_query(
                &mut state,
                &Action::SubmitCellEditWrite,
                Instant::now(),
                &AppServices::stub(),
            );
            assert!(
                effects
                    .into_effects()
                    .expect("reducer should handle action")
                    .is_empty()
            );
            assert_eq!(
                state.messages.last_error.as_deref(),
                Some("Write is unavailable while query is running")
            );
        }

        #[test]
        fn write_rejects_stale_table_detail() {
            let mut state = editable_state();
            if let Some(mut detail) = state.session.table_detail().cloned() {
                detail.name = "posts".to_string();
                state.session.set_table_detail_raw(Some(detail));
            }

            let effects = dispatch_query(
                &mut state,
                &Action::SubmitCellEditWrite,
                Instant::now(),
                &AppServices::stub(),
            );
            assert!(
                effects
                    .into_effects()
                    .expect("reducer should handle action")
                    .is_empty()
            );
            assert_eq!(
                state.messages.last_error.as_deref(),
                Some("Table metadata does not match current preview target")
            );
        }

        #[test]
        fn submit_write_opens_confirm_dialog() {
            let mut state = editable_state();

            let effects = dispatch_query(
                &mut state,
                &Action::SubmitCellEditWrite,
                Instant::now(),
                &AppServices::stub(),
            )
            .unwrap();
            assert_eq!(effects.len(), 1);

            let dispatched = match &effects[0] {
                Effect::DispatchActions(actions) => actions.first().expect("action"),
                other => panic!("expected DispatchActions, got {other:?}"),
            };
            match dispatched {
                Action::OpenWritePreviewConfirm(preview) => {
                    assert!(preview.sql.contains("UPDATE"));
                }
                other => panic!("expected OpenWritePreviewConfirm, got {other:?}"),
            }
        }

        fn editable_state_with_jsonb() -> AppState {
            let mut state = AppState::new("test_project".to_string());
            state.session.dsn = Some("postgres://localhost/test".to_string());
            state
                .query
                .set_current_result(editable_preview_result_with_jsonb());
            state
                .session
                .set_table_detail_raw(Some(jsonb_table_detail()));
            state.query.pagination.schema = "public".to_string();
            state.query.pagination.table = "users".to_string();
            state.modal.set_mode(InputMode::CellEdit);
            // col 2 = metadata (jsonb)
            state
                .result_interaction
                .begin_cell_edit(0, 2, r#"{"role":"admin"}"#.to_string());
            state
                .result_interaction
                .cell_edit_input_mut()
                .set_content(r#"{"role":"user"}"#.to_string());
            state
        }

        #[test]
        fn jsonb_column_produces_structured_diff() {
            let mut state = editable_state_with_jsonb();

            let effects = dispatch_query(
                &mut state,
                &Action::SubmitCellEditWrite,
                Instant::now(),
                &AppServices::stub(),
            )
            .unwrap();

            let preview = match &effects[0] {
                Effect::DispatchActions(actions) => match actions.first().expect("action") {
                    Action::OpenWritePreviewConfirm(preview) => preview.clone(),
                    other => panic!("expected OpenWritePreviewConfirm, got {other:?}"),
                },
                other => panic!("expected DispatchActions, got {other:?}"),
            };
            assert!(
                preview.diff[0].json_diff.is_some(),
                "jsonb column should have structured diff"
            );
        }

        #[test]
        fn text_column_with_json_content_returns_none_diff() {
            // col 1 = name (text), but we edit it with JSON content
            let mut state = editable_state();
            state
                .result_interaction
                .begin_cell_edit(0, 1, r#"{"key":"old"}"#.to_string());
            state
                .result_interaction
                .cell_edit_input_mut()
                .set_content(r#"{"key":"new"}"#.to_string());

            let effects = dispatch_query(
                &mut state,
                &Action::SubmitCellEditWrite,
                Instant::now(),
                &AppServices::stub(),
            )
            .unwrap();

            let preview = match &effects[0] {
                Effect::DispatchActions(actions) => match actions.first().expect("action") {
                    Action::OpenWritePreviewConfirm(preview) => preview.clone(),
                    other => panic!("expected OpenWritePreviewConfirm, got {other:?}"),
                },
                other => panic!("expected DispatchActions, got {other:?}"),
            };
            assert!(
                preview.diff[0].json_diff.is_none(),
                "text column should not have structured diff even if value looks like JSON"
            );
        }

        #[test]
        fn confirm_dialog_displays_and_executes_same_sql() {
            let mut state = editable_state();

            let effects = dispatch_query(
                &mut state,
                &Action::SubmitCellEditWrite,
                Instant::now(),
                &AppServices::stub(),
            )
            .unwrap();
            let preview = match &effects[0] {
                Effect::DispatchActions(actions) => match actions.first().expect("action") {
                    Action::OpenWritePreviewConfirm(preview) => preview.clone(),
                    other => panic!("expected OpenWritePreviewConfirm, got {other:?}"),
                },
                other => panic!("expected DispatchActions, got {other:?}"),
            };
            let expected_sql = preview.sql.clone();

            dispatch_query(
                &mut state,
                &Action::OpenWritePreviewConfirm(preview),
                Instant::now(),
                &AppServices::stub(),
            );

            assert_eq!(
                state
                    .result_interaction
                    .pending_write_preview()
                    .map(|p| p.sql.as_str()),
                Some(expected_sql.as_str())
            );
            match state.confirm_dialog.intent() {
                Some(crate::model::shared::confirm_dialog::ConfirmIntent::ExecuteWrite {
                    sql,
                    blocked,
                }) => {
                    assert_eq!(sql, &expected_sql);
                    assert!(!blocked);
                }
                other => panic!("expected ExecuteWrite intent, got {other:?}"),
            }
        }

        #[test]
        fn execute_write_success_refreshes_preview_page() {
            let mut state = editable_state();
            state.query.pagination.current_page = 2;
            let action = write_succeeded_action(&mut state, 1);

            let effects =
                dispatch_query(&mut state, &action, Instant::now(), &AppServices::stub()).unwrap();

            assert_eq!(state.input_mode(), InputMode::Normal);
            assert_eq!(state.query.status(), QueryStatus::Running);
            assert!(state.query.start_time().is_some());
            assert_eq!(effects.len(), 1);
            match &effects[0] {
                Effect::ExecutePreview {
                    offset,
                    target_page,
                    ..
                } => {
                    assert_eq!(*offset, 2 * PREVIEW_PAGE_SIZE);
                    assert_eq!(*target_page, 2);
                }
                other => panic!("expected ExecutePreview, got {other:?}"),
            }
        }

        #[test]
        fn execute_write_with_non_one_row_sets_error() {
            let mut state = editable_state();
            let action = write_succeeded_action(&mut state, 0);

            let effects =
                dispatch_query(&mut state, &action, Instant::now(), &AppServices::stub()).unwrap();

            assert!(effects.is_empty());
            assert_eq!(state.input_mode(), InputMode::CellEdit);
            assert_eq!(
                state.messages.last_error.as_deref(),
                Some("UPDATE expected 1 row, but affected 0 rows")
            );
        }

        #[test]
        fn stale_write_success_does_not_refresh_or_set_message() {
            let mut state = editable_state();
            let old_run_id = begin_query_run(&mut state);
            let _ = begin_query_run(&mut state);

            let effects = dispatch_query(
                &mut state,
                &Action::ExecuteWriteSucceeded {
                    dsn: "postgres://localhost/test".to_string(),
                    run_id: old_run_id,
                    affected_rows: 1,
                },
                Instant::now(),
                &AppServices::stub(),
            )
            .unwrap();

            assert!(effects.is_empty());
            assert!(state.messages.last_success.is_none());
            assert!(state.query.is_running());
        }
    }

    mod delete_write_flow {
        use super::*;
        use crate::ports::outbound::DbOperationError;

        fn delete_preview() -> WritePreview {
            WritePreview {
                operation: WriteOperation::Delete,
                sql: "DELETE FROM \"public\".\"users\"\nWHERE \"id\" = '2';".to_string(),
                target_summary: TargetSummary {
                    schema: "public".to_string(),
                    table: "users".to_string(),
                    key_values: vec![("id".to_string(), "2".to_string())],
                },
                diff: vec![],
                guardrail: GuardrailDecision {
                    risk_level: RiskLevel::Low,
                    blocked: false,
                    reason: None,
                    target_summary: None,
                },
            }
        }

        #[test]
        fn open_write_preview_confirm_for_delete_sets_normal_return_mode() {
            let mut state = create_test_state();
            state.modal.set_mode(InputMode::Normal);
            let preview = delete_preview();

            let effects = dispatch_query(
                &mut state,
                &Action::OpenWritePreviewConfirm(Box::new(preview)),
                Instant::now(),
                &AppServices::stub(),
            )
            .unwrap();

            assert!(effects.is_empty());
            assert_eq!(state.input_mode(), InputMode::ConfirmDialog);
            assert_eq!(state.modal.return_destination(), InputMode::Normal);
            assert_eq!(
                state.confirm_dialog.title(),
                "Confirm DELETE: 1 row from users"
            );
        }

        #[test]
        fn open_write_preview_confirm_for_delete_sets_refresh_target_count() {
            let mut state = create_test_state();
            state.modal.set_mode(InputMode::Normal);
            state.query.set_delete_refresh_target(0, Some(2), 3);
            let preview = delete_preview();

            let effects = dispatch_query(
                &mut state,
                &Action::OpenWritePreviewConfirm(Box::new(preview)),
                Instant::now(),
                &AppServices::stub(),
            )
            .unwrap();

            assert!(effects.is_empty());
            assert_eq!(
                state.query.pending_delete_refresh_target(),
                Some(DeleteRefreshTarget {
                    target_page: 0,
                    target_row: Some(2),
                    expected_delete_count: 3,
                })
            );
            assert_eq!(
                state.confirm_dialog.title(),
                "Confirm DELETE: 3 rows from users"
            );
        }

        #[test]
        fn execute_write_success_for_delete_refreshes_target_page() {
            let mut state = create_test_state();
            state.query.pagination.schema = "public".to_string();
            state.query.pagination.table = "users".to_string();
            state.query.set_delete_refresh_target(1, Some(499), 1);
            state.result_interaction.set_write_preview(delete_preview());
            let action = write_succeeded_action(&mut state, 1);

            let effects =
                dispatch_query(&mut state, &action, Instant::now(), &AppServices::stub()).unwrap();

            assert_eq!(state.input_mode(), InputMode::Normal);
            assert_eq!(
                state.query.post_delete_row_selection(),
                PostDeleteRowSelection::Select(499)
            );
            assert_eq!(
                state.messages.last_success.as_deref(),
                Some("Deleted 1 row")
            );
            assert_eq!(effects.len(), 1);
            match &effects[0] {
                Effect::ExecutePreview {
                    offset,
                    target_page,
                    ..
                } => {
                    assert_eq!(*offset, PREVIEW_PAGE_SIZE);
                    assert_eq!(*target_page, 1);
                }
                other => panic!("expected ExecutePreview, got {other:?}"),
            }
        }

        #[test]
        fn execute_write_non_one_rows_for_delete_sets_error() {
            let mut state = create_test_state();
            state.query.pagination.schema = "public".to_string();
            state.query.pagination.table = "users".to_string();
            state.result_interaction.set_write_preview(delete_preview());
            let action = write_succeeded_action(&mut state, 0);

            let effects =
                dispatch_query(&mut state, &action, Instant::now(), &AppServices::stub()).unwrap();

            assert_eq!(state.input_mode(), InputMode::Normal);
            assert_eq!(
                state.messages.last_error.as_deref(),
                Some("DELETE expected 1 row, but affected 0 rows")
            );
            assert_eq!(effects.len(), 1);
        }

        #[test]
        fn execute_write_failed_for_delete_returns_to_normal_mode() {
            let mut state = create_test_state();
            state.result_interaction.set_write_preview(delete_preview());
            let action = write_failed_action(
                &mut state,
                DbOperationError::QueryFailed("boom".to_string()),
            );

            let effects =
                dispatch_query(&mut state, &action, Instant::now(), &AppServices::stub()).unwrap();

            assert!(effects.is_empty());
            assert_eq!(state.input_mode(), InputMode::Normal);
            assert_eq!(
                state.messages.last_error.as_deref(),
                Some("Query failed: boom. Review the database error details and SQL.")
            );
        }

        #[test]
        fn query_completed_restores_pending_row_selection() {
            let mut state = create_test_state();
            state.session.set_selection_generation(1);
            state
                .query
                .set_post_delete_selection(PostDeleteRowSelection::Select(1000));
            let action = query_completed_action(&mut state, preview_result(3), 1, Some(0));

            dispatch_query(&mut state, &action, Instant::now(), &AppServices::stub());

            assert_eq!(state.result_interaction.selection().row(), Some(2));
            assert_eq!(
                state.query.post_delete_row_selection(),
                PostDeleteRowSelection::Keep
            );
        }

        #[test]
        fn query_completed_clears_selection_when_requested() {
            let mut state = create_test_state();
            state.session.set_selection_generation(1);
            state.result_interaction.activate_cell(0, 0);
            state
                .query
                .set_post_delete_selection(PostDeleteRowSelection::Clear);
            let action = query_completed_action(&mut state, preview_result(2), 1, Some(0));

            dispatch_query(&mut state, &action, Instant::now(), &AppServices::stub());

            assert_eq!(state.result_interaction.selection().row(), None);
            assert_eq!(
                state.query.post_delete_row_selection(),
                PostDeleteRowSelection::Keep
            );
        }
    }
}
