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

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

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

    #[test]
    fn toggle_panel_open_and_focus() {
        let m = two_cat_model();
        let layout = make_layout(&m);
        let reg = make_registry();
        let ctx = make_ctx(&m, &layout, &reg);
        let cmd = TogglePanelAndFocus {
            panel: effect::Panel::Formula,
            open: true,
            focused: true,
        };
        let effects = cmd.execute(&ctx);
        assert_eq!(effects.len(), 2);
        let dbg = format!("{:?}", effects[1]);
        assert!(
            dbg.contains("FormulaPanel"),
            "Expected FormulaPanel mode, got: {dbg}"
        );
    }

    #[test]
    fn toggle_panel_close_and_unfocus() {
        let m = two_cat_model();
        let layout = make_layout(&m);
        let reg = make_registry();
        let ctx = make_ctx(&m, &layout, &reg);
        let cmd = TogglePanelAndFocus {
            panel: effect::Panel::Formula,
            open: false,
            focused: false,
        };
        let effects = cmd.execute(&ctx);
        assert_eq!(effects.len(), 2);
    }

    #[test]
    fn cycle_panel_focus_with_no_panels_open() {
        let m = two_cat_model();
        let layout = make_layout(&m);
        let reg = make_registry();
        let ctx = make_ctx(&m, &layout, &reg);
        let cmd = CyclePanelFocus {
            formula_open: false,
            category_open: false,
            view_open: false,
        };
        let effects = cmd.execute(&ctx);
        assert!(effects.is_empty());
    }

    #[test]
    fn cycle_panel_focus_with_formula_panel_open() {
        let m = two_cat_model();
        let layout = make_layout(&m);
        let reg = make_registry();
        let mut ctx = make_ctx(&m, &layout, &reg);
        ctx.formula_panel_open = true;
        let cmd = CyclePanelFocus {
            formula_open: true,
            category_open: false,
            view_open: false,
        };
        let effects = cmd.execute(&ctx);
        assert_eq!(effects.len(), 1);
        let dbg = format!("{:?}", effects[0]);
        assert!(
            dbg.contains("FormulaPanel"),
            "Expected FormulaPanel, got: {dbg}"
        );
    }

    #[test]
    fn cycle_panel_focus_with_multiple_panels() {
        let m = two_cat_model();
        let layout = make_layout(&m);
        let reg = make_registry();
        let mut ctx = make_ctx(&m, &layout, &reg);
        ctx.formula_panel_open = true;
        ctx.category_panel_open = true;
        let cmd = CyclePanelFocus {
            formula_open: true,
            category_open: true,
            view_open: false,
        };
        let effects = cmd.execute(&ctx);
        assert_eq!(effects.len(), 1);
        let dbg = effects_debug(&effects);
        assert!(
            dbg.contains("FormulaPanel") || dbg.contains("CategoryPanel"),
            "Expected panel focus, got: {dbg}"
        );
    }

    #[test]
    fn move_panel_cursor_down_from_zero() {
        let m = two_cat_model();
        let layout = make_layout(&m);
        let reg = make_registry();
        let ctx = make_ctx(&m, &layout, &reg);
        let cmd = MovePanelCursor {
            panel: effect::Panel::Formula,
            delta: 1,
            current: 0,
            max: 5,
        };
        let effects = cmd.execute(&ctx);
        assert_eq!(effects.len(), 1);
        let dbg = effects_debug(&effects);
        assert!(
            dbg.contains("SetPanelCursor"),
            "Expected SetPanelCursor, got: {dbg}"
        );
    }

    #[test]
    fn move_panel_cursor_clamps_at_zero() {
        let m = two_cat_model();
        let layout = make_layout(&m);
        let reg = make_registry();
        let ctx = make_ctx(&m, &layout, &reg);
        let cmd = MovePanelCursor {
            panel: effect::Panel::Formula,
            delta: -1,
            current: 0,
            max: 5,
        };
        let effects = cmd.execute(&ctx);
        assert!(effects.is_empty());
    }

    #[test]
    fn move_panel_cursor_with_zero_max_produces_nothing() {
        let m = two_cat_model();
        let layout = make_layout(&m);
        let reg = make_registry();
        let ctx = make_ctx(&m, &layout, &reg);
        let cmd = MovePanelCursor {
            panel: effect::Panel::Formula,
            delta: 1,
            current: 0,
            max: 0,
        };
        let effects = cmd.execute(&ctx);
        assert!(effects.is_empty());
    }

    #[test]
    fn delete_formula_at_cursor_with_formulas() {
        let mut m = two_cat_model();
        m.model.add_formula(crate::formula::ast::Formula {
            raw: "Profit = Revenue - Cost".to_string(),
            target: "Profit".to_string(),
            target_category: "Type".to_string(),
            expr: crate::formula::ast::Expr::Number(0.0),
            filter: None,
        });
        let layout = make_layout(&m);
        let reg = make_registry();
        let ctx = make_ctx(&m, &layout, &reg);
        let effects = DeleteFormulaAtCursor.execute(&ctx);
        let dbg = effects_debug(&effects);
        assert!(
            dbg.contains("RemoveFormula"),
            "Expected RemoveFormula, got: {dbg}"
        );
    }

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

    #[test]
    fn switch_view_at_cursor_out_of_bounds_returns_empty() {
        let m = two_cat_model();
        let layout = make_layout(&m);
        let reg = make_registry();
        let mut ctx = make_ctx(&m, &layout, &reg);
        ctx.view_panel_cursor = 999;
        let effects = SwitchViewAtCursor.execute(&ctx);
        assert!(effects.is_empty());
    }

    #[test]
    fn create_and_switch_view_names_incrementally() {
        let m = two_cat_model();
        let layout = make_layout(&m);
        let reg = make_registry();
        let ctx = make_ctx(&m, &layout, &reg);
        let effects = CreateAndSwitchView.execute(&ctx);
        let dbg = effects_debug(&effects);
        assert!(
            dbg.contains("CreateView"),
            "Expected CreateView, got: {dbg}"
        );
        assert!(
            dbg.contains("SwitchView"),
            "Expected SwitchView, got: {dbg}"
        );
        assert!(
            dbg.contains("Normal"),
            "Expected return to Normal, got: {dbg}"
        );
    }

    #[test]
    fn delete_view_at_cursor_zero_does_not_adjust_cursor() {
        let m = two_cat_model();
        let layout = make_layout(&m);
        let reg = make_registry();
        let ctx = make_ctx(&m, &layout, &reg);
        let effects = DeleteViewAtCursor.execute(&ctx);
        let dbg = effects_debug(&effects);
        assert!(
            dbg.contains("DeleteView"),
            "Expected DeleteView, got: {dbg}"
        );
        assert!(
            !dbg.contains("SetPanelCursor"),
            "Expected no cursor adjustment at position 0, got: {dbg}"
        );
    }
}

