use crate::model::cell::CellValue;
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::model::cell::{CellKey, CellValue};

    #[test]
    fn search_navigate_with_empty_query_returns_nothing() {
        let m = two_cat_model();
        let layout = make_layout(&m);
        let reg = make_registry();
        let ctx = make_ctx(&m, &layout, &reg);
        let cmd = SearchNavigate(true);
        let effects = cmd.execute(&ctx);
        assert!(effects.is_empty());
    }

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

    #[test]
    fn exit_search_mode_clears_flag() {
        let m = two_cat_model();
        let layout = make_layout(&m);
        let reg = make_registry();
        let ctx = make_ctx(&m, &layout, &reg);
        let effects = ExitSearchMode.execute(&ctx);
        assert_eq!(effects.len(), 1);
        let dbg = effects_debug(&effects);
        assert!(
            dbg.contains("SetSearchMode(false)"),
            "Expected search mode off, got: {dbg}"
        );
    }

    #[test]
    fn search_navigate_forward_with_matching_value() {
        let mut m = two_cat_model();
        m.model.set_cell(
            CellKey::new(vec![
                ("Type".into(), "Food".into()),
                ("Month".into(), "Jan".into()),
            ]),
            CellValue::Number(42.0),
        );
        m.model.set_cell(
            CellKey::new(vec![
                ("Type".into(), "Clothing".into()),
                ("Month".into(), "Feb".into()),
            ]),
            CellValue::Number(99.0),
        );
        let layout = make_layout(&m);
        let reg = make_registry();
        let mut ctx = make_ctx(&m, &layout, &reg);
        ctx.search_query = "99";
        let cmd = SearchNavigate(true);
        let effects = cmd.execute(&ctx);
        if !effects.is_empty() {
            let dbg = effects_debug(&effects);
            assert!(
                dbg.contains("SetSelected"),
                "Expected SetSelected, got: {dbg}"
            );
        }
    }
}

/// Navigate to the next or previous search match.
#[derive(Debug)]
pub struct SearchNavigate(pub bool);
impl Cmd for SearchNavigate {
    fn name(&self) -> &'static str {
        "search-navigate"
    }
    fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
        let query = ctx.search_query.to_lowercase();
        if query.is_empty() {
            return vec![];
        }

        let (cur_row, cur_col) = ctx.selected;
        let total_rows = ctx.row_count().max(1);
        let total_cols = ctx.col_count().max(1);
        let total = total_rows * total_cols;
        let cur_flat = cur_row * total_cols + cur_col;

        let matches: Vec<usize> = (0..total)
            .filter(|&flat| {
                let ri = flat / total_cols;
                let ci = flat % total_cols;
                let key = match ctx.layout.cell_key(ri, ci) {
                    Some(k) => k,
                    None => return false,
                };
                let s = match ctx.model.evaluate_aggregated(&key, ctx.none_cats()) {
                    Some(CellValue::Number(n)) => format!("{n}"),
                    Some(CellValue::Text(t)) => t,
                    Some(CellValue::Error(e)) => format!("ERR:{e}"),
                    None => String::new(),
                };
                s.to_lowercase().contains(&query)
            })
            .collect();

        if matches.is_empty() {
            return vec![effect::set_status(format!(
                "No matches for '{}'",
                ctx.search_query
            ))];
        }

        let target_flat = if self.0 {
            matches
                .iter()
                .find(|&&f| f > cur_flat)
                .or_else(|| matches.first())
                .copied()
        } else {
            matches
                .iter()
                .rev()
                .find(|&&f| f < cur_flat)
                .or_else(|| matches.last())
                .copied()
        };

        if let Some(flat) = target_flat {
            let ri = flat / total_cols;
            let ci = flat % total_cols;
            let mut effects: Vec<Box<dyn Effect>> = vec![effect::set_selected(ri, ci)];
            if ri < ctx.row_offset {
                effects.push(Box::new(effect::SetRowOffset(ri)));
            }
            if ci < ctx.col_offset {
                effects.push(Box::new(effect::SetColOffset(ci)));
            }
            effects.push(effect::set_status(format!(
                "Match {}/{} for '{}'",
                matches.iter().position(|&f| f == flat).unwrap_or(0) + 1,
                matches.len(),
                ctx.search_query,
            )));
            effects
        } else {
            vec![]
        }
    }
}

/// If search query is active, navigate backward; otherwise open CategoryAdd.
#[derive(Debug)]
pub struct SearchOrCategoryAdd;
impl Cmd for SearchOrCategoryAdd {
    fn name(&self) -> &'static str {
        "search-or-category-add"
    }
    fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
        if !ctx.search_query.is_empty() {
            SearchNavigate(false).execute(ctx)
        } else {
            vec![
                Box::new(effect::SetPanelOpen {
                    panel: Panel::Category,
                    open: true,
                }),
                effect::change_mode(AppMode::category_add()),
            ]
        }
    }
}

/// Exit search mode (clears search_mode flag).
#[derive(Debug)]
pub struct ExitSearchMode;
impl Cmd for ExitSearchMode {
    fn name(&self) -> &'static str {
        "exit-search-mode"
    }
    fn execute(&self, _ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
        vec![Box::new(effect::SetSearchMode(false))]
    }
}
