use std::collections::HashMap;

use anyhow::{Result, anyhow};
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};

use super::category::{Category, CategoryId};
use super::cell::{CellKey, CellValue, DataStore};
use crate::formula::{AggFunc, Formula};

const MAX_CATEGORIES: usize = 12;

/// Pure-data document model: categories, cells, and formulas.
///
/// `Model` intentionally does **not** know about views. The view axes and
/// per-view state live in [`crate::workbook::Workbook`], which wraps a
/// `Model` with the view ensemble. Cross-slice operations — adding a
/// category and registering it on every view, for example — are therefore
/// methods on `Workbook`, not `Model`. This breaks the former `Model ↔ View`
/// cycle so the `model/` and `view/` modules can be lifted into a shared
/// `improvise-core` crate without pulling view code into pure data types.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Model {
    pub name: String,
    pub categories: IndexMap<String, Category>,
    pub data: DataStore,
    formulas: Vec<Formula>,
    next_category_id: CategoryId,
    /// Per-measure aggregation function (measure item name → agg func).
    /// Used when collapsing categories on `Axis::None`. Defaults to SUM.
    #[serde(default)]
    pub measure_agg: HashMap<String, AggFunc>,
    /// Cached formula evaluation results. Recomputed by `recompute_formulas`.
    #[serde(skip)]
    formula_cache: HashMap<CellKey, CellValue>,
}

impl Model {
    pub fn new(name: impl Into<String>) -> Self {
        use crate::model::category::CategoryKind;
        let name = name.into();
        let mut categories = IndexMap::new();
        // Virtual categories — always present.
        categories.insert(
            "_Index".to_string(),
            Category::new(0, "_Index").with_kind(CategoryKind::VirtualIndex),
        );
        categories.insert(
            "_Dim".to_string(),
            Category::new(1, "_Dim").with_kind(CategoryKind::VirtualDim),
        );
        categories.insert(
            "_Measure".to_string(),
            Category::new(2, "_Measure").with_kind(CategoryKind::VirtualMeasure),
        );
        Self {
            name,
            categories,
            data: DataStore::new(),
            formulas: Vec::new(),
            next_category_id: 3,
            measure_agg: HashMap::new(),
            formula_cache: HashMap::new(),
        }
    }

    /// Add a pivot category. Enforces the `MAX_CATEGORIES` limit for regular
    /// categories. The caller (typically [`crate::workbook::Workbook`]) is
    /// responsible for registering the new category on every view.
    pub fn add_category(&mut self, name: impl Into<String>) -> Result<CategoryId> {
        let name = name.into();
        // Only regular pivot categories count against the limit.
        let regular_count = self
            .categories
            .values()
            .filter(|c| c.kind.is_regular())
            .count();
        if regular_count >= MAX_CATEGORIES {
            return Err(anyhow!("Maximum of {MAX_CATEGORIES} categories reached"));
        }
        if self.categories.contains_key(&name) {
            return Ok(self.categories[&name].id);
        }
        let id = self.next_category_id;
        self.next_category_id += 1;
        self.categories
            .insert(name.clone(), Category::new(id, name.clone()));
        Ok(id)
    }

    /// Add a Label-kind category: stored alongside regular categories so
    /// records views can display it, but excluded from the pivot-category
    /// count limit. The caller is responsible for setting the view axis
    /// (typically to `Axis::None`).
    pub fn add_label_category(&mut self, name: impl Into<String>) -> Result<CategoryId> {
        use crate::model::category::CategoryKind;
        let name = name.into();
        if self.categories.contains_key(&name) {
            return Ok(self.categories[&name].id);
        }
        let id = self.next_category_id;
        self.next_category_id += 1;
        let cat = Category::new(id, name.clone()).with_kind(CategoryKind::Label);
        self.categories.insert(name.clone(), cat);
        Ok(id)
    }

    /// Remove a category and all cells that reference it. The caller is
    /// responsible for removing the category from any views that referenced
    /// it.
    pub fn remove_category(&mut self, name: &str) {
        if !self.categories.contains_key(name) {
            return;
        }
        self.categories.shift_remove(name);
        // Remove cells that have a coord in this category
        let to_remove: Vec<CellKey> = self
            .data
            .iter_cells()
            .filter(|(k, _)| k.get(name).is_some())
            .map(|(k, _)| k)
            .collect();
        for k in to_remove {
            self.data.remove(&k);
        }
        // Remove formulas targeting this category
        self.formulas.retain(|f| f.target_category != name);
    }

    /// Remove an item from a category and all cells that reference it.
    pub fn remove_item(&mut self, cat_name: &str, item_name: &str) {
        if let Some(cat) = self.categories.get_mut(cat_name) {
            cat.remove_item(item_name);
        }
        let to_remove: Vec<CellKey> = self
            .data
            .iter_cells()
            .filter(|(k, _)| k.get(cat_name) == Some(item_name))
            .map(|(k, _)| k)
            .collect();
        for k in to_remove {
            self.data.remove(&k);
        }
    }

    pub fn category_mut(&mut self, name: &str) -> Option<&mut Category> {
        self.categories.get_mut(name)
    }

    pub fn category(&self, name: &str) -> Option<&Category> {
        self.categories.get(name)
    }

    pub fn set_cell(&mut self, key: CellKey, value: CellValue) {
        self.data.set(key, value);
    }

    pub fn clear_cell(&mut self, key: &CellKey) {
        self.data.remove(key);
    }

    pub fn get_cell(&self, key: &CellKey) -> Option<&CellValue> {
        self.data.get(key)
    }

    pub fn add_formula(&mut self, formula: Formula) {
        // For non-_Measure target categories, add the target as a category item
        // so it appears in the grid. _Measure targets are dynamically included
        // via measure_item_names().
        if formula.target_category != "_Measure"
            && let Some(cat) = self.categories.get_mut(&formula.target_category)
        {
            cat.add_item(&formula.target);
        }
        // Replace if same target within the same category
        if let Some(pos) = self.formulas.iter().position(|f| {
            f.target == formula.target && f.target_category == formula.target_category
        }) {
            self.formulas[pos] = formula;
        } else {
            self.formulas.push(formula);
        }
    }

    pub fn remove_formula(&mut self, target: &str, target_category: &str) {
        self.formulas
            .retain(|f| !(f.target == target && f.target_category == target_category));
    }

    pub fn formulas(&self) -> &[Formula] {
        &self.formulas
    }

    /// Return all category names
    /// Names of all categories (including virtual ones).
    pub fn category_names(&self) -> Vec<&str> {
        self.categories.keys().map(|s| s.as_str()).collect()
    }

    /// Effective item names for the _Measure category: the union of items
    /// explicitly in the category plus all formula targets (for formulas
    /// targeting _Measure). Preserves insertion order of explicit items,
    /// then appends formula targets not already present.
    pub fn measure_item_names(&self) -> Vec<String> {
        let mut names: Vec<String> = self
            .category("_Measure")
            .map(|c| {
                c.ordered_item_names()
                    .iter()
                    .map(|s| s.to_string())
                    .collect()
            })
            .unwrap_or_default();
        for f in &self.formulas {
            if f.target_category == "_Measure" && !names.iter().any(|n| n == &f.target) {
                names.push(f.target.clone());
            }
        }
        names
    }

    /// Effective item names for any category. For _Measure, this includes
    /// formula targets dynamically. For all others, delegates to
    /// `ordered_item_names()`.
    pub fn effective_item_names(&self, cat_name: &str) -> Vec<String> {
        if cat_name == "_Measure" {
            self.measure_item_names()
        } else {
            self.category(cat_name)
                .map(|c| {
                    c.ordered_item_names()
                        .iter()
                        .map(|s| s.to_string())
                        .collect()
                })
                .unwrap_or_default()
        }
    }

    /// Category names excluding virtual categories (_Index, _Dim).
    pub fn regular_category_names(&self) -> Vec<&str> {
        self.categories
            .iter()
            .filter(|(_, c)| c.kind.is_regular())
            .map(|(name, _)| name.as_str())
            .collect()
    }

    /// Evaluate a computed value at a given key, considering formulas.
    /// Returns None when the cell is empty (no stored value, no applicable formula).
    /// Maximum formula evaluation depth. Circular references return None
    /// instead of stack-overflowing.
    const MAX_EVAL_DEPTH: u8 = 16;

    pub fn evaluate(&self, key: &CellKey) -> Option<CellValue> {
        self.evaluate_depth(key, Self::MAX_EVAL_DEPTH)
    }

    fn evaluate_depth(&self, key: &CellKey, depth: u8) -> Option<CellValue> {
        if depth == 0 {
            return Some(CellValue::Error("circular".into()));
        }
        for formula in &self.formulas {
            if let Some(item_val) = key.get(&formula.target_category)
                && item_val == formula.target
            {
                return self.eval_formula_depth(formula, key, depth - 1);
            }
        }
        self.data.get(key).cloned()
    }