// ── Panel commands ──────────────────────────────────────────────────────

/// Toggle a panel's visibility; if it opens, focus it (enter its mode).
#[derive(Debug)]
pub struct TogglePanelAndFocus {
    pub panel: Panel,
    pub open: bool,
    pub focused: bool,
}
impl Cmd for TogglePanelAndFocus {
    fn name(&self) -> &'static str {
        "toggle-panel-and-focus"
    }
    fn execute(&self, _ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
        let mut effects: Vec<Box<dyn Effect>> = Vec::new();
        effects.push(Box::new(effect::SetPanelOpen {
            panel: self.panel,
            open: self.open,
        }));
        if self.focused {
            effects.push(effect::change_mode(self.panel.mode()));
        } else {
            effects.push(effect::change_mode(AppMode::Normal));
        }
        effects
    }
}

/// Toggle a panel's visibility without changing mode.
#[derive(Debug)]
pub struct TogglePanelVisibility {
    pub panel: Panel,
    pub currently_open: bool,
}
impl Cmd for TogglePanelVisibility {
    fn name(&self) -> &'static str {
        "toggle-panel-visibility"
    }
    fn execute(&self, _ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
        vec![Box::new(effect::SetPanelOpen {
            panel: self.panel,
            open: !self.currently_open,
        })]
    }
}

