use crate::model::cell::CellValue;
use crate::ui::effect::{self, Effect};
use crate::view::Axis;

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

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

    #[test]
    fn add_category_cmd_produces_add_category_effect() {
        let m = two_cat_model();
        let layout = make_layout(&m);
        let reg = make_registry();
        let ctx = make_ctx(&m, &layout, &reg);
        let cmd = AddCategoryCmd(vec!["Region".to_string()]);
        let effects = cmd.execute(&ctx);
        assert_eq!(effects.len(), 1);
        let dbg = effects_debug(&effects);
        assert!(
            dbg.contains("AddCategory"),
            "Expected AddCategory, got: {dbg}"
        );
    }

    #[test]
    fn set_cell_cmd_parses_coords_correctly() {
        let m = two_cat_model();
        let layout = make_layout(&m);
        let reg = make_registry();
        let ctx = make_ctx(&m, &layout, &reg);
        let cmd = SetCellCmd(vec![
            "42".to_string(),
            "Type/Food".to_string(),
            "Month/Jan".to_string(),
        ]);
        let effects = cmd.execute(&ctx);
        assert_eq!(effects.len(), 1);
        let dbg = effects_debug(&effects);
        assert!(dbg.contains("SetCell"), "Expected SetCell, got: {dbg}");
    }

    #[test]
    fn set_axis_cmd_recognizes_column_alias() {
        let m = two_cat_model();
        let layout = make_layout(&m);
        let reg = make_registry();
        let ctx = make_ctx(&m, &layout, &reg);
        let cmd = SetAxisCmd(vec!["Type".to_string(), "col".to_string()]);
        let effects = cmd.execute(&ctx);
        assert_eq!(effects.len(), 1);
        let dbg = effects_debug(&effects);
        assert!(dbg.contains("SetAxis"), "Expected SetAxis, got: {dbg}");
    }

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

    #[test]
    fn write_cmd_with_path_saves_as() {
        let m = two_cat_model();
        let layout = make_layout(&m);
        let reg = make_registry();
        let ctx = make_ctx(&m, &layout, &reg);
        let cmd = WriteCmd(vec!["/tmp/out.improv".to_string()]);
        let effects = cmd.execute(&ctx);
        assert_eq!(effects.len(), 1);
        let dbg = effects_debug(&effects);
        assert!(dbg.contains("SaveAs"), "Expected SaveAs, got: {dbg}");
    }
}

// ── Parseable model-mutation commands ────────────────────────────────────────
// These are thin Cmd wrappers around effects, constructible from string args.
// They share the same execution path as keymap-dispatched commands.

macro_rules! effect_cmd {
    ($name:ident, $cmd_name:expr, $parse:expr, $exec:expr) => {
        #[derive(Debug)]
        pub struct $name(pub Vec<String>);
        impl Cmd for $name {
            fn name(&self) -> &'static str {
                $cmd_name
            }
            fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
                let args = &self.0;
                #[allow(clippy::redundant_closure_call)]
                ($exec)(args, ctx)
            }
        }
        impl $name {
            pub fn parse(args: &[String]) -> Result<Box<dyn Cmd>, String> {
                #[allow(clippy::redundant_closure_call)]
                ($parse)(args)?;
                Ok(Box::new($name(args.to_vec())))
            }
        }
    };
}

effect_cmd!(
    AddCategoryCmd,
    "add-category",
    |args: &[String]| require_args("add-category", args, 1),
    |args: &Vec<String>, _ctx: &CmdContext| -> Vec<Box<dyn Effect>> {
        vec![Box::new(effect::AddCategory(args[0].clone()))]
    }
);

effect_cmd!(
    AddItemCmd,
    "add-item",
    |args: &[String]| require_args("add-item", args, 2),
    |args: &Vec<String>, _ctx: &CmdContext| -> Vec<Box<dyn Effect>> {
        vec![Box::new(effect::AddItem {
            category: args[0].clone(),
            item: args[1].clone(),
        })]
    }
);

effect_cmd!(
    AddItemsCmd,
    "add-items",
    |args: &[String]| {
        if args.len() < 2 {
            Err("add-items requires a category and at least one item".to_string())
        } else {
            Ok(())
        }
    },
    |args: &Vec<String>, _ctx: &CmdContext| -> Vec<Box<dyn Effect>> {
        let category = &args[0];
        args[1..]
            .iter()
            .map(|item| -> Box<dyn Effect> {
                Box::new(effect::AddItem {
                    category: category.clone(),
                    item: item.clone(),
                })
            })
            .collect()
    }
);

effect_cmd!(
    AddItemInGroupCmd,
    "add-item-in-group",
    |args: &[String]| require_args("add-item-in-group", args, 3),
    |args: &Vec<String>, _ctx: &CmdContext| -> Vec<Box<dyn Effect>> {
        vec![Box::new(effect::AddItemInGroup {
            category: args[0].clone(),
            item: args[1].clone(),
            group: args[2].clone(),
        })]
    }
);

