use crossterm::event::KeyCode;

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

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

#[cfg(test)]
mod tests {
    use std::collections::HashMap;

    use super::*;
    use crate::command::cmd::test_helpers::*;

    #[test]
    fn command_mode_backspace_pops_char() {
        let m = two_cat_model();
        let layout = make_layout(&m);
        let reg = make_registry();
        let mut bufs = HashMap::new();
        bufs.insert("command".to_string(), "hel".to_string());
        let mut ctx = make_ctx(&m, &layout, &reg);
        ctx.buffers = &bufs;
        let effects = CommandModeBackspace.execute(&ctx);
        let dbg = effects_debug(&effects);
        assert!(dbg.contains("SetBuffer"), "Expected SetBuffer, got: {dbg}");
        assert!(dbg.contains("he"), "Expected 'he' after pop, got: {dbg}");
    }

    #[test]
    fn command_mode_backspace_on_empty_returns_to_normal() {
        let m = two_cat_model();
        let layout = make_layout(&m);
        let reg = make_registry();
        let mut bufs = HashMap::new();
        bufs.insert("command".to_string(), "".to_string());
        let mut ctx = make_ctx(&m, &layout, &reg);
        ctx.buffers = &bufs;
        let effects = CommandModeBackspace.execute(&ctx);
        let dbg = effects_debug(&effects);
        assert!(
            dbg.contains("Normal"),
            "Expected return to Normal, got: {dbg}"
        );
    }

    #[test]
    fn quit_when_dirty_shows_warning() {
        let m = two_cat_model();
        let mut bufs = HashMap::new();
        bufs.insert("command".to_string(), "q".to_string());
        let layout = make_layout(&m);
        let reg = make_registry();
        let mut ctx = make_ctx(&m, &layout, &reg);
        ctx.dirty = true;
        ctx.buffers = &bufs;
        let cmd = ExecuteCommand;
        let effects = cmd.execute(&ctx);
        let dbg = format!("{:?}", effects);
        assert!(dbg.contains("SetStatus"), "Expected SetStatus, got: {dbg}");
    }

    #[test]
    fn quit_when_clean_produces_quit_mode() {
        let m = two_cat_model();
        let mut bufs = HashMap::new();
        bufs.insert("command".to_string(), "q".to_string());
        let layout = make_layout(&m);
        let reg = make_registry();
        let mut ctx = make_ctx(&m, &layout, &reg);
        ctx.buffers = &bufs;
        let cmd = ExecuteCommand;
        let effects = cmd.execute(&ctx);
        assert!(
            effects.iter().any(|e| e.changes_mode()),
            "Expected a mode-changing effect"
        );
    }

    #[test]
    fn execute_command_empty_returns_to_normal() {
        let m = two_cat_model();
        let layout = make_layout(&m);
        let reg = make_registry();
        let mut bufs = HashMap::new();
        bufs.insert("command".to_string(), "".to_string());
        let mut ctx = make_ctx(&m, &layout, &reg);
        ctx.buffers = &bufs;
        let effects = ExecuteCommand.execute(&ctx);
        let dbg = effects_debug(&effects);
        assert!(dbg.contains("Normal"), "Expected Normal mode, got: {dbg}");
    }

    #[test]
    fn execute_command_invalid_shows_error_status() {
        let m = two_cat_model();
        let layout = make_layout(&m);
        let reg = make_registry();
        let mut bufs = HashMap::new();
        bufs.insert("command".to_string(), "nonexistent-command".to_string());
        let mut ctx = make_ctx(&m, &layout, &reg);
        ctx.buffers = &bufs;
        let effects = ExecuteCommand.execute(&ctx);
        let dbg = effects_debug(&effects);
        assert!(
            dbg.contains("Normal"),
            "Expected Normal mode on error, got: {dbg}"
        );
    }