/// Tab through open panels, entering the first open panel's mode.
#[derive(Debug)]
pub struct CyclePanelFocus {
    pub formula_open: bool,
    pub category_open: bool,
    pub view_open: bool,
}
impl Cmd for CyclePanelFocus {
    fn name(&self) -> &'static str {
        "cycle-panel-focus"
    }
    fn execute(&self, _ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
        if self.formula_open {
            vec![effect::change_mode(AppMode::FormulaPanel)]
        } else if self.category_open {
            vec![effect::change_mode(AppMode::CategoryPanel)]
        } else if self.view_open {
            vec![effect::change_mode(AppMode::ViewPanel)]
        } else {
            vec![]
        }
    }
}

// ── Panel cursor commands ────────────────────────────────────────────────────

/// Move a panel cursor by delta, clamping to bounds.
#[derive(Debug)]
pub struct MovePanelCursor {
    pub panel: Panel,
    pub delta: i32,
    pub current: usize,
    pub max: usize,
}
impl Cmd for MovePanelCursor {
    fn name(&self) -> &'static str {
        "move-panel-cursor"
    }
    fn execute(&self, _ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
        let cursor = self.current;
        let max = self.max;
        if max == 0 {
            return vec![];
        }
        let clamped_cursor = cursor.min(max - 1);
        let new = (clamped_cursor as i32 + self.delta).clamp(0, (max - 1) as i32) as usize;
        if new != cursor {
            vec![Box::new(effect::SetPanelCursor {
                panel: self.panel,
                cursor: new,
            })]
        } else {
            vec![]
        }
    }
}

// ── Formula panel commands ──────────────────────────────────────────────────

/// Enter formula edit mode with an empty buffer.
#[derive(Debug)]
pub struct EnterFormulaEdit;
impl Cmd for EnterFormulaEdit {
    fn name(&self) -> &'static str {
        "enter-formula-edit"
    }
    fn execute(&self, _ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
        vec![effect::change_mode(AppMode::formula_edit())]
    }
}

/// Delete the formula at the current cursor position.
#[derive(Debug)]
pub struct DeleteFormulaAtCursor;
impl Cmd for DeleteFormulaAtCursor {
    fn name(&self) -> &'static str {
        "delete-formula-at-cursor"
    }
    fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
        let formulas = ctx.model.formulas();
        let cursor = ctx.formula_cursor.min(formulas.len().saturating_sub(1));
        if cursor < formulas.len() {
            let f = &formulas[cursor];
            let mut effects: Vec<Box<dyn Effect>> = vec![
                Box::new(effect::RemoveFormula {
                    target: f.target.clone(),
                    target_category: f.target_category.clone(),
                }),
                effect::mark_dirty(),
            ];
            if cursor > 0 {
                effects.push(Box::new(effect::SetPanelCursor {
                    panel: Panel::Formula,
                    cursor: cursor - 1,
                }));
            }
            effects
        } else {
            vec![]
        }
    }
}

// ── Category panel commands ─────────────────────────────────────────────────

/// Cycle the axis assignment of the category at the cursor.
#[derive(Debug)]
pub struct CycleAxisAtCursor;
impl Cmd for CycleAxisAtCursor {
    fn name(&self) -> &'static str {
        "cycle-axis-at-cursor"
    }
    fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
        if let Some(cat_name) = ctx.cat_at_cursor() {
            vec![Box::new(effect::CycleAxis(cat_name))]
        } else {
            vec![effect::set_status(
                "Move cursor to a category header to change axis".to_string(),
            )]
        }
    }
}

/// Enter ItemAdd mode for the category at the panel cursor.
#[derive(Debug)]
pub struct OpenItemAddAtCursor;
impl Cmd for OpenItemAddAtCursor {
    fn name(&self) -> &'static str {
        "open-item-add-at-cursor"
    }
    fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
        if let Some(cat_name) = ctx.cat_at_cursor() {
            vec![effect::change_mode(AppMode::item_add(cat_name))]
        } else {
            vec![effect::set_status(
                "No category selected. Press n to add a category first.",
            )]
        }
    }
}

/// Toggle expand/collapse of the category at the tree cursor.
#[derive(Debug)]
pub struct ToggleCatExpand;
impl Cmd for ToggleCatExpand {
    fn name(&self) -> &'static str {
        "toggle-cat-expand"
    }
    fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
        if let Some(cat_name) = ctx.cat_at_cursor() {
            vec![Box::new(effect::ToggleCatExpand(cat_name))]
        } else {
            vec![]
        }
    }
}