effect_cmd!(
    SetCellCmd,
    "set-cell",
    |args: &[String]| {
        if args.len() < 2 {
            Err("set-cell requires a value and at least one Cat/Item coordinate".to_string())
        } else {
            Ok(())
        }
    },
    |args: &Vec<String>, _ctx: &CmdContext| -> Vec<Box<dyn Effect>> {
        let value = if let Ok(n) = args[0].parse::<f64>() {
            CellValue::Number(n)
        } else {
            CellValue::Text(args[0].clone())
        };
        let coords: Vec<(String, String)> = args[1..]
            .iter()
            .filter_map(|s| {
                let (cat, item) = s.split_once('/')?;
                Some((cat.to_string(), item.to_string()))
            })
            .collect();
        let key = crate::model::cell::CellKey::new(coords);
        vec![Box::new(effect::SetCell(key, value))]
    }
);

effect_cmd!(
    AddFormulaCmd,
    "add-formula",
    |args: &[String]| {
        if args.is_empty() || args.len() > 2 {
            return Err(format!(
                "add-formula requires 1-2 argument(s), got {}",
                args.len()
            ));
        }
        Ok(())
    },
    |args: &Vec<String>, _ctx: &CmdContext| -> Vec<Box<dyn Effect>> {
        // 1 arg: formula text (target_category defaults to _Measure)
        // 2 args: target_category, formula text
        let (cat, raw) = if args.len() == 2 {
            (args[0].clone(), args[1].clone())
        } else {
            ("_Measure".to_string(), args[0].clone())
        };
        vec![Box::new(effect::AddFormula {
            target_category: cat,
            raw,
        })]
    }
);

effect_cmd!(
    ClearBufferCmd,
    "clear-buffer",
    |args: &[String]| require_args("clear-buffer", args, 1),
    |args: &Vec<String>, _ctx: &CmdContext| -> Vec<Box<dyn Effect>> {
        vec![Box::new(effect::SetBuffer {
            name: args[0].clone(),
            value: String::new(),
        })]
    }
);

effect_cmd!(
    RemoveFormulaCmd,
    "remove-formula",
    |args: &[String]| require_args("remove-formula", args, 2),
    |args: &Vec<String>, _ctx: &CmdContext| -> Vec<Box<dyn Effect>> {
        vec![Box::new(effect::RemoveFormula {
            target_category: args[0].clone(),
            target: args[1].clone(),
        })]
    }
);

effect_cmd!(
    CreateViewCmd,
    "create-view",
    |args: &[String]| require_args("create-view", args, 1),
    |args: &Vec<String>, _ctx: &CmdContext| -> Vec<Box<dyn Effect>> {
        vec![Box::new(effect::CreateView(args[0].clone()))]
    }
);

effect_cmd!(
    DeleteViewCmd,
    "delete-view",
    |args: &[String]| require_args("delete-view", args, 1),
    |args: &Vec<String>, _ctx: &CmdContext| -> Vec<Box<dyn Effect>> {
        vec![Box::new(effect::DeleteView(args[0].clone()))]
    }
);

effect_cmd!(
    SwitchViewCmd,
    "switch-view",
    |args: &[String]| require_args("switch-view", args, 1),
    |args: &Vec<String>, _ctx: &CmdContext| -> Vec<Box<dyn Effect>> {
        vec![Box::new(effect::SwitchView(args[0].clone()))]
    }
);

effect_cmd!(
    SetAxisCmd,
    "set-axis",
    |args: &[String]| require_args("set-axis", args, 2),
    |args: &Vec<String>, _ctx: &CmdContext| -> Vec<Box<dyn Effect>> {
        let axis = match args[1].to_lowercase().as_str() {
            "row" => Axis::Row,
            "column" | "col" => Axis::Column,
            "page" => Axis::Page,
            "none" => Axis::None,
            _ => return vec![], // parse step already validated
        };
        vec![Box::new(effect::SetAxis {
            category: args[0].clone(),
            axis,
        })]
    }
);

effect_cmd!(
    SetPageCmd,
    "set-page",
    |args: &[String]| require_args("set-page", args, 2),
    |args: &Vec<String>, _ctx: &CmdContext| -> Vec<Box<dyn Effect>> {
        vec![Box::new(effect::SetPageSelection {
            category: args[0].clone(),
            item: args[1].clone(),
        })]
    }
);

effect_cmd!(
    ToggleGroupCmd,
    "toggle-group",
    |args: &[String]| require_args("toggle-group", args, 2),
    |args: &Vec<String>, _ctx: &CmdContext| -> Vec<Box<dyn Effect>> {
        vec![Box::new(effect::ToggleGroup {
            category: args[0].clone(),
            group: args[1].clone(),
        })]
    }
);