    /// Evaluate a cell, aggregating over any hidden (None-axis) categories.
    /// When `none_cats` is empty, delegates to `evaluate`.
    /// Otherwise, uses `matching_cells` with the partial key and aggregates
    /// using the measure's agg function (default SUM).
    pub fn evaluate_aggregated(&self, key: &CellKey, none_cats: &[String]) -> Option<CellValue> {
        if none_cats.is_empty() {
            return self.evaluate(key);
        }

        // Check formula cache first
        if let Some(cached) = self.formula_cache.get(key) {
            return Some(cached.clone());
        }

        // Aggregate raw data across all None-axis categories
        let values: Vec<f64> = self
            .data
            .matching_values(&key.0)
            .into_iter()
            .filter_map(|v| v.as_f64())
            .collect();

        if values.is_empty() {
            return None;
        }

        // Determine agg func from measure_agg map, defaulting to SUM
        let agg = key
            .get("_Measure")
            .and_then(|m| self.measure_agg.get(m))
            .unwrap_or(&AggFunc::Sum);

        let result = match agg {
            AggFunc::Sum => values.iter().sum(),
            AggFunc::Avg => values.iter().sum::<f64>() / values.len() as f64,
            AggFunc::Min => values.iter().cloned().reduce(f64::min)?,
            AggFunc::Max => values.iter().cloned().reduce(f64::max)?,
            AggFunc::Count => values.len() as f64,
        };
        Some(CellValue::Number(result))
    }

    /// Evaluate aggregated as f64, returning 0.0 for empty cells.
    pub fn evaluate_aggregated_f64(&self, key: &CellKey, none_cats: &[String]) -> f64 {
        self.evaluate_aggregated(key, none_cats)
            .and_then(|v| v.as_f64())
            .unwrap_or(0.0)
    }

    //pub fn eval_formula(&self, formula: &Formula, context: &CellKey) -> Option<CellValue> {
    //    self.eval_formula_depth(formula, context, Self::MAX_EVAL_DEPTH)
    //}

    /// Recompute all formula cells until values stabilize (fixed point).
    /// Call this before rendering or exporting — it populates `formula_cache`
    /// so that `evaluate_aggregated` can look up formula results without
    /// recursive evaluation.
    pub fn recompute_formulas(&mut self, none_cats: &[String]) {
        self.formula_cache.clear();
        if self.formulas.is_empty() {
            return;
        }

        // Collect all unique coordinate "stems" from raw data by stripping
        // the formula's target category. Each stem × formula target = one
        // cell to evaluate.
        let formula_cats: Vec<(String, String)> = self
            .formulas
            .iter()
            .map(|f| (f.target_category.clone(), f.target.clone()))
            .collect();

        // Gather all unique partial keys (stems) for each formula category
        let mut stems: Vec<CellKey> = Vec::new();
        for (target_cat, _) in &formula_cats {
            for (key, _) in self.data.iter_cells() {
                let stem = key.without(target_cat);
                // Also strip none_cats from the stem since the grid queries
                // without those coordinates
                let mut stripped = stem;
                for nc in none_cats {
                    stripped = stripped.without(nc);
                }
                if !stems.contains(&stripped) {
                    stems.push(stripped);
                }
            }
        }

        // Fixed-point iteration
        for _pass in 0..Self::MAX_EVAL_DEPTH {
            let mut changed = false;
            for (target_cat, target_name) in &formula_cats {
                for stem in &stems {
                    let key = stem.clone().with(target_cat, target_name);
                    // Evaluate this formula cell using current state
                    // (raw data aggregation + existing cache values)
                    let new_val = self.evaluate_formula_cell(&key, none_cats);
                    let old_val = self.formula_cache.get(&key);
                    if old_val != new_val.as_ref() {
                        changed = true;
                        if let Some(v) = new_val {
                            self.formula_cache.insert(key, v);
                        }
                    }
                }
            }
            if !changed {
                break;
            }
        }
    }

    /// Evaluate a single formula cell for the fixed-point pass.
    /// Uses raw data aggregation for non-formula refs and the cache for formula refs.
    fn evaluate_formula_cell(&self, key: &CellKey, none_cats: &[String]) -> Option<CellValue> {
        for formula in &self.formulas {
            if let Some(item_val) = key.get(&formula.target_category)
                && item_val == formula.target
            {
                return self.eval_formula_with_cache(formula, key, none_cats);
            }
        }
        None
    }