/// Filter to item: when on an item row, set the category to Page with the
/// item as the filter value.
#[derive(Debug)]
pub struct FilterToItem;
impl Cmd for FilterToItem {
    fn name(&self) -> &'static str {
        "filter-to-item"
    }
    fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
        use crate::ui::cat_tree::CatTreeEntry;
        match ctx.cat_tree_entry() {
            Some(CatTreeEntry::Item {
                cat_name,
                item_name,
            }) => {
                vec![
                    Box::new(effect::SetAxis {
                        category: cat_name.clone(),
                        axis: crate::view::Axis::Page,
                    }),
                    Box::new(effect::SetPageSelection {
                        category: cat_name.clone(),
                        item: item_name.clone(),
                    }),
                    effect::set_status(format!("Filter: {cat_name} = {item_name}")),
                ]
            }
            Some(CatTreeEntry::Category { .. }) => {
                // On a category header — toggle expand instead
                ToggleCatExpand.execute(ctx)
            }
            None => vec![],
        }
    }
}

/// Delete the category or item at the panel cursor.
/// On a category header -> delete the whole category.
/// On an item row -> delete just that item.
#[derive(Debug)]
pub struct DeleteCategoryAtCursor;
impl Cmd for DeleteCategoryAtCursor {
    fn name(&self) -> &'static str {
        "delete-category-at-cursor"
    }
    fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
        use crate::ui::cat_tree::CatTreeEntry;
        match ctx.cat_tree_entry() {
            Some(CatTreeEntry::Category { name, .. }) => {
                vec![
                    Box::new(effect::RemoveCategory(name.clone())),
                    effect::mark_dirty(),
                    effect::set_status(format!("Deleted category '{name}'")),
                ]
            }
            Some(CatTreeEntry::Item {
                cat_name,
                item_name,
            }) => {
                vec![
                    Box::new(effect::RemoveItem {
                        category: cat_name.clone(),
                        item: item_name.clone(),
                    }),
                    effect::mark_dirty(),
                    effect::set_status(format!("Deleted item '{item_name}' from '{cat_name}'")),
                ]
            }
            None => vec![effect::set_status("No category to delete")],
        }
    }
}

// ── View panel commands ─────────────────────────────────────────────────────

/// Switch to the view at the panel cursor and return to Normal mode.
#[derive(Debug)]
pub struct SwitchViewAtCursor;
impl Cmd for SwitchViewAtCursor {
    fn name(&self) -> &'static str {
        "switch-view-at-cursor"
    }
    fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
        let view_names: Vec<String> = ctx.workbook.views.keys().cloned().collect();
        if let Some(name) = view_names.get(ctx.view_panel_cursor) {
            vec![
                Box::new(effect::SwitchView(name.clone())),
                effect::change_mode(AppMode::Normal),
            ]
        } else {
            vec![]
        }
    }
}

/// Create a new view, switch to it, and return to Normal mode.
#[derive(Debug)]
pub struct CreateAndSwitchView;
impl Cmd for CreateAndSwitchView {
    fn name(&self) -> &'static str {
        "create-and-switch-view"
    }
    fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
        let name = format!("View {}", ctx.workbook.views.len() + 1);
        vec![
            Box::new(effect::CreateView(name.clone())),
            Box::new(effect::SwitchView(name)),
            effect::mark_dirty(),
            effect::change_mode(AppMode::Normal),
        ]
    }
}

/// Delete the view at the panel cursor.
#[derive(Debug)]
pub struct DeleteViewAtCursor;
impl Cmd for DeleteViewAtCursor {
    fn name(&self) -> &'static str {
        "delete-view-at-cursor"
    }
    fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
        let view_names: Vec<String> = ctx.workbook.views.keys().cloned().collect();
        if let Some(name) = view_names.get(ctx.view_panel_cursor) {
            let mut effects: Vec<Box<dyn Effect>> = vec![
                Box::new(effect::DeleteView(name.clone())),
                effect::mark_dirty(),
            ];
            if ctx.view_panel_cursor > 0 {
                effects.push(Box::new(effect::SetPanelCursor {
                    panel: Panel::View,
                    cursor: ctx.view_panel_cursor - 1,
                }));
            }
            effects
        } else {
            vec![]
        }
    }
}