effect_cmd!(
    HideItemCmd,
    "hide-item",
    |args: &[String]| require_args("hide-item", args, 2),
    |args: &Vec<String>, _ctx: &CmdContext| -> Vec<Box<dyn Effect>> {
        vec![Box::new(effect::HideItem {
            category: args[0].clone(),
            item: args[1].clone(),
        })]
    }
);

effect_cmd!(
    ShowItemCmd,
    "show-item",
    |args: &[String]| require_args("show-item", args, 2),
    |args: &Vec<String>, _ctx: &CmdContext| -> Vec<Box<dyn Effect>> {
        vec![Box::new(effect::ShowItem {
            category: args[0].clone(),
            item: args[1].clone(),
        })]
    }
);

effect_cmd!(
    SaveAsCmd,
    "save-as",
    |args: &[String]| require_args("save-as", args, 1),
    |args: &Vec<String>, _ctx: &CmdContext| -> Vec<Box<dyn Effect>> {
        vec![Box::new(effect::SaveAs(std::path::PathBuf::from(&args[0])))]
    }
);

effect_cmd!(
    SetFormatCmd,
    "set-format",
    |args: &[String]| require_args("set-format", args, 1),
    |args: &Vec<String>, _ctx: &CmdContext| -> Vec<Box<dyn Effect>> {
        vec![
            Box::new(effect::SetNumberFormat(args.join(" "))),
            effect::mark_dirty(),
        ]
    }
);

effect_cmd!(
    ImportCmd,
    "import",
    |args: &[String]| require_args("import", args, 1),
    |args: &Vec<String>, _ctx: &CmdContext| -> Vec<Box<dyn Effect>> {
        vec![Box::new(effect::StartImportWizard(args[0].clone()))]
    }
);

effect_cmd!(
    ExportCmd,
    "export",
    |_args: &[String]| -> Result<(), String> { Ok(()) },
    |args: &Vec<String>, _ctx: &CmdContext| -> Vec<Box<dyn Effect>> {
        let path = args.first().map(|s| s.as_str()).unwrap_or("export.csv");
        vec![Box::new(effect::ExportCsv(std::path::PathBuf::from(path)))]
    }
);

effect_cmd!(
    WriteCmd,
    "w",
    |_args: &[String]| -> Result<(), String> { Ok(()) },
    |args: &Vec<String>, _ctx: &CmdContext| -> Vec<Box<dyn Effect>> {
        if args.is_empty() {
            vec![Box::new(effect::Save)]
        } else {
            vec![Box::new(effect::SaveAs(std::path::PathBuf::from(&args[0])))]
        }
    }
);

effect_cmd!(
    HelpCmd,
    "help",
    |_args: &[String]| -> Result<(), String> { Ok(()) },
    |_args: &Vec<String>, _ctx: &CmdContext| -> Vec<Box<dyn Effect>> {
        vec![
            effect::help_page_set(0),
            effect::change_mode(crate::ui::app::AppMode::Help),
        ]
    }
);

effect_cmd!(
    HelpPageNextCmd,
    "help-page-next",
    |_args: &[String]| -> Result<(), String> { Ok(()) },
    |_args: &Vec<String>, _ctx: &CmdContext| -> Vec<Box<dyn Effect>> {
        vec![effect::help_page_next()]
    }
);

effect_cmd!(
    HelpPagePrevCmd,
    "help-page-prev",
    |_args: &[String]| -> Result<(), String> { Ok(()) },
    |_args: &Vec<String>, _ctx: &CmdContext| -> Vec<Box<dyn Effect>> {
        vec![effect::help_page_prev()]
    }
);

effect_cmd!(
    LoadModelCmd,
    "load",
    |args: &[String]| require_args("load", args, 1),
    |args: &Vec<String>, _ctx: &CmdContext| -> Vec<Box<dyn Effect>> {
        vec![Box::new(effect::LoadModel(std::path::PathBuf::from(
            &args[0],
        )))]
    }
);

effect_cmd!(
    ExportCsvCmd,
    "export-csv",
    |args: &[String]| require_args("export-csv", args, 1),
    |args: &Vec<String>, _ctx: &CmdContext| -> Vec<Box<dyn Effect>> {
        vec![Box::new(effect::ExportCsv(std::path::PathBuf::from(
            &args[0],
        )))]
    }
);

effect_cmd!(
    ImportJsonCmd,
    "import-json",
    |args: &[String]| {
        if args.is_empty() {
            Err("import-json requires a path".to_string())
        } else {
            Ok(())
        }
    },
    |args: &Vec<String>, _ctx: &CmdContext| -> Vec<Box<dyn Effect>> {
        vec![Box::new(effect::ImportJsonHeadless {
            path: std::path::PathBuf::from(&args[0]),
            model_name: args.get(1).cloned(),
            array_path: args.get(2).cloned(),
        })]
    }
);