    #[test]
    fn execute_command_valid_runs_command() {
        let m = two_cat_model();
        let layout = make_layout(&m);
        let reg = make_registry();
        let mut bufs = HashMap::new();
        bufs.insert("command".to_string(), "add-category Region".to_string());
        let mut ctx = make_ctx(&m, &layout, &reg);
        ctx.buffers = &bufs;
        let effects = ExecuteCommand.execute(&ctx);
        let dbg = effects_debug(&effects);
        assert!(
            dbg.contains("AddCategory"),
            "Expected AddCategory effect, got: {dbg}"
        );
    }
}

/// Append the pressed character to a named buffer.
#[derive(Debug)]
pub struct AppendChar {
    pub buffer: String,
}
impl Cmd for AppendChar {
    fn name(&self) -> &'static str {
        "append-char"
    }
    fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
        if let KeyCode::Char(c) = ctx.key_code {
            let mut val = read_buffer(ctx, &self.buffer);
            val.push(c);
            if self.buffer == "search" {
                vec![Box::new(effect::SetSearchQuery(val))]
            } else {
                vec![Box::new(effect::SetBuffer {
                    name: self.buffer.clone(),
                    value: val,
                })]
            }
        } else {
            vec![]
        }
    }
}

/// Pop the last character from a named buffer.
#[derive(Debug)]
pub struct PopChar {
    pub buffer: String,
}
impl Cmd for PopChar {
    fn name(&self) -> &'static str {
        "pop-char"
    }
    fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
        let mut val = read_buffer(ctx, &self.buffer);
        val.pop();
        if self.buffer == "search" {
            vec![Box::new(effect::SetSearchQuery(val))]
        } else {
            vec![Box::new(effect::SetBuffer {
                name: self.buffer.clone(),
                value: val,
            })]
        }
    }
}

/// Handle backspace in command mode — pop char or return to Normal if empty.
#[derive(Debug)]
pub struct CommandModeBackspace;
impl Cmd for CommandModeBackspace {
    fn name(&self) -> &'static str {
        "command-mode-backspace"
    }
    fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
        let val = ctx.buffers.get("command").cloned().unwrap_or_default();
        if val.is_empty() {
            vec![effect::change_mode(AppMode::Normal)]
        } else {
            let mut val = val;
            val.pop();
            vec![Box::new(effect::SetBuffer {
                name: "command".to_string(),
                value: val,
            })]
        }
    }
}

// ── Wizard command ──────────────────────────────────────────────────────────

/// Dispatch the current key to the import wizard effect.
#[derive(Debug)]
pub struct HandleWizardKey;
impl Cmd for HandleWizardKey {
    fn name(&self) -> &'static str {
        "handle-wizard-key"
    }
    fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
        vec![Box::new(effect::WizardKey {
            key_code: ctx.key_code,
        })]
    }
}

// ── Command mode execution ──────────────────────────────────────────────────

/// Execute the command in the "command" buffer (the `:` command line).
#[derive(Debug)]
/// Execute the `:` command buffer by delegating to the command registry.
/// The `:` prompt is just another frontend to the scripting language —
/// same parser as `improvise script`.
pub struct ExecuteCommand;
impl Cmd for ExecuteCommand {
    fn name(&self) -> &'static str {
        "execute-command"
    }
    fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
        let raw = ctx.buffers.get("command").cloned().unwrap_or_default();
        let raw = raw.trim().to_string();
        if raw.is_empty() {
            return vec![effect::change_mode(AppMode::Normal)];
        }

        match crate::command::parse::parse_line_with(ctx.registry, &raw) {
            Ok(cmds) => {
                let mut effects: Vec<Box<dyn Effect>> = Vec::new();
                for cmd in cmds {
                    effects.extend(cmd.execute(ctx));
                }
                // Return to Normal unless a command already changed mode
                if !effects.iter().any(|e| e.changes_mode()) {
                    effects.push(effect::change_mode(AppMode::Normal));
                }
                effects
            }
            Err(msg) => {
                vec![
                    effect::set_status(format!(":{raw} — {msg}")),
                    effect::change_mode(AppMode::Normal),
                ]
            }
        }
    }
}