    /// Evaluate a formula using the formula_cache for ref resolution
    /// and raw data aggregation for non-formula values.
    fn eval_formula_with_cache(
        &self,
        formula: &Formula,
        context: &CellKey,
        none_cats: &[String],
    ) -> Option<CellValue> {
        use crate::formula::Expr;

        if let Some(filter) = &formula.filter {
            let matches = context
                .get(&filter.category)
                .map(|v| v == filter.item.as_str())
                .unwrap_or(false);
            if !matches {
                // Fall back to aggregated raw data
                return self.aggregate_raw(context, none_cats);
            }
        }

        fn find_item_category<'a>(model: &'a Model, item_name: &str) -> Option<&'a str> {
            for (cat_name, cat) in &model.categories {
                if cat.items.contains_key(item_name) {
                    return Some(cat_name.as_str());
                }
            }
            // Fall back to formula targets: if a formula defines this item
            // in _Measure, resolve it there.
            for f in model.formulas() {
                if f.target == item_name && f.target_category == "_Measure" {
                    return Some("_Measure");
                }
            }
            None
        }

        fn eval_expr_cached(
            expr: &Expr,
            context: &CellKey,
            model: &Model,
            target_category: &str,
            none_cats: &[String],
        ) -> Result<f64, String> {
            use crate::formula::BinOp;
            match expr {
                Expr::Number(n) => Ok(*n),
                Expr::Ref(name) => {
                    let cat =
                        find_item_category(model, name).ok_or_else(|| format!("ref:{name}"))?;
                    let ref_key = context.clone().with(cat, name);
                    // Check formula cache first, then aggregate raw data
                    if let Some(cached) = model.formula_cache.get(&ref_key) {
                        return match cached {
                            CellValue::Number(n) => Ok(*n),
                            CellValue::Error(e) => Err(e.clone()),
                            _ => Err(format!("ref:{name}")),
                        };
                    }
                    // Not a formula result — aggregate raw data
                    match model.aggregate_raw(&ref_key, none_cats) {
                        Some(CellValue::Number(n)) => Ok(n),
                        Some(CellValue::Error(e)) => Err(e),
                        _ => Err(format!("ref:{name}")),
                    }
                }
                Expr::BinOp(op, l, r) => {
                    let lv = eval_expr_cached(l, context, model, target_category, none_cats)?;
                    let rv = eval_expr_cached(r, context, model, target_category, none_cats)?;
                    match op {
                        BinOp::Add => Ok(lv + rv),
                        BinOp::Sub => Ok(lv - rv),
                        BinOp::Mul => Ok(lv * rv),
                        BinOp::Div => {
                            if rv == 0.0 {
                                Err("div/0".into())
                            } else {
                                Ok(lv / rv)
                            }
                        }
                        BinOp::Pow => Ok(lv.powf(rv)),
                        BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Gt | BinOp::Le | BinOp::Ge => {
                            Err("type".into())
                        }
                    }
                }
                Expr::UnaryMinus(e) => Ok(-eval_expr_cached(
                    e,
                    context,
                    model,
                    target_category,
                    none_cats,
                )?),
                Expr::Agg(func, inner, agg_filter) => {
                    use crate::formula::AggFunc;
                    let mut partial = context.without(target_category);
                    if let Expr::Ref(item_name) = inner.as_ref()
                        && let Some(cat) = find_item_category(model, item_name)
                    {
                        partial = partial.with(cat, item_name.as_str());
                    }
                    if let Some(f) = agg_filter {
                        partial = partial.with(&f.category, &f.item);
                    }
                    let values: Vec<f64> = model
                        .data
                        .matching_values(&partial.0)
                        .into_iter()
                        .filter_map(|v| v.as_f64())
                        .collect();
                    match func {
                        AggFunc::Sum => Ok(values.iter().sum()),
                        AggFunc::Avg => {
                            if values.is_empty() {
                                Err("empty".into())
                            } else {
                                Ok(values.iter().sum::<f64>() / values.len() as f64)
                            }
                        }
                        AggFunc::Min => values
                            .iter()
                            .cloned()
                            .reduce(f64::min)
                            .ok_or_else(|| "empty".into()),
                        AggFunc::Max => values
                            .iter()
                            .cloned()
                            .reduce(f64::max)
                            .ok_or_else(|| "empty".into()),
                        AggFunc::Count => Ok(values.len() as f64),
                    }
                }
                Expr::If(cond, then, else_) => {
                    let cv = eval_bool_cached(cond, context, model, target_category, none_cats)?;
                    if cv {
                        eval_expr_cached(then, context, model, target_category, none_cats)
                    } else {
                        eval_expr_cached(else_, context, model, target_category, none_cats)
                    }
                }
            }
        }

        fn eval_bool_cached(
            expr: &Expr,
            context: &CellKey,
            model: &Model,
            target_category: &str,
            none_cats: &[String],
        ) -> Result<bool, String> {
            use crate::formula::BinOp;
            match expr {
                Expr::BinOp(op, l, r) => {
                    let lv = eval_expr_cached(l, context, model, target_category, none_cats)?;
                    let rv = eval_expr_cached(r, context, model, target_category, none_cats)?;
                    match op {
                        BinOp::Eq => Ok((lv - rv).abs() < 1e-10),
                        BinOp::Ne => Ok((lv - rv).abs() >= 1e-10),
                        BinOp::Lt => Ok(lv < rv),
                        BinOp::Gt => Ok(lv > rv),
                        BinOp::Le => Ok(lv <= rv),
                        BinOp::Ge => Ok(lv >= rv),
                        _ => Err("type".into()),
                    }
                }
                _ => Err("type".into()),
            }
        }

        match eval_expr_cached(
            &formula.expr,
            context,
            self,
            &formula.target_category,
            none_cats,
        ) {
            Ok(n) => Some(CellValue::Number(n)),
            Err(e) => Some(CellValue::Error(e)),
        }
    }

    /// Aggregate raw data (not formulas) over hidden dimensions.
    fn aggregate_raw(&self, key: &CellKey, none_cats: &[String]) -> Option<CellValue> {
        if none_cats.is_empty() {
            return self.data.get(key).cloned();
        }
        let values: Vec<f64> = self
            .data
            .matching_values(&key.0)
            .into_iter()
            .filter_map(|v| v.as_f64())
            .collect();
        if values.is_empty() {
            return None;
        }
        let agg = key
            .get("_Measure")
            .and_then(|m| self.measure_agg.get(m))
            .unwrap_or(&AggFunc::Sum);
        let result = match agg {
            AggFunc::Sum => values.iter().sum(),
            AggFunc::Avg => values.iter().sum::<f64>() / values.len() as f64,
            AggFunc::Min => values.iter().cloned().reduce(f64::min)?,
            AggFunc::Max => values.iter().cloned().reduce(f64::max)?,
            AggFunc::Count => values.len() as f64,
        };
        Some(CellValue::Number(result))
    }

    fn eval_formula_depth(
        &self,
        formula: &Formula,
        context: &CellKey,
        depth: u8,
    ) -> Option<CellValue> {
        use crate::formula::{AggFunc, Expr};

        // Check WHERE filter first
        if let Some(filter) = &formula.filter {
            let matches = context
                .get(&filter.category)
                .map(|v| v == filter.item.as_str())
                .unwrap_or(false);
            if !matches {
                return self.data.get(context).cloned();
            }
        }

        fn find_item_category<'a>(model: &'a Model, item_name: &str) -> Option<&'a str> {
            for (cat_name, cat) in &model.categories {
                if cat.items.contains_key(item_name) {
                    return Some(cat_name.as_str());
                }
            }
            // Fall back to formula targets: if a formula defines this item
            // in _Measure, resolve it there.
            for f in model.formulas() {
                if f.target == item_name && f.target_category == "_Measure" {
                    return Some("_Measure");
                }
            }
            None
        }

        /// Evaluate an expression, returning Ok(f64) or Err(reason).
        /// Errors propagate immediately — a circular reference in any
        /// sub-expression short-circuits the entire formula.
        fn eval_expr(
            expr: &Expr,
            context: &CellKey,
            model: &Model,
            target_category: &str,
            depth: u8,
        ) -> Result<f64, String> {
            match expr {
                Expr::Number(n) => Ok(*n),
                Expr::Ref(name) => {
                    let cat =
                        find_item_category(model, name).ok_or_else(|| format!("ref:{name}"))?;
                    let new_key = context.clone().with(cat, name);
                    match model.evaluate_depth(&new_key, depth) {
                        Some(CellValue::Number(n)) => Ok(n),
                        Some(CellValue::Error(e)) => Err(e),
                        _ => Err(format!("ref:{name}")),
                    }
                }
                Expr::BinOp(op, l, r) => {
                    use crate::formula::BinOp;
                    let lv = eval_expr(l, context, model, target_category, depth)?;
                    let rv = eval_expr(r, context, model, target_category, depth)?;
                    match op {
                        BinOp::Add => Ok(lv + rv),
                        BinOp::Sub => Ok(lv - rv),
                        BinOp::Mul => Ok(lv * rv),
                        BinOp::Div => {
                            if rv == 0.0 {
                                Err("div/0".into())
                            } else {
                                Ok(lv / rv)
                            }
                        }
                        BinOp::Pow => Ok(lv.powf(rv)),
                        BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Gt | BinOp::Le | BinOp::Ge => {
                            Err("type".into())
                        }
                    }
                }
                Expr::UnaryMinus(e) => Ok(-eval_expr(e, context, model, target_category, depth)?),
                Expr::Agg(func, inner, agg_filter) => {
                    let mut partial = context.without(target_category);
                    if let Expr::Ref(item_name) = inner.as_ref()
                        && let Some(cat) = find_item_category(model, item_name)
                    {
                        partial = partial.with(cat, item_name.as_str());
                    }
                    if let Some(f) = agg_filter {
                        partial = partial.with(&f.category, &f.item);
                    }
                    let values: Vec<f64> = model
                        .data
                        .matching_values(&partial.0)
                        .into_iter()
                        .filter_map(|v| v.as_f64())
                        .collect();
                    match func {
                        AggFunc::Sum => Ok(values.iter().sum()),
                        AggFunc::Avg => {
                            if values.is_empty() {
                                Err("empty".into())
                            } else {
                                Ok(values.iter().sum::<f64>() / values.len() as f64)
                            }
                        }
                        AggFunc::Min => values
                            .iter()
                            .cloned()
                            .reduce(f64::min)
                            .ok_or_else(|| "empty".into()),
                        AggFunc::Max => values
                            .iter()
                            .cloned()
                            .reduce(f64::max)
                            .ok_or_else(|| "empty".into()),
                        AggFunc::Count => Ok(values.len() as f64),
                    }
                }
                Expr::If(cond, then, else_) => {
                    let cv = eval_bool(cond, context, model, target_category, depth)?;
                    if cv {
                        eval_expr(then, context, model, target_category, depth)
                    } else {
                        eval_expr(else_, context, model, target_category, depth)
                    }
                }
            }
        }

        fn eval_bool(
            expr: &Expr,
            context: &CellKey,
            model: &Model,
            target_category: &str,
            depth: u8,
        ) -> Result<bool, String> {
            use crate::formula::BinOp;
            match expr {
                Expr::BinOp(op, l, r) => {
                    let lv = eval_expr(l, context, model, target_category, depth)?;
                    let rv = eval_expr(r, context, model, target_category, depth)?;
                    match op {
                        BinOp::Eq => Ok((lv - rv).abs() < 1e-10),
                        BinOp::Ne => Ok((lv - rv).abs() >= 1e-10),
                        BinOp::Lt => Ok(lv < rv),
                        BinOp::Gt => Ok(lv > rv),
                        BinOp::Le => Ok(lv <= rv),
                        BinOp::Ge => Ok(lv >= rv),
                        BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Pow => {
                            Err("type".into())
                        }
                    }
                }
                _ => Err("type".into()),
            }
        }

        match eval_expr(
            &formula.expr,
            context,
            self,
            &formula.target_category,
            depth,
        ) {
            Ok(n) => Some(CellValue::Number(n)),
            Err(e) => Some(CellValue::Error(e)),
        }
    }
}

#[cfg(test)]
mod model_tests {
    use super::Model;
    use crate::model::cell::{CellKey, CellValue};

    fn coord(pairs: &[(&str, &str)]) -> CellKey {
        CellKey::new(
            pairs
                .iter()
                .map(|(c, i)| (c.to_string(), i.to_string()))
                .collect(),
        )
    }

    #[test]
    fn add_category_creates_entry() {
        let mut m = Model::new("Test");
        m.add_category("Region").unwrap();
        assert!(m.category("Region").is_some());
    }

    #[test]
    fn add_category_duplicate_is_idempotent() {
        let mut m = Model::new("Test");
        let id1 = m.add_category("Region").unwrap();
        let id2 = m.add_category("Region").unwrap();
        assert_eq!(id1, id2);
        // Region + 3 virtuals (_Index, _Dim, _Measure)
        assert_eq!(m.category_names().len(), 4);
    }

    #[test]
    fn add_category_max_limit() {
        let mut m = Model::new("Test");
        for i in 0..12 {
            m.add_category(format!("Cat{i}")).unwrap();
        }
        assert!(m.add_category("TooMany").is_err());
    }

    #[test]
    fn set_and_get_cell_roundtrip() {
        let mut m = Model::new("Test");
        m.add_category("Region").unwrap();
        m.add_category("_Measure").unwrap();
        let k = coord(&[("Region", "East"), ("_Measure", "Revenue")]);
        m.set_cell(k.clone(), CellValue::Number(500.0));
        assert_eq!(m.get_cell(&k), Some(&CellValue::Number(500.0)));
    }

    #[test]
    fn get_unset_cell_returns_empty() {
        let m = Model::new("Test");
        let k = coord(&[("Region", "East")]);
        assert_eq!(m.get_cell(&k), None);
    }

    #[test]
    fn overwrite_cell() {
        let mut m = Model::new("Test");
        let k = coord(&[("Region", "East")]);
        m.set_cell(k.clone(), CellValue::Number(1.0));
        m.set_cell(k.clone(), CellValue::Number(2.0));
        assert_eq!(m.get_cell(&k), Some(&CellValue::Number(2.0)));
    }

