use crate::ui::app::AppMode;
use crate::ui::effect::{self, Effect};

use super::core::{Cmd, CmdContext};
use super::grid::DrillIntoCell;

#[cfg(test)]
mod tests {
    use super::*;
    use crate::command::cmd::test_helpers::*;
    use crate::workbook::Workbook;

    #[test]
    fn enter_tile_select_with_categories() {
        let m = two_cat_model();
        let layout = make_layout(&m);
        let reg = make_registry();
        let ctx = make_ctx(&m, &layout, &reg);
        let cmd = EnterTileSelect;
        let effects = cmd.execute(&ctx);
        assert_eq!(effects.len(), 2);
        let dbg = format!("{:?}", effects[1]);
        assert!(
            dbg.contains("TileSelect"),
            "Expected TileSelect mode, got: {dbg}"
        );
    }

    #[test]
    fn enter_tile_select_no_categories() {
        let m = Workbook::new("Empty");
        let layout = make_layout(&m);
        let reg = make_registry();
        let ctx = make_ctx(&m, &layout, &reg);
        let cmd = EnterTileSelect;
        let effects = cmd.execute(&ctx);
        assert_eq!(effects.len(), 2);
    }

    #[test]
    fn enter_export_prompt_sets_mode() {
        let m = two_cat_model();
        let layout = make_layout(&m);
        let reg = make_registry();
        let ctx = make_ctx(&m, &layout, &reg);
        let effects = EnterExportPrompt.execute(&ctx);
        let dbg = effects_debug(&effects);
        assert!(
            dbg.contains("ExportPrompt"),
            "Expected ExportPrompt mode, got: {dbg}"
        );
    }

    #[test]
    fn force_quit_always_produces_quit_mode() {
        let m = two_cat_model();
        let layout = make_layout(&m);
        let reg = make_registry();
        let mut ctx = make_ctx(&m, &layout, &reg);
        ctx.dirty = true;
        let effects = ForceQuit.execute(&ctx);
        assert_eq!(effects.len(), 1);
        let dbg = effects_debug(&effects);
        assert!(dbg.contains("Quit"), "Expected Quit mode, got: {dbg}");
    }

    #[test]
    fn save_and_quit_produces_save_then_quit() {
        let m = two_cat_model();
        let layout = make_layout(&m);
        let reg = make_registry();
        let ctx = make_ctx(&m, &layout, &reg);
        let effects = SaveAndQuit.execute(&ctx);
        assert_eq!(effects.len(), 2);
        let dbg = effects_debug(&effects);
        assert!(dbg.contains("Save"), "Expected Save, got: {dbg}");
        assert!(dbg.contains("Quit"), "Expected Quit, got: {dbg}");
    }

    #[test]
    fn edit_or_drill_without_aggregation_enters_edit() {
        let m = two_cat_model();
        let layout = make_layout(&m);
        let reg = make_registry();
        let ctx = make_ctx(&m, &layout, &reg);
        let effects = EditOrDrill {
            edit_mode: AppMode::editing(),
        }
        .execute(&ctx);
        assert_eq!(effects.len(), 2);
        let dbg = effects_debug(&effects);
        assert!(dbg.contains("Editing"), "Expected Editing mode, got: {dbg}");
    }

    /// EditOrDrill must trust its `edit_mode` parameter rather than checking
    /// `ctx.mode` — the records-normal keymap supplies `records-editing`,
    /// but the command itself never inspects the runtime mode. This is the
    /// parallel of the (deleted) `enter_edit_mode_produces_editing_mode`
    /// test for the records branch.
    #[test]
    fn edit_or_drill_passes_records_editing_mode_through() {
        let m = two_cat_model();
        let layout = make_layout(&m);
        let reg = make_registry();
        // Note: ctx.mode is still Normal here — the command must not look at it.
        let ctx = make_ctx(&m, &layout, &reg);
        let effects = EditOrDrill {
            edit_mode: AppMode::records_editing(),
        }
        .execute(&ctx);
        assert_eq!(effects.len(), 2);
        let dbg = effects_debug(&effects);
        assert!(
            dbg.contains("RecordsEditing"),
            "Expected RecordsEditing mode, got: {dbg}"
        );
    }