    #[test]
    fn three_category_model_independent_cells() {
        let mut m = Model::new("Test");
        m.add_category("Region").unwrap();
        m.add_category("Product").unwrap();
        m.add_category("_Measure").unwrap();
        let k1 = coord(&[
            ("Region", "East"),
            ("Product", "Shirts"),
            ("_Measure", "Revenue"),
        ]);
        let k2 = coord(&[
            ("Region", "West"),
            ("Product", "Shirts"),
            ("_Measure", "Revenue"),
        ]);
        let k3 = coord(&[
            ("Region", "East"),
            ("Product", "Pants"),
            ("_Measure", "Revenue"),
        ]);
        let k4 = coord(&[
            ("Region", "East"),
            ("Product", "Shirts"),
            ("_Measure", "Cost"),
        ]);
        m.set_cell(k1.clone(), CellValue::Number(100.0));
        m.set_cell(k2.clone(), CellValue::Number(200.0));
        m.set_cell(k3.clone(), CellValue::Number(300.0));
        m.set_cell(k4.clone(), CellValue::Number(40.0));
        assert_eq!(m.get_cell(&k1), Some(&CellValue::Number(100.0)));
        assert_eq!(m.get_cell(&k2), Some(&CellValue::Number(200.0)));
        assert_eq!(m.get_cell(&k3), Some(&CellValue::Number(300.0)));
        assert_eq!(m.get_cell(&k4), Some(&CellValue::Number(40.0)));
    }

    #[test]
    fn remove_category_deletes_category_and_cells() {
        let mut m = Model::new("Test");
        m.add_category("Region").unwrap();
        m.add_category("Product").unwrap();
        m.category_mut("Region").unwrap().add_item("East");
        m.category_mut("Product").unwrap().add_item("Shirts");
        m.set_cell(
            coord(&[("Region", "East"), ("Product", "Shirts")]),
            CellValue::Number(42.0),
        );
        m.remove_category("Region");
        assert!(m.category("Region").is_none());
        // Cells referencing Region should be gone
        assert_eq!(
            m.data.iter_cells().count(),
            0,
            "all cells with Region coord should be removed"
        );
    }

    #[test]
    fn evaluate_aggregated_sums_over_hidden_dimension() {
        let mut m = Model::new("Test");
        m.add_category("Payee").unwrap();
        m.add_category("Date").unwrap();
        m.add_category("_Measure").unwrap();
        m.category_mut("Payee").unwrap().add_item("Acme");
        m.category_mut("Date").unwrap().add_item("Jan-01");
        m.category_mut("Date").unwrap().add_item("Jan-02");
        m.category_mut("_Measure").unwrap().add_item("Amount");

        m.set_cell(
            coord(&[
                ("Payee", "Acme"),
                ("Date", "Jan-01"),
                ("_Measure", "Amount"),
            ]),
            CellValue::Number(100.0),
        );
        m.set_cell(
            coord(&[
                ("Payee", "Acme"),
                ("Date", "Jan-02"),
                ("_Measure", "Amount"),
            ]),
            CellValue::Number(50.0),
        );

        // Without hidden dims, returns None for partial key
        let partial_key = coord(&[("Payee", "Acme"), ("_Measure", "Amount")]);
        assert_eq!(m.evaluate(&partial_key), None);

        // With Date as hidden dimension, aggregates to SUM
        let none_cats = vec!["Date".to_string()];
        let result = m.evaluate_aggregated(&partial_key, &none_cats);
        assert_eq!(result, Some(CellValue::Number(150.0)));
    }

    /// Formula targets should appear in measure_item_names() without being
    /// explicitly added to the _Measure category. _Measure is dynamically
    /// computed from data items + formula targets.
    #[test]
    fn measure_item_names_includes_formula_targets() {
        use crate::formula::parse_formula;

        let mut m = Model::new("Test");
        m.add_category("Region").unwrap();
        m.category_mut("Region").unwrap().add_item("East");
        m.category_mut("_Measure").unwrap().add_item("Revenue");
        m.category_mut("_Measure").unwrap().add_item("Cost");

        m.add_formula(parse_formula("Profit = Revenue - Cost", "_Measure").unwrap());

        let names = m.measure_item_names();
        assert!(
            names.contains(&"Revenue".to_string()),
            "measure_item_names should include data items"
        );
        assert!(
            names.contains(&"Profit".to_string()),
            "measure_item_names should include formula targets"
        );
        // Formula target should NOT be in the category's own items
        assert!(
            !m.category("_Measure")
                .unwrap()
                .ordered_item_names()
                .contains(&"Profit"),
            "formula targets should not be added to the category directly"
        );
    }

    #[test]
    fn evaluate_aggregated_no_hidden_delegates_to_evaluate() {
        let mut m = Model::new("Test");
        m.add_category("Region").unwrap();
        m.category_mut("Region").unwrap().add_item("East");
        m.set_cell(coord(&[("Region", "East")]), CellValue::Number(42.0));
        let key = coord(&[("Region", "East")]);
        assert_eq!(
            m.evaluate_aggregated(&key, &[]),
            Some(CellValue::Number(42.0))
        );
    }

    #[test]
    fn evaluate_aggregated_respects_measure_agg() {
        use crate::formula::AggFunc;

        let mut m = Model::new("Test");
        m.add_category("Payee").unwrap();
        m.add_category("Date").unwrap();
        m.add_category("_Measure").unwrap();
        m.category_mut("Payee").unwrap().add_item("Acme");
        m.category_mut("Date").unwrap().add_item("D1");
        m.category_mut("Date").unwrap().add_item("D2");
        m.category_mut("_Measure").unwrap().add_item("Price");

        m.set_cell(
            coord(&[("Payee", "Acme"), ("Date", "D1"), ("_Measure", "Price")]),
            CellValue::Number(10.0),
        );
        m.set_cell(
            coord(&[("Payee", "Acme"), ("Date", "D2"), ("_Measure", "Price")]),
            CellValue::Number(30.0),
        );

        m.measure_agg.insert("Price".to_string(), AggFunc::Avg);

        let key = coord(&[("Payee", "Acme"), ("_Measure", "Price")]);
        let none_cats = vec!["Date".to_string()];
        let result = m.evaluate_aggregated(&key, &none_cats);
        assert_eq!(result, Some(CellValue::Number(20.0))); // avg(10, 30) = 20
    }
}

#[cfg(test)]
mod formula_tests {
    use super::Model;
    use crate::formula::parse_formula;
    use crate::model::cell::{CellKey, CellValue};

    fn coord(pairs: &[(&str, &str)]) -> CellKey {
        CellKey::new(
            pairs
                .iter()
                .map(|(c, i)| (c.to_string(), i.to_string()))
                .collect(),
        )
    }

    fn approx_eq(a: f64, b: f64) -> bool {
        (a - b).abs() < 1e-9
    }

    fn revenue_cost_model() -> Model {
        let mut m = Model::new("Test");
        m.add_category("_Measure").unwrap();
        m.add_category("Region").unwrap();
        if let Some(cat) = m.category_mut("_Measure") {
            cat.add_item("Revenue");
            cat.add_item("Cost");
            cat.add_item("Profit");
        }
        if let Some(cat) = m.category_mut("Region") {
            cat.add_item("East");
            cat.add_item("West");
        }
        m.set_cell(
            coord(&[("_Measure", "Revenue"), ("Region", "East")]),
            CellValue::Number(1000.0),
        );
        m.set_cell(
            coord(&[("_Measure", "Cost"), ("Region", "East")]),
            CellValue::Number(600.0),
        );
        m.set_cell(
            coord(&[("_Measure", "Revenue"), ("Region", "West")]),
            CellValue::Number(800.0),
        );
        m.set_cell(
            coord(&[("_Measure", "Cost"), ("Region", "West")]),
            CellValue::Number(500.0),
        );
        m
    }

    #[test]
    fn profit_equals_revenue_minus_cost() {
        let mut m = revenue_cost_model();
        m.add_formula(parse_formula("Profit = Revenue - Cost", "_Measure").unwrap());
        let k = coord(&[("_Measure", "Profit"), ("Region", "East")]);
        assert_eq!(m.evaluate(&k), Some(CellValue::Number(400.0)));
    }

    #[test]
    fn formula_evaluates_per_region() {
        let mut m = revenue_cost_model();
        m.add_formula(parse_formula("Profit = Revenue - Cost", "_Measure").unwrap());
        let east = m.evaluate(&coord(&[("_Measure", "Profit"), ("Region", "East")]));
        let west = m.evaluate(&coord(&[("_Measure", "Profit"), ("Region", "West")]));
        assert_eq!(east, Some(CellValue::Number(400.0)));
        assert_eq!(west, Some(CellValue::Number(300.0)));
    }

    #[test]
    fn formula_multiplication() {
        let mut m = revenue_cost_model();
        m.add_formula(parse_formula("Tax = Revenue * 0.1", "_Measure").unwrap());
        if let Some(cat) = m.category_mut("_Measure") {
            cat.add_item("Tax");
        }
        let val = m
            .evaluate(&coord(&[("_Measure", "Tax"), ("Region", "East")]))
            .and_then(|v| v.as_f64())
            .unwrap();
        assert!(approx_eq(val, 100.0));
    }

    #[test]
    fn formula_division() {
        let mut m = revenue_cost_model();
        m.add_formula(parse_formula("Profit = Revenue - Cost", "_Measure").unwrap());
        m.add_formula(parse_formula("Margin = Profit / Revenue", "_Measure").unwrap());
        if let Some(cat) = m.category_mut("_Measure") {
            cat.add_item("Profit");
            cat.add_item("Margin");
        }
        let val = m
            .evaluate(&coord(&[("_Measure", "Margin"), ("Region", "East")]))
            .and_then(|v| v.as_f64())
            .unwrap();
        assert!(approx_eq(val, 0.4));
    }

    #[test]
    fn division_by_zero_yields_empty() {
        let mut m = Model::new("Test");
        m.add_category("_Measure").unwrap();
        m.add_category("Region").unwrap();
        if let Some(cat) = m.category_mut("_Measure") {
            cat.add_item("Revenue");
            cat.add_item("Zero");
            cat.add_item("Result");
        }
        m.set_cell(
            coord(&[("_Measure", "Revenue"), ("Region", "East")]),
            CellValue::Number(100.0),
        );
        m.set_cell(
            coord(&[("_Measure", "Zero"), ("Region", "East")]),
            CellValue::Number(0.0),
        );
        m.add_formula(parse_formula("Result = Revenue / Zero", "_Measure").unwrap());
        // Division by zero yields an error, not a blank or misleading zero.
        assert_eq!(
            m.evaluate(&coord(&[("_Measure", "Result"), ("Region", "East")])),
            Some(CellValue::Error("div/0".into()))
        );
    }

    #[test]
    fn unary_minus() {
        let mut m = revenue_cost_model();
        m.add_formula(parse_formula("NegRevenue = -Revenue", "_Measure").unwrap());
        if let Some(cat) = m.category_mut("_Measure") {
            cat.add_item("NegRevenue");
        }
        let k = coord(&[("_Measure", "NegRevenue"), ("Region", "East")]);
        assert_eq!(m.evaluate(&k), Some(CellValue::Number(-1000.0)));
    }

    #[test]
    fn power_operator() {
        let mut m = Model::new("Test");
        m.add_category("_Measure").unwrap();
        if let Some(cat) = m.category_mut("_Measure") {
            cat.add_item("Base");
            cat.add_item("Squared");
        }
        m.set_cell(coord(&[("_Measure", "Base")]), CellValue::Number(4.0));
        m.add_formula(parse_formula("Squared = Base ^ 2", "_Measure").unwrap());
        assert_eq!(
            m.evaluate(&coord(&[("_Measure", "Squared")])),
            Some(CellValue::Number(16.0))
        );
    }

    #[test]
    fn formula_with_missing_ref_returns_empty() {
        let mut m = revenue_cost_model();
        m.add_formula(parse_formula("Ghost = NoSuchField - Cost", "_Measure").unwrap());
        if let Some(cat) = m.category_mut("_Measure") {
            cat.add_item("Ghost");
        }
        let k = coord(&[("_Measure", "Ghost"), ("Region", "East")]);
        assert!(
            matches!(m.evaluate(&k), Some(CellValue::Error(_))),
            "missing ref should produce an error, got: {:?}",
            m.evaluate(&k)
        );
    }

    /// Circular formula references must produce an error, not stack overflow.
    #[test]
    fn circular_formula_returns_error() {
        let mut m = Model::new("Test");
        m.add_category("_Measure").unwrap();
        if let Some(cat) = m.category_mut("_Measure") {
            cat.add_item("A");
            cat.add_item("B");
        }
        m.add_formula(parse_formula("A = B + 1", "_Measure").unwrap());
        m.add_formula(parse_formula("B = A + 1", "_Measure").unwrap());
        let result = m.evaluate(&coord(&[("_Measure", "A")]));
        assert!(
            matches!(result, Some(CellValue::Error(_))),
            "circular reference should produce an error, got: {:?}",
            result
        );
    }

    /// Self-referencing formula must produce an error.
    #[test]
    fn self_referencing_formula_returns_error() {
        let mut m = Model::new("Test");
        m.add_category("_Measure").unwrap();
        if let Some(cat) = m.category_mut("_Measure") {
            cat.add_item("X");
        }
        m.add_formula(parse_formula("X = X + 1", "_Measure").unwrap());
        let result = m.evaluate(&coord(&[("_Measure", "X")]));
        assert!(
            matches!(result, Some(CellValue::Error(_))),
            "self-reference should produce an error, got: {:?}",
            result
        );
    }

    #[test]
    fn formula_where_applied_to_matching_region() {
        let mut m = revenue_cost_model();
        m.add_formula(
            parse_formula("EastOnly = Revenue WHERE Region = \"East\"", "_Measure").unwrap(),
        );
        if let Some(cat) = m.category_mut("_Measure") {
            cat.add_item("EastOnly");
        }
        let val = m
            .evaluate(&coord(&[("_Measure", "EastOnly"), ("Region", "East")]))
            .and_then(|v| v.as_f64())
            .unwrap();
        assert!(approx_eq(val, 1000.0));
    }

    #[test]
    fn formula_where_not_applied_to_non_matching_region() {
        let mut m = revenue_cost_model();
        m.add_formula(
            parse_formula("EastOnly = Revenue WHERE Region = \"East\"", "_Measure").unwrap(),
        );
        if let Some(cat) = m.category_mut("_Measure") {
            cat.add_item("EastOnly");
        }
        assert_eq!(
            m.evaluate(&coord(&[("_Measure", "EastOnly"), ("Region", "West")])),
            None
        );
    }

    #[test]
    fn add_formula_replaces_same_target() {
        let mut m = revenue_cost_model();
        m.add_formula(parse_formula("Profit = Revenue - Cost", "_Measure").unwrap());
        m.add_formula(parse_formula("Profit = Revenue - Cost - 100", "_Measure").unwrap());
        assert_eq!(m.formulas.len(), 1);
        let k = coord(&[("_Measure", "Profit"), ("Region", "East")]);
        assert_eq!(m.evaluate(&k), Some(CellValue::Number(300.0)));
    }

    #[test]
    fn remove_formula() {
        let mut m = revenue_cost_model();
        m.add_formula(parse_formula("Profit = Revenue - Cost", "_Measure").unwrap());
        m.remove_formula("Profit", "_Measure");
        assert!(m.formulas.is_empty());
        let k = coord(&[("_Measure", "Profit"), ("Region", "East")]);
        assert_eq!(m.evaluate(&k), None);
    }

    #[test]
    fn sum_aggregation_across_region() {
        let mut m = revenue_cost_model();
        m.add_formula(parse_formula("Total = SUM(Revenue)", "_Measure").unwrap());
        if let Some(cat) = m.category_mut("_Measure") {
            cat.add_item("Total");
        }
        let val = m
            .evaluate(&coord(&[("_Measure", "Total"), ("Region", "East")]))
            .and_then(|v| v.as_f64())
            .unwrap();
        // Revenue(East)=1000 only — Cost must not be included
        assert_eq!(val, 1000.0);
    }

    #[test]
    fn count_aggregation() {
        let mut m = Model::new("Test");
        m.add_category("_Measure").unwrap();
        m.add_category("Region").unwrap();
        if let Some(cat) = m.category_mut("_Measure") {
            cat.add_item("Sales");
            cat.add_item("Count");
        }
        for region in ["East", "West", "North"] {
            m.set_cell(
                coord(&[("_Measure", "Sales"), ("Region", region)]),
                CellValue::Number(100.0),
            );
        }
        m.add_formula(parse_formula("Count = COUNT(Sales)", "_Measure").unwrap());
        let val = m
            .evaluate(&coord(&[("_Measure", "Count"), ("Region", "East")]))
            .and_then(|v| v.as_f64())
            .unwrap();
        assert!(val >= 1.0);
    }

    #[test]
    fn if_true_branch() {
        let mut m = Model::new("Test");
        m.add_category("_Measure").unwrap();
        if let Some(cat) = m.category_mut("_Measure") {
            cat.add_item("X");
            cat.add_item("Result");
        }
        m.set_cell(coord(&[("_Measure", "X")]), CellValue::Number(10.0));
        m.add_formula(parse_formula("Result = IF(X > 5, 1, 0)", "_Measure").unwrap());
        assert_eq!(
            m.evaluate(&coord(&[("_Measure", "Result")])),
            Some(CellValue::Number(1.0))
        );
    }

    #[test]
    fn if_false_branch() {
        let mut m = Model::new("Test");
        m.add_category("_Measure").unwrap();
        if let Some(cat) = m.category_mut("_Measure") {
            cat.add_item("X");
            cat.add_item("Result");
        }
        m.set_cell(coord(&[("_Measure", "X")]), CellValue::Number(3.0));
        m.add_formula(parse_formula("Result = IF(X > 5, 1, 0)", "_Measure").unwrap());
        assert_eq!(
            m.evaluate(&coord(&[("_Measure", "Result")])),
            Some(CellValue::Number(0.0))
        );
    }

    // ── Bug regression tests ─────────────────────────────────────────────────

    /// Bug: WHERE filter falls through when its category is absent from the key.
    /// A formula `Profit = 42 WHERE Region = "East"` evaluated against a key
    /// with no Region coordinate should NOT apply the formula — the WHERE
    /// condition cannot be satisfied, so the raw cell value (Empty) must be
    /// returned.  Currently the `if let Some(item_val)` in eval_formula fails
    /// to bind (category absent → None) and falls through, applying the formula
    /// unconditionally.
    #[test]
    fn where_filter_absent_category_does_not_apply_formula() {
        let mut m = Model::new("Test");
        m.add_category("_Measure").unwrap();
        m.add_category("Region").unwrap();
        if let Some(cat) = m.category_mut("_Measure") {
            cat.add_item("Profit");
        }
        if let Some(cat) = m.category_mut("Region") {
            cat.add_item("East");
        }
        // Formula only applies WHERE Region = "East"
        m.add_formula(parse_formula("Profit = 42 WHERE Region = \"East\"", "_Measure").unwrap());
        // Key has no Region coordinate — WHERE clause cannot be satisfied
        let key_no_region = coord(&[("_Measure", "Profit")]);
        // Expected: Empty (formula should not apply)
        // Bug: returns Number(42) — formula applied because absent category falls through
        assert_eq!(m.evaluate(&key_no_region), None);
    }