    /// `EnterEditAtCursorCmd` must hand its `target_mode` straight through
    /// to the `EnterEditAtCursor` effect — the keymap (records `o` sequence
    /// or commit-and-advance) decides; the command never inspects ctx.
    #[test]
    fn enter_edit_at_cursor_cmd_passes_target_mode_to_effect() {
        let m = two_cat_model();
        let layout = make_layout(&m);
        let reg = make_registry();
        let ctx = make_ctx(&m, &layout, &reg);
        let effects = EnterEditAtCursorCmd {
            target_mode: AppMode::records_editing(),
        }
        .execute(&ctx);
        assert_eq!(effects.len(), 1);
        let dbg = format!("{:?}", effects[0]);
        assert!(
            dbg.contains("RecordsEditing"),
            "Expected RecordsEditing target_mode, got: {dbg}"
        );
    }

    /// The edit branch pre-fills the `edit` buffer with the cell's current
    /// display value so the user can modify rather than retype.
    #[test]
    fn edit_or_drill_pre_fills_edit_buffer_with_display_value() {
        let m = two_cat_model();
        let layout = make_layout(&m);
        let reg = make_registry();
        let mut ctx = make_ctx(&m, &layout, &reg);
        ctx.display_value = "42".to_string();
        let effects = EditOrDrill {
            edit_mode: AppMode::editing(),
        }
        .execute(&ctx);
        let dbg = effects_debug(&effects);
        assert!(
            dbg.contains("SetBuffer") && dbg.contains("\"edit\"") && dbg.contains("\"42\""),
            "Expected SetBuffer(\"edit\", \"42\"), got: {dbg}"
        );
    }

    #[test]
    fn enter_search_mode_sets_flag_and_clears_query() {
        let m = two_cat_model();
        let layout = make_layout(&m);
        let reg = make_registry();
        let ctx = make_ctx(&m, &layout, &reg);
        let effects = EnterSearchMode.execute(&ctx);
        assert_eq!(effects.len(), 2);
        let dbg = effects_debug(&effects);
        assert!(
            dbg.contains("SetSearchMode(true)"),
            "Expected search mode on, got: {dbg}"
        );
        assert!(
            dbg.contains("SetSearchQuery"),
            "Expected query reset, got: {dbg}"
        );
    }
}

// ── Mode change commands ─────────────────────────────────────────────────────

#[derive(Debug)]
pub struct EnterMode(pub AppMode);
impl Cmd for EnterMode {
    fn name(&self) -> &'static str {
        "enter-mode"
    }
    fn execute(&self, _ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
        let mut effects: Vec<Box<dyn Effect>> = Vec::new();
        // Clear the corresponding buffer when entering a text-entry mode
        if let Some(mb) = self.0.minibuffer() {
            effects.push(Box::new(effect::SetBuffer {
                name: mb.buffer_key.to_string(),
                value: String::new(),
            }));
        }
        effects.push(effect::change_mode(self.0.clone()));
        effects
    }
}

#[derive(Debug)]
pub struct ForceQuit;
impl Cmd for ForceQuit {
    fn name(&self) -> &'static str {
        "force-quit"
    }
    fn execute(&self, _ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
        vec![effect::change_mode(AppMode::Quit)]
    }
}

/// Quit with dirty check — refuses if unsaved changes exist.
#[derive(Debug)]
pub struct Quit;
impl Cmd for Quit {
    fn name(&self) -> &'static str {
        "q"
    }
    fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
        if ctx.dirty {
            vec![effect::set_status(
                "Unsaved changes. Use :q! to force quit or :wq to save+quit.",
            )]
        } else {
            vec![effect::change_mode(AppMode::Quit)]
        }
    }
}