    /// Bug: SUM(Revenue) ignores its inner expression and sums all numeric
    /// cells matching the partial key, including unrelated items (e.g. Cost).
    /// With Revenue=100 and Cost=50 both stored for Region=East, evaluating
    /// `Total = SUM(Revenue)` at {Measure=Total, Region=East} should return
    /// 100 (only Revenue), not 150 (Revenue + Cost).
    #[test]
    fn sum_inner_expression_constrains_which_cells_are_summed() {
        let mut m = Model::new("Test");
        m.add_category("_Measure").unwrap();
        m.add_category("Region").unwrap();
        if let Some(cat) = m.category_mut("_Measure") {
            cat.add_item("Revenue");
            cat.add_item("Cost");
            cat.add_item("Total");
        }
        if let Some(cat) = m.category_mut("Region") {
            cat.add_item("East");
        }
        m.set_cell(
            coord(&[("_Measure", "Revenue"), ("Region", "East")]),
            CellValue::Number(100.0),
        );
        m.set_cell(
            coord(&[("_Measure", "Cost"), ("Region", "East")]),
            CellValue::Number(50.0),
        );
        m.add_formula(parse_formula("Total = SUM(Revenue)", "_Measure").unwrap());
        // Expected: 100 (SUM constrainted to Revenue only)
        // Bug: returns 150 — inner Ref("Revenue") is ignored, Cost is also summed
        assert_eq!(
            m.evaluate(&coord(&[("_Measure", "Total"), ("Region", "East")])),
            Some(CellValue::Number(100.0)),
        );
    }

    /// Bug: add_formula deduplicates by `target` name alone, ignoring
    /// `target_category`.  Two formulas for the same item name in different
    /// categories should coexist; adding the second should not silently
    /// replace the first.
    #[test]
    fn add_formula_same_target_name_different_category_both_coexist() {
        let mut m = Model::new("Test");
        m.add_category("_Measure").unwrap();
        m.add_category("KPI").unwrap();
        if let Some(c) = m.category_mut("_Measure") {
            c.add_item("Profit");
        }
        if let Some(c) = m.category_mut("KPI") {
            c.add_item("Profit");
        }

        m.add_formula(parse_formula("Profit = 1", "_Measure").unwrap());
        m.add_formula(parse_formula("Profit = 2", "KPI").unwrap());

        // Both formulas target different categories — they must coexist.
        // Bug: len == 1 because the second replaced the first.
        assert_eq!(m.formulas.len(), 2);
    }

    /// Consequence of the same bug: evaluating the formula that was silently
    /// dropped returns Empty instead of the expected computed value.
    #[test]
    fn add_formula_same_target_name_different_category_evaluates_independently() {
        let mut m = Model::new("Test");
        m.add_category("_Measure").unwrap();
        m.add_category("KPI").unwrap();
        if let Some(c) = m.category_mut("_Measure") {
            c.add_item("Profit");
        }
        if let Some(c) = m.category_mut("KPI") {
            c.add_item("Profit");
        }

        m.add_formula(parse_formula("Profit = 1", "_Measure").unwrap());
        m.add_formula(parse_formula("Profit = 2", "KPI").unwrap());

        // Measure formula → 1, KPI formula → 2
        // Bug: first formula was replaced; {Measure=Profit} evaluates to Empty.
        assert_eq!(
            m.evaluate(&coord(&[("_Measure", "Profit")])),
            Some(CellValue::Number(1.0))
        );
        assert_eq!(
            m.evaluate(&coord(&[("KPI", "Profit")])),
            Some(CellValue::Number(2.0))
        );
    }

    // ── Precision guarantee: formulas compute at full f64 precision ────────

    /// Display rounds to the view's decimal setting, but the underlying
    /// model must keep full f64 precision so chained calculations don't
    /// accumulate rounding errors.
    #[test]
    fn formula_chain_preserves_full_precision() {
        let mut m = Model::new("Test");
        m.add_category("_Measure").unwrap();
        if let Some(cat) = m.category_mut("_Measure") {
            cat.add_item("Price");
            cat.add_item("Tax");
            cat.add_item("Total");
        }
        // Price = 10.0, Tax = Price * 0.075 = 0.75, Total = Price + Tax = 10.75
        m.set_cell(coord(&[("_Measure", "Price")]), CellValue::Number(10.0));
        m.add_formula(parse_formula("Tax = Price * 0.075", "_Measure").unwrap());
        m.add_formula(parse_formula("Total = Price + Tax", "_Measure").unwrap());

        let tax = m
            .evaluate(&coord(&[("_Measure", "Tax")]))
            .and_then(|v| v.as_f64())
            .unwrap();
        let total = m
            .evaluate(&coord(&[("_Measure", "Total")]))
            .and_then(|v| v.as_f64())
            .unwrap();

        // Full precision: Tax is exactly 0.75, Total is exactly 10.75
        assert!(
            approx_eq(tax, 0.75),
            "Tax should be 0.75 (full prec), got {tax}"
        );
        assert!(
            approx_eq(total, 10.75),
            "Total should be 10.75 (full prec), got {total}"
        );

        // If display rounded Tax to 0 decimals (showing "1"), and the formula
        // used that rounded value, Total would be 11 instead of 10.75.
        // This proves the formula uses the raw f64, not the display string.
        use crate::format::format_f64;
        let tax_display = format_f64(tax, true, 0);
        assert_eq!(tax_display, "1", "display rounds 0.75 → 1");
        // But the computed Total is 10.75, not 10 + 1 = 11
        assert!(
            approx_eq(total, 10.75),
            "Total must use full-precision Tax (0.75), not display-rounded (1)"
        );
    }

    /// Bug: remove_formula matches by target name alone, so removing "Profit"
    /// in "_Measure" also destroys the "Profit" formula in "KPI".
    /// After targeted removal, the other category's formula must survive.
    #[test]
    fn remove_formula_only_removes_specified_target_category() {
        let mut m = Model::new("Test");
        m.add_category("_Measure").unwrap();
        m.add_category("KPI").unwrap();
        if let Some(c) = m.category_mut("_Measure") {
            c.add_item("Profit");
        }
        if let Some(c) = m.category_mut("KPI") {
            c.add_item("Profit");
        }

        m.add_formula(parse_formula("Profit = 1", "_Measure").unwrap());
        m.add_formula(parse_formula("Profit = 2", "KPI").unwrap());

        // Remove only the Measure formula
        m.remove_formula("Profit", "_Measure");

        // KPI formula must survive
        // Bug: remove_formula("Profit") wipes both; formulas.len() == 0
        assert_eq!(m.formulas.len(), 1);
        assert_eq!(
            m.evaluate(&coord(&[("KPI", "Profit")])),
            Some(CellValue::Number(2.0))
        );
    }
}

#[cfg(test)]
mod five_category {
    use super::Model;
    use crate::formula::parse_formula;
    use crate::model::cell::{CellKey, CellValue};
    use crate::view::Axis;
    use crate::workbook::Workbook;

    const DATA: &[(&str, &str, &str, &str, f64, f64)] = &[
        ("East", "Shirts", "Online", "Q1", 1_000.0, 600.0),
        ("East", "Shirts", "Online", "Q2", 1_200.0, 700.0),
        ("East", "Shirts", "Retail", "Q1", 800.0, 500.0),
        ("East", "Shirts", "Retail", "Q2", 900.0, 540.0),
        ("East", "Pants", "Online", "Q1", 500.0, 300.0),
        ("East", "Pants", "Online", "Q2", 600.0, 360.0),
        ("East", "Pants", "Retail", "Q1", 400.0, 240.0),
        ("East", "Pants", "Retail", "Q2", 450.0, 270.0),
        ("West", "Shirts", "Online", "Q1", 700.0, 420.0),
        ("West", "Shirts", "Online", "Q2", 750.0, 450.0),
        ("West", "Shirts", "Retail", "Q1", 600.0, 360.0),
        ("West", "Shirts", "Retail", "Q2", 650.0, 390.0),
        ("West", "Pants", "Online", "Q1", 300.0, 180.0),
        ("West", "Pants", "Online", "Q2", 350.0, 210.0),
        ("West", "Pants", "Retail", "Q1", 250.0, 150.0),
        ("West", "Pants", "Retail", "Q2", 280.0, 168.0),
    ];

    fn coord(region: &str, product: &str, channel: &str, time: &str, measure: &str) -> CellKey {
        CellKey::new(vec![
            ("Channel".to_string(), channel.to_string()),
            ("_Measure".to_string(), measure.to_string()),
            ("Product".to_string(), product.to_string()),
            ("Region".to_string(), region.to_string()),
            ("Time".to_string(), time.to_string()),
        ])
    }