/// Save then quit.
#[derive(Debug)]
pub struct SaveAndQuit;
impl Cmd for SaveAndQuit {
    fn name(&self) -> &'static str {
        "wq"
    }
    fn execute(&self, _ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
        vec![Box::new(effect::Save), effect::change_mode(AppMode::Quit)]
    }
}

// ── Editing entry ───────────────────────────────────────────────────────

/// Smart dispatch for i/a: if the cursor is on an aggregated pivot cell
/// (categories on `Axis::None` and the cell is not a synthetic records-mode
/// row), drill into it instead of editing. Otherwise pre-fill the edit
/// buffer with the displayed cell value and enter `edit_mode`.
///
/// `edit_mode` is supplied by the keymap binding — the command itself is
/// mode-agnostic, so the records-normal keymap passes `records-editing`
/// while the normal keymap passes `editing`.
#[derive(Debug)]
pub struct EditOrDrill {
    pub edit_mode: AppMode,
}
impl Cmd for EditOrDrill {
    fn name(&self) -> &'static str {
        "edit-or-drill"
    }
    fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
        // Only consider regular (non-virtual, non-label) categories on None
        // as true aggregation. Virtuals like _Index/_Dim are always None in
        // pivot mode and don't imply aggregation.
        let regular_none = ctx.none_cats().iter().any(|c| {
            ctx.model
                .category(c)
                .map(|cat| cat.kind.is_regular())
                .unwrap_or(false)
        });
        // Synthetic records-mode cells are never aggregated — edit directly.
        // (This is a layout property, not a mode flag.)
        let is_synthetic = ctx.synthetic_record_at_cursor().is_some();
        let is_aggregated = !is_synthetic && regular_none;
        if is_aggregated {
            let Some(key) = ctx.cell_key().clone() else {
                return vec![effect::set_status("cannot drill — no cell at cursor")];
            };
            return DrillIntoCell { key }.execute(ctx);
        }
        vec![
            Box::new(effect::SetBuffer {
                name: "edit".to_string(),
                value: ctx.display_value.clone(),
            }),
            effect::change_mode(self.edit_mode.clone()),
        ]
    }
}

/// Thin command wrapper around the `EnterEditAtCursor` effect so it can
/// participate in `Binding::Sequence`. `target_mode` is supplied as the
/// command argument by the keymap binding.
#[derive(Debug)]
pub struct EnterEditAtCursorCmd {
    pub target_mode: AppMode,
}
impl Cmd for EnterEditAtCursorCmd {
    fn name(&self) -> &'static str {
        "enter-edit-at-cursor"
    }
    fn execute(&self, _ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
        vec![Box::new(effect::EnterEditAtCursor {
            target_mode: self.target_mode.clone(),
        })]
    }
}

/// Enter export prompt mode.
#[derive(Debug)]
pub struct EnterExportPrompt;
impl Cmd for EnterExportPrompt {
    fn name(&self) -> &'static str {
        "enter-export-prompt"
    }
    fn execute(&self, _ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
        vec![effect::change_mode(AppMode::export_prompt())]
    }
}

/// Enter search mode.
#[derive(Debug)]
pub struct EnterSearchMode;
impl Cmd for EnterSearchMode {
    fn name(&self) -> &'static str {
        "search"
    }
    fn execute(&self, _ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
        vec![
            Box::new(effect::SetSearchMode(true)),
            Box::new(effect::SetSearchQuery(String::new())),
        ]
    }
}

/// Enter tile select mode.
#[derive(Debug)]
pub struct EnterTileSelect;
impl Cmd for EnterTileSelect {
    fn name(&self) -> &'static str {
        "enter-tile-select"
    }
    fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
        let count = ctx.model.category_names().len();
        if count > 0 {
            vec![
                Box::new(effect::SetTileCatIdx(0)),
                effect::change_mode(AppMode::TileSelect),
            ]
        } else {
            vec![]
        }
    }
}