    fn build_model() -> Model {
        let mut m = Model::new("Sales");
        for cat in ["Region", "Product", "Channel", "Time", "_Measure"] {
            m.add_category(cat).unwrap();
        }
        for cat in ["Region", "Product", "Channel", "Time"] {
            let items: &[&str] = match cat {
                "Region" => &["East", "West"],
                "Product" => &["Shirts", "Pants"],
                "Channel" => &["Online", "Retail"],
                "Time" => &["Q1", "Q2"],
                _ => &[],
            };
            if let Some(c) = m.category_mut(cat) {
                for &item in items {
                    c.add_item(item);
                }
            }
        }
        if let Some(c) = m.category_mut("_Measure") {
            for &item in &["Revenue", "Cost", "Profit", "Margin", "Total"] {
                c.add_item(item);
            }
        }
        for &(region, product, channel, time, rev, cost) in DATA {
            m.set_cell(
                coord(region, product, channel, time, "Revenue"),
                CellValue::Number(rev),
            );
            m.set_cell(
                coord(region, product, channel, time, "Cost"),
                CellValue::Number(cost),
            );
        }
        m.add_formula(parse_formula("Profit = Revenue - Cost", "_Measure").unwrap());
        m.add_formula(parse_formula("Margin = Profit / Revenue", "_Measure").unwrap());
        m.add_formula(parse_formula("Total  = SUM(Revenue)", "_Measure").unwrap());
        m
    }

    /// Build a Workbook whose model matches `build_model()`. Used by the
    /// view-management tests in this module: view state lives on Workbook,
    /// not Model, so those tests need the wrapper.
    fn build_workbook() -> Workbook {
        let mut wb = Workbook::new("Sales");
        for cat in ["Region", "Product", "Channel", "Time", "_Measure"] {
            wb.add_category(cat).unwrap();
        }
        for cat in ["Region", "Product", "Channel", "Time"] {
            let items: &[&str] = match cat {
                "Region" => &["East", "West"],
                "Product" => &["Shirts", "Pants"],
                "Channel" => &["Online", "Retail"],
                "Time" => &["Q1", "Q2"],
                _ => &[],
            };
            if let Some(c) = wb.model.category_mut(cat) {
                for &item in items {
                    c.add_item(item);
                }
            }
        }
        if let Some(c) = wb.model.category_mut("_Measure") {
            for &item in &["Revenue", "Cost", "Profit", "Margin", "Total"] {
                c.add_item(item);
            }
        }
        for &(region, product, channel, time, rev, cost) in DATA {
            wb.model.set_cell(
                coord(region, product, channel, time, "Revenue"),
                CellValue::Number(rev),
            );
            wb.model.set_cell(
                coord(region, product, channel, time, "Cost"),
                CellValue::Number(cost),
            );
        }
        wb.model
            .add_formula(parse_formula("Profit = Revenue - Cost", "_Measure").unwrap());
        wb.model
            .add_formula(parse_formula("Margin = Profit / Revenue", "_Measure").unwrap());
        wb.model
            .add_formula(parse_formula("Total  = SUM(Revenue)", "_Measure").unwrap());
        wb
    }

    fn approx(a: f64, b: f64) -> bool {
        (a - b).abs() < 1e-9
    }

    #[test]
    fn all_sixteen_revenue_cells_stored() {
        let m = build_model();
        let count = DATA
            .iter()
            .filter(|&&(r, p, c, t, _, _)| m.get_cell(&coord(r, p, c, t, "Revenue")).is_some())
            .count();
        assert_eq!(count, 16);
    }

    #[test]
    fn all_sixteen_cost_cells_stored() {
        let m = build_model();
        let count = DATA
            .iter()
            .filter(|&&(r, p, c, t, _, _)| m.get_cell(&coord(r, p, c, t, "Cost")).is_some())
            .count();
        assert_eq!(count, 16);
    }

    #[test]
    fn spot_check_raw_revenue() {
        let m = build_model();
        assert_eq!(
            m.get_cell(&coord("East", "Shirts", "Online", "Q1", "Revenue")),
            Some(&CellValue::Number(1_000.0))
        );
        assert_eq!(
            m.get_cell(&coord("West", "Pants", "Retail", "Q2", "Revenue")),
            Some(&CellValue::Number(280.0))
        );
    }

    #[test]
    fn distinct_cells_do_not_alias() {
        let m = build_model();
        let a = m.get_cell(&coord("East", "Shirts", "Online", "Q1", "Revenue"));
        let b = m.get_cell(&coord("West", "Pants", "Retail", "Q2", "Revenue"));
        assert_ne!(a, b);
    }

    #[test]
    fn profit_formula_correct_at_every_intersection() {
        let m = build_model();
        for &(region, product, channel, time, rev, cost) in DATA {
            let expected = rev - cost;
            let actual = m
                .evaluate(&coord(region, product, channel, time, "Profit"))
                .and_then(|v| v.as_f64())
                .unwrap_or_else(|| panic!("Profit empty at {region}/{product}/{channel}/{time}"));
            assert!(
                approx(actual, expected),
                "Profit at {region}/{product}/{channel}/{time}: expected {expected}, got {actual}"
            );
        }
    }

    #[test]
    fn margin_formula_correct_at_every_intersection() {
        let m = build_model();
        for &(region, product, channel, time, rev, cost) in DATA {
            let expected = (rev - cost) / rev;
            let actual = m
                .evaluate(&coord(region, product, channel, time, "Margin"))
                .and_then(|v| v.as_f64())
                .unwrap_or_else(|| panic!("Margin empty at {region}/{product}/{channel}/{time}"));
            assert!(
                approx(actual, expected),
                "Margin at {region}/{product}/{channel}/{time}: expected {expected:.4}, got {actual:.4}"
            );
        }
    }

    #[test]
    fn chained_formula_profit_feeds_margin() {
        let m = build_model();
        let margin = m
            .evaluate(&coord("East", "Shirts", "Online", "Q1", "Margin"))
            .and_then(|v| v.as_f64())
            .unwrap();
        assert!(approx(margin, 0.4), "expected 0.4, got {margin}");
    }

    #[test]
    fn update_revenue_updates_profit_and_margin() {
        let mut m = build_model();
        m.set_cell(
            coord("East", "Shirts", "Online", "Q1", "Revenue"),
            CellValue::Number(1_500.0),
        );
        let profit = m
            .evaluate(&coord("East", "Shirts", "Online", "Q1", "Profit"))
            .and_then(|v| v.as_f64())
            .unwrap();
        assert!(approx(profit, 900.0), "expected 900, got {profit}");
        let margin = m
            .evaluate(&coord("East", "Shirts", "Online", "Q1", "Margin"))
            .and_then(|v| v.as_f64())
            .unwrap();
        assert!(approx(margin, 0.6), "expected 0.6, got {margin}");
    }

    #[test]
    fn sum_revenue_for_east_region() {
        let m = build_model();
        let key = CellKey::new(vec![
            ("_Measure".to_string(), "Total".to_string()),
            ("Region".to_string(), "East".to_string()),
        ]);
        let total = m.evaluate(&key).and_then(|v| v.as_f64()).unwrap();
        let expected: f64 = DATA
            .iter()
            .filter(|&&(r, _, _, _, _, _)| r == "East")
            .map(|&(_, _, _, _, rev, _)| rev)
            .sum();
        assert!(approx(total, expected), "expected {expected}, got {total}");
    }

    #[test]
    fn sum_revenue_for_online_channel() {
        let m = build_model();
        let key = CellKey::new(vec![
            ("Channel".to_string(), "Online".to_string()),
            ("_Measure".to_string(), "Total".to_string()),
        ]);
        let total = m.evaluate(&key).and_then(|v| v.as_f64()).unwrap();
        let expected: f64 = DATA
            .iter()
            .filter(|&&(_, _, ch, _, _, _)| ch == "Online")
            .map(|&(_, _, _, _, rev, _)| rev)
            .sum();
        assert!(approx(total, expected), "expected {expected}, got {total}");
    }

    #[test]
    fn sum_revenue_for_shirts_q1() {
        let m = build_model();
        let key = CellKey::new(vec![
            ("_Measure".to_string(), "Total".to_string()),
            ("Product".to_string(), "Shirts".to_string()),
            ("Time".to_string(), "Q1".to_string()),
        ]);
        let total = m.evaluate(&key).and_then(|v| v.as_f64()).unwrap();
        let expected: f64 = DATA
            .iter()
            .filter(|&&(_, p, _, t, _, _)| p == "Shirts" && t == "Q1")
            .map(|&(_, _, _, _, rev, _)| rev)
            .sum();
        assert!(approx(total, expected), "expected {expected}, got {total}");
    }

    #[test]
    fn sum_all_revenue_equals_grand_total() {
        let m = build_model();
        let key = CellKey::new(vec![("_Measure".to_string(), "Total".to_string())]);
        let total = m.evaluate(&key).and_then(|v| v.as_f64()).unwrap();
        let expected: f64 = DATA.iter().map(|&(_, _, _, _, rev, _)| rev).sum();
        assert!(approx(total, expected), "expected {expected}, got {total}");
    }

    #[test]
    fn default_view_first_two_on_axes_rest_on_page() {
        let wb = build_workbook();
        let v = wb.active_view();
        assert_eq!(v.axis_of("Region"), Axis::Row);
        assert_eq!(v.axis_of("Product"), Axis::Column);
        assert_eq!(v.axis_of("Channel"), Axis::Page);
        assert_eq!(v.axis_of("Time"), Axis::Page);
        assert_eq!(v.axis_of("_Measure"), Axis::Page);
    }

    #[test]
    fn rearranging_axes_does_not_affect_data() {
        let mut wb = build_workbook();
        {
            let v = wb.active_view_mut();
            v.set_axis("Region", Axis::Page);
            v.set_axis("Product", Axis::Page);
            v.set_axis("Channel", Axis::Row);
            v.set_axis("Time", Axis::Column);
            v.set_axis("_Measure", Axis::Page);
        }
        assert_eq!(
            wb.model
                .get_cell(&coord("East", "Shirts", "Online", "Q1", "Revenue")),
            Some(&CellValue::Number(1_000.0))
        );
    }

    #[test]
    fn two_views_have_independent_axis_assignments() {
        let mut wb = build_workbook();
        wb.create_view("Pivot");
        {
            let v = wb.views.get_mut("Pivot").unwrap();
            v.set_axis("Time", Axis::Row);
            v.set_axis("Channel", Axis::Column);
            v.set_axis("Region", Axis::Page);
            v.set_axis("Product", Axis::Page);
            v.set_axis("_Measure", Axis::Page);
        }
        assert_eq!(
            wb.views.get("Default").unwrap().axis_of("Region"),
            Axis::Row
        );
        assert_eq!(wb.views.get("Pivot").unwrap().axis_of("Time"), Axis::Row);
        assert_eq!(
            wb.views.get("Pivot").unwrap().axis_of("Channel"),
            Axis::Column
        );
    }

    #[test]
    fn page_selections_are_per_view() {
        let mut wb = build_workbook();
        wb.create_view("West only");
        if let Some(v) = wb.views.get_mut("West only") {
            v.set_page_selection("Region", "West");
        }
        assert_eq!(
            wb.views.get("Default").unwrap().page_selection("Region"),
            None
        );
        assert_eq!(
            wb.views.get("West only").unwrap().page_selection("Region"),
            Some("West")
        );
    }

    #[test]
    fn five_categories_well_within_limit() {
        let m = build_model();
        // 4 regular (Region, Product, Channel, Time) + 3 virtual (_Index, _Dim, _Measure)
        assert_eq!(m.category_names().len(), 7);
        let mut m2 = build_model();
        for i in 0..8 {
            m2.add_category(format!("Extra{i}")).unwrap();
        }
        // 12 regular + 3 virtuals = 15
        assert_eq!(m2.category_names().len(), 15);
        assert!(m2.add_category("OneMore").is_err());
    }
}

#[cfg(test)]
mod prop_tests {
    use super::Model;
    use crate::formula::parse_formula;
    use crate::model::cell::{CellKey, CellValue};
    use proptest::prelude::*;

    fn finite_f64() -> impl Strategy<Value = f64> {
        prop::num::f64::NORMAL.prop_filter("finite", |f| f.is_finite())
    }

    proptest! {
        /// evaluate() on a plain cell (no formula) returns exactly what was stored.
        #[test]
        fn evaluate_returns_stored_value_when_no_formula_applies(
            item_a in "[a-z]{1,6}",
            item_b in "[a-z]{1,6}",
            val in finite_f64(),
        ) {
            let mut m = Model::new("T");
            m.add_category("Cat").unwrap();
            if let Some(c) = m.category_mut("Cat") {
                c.add_item(&item_a);
                c.add_item(&item_b);
            }
            let key = CellKey::new(vec![("Cat".into(), item_a.clone())]);
            m.set_cell(key.clone(), CellValue::Number(val));
            prop_assert_eq!(m.evaluate(&key), Some(CellValue::Number(val)));
        }

        /// Writing to cell A does not change cell B when the keys differ.
        #[test]
        fn cells_with_different_items_are_independent(
            item_a in "[a-z]{1,5}",
            item_b in "[g-z]{1,5}",
            val_a in finite_f64(),
            val_b in finite_f64(),
        ) {
            prop_assume!(item_a != item_b);
            let mut m = Model::new("T");
            m.add_category("Cat").unwrap();
            if let Some(c) = m.category_mut("Cat") {
                c.add_item(&item_a);
                c.add_item(&item_b);
            }
            let key_a = CellKey::new(vec![("Cat".into(), item_a)]);
            let key_b = CellKey::new(vec![("Cat".into(), item_b)]);
            m.set_cell(key_a.clone(), CellValue::Number(val_a));
            m.set_cell(key_b.clone(), CellValue::Number(val_b));
            prop_assert_eq!(m.evaluate(&key_a), Some(CellValue::Number(val_a)));
            prop_assert_eq!(m.evaluate(&key_b), Some(CellValue::Number(val_b)));
        }

        /// Adding a category does not overwrite previously stored cell values.
        #[test]
        fn adding_category_preserves_existing_cells(
            val in finite_f64(),
            new_cat in "[g-z]{1,6}",
        ) {
            let mut m = Model::new("T");
            m.add_category("_Measure").unwrap();
            if let Some(c) = m.category_mut("_Measure") { c.add_item("Revenue"); }
            let key = CellKey::new(vec![("_Measure".into(), "Revenue".into())]);
            m.set_cell(key.clone(), CellValue::Number(val));
            let _ = m.add_category(&new_cat); // may succeed or hit the 12-cat limit
            prop_assert_eq!(m.evaluate(&key), Some(CellValue::Number(val)));
        }

        /// evaluate() is deterministic: calling it twice on the same key and model
        /// yields the same result.
        #[test]
        fn evaluate_is_deterministic(val_rev in finite_f64(), val_cost in finite_f64()) {
            let mut m = Model::new("T");
            m.add_category("_Measure").unwrap();
            m.add_category("Region").unwrap();
            if let Some(c) = m.category_mut("_Measure") {
                c.add_item("Revenue");
                c.add_item("Cost");
                c.add_item("Profit");
            }
            if let Some(c) = m.category_mut("Region") { c.add_item("East"); }
            m.set_cell(
                CellKey::new(vec![("_Measure".into(),"Revenue".into()),("Region".into(),"East".into())]),
                CellValue::Number(val_rev),
            );
            m.set_cell(
                CellKey::new(vec![("_Measure".into(),"Cost".into()),("Region".into(),"East".into())]),
                CellValue::Number(val_cost),
            );
            m.add_formula(parse_formula("Profit = Revenue - Cost", "_Measure").unwrap());
            let key = CellKey::new(vec![
                ("_Measure".into(), "Profit".into()),
                ("Region".into(), "East".into()),
            ]);
            prop_assert_eq!(m.evaluate(&key), m.evaluate(&key));
        }

        /// Formula result equals Revenue − Cost for arbitrary finite inputs.
        #[test]
        fn formula_arithmetic_correct(
            rev in finite_f64(),
            cost in finite_f64(),
        ) {
            let mut m = Model::new("T");
            m.add_category("_Measure").unwrap();
            m.add_category("Region").unwrap();
            if let Some(c) = m.category_mut("_Measure") {
                c.add_item("Revenue");
                c.add_item("Cost");
                c.add_item("Profit");
            }
            if let Some(c) = m.category_mut("Region") { c.add_item("East"); }
            let rev_key = CellKey::new(vec![("_Measure".into(),"Revenue".into()),("Region".into(),"East".into())]);
            let cost_key = CellKey::new(vec![("_Measure".into(),"Cost".into()),("Region".into(),"East".into())]);
            let profit_key = CellKey::new(vec![("_Measure".into(),"Profit".into()),("Region".into(),"East".into())]);
            m.set_cell(rev_key, CellValue::Number(rev));
            m.set_cell(cost_key, CellValue::Number(cost));
            m.add_formula(parse_formula("Profit = Revenue - Cost", "_Measure").unwrap());
            let expected = rev - cost;
            prop_assert_eq!(m.evaluate(&profit_key), Some(CellValue::Number(expected)));
        }

        /// Removing a formula restores the raw stored value (or Empty).
        #[test]
        fn removing_formula_restores_raw_value(rev in finite_f64(), cost in finite_f64()) {
            let mut m = Model::new("T");
            m.add_category("_Measure").unwrap();
            m.add_category("Region").unwrap();
            if let Some(c) = m.category_mut("_Measure") {
                c.add_item("Revenue"); c.add_item("Cost"); c.add_item("Profit");
            }
            if let Some(c) = m.category_mut("Region") { c.add_item("East"); }
            let profit_key = CellKey::new(vec![
                ("_Measure".into(),"Profit".into()),("Region".into(),"East".into()),
            ]);
            m.set_cell(
                CellKey::new(vec![("_Measure".into(),"Revenue".into()),("Region".into(),"East".into())]),
                CellValue::Number(rev),
            );
            m.set_cell(
                CellKey::new(vec![("_Measure".into(),"Cost".into()),("Region".into(),"East".into())]),
                CellValue::Number(cost),
            );
            m.add_formula(parse_formula("Profit = Revenue - Cost", "_Measure").unwrap());
            // Formula active — result is rev - cost
            let with_formula = m.evaluate(&profit_key);
            prop_assert_eq!(with_formula, Some(CellValue::Number(rev - cost)));
            // Remove formula — cell has no raw value, so Empty
            m.remove_formula("Profit", "_Measure");
            prop_assert_eq!(m.evaluate(&profit_key), None);
        }
    }
}
