use std::collections::HashMap;

use serde_json::{json, Value};

use crate::research::types::{
    EvaluateFinancialModelResult, EvaluatedModelCell, ModelDependencyEdge, ModelFormulaError,
};

#[derive(Debug, Clone, PartialEq)]
enum FormulaValue {
    Number(f64),
    Text(String),
    Bool(bool),
    Blank,
    Array(Vec<FormulaValue>),
}

impl FormulaValue {
    fn as_number(&self) -> Result<f64, String> {
        match self {
            Self::Number(value) => Ok(*value),
            Self::Bool(value) => Ok(if *value { 1.0 } else { 0.0 }),
            Self::Text(value) => value
                .parse::<f64>()
                .map_err(|_| format!("expected number, got '{value}'")),
            Self::Blank => Ok(0.0),
            Self::Array(_) => Err("expected scalar, got range".to_string()),
        }
    }

    fn flatten_numbers(&self) -> Result<Vec<f64>, String> {
        match self {
            Self::Array(values) => values
                .iter()
                .flat_map(|value| match value {
                    FormulaValue::Array(nested) => nested.iter().collect::<Vec<_>>(),
                    scalar => vec![scalar],
                })
                .map(FormulaValue::as_number)
                .collect(),
            scalar => scalar.as_number().map(|value| vec![value]),
        }
    }

    fn to_json(&self) -> Value {
        match self {
            Self::Number(value) => json!(value),
            Self::Text(value) => json!(value),
            Self::Bool(value) => json!(value),
            Self::Blank => Value::Null,
            Self::Array(values) => Value::Array(values.iter().map(FormulaValue::to_json).collect()),
        }
    }
}

#[derive(Debug, Clone)]
struct Sheet {
    id: String,
    name: String,
    cells: HashMap<String, Value>,
}

#[derive(Debug)]
struct WorkbookFormulaContext {
    sheets: Vec<Sheet>,
    sheet_index: HashMap<String, usize>,
    evaluated: HashMap<(String, String), FormulaValue>,
    dependency_graph: Vec<ModelDependencyEdge>,
    formula_errors: Vec<ModelFormulaError>,
}

pub fn evaluate_workbook(workbook_json: &Value) -> EvaluateFinancialModelResult {
    let mut context = WorkbookFormulaContext::from_workbook(workbook_json);
    let refs = context
        .sheets
        .iter()
        .flat_map(|sheet| {
            sheet
                .cells
                .keys()
                .map(|address| (sheet.id.clone(), address.clone()))
                .collect::<Vec<_>>()
        })
        .collect::<Vec<_>>();

    for (sheet_id, address) in refs {
        let mut stack = Vec::new();
        let _ = context.evaluate_cell(&sheet_id, &address, &mut stack);
    }

    let evaluated_cells = context
        .evaluated
        .into_iter()
        .map(|((sheet_id, address), value)| EvaluatedModelCell {
            sheet_id,
            address,
            value: value.to_json(),
        })
        .collect();

    EvaluateFinancialModelResult {
        evaluated_cells,
        formula_errors: context.formula_errors,
        dependency_graph: context.dependency_graph,
    }
}

impl WorkbookFormulaContext {
    fn from_workbook(workbook_json: &Value) -> Self {
        let sheets = workbook_json
            .get("sheets")
            .and_then(Value::as_array)
            .into_iter()
            .flatten()
            .filter_map(|sheet| {
                let id = sheet.get("id")?.as_str()?.to_string();
                let name = sheet
                    .get("name")
                    .and_then(Value::as_str)
                    .unwrap_or(id.as_str())
                    .to_string();
                let cells = sheet
                    .get("cells")
                    .and_then(Value::as_object)
                    .map(|cells| {
                        cells
                            .iter()
                            .map(|(address, cell)| (normalize_address(address), cell.clone()))
                            .collect::<HashMap<_, _>>()
                    })
                    .unwrap_or_default();
                Some(Sheet { id, name, cells })
            })
            .collect::<Vec<_>>();

        let mut sheet_index = HashMap::new();
        for (index, sheet) in sheets.iter().enumerate() {
            sheet_index.insert(sheet.id.to_ascii_lowercase(), index);
            sheet_index.insert(sheet.name.to_ascii_lowercase(), index);
        }

        Self {
            sheets,
            sheet_index,
            evaluated: HashMap::new(),
            dependency_graph: Vec::new(),
            formula_errors: Vec::new(),
        }
    }

    fn evaluate_cell(
        &mut self,
        sheet_id: &str,
        address: &str,
        stack: &mut Vec<(String, String)>,
    ) -> Result<FormulaValue, String> {
        let key = (sheet_id.to_string(), normalize_address(address));
        if let Some(value) = self.evaluated.get(&key) {
            return Ok(value.clone());
        }
        if stack.contains(&key) {
            let message = format!("circular reference at {}!{}", key.0, key.1);
            self.formula_errors.push(ModelFormulaError {
                sheet_id: key.0,
                address: key.1,
                formula: String::new(),
                message: message.clone(),
            });
            return Err(message);
        }

        let cell = self
            .sheet(sheet_id)
            .and_then(|sheet| sheet.cells.get(&key.1).cloned())
            .unwrap_or(Value::Null);
        let value = if let Some(formula) = cell.get("formula").and_then(Value::as_str) {
            let expression = formula.strip_prefix('=').unwrap_or(formula);
            stack.push(key.clone());
            let result = self.evaluate_expression(sheet_id, expression, &key.1, stack);
            stack.pop();
            match result {
                Ok(value) => value,
                Err(message) => {
                    self.formula_errors.push(ModelFormulaError {
                        sheet_id: sheet_id.to_string(),
                        address: key.1.clone(),
                        formula: formula.to_string(),
                        message: message.clone(),
                    });
                    return Err(message);
                }
            }
        } else {
            json_to_formula_value(cell.get("value").unwrap_or(&Value::Null))
        };

        self.evaluated.insert(key, value.clone());
        Ok(value)
    }

    fn evaluate_expression(
        &mut self,
        current_sheet_id: &str,
        expression: &str,
        current_address: &str,
        stack: &mut Vec<(String, String)>,
    ) -> Result<FormulaValue, String> {
        let mut parser = Parser::new(expression);
        let ast = parser.parse_expression()?;
        parser.expect_end()?;
        self.evaluate_ast(current_sheet_id, current_address, &ast, stack)
    }

    fn evaluate_ast(
        &mut self,
        current_sheet_id: &str,
        current_address: &str,
        ast: &Ast,
        stack: &mut Vec<(String, String)>,
    ) -> Result<FormulaValue, String> {
        match ast {
            Ast::Number(value) => Ok(FormulaValue::Number(*value)),
            Ast::Text(value) => Ok(FormulaValue::Text(value.clone())),
            Ast::Cell(reference) => {
                self.evaluate_reference(current_sheet_id, current_address, reference, stack)
            }
            Ast::Range(reference) => {
                self.evaluate_range(current_sheet_id, current_address, reference, stack)
            }
            Ast::Unary { op, expr } => {
                let value = self
                    .evaluate_ast(current_sheet_id, current_address, expr, stack)?
                    .as_number()?;
                Ok(FormulaValue::Number(if *op == '-' {
                    -value
                } else {
                    value
                }))
            }
            Ast::Binary { op, left, right } => {
                let left = self
                    .evaluate_ast(current_sheet_id, current_address, left, stack)?
                    .as_number()?;
                let right = self
                    .evaluate_ast(current_sheet_id, current_address, right, stack)?
                    .as_number()?;
                let value = match op {
                    '+' => left + right,
                    '-' => left - right,
                    '*' => left * right,
                    '/' if right.abs() < f64::EPSILON => return Err("division by zero".to_string()),
                    '/' => left / right,
                    _ => return Err(format!("unsupported operator {op}")),
                };
                Ok(FormulaValue::Number(value))
            }
            Ast::Function { name, args } => {
                let values = args
                    .iter()
                    .map(|arg| self.evaluate_ast(current_sheet_id, current_address, arg, stack))
                    .collect::<Result<Vec<_>, _>>()?;
                evaluate_function(name, &values)
            }
        }
    }

    fn evaluate_reference(
        &mut self,
        current_sheet_id: &str,
        current_address: &str,
        reference: &CellReference,
        stack: &mut Vec<(String, String)>,
    ) -> Result<FormulaValue, String> {
        let target_sheet = self.resolve_sheet(current_sheet_id, reference.sheet.as_deref())?;
        self.dependency_graph.push(ModelDependencyEdge {
            from_sheet_id: current_sheet_id.to_string(),
            from_address: normalize_address(current_address),
            to_sheet_id: target_sheet.clone(),
            to_address: normalize_address(&reference.address),
        });
        self.evaluate_cell(&target_sheet, &reference.address, stack)
    }

    fn evaluate_range(
        &mut self,
        current_sheet_id: &str,
        current_address: &str,
        reference: &RangeReference,
        stack: &mut Vec<(String, String)>,
    ) -> Result<FormulaValue, String> {
        let target_sheet = self.resolve_sheet(current_sheet_id, reference.sheet.as_deref())?;
        let (start_col, start_row) = split_address(&reference.start)?;
        let (end_col, end_row) = split_address(&reference.end)?;
        let mut values = Vec::new();
        for row in start_row.min(end_row)..=start_row.max(end_row) {
            for col in start_col.min(end_col)..=start_col.max(end_col) {
                let address = format!("{}{}", column_index_to_name(col), row);
                self.dependency_graph.push(ModelDependencyEdge {
                    from_sheet_id: current_sheet_id.to_string(),
                    from_address: normalize_address(current_address),
                    to_sheet_id: target_sheet.clone(),
                    to_address: address.clone(),
                });
                values.push(self.evaluate_cell(&target_sheet, &address, stack)?);
            }
        }
        Ok(FormulaValue::Array(values))
    }

    fn resolve_sheet(&self, current_sheet_id: &str, sheet: Option<&str>) -> Result<String, String> {
        let Some(sheet) = sheet else {
            return Ok(current_sheet_id.to_string());
        };
        self.sheet_index
            .get(&sheet.to_ascii_lowercase())
            .map(|index| self.sheets[*index].id.clone())
            .ok_or_else(|| format!("unknown sheet '{sheet}'"))
    }

    fn sheet(&self, sheet_id: &str) -> Option<&Sheet> {
        self.sheet_index
            .get(&sheet_id.to_ascii_lowercase())
            .and_then(|index| self.sheets.get(*index))
    }
}

#[derive(Debug, Clone, PartialEq)]
struct CellReference {
    sheet: Option<String>,
    address: String,
}

#[derive(Debug, Clone, PartialEq)]
struct RangeReference {
    sheet: Option<String>,
    start: String,
    end: String,
}

#[derive(Debug, Clone, PartialEq)]
enum Ast {
    Number(f64),
    Text(String),
    Cell(CellReference),
    Range(RangeReference),
    Unary {
        op: char,
        expr: Box<Ast>,
    },
    Binary {
        op: char,
        left: Box<Ast>,
        right: Box<Ast>,
    },
    Function {
        name: String,
        args: Vec<Ast>,
    },
}

struct Parser<'a> {
    input: &'a str,
    chars: Vec<char>,
    pos: usize,
}

impl<'a> Parser<'a> {
    fn new(input: &'a str) -> Self {
        Self {
            input,
            chars: input.chars().collect(),
            pos: 0,
        }
    }

    fn parse_expression(&mut self) -> Result<Ast, String> {
        self.parse_binary(0)
    }

    fn expect_end(&mut self) -> Result<(), String> {
        self.skip_ws();
        if self.pos == self.chars.len() {
            Ok(())
        } else {
            Err(format!(
                "unexpected token near '{}'",
                &self.input[self.byte_pos()..]
            ))
        }
    }

    fn parse_binary(&mut self, min_precedence: u8) -> Result<Ast, String> {
        let mut left = self.parse_unary()?;
        loop {
            self.skip_ws();
            let Some(op) = self.peek() else { break };
            let precedence = match op {
                '+' | '-' => 1,
                '*' | '/' => 2,
                _ => break,
            };
            if precedence < min_precedence {
                break;
            }
            self.pos += 1;
            let right = self.parse_binary(precedence + 1)?;
            left = Ast::Binary {
                op,
                left: Box::new(left),
                right: Box::new(right),
            };
        }
        Ok(left)
    }

    fn parse_unary(&mut self) -> Result<Ast, String> {
        self.skip_ws();
        if matches!(self.peek(), Some('+') | Some('-')) {
            let op = self.next().unwrap();
            return Ok(Ast::Unary {
                op,
                expr: Box::new(self.parse_unary()?),
            });
        }
        self.parse_primary()
    }

    fn parse_primary(&mut self) -> Result<Ast, String> {
        self.skip_ws();
        match self.peek() {
            Some('"') => self.parse_string().map(Ast::Text),
            Some('0'..='9' | '.') => self.parse_number().map(Ast::Number),
            Some('(') => {
                self.pos += 1;
                let expr = self.parse_expression()?;
                self.consume(')')?;
                Ok(expr)
            }
            Some('\'') => self.parse_reference_or_range(),
            Some(c) if c.is_ascii_alphabetic() || c == '_' || c == '$' => {
                self.parse_identifier_reference_or_function()
            }
            Some(c) => Err(format!("unexpected character '{c}'")),
            None => Err("unexpected end of formula".to_string()),
        }
    }

    fn parse_identifier_reference_or_function(&mut self) -> Result<Ast, String> {
        let start = self.pos;
        let token = self.parse_reference_token()?;
        self.skip_ws();
        if self.peek() == Some('(') && !looks_like_cell_address(&token) {
            self.pos += 1;
            let mut args = Vec::new();
            self.skip_ws();
            if self.peek() != Some(')') {
                loop {
                    args.push(self.parse_expression()?);
                    self.skip_ws();
                    if self.peek() == Some(',') {
                        self.pos += 1;
                        continue;
                    }
                    break;
                }
            }
            self.consume(')')?;
            return Ok(Ast::Function { name: token, args });
        }
        self.pos = start;
        self.parse_reference_or_range()
    }

    fn parse_reference_or_range(&mut self) -> Result<Ast, String> {
        let (sheet, start) = self.parse_qualified_address()?;
        self.skip_ws();
        if self.peek() == Some(':') {
            self.pos += 1;
            let (end_sheet, end) = self.parse_qualified_address()?;
            return Ok(Ast::Range(RangeReference {
                sheet: end_sheet.or(sheet),
                start,
                end,
            }));
        }
        Ok(Ast::Cell(CellReference {
            sheet,
            address: start,
        }))
    }

    fn parse_qualified_address(&mut self) -> Result<(Option<String>, String), String> {
        self.skip_ws();
        let first = if self.peek() == Some('\'') {
            self.parse_quoted_sheet_name()?
        } else {
            self.parse_reference_token()?
        };
        self.skip_ws();
        if self.peek() == Some('!') {
            self.pos += 1;
            let address = self.parse_reference_token()?;
            validate_address(&address)?;
            Ok((Some(first), normalize_address(&address)))
        } else {
            validate_address(&first)?;
            Ok((None, normalize_address(&first)))
        }
    }

    fn parse_quoted_sheet_name(&mut self) -> Result<String, String> {
        self.consume('\'')?;
        let mut value = String::new();
        while let Some(char) = self.next() {
            if char == '\'' {
                return Ok(value);
            }
            value.push(char);
        }
        Err("unterminated quoted sheet name".to_string())
    }

    fn parse_reference_token(&mut self) -> Result<String, String> {
        self.skip_ws();
        let mut token = String::new();
        while let Some(char) = self.peek() {
            if char.is_ascii_alphanumeric() || char == '_' || char == '$' || char == '.' {
                token.push(char);
                self.pos += 1;
            } else {
                break;
            }
        }
        if token.is_empty() {
            Err("expected reference".to_string())
        } else {
            Ok(token)
        }
    }

    fn parse_string(&mut self) -> Result<String, String> {
        self.consume('"')?;
        let mut value = String::new();
        while let Some(char) = self.next() {
            if char == '"' {
                return Ok(value);
            }
            value.push(char);
        }
        Err("unterminated string literal".to_string())
    }

    fn parse_number(&mut self) -> Result<f64, String> {
        let mut value = String::new();
        while let Some(char) = self.peek() {
            if char.is_ascii_digit() || char == '.' {
                value.push(char);
                self.pos += 1;
            } else {
                break;
            }
        }
        value
            .parse::<f64>()
            .map_err(|_| format!("invalid number '{value}'"))
    }

    fn consume(&mut self, expected: char) -> Result<(), String> {
        self.skip_ws();
        if self.peek() == Some(expected) {
            self.pos += 1;
            Ok(())
        } else {
            Err(format!("expected '{expected}'"))
        }
    }

    fn skip_ws(&mut self) {
        while matches!(self.peek(), Some(c) if c.is_ascii_whitespace()) {
            self.pos += 1;
        }
    }

    fn peek(&self) -> Option<char> {
        self.chars.get(self.pos).copied()
    }

    fn next(&mut self) -> Option<char> {
        let value = self.peek()?;
        self.pos += 1;
        Some(value)
    }

    fn byte_pos(&self) -> usize {
        self.chars
            .iter()
            .take(self.pos)
            .map(|char| char.len_utf8())
            .sum()
    }
}

fn evaluate_function(name: &str, values: &[FormulaValue]) -> Result<FormulaValue, String> {
    let upper = name.to_ascii_uppercase();
    let numbers = || -> Result<Vec<f64>, String> {
        let mut output = Vec::new();
        for value in values {
            output.extend(value.flatten_numbers()?);
        }
        Ok(output)
    };
    let number = |index: usize| {
        values
            .get(index)
            .ok_or_else(|| format!("{upper} missing argument {}", index + 1))?
            .as_number()
    };

    match upper.as_str() {
        "SUM" => Ok(FormulaValue::Number(numbers()?.iter().sum())),
        "AVG" | "AVERAGE" => {
            let numbers = numbers()?;
            Ok(FormulaValue::Number(
                numbers.iter().sum::<f64>() / numbers.len() as f64,
            ))
        }
        "MIN" => Ok(FormulaValue::Number(
            numbers()?.into_iter().fold(f64::INFINITY, f64::min),
        )),
        "MAX" => Ok(FormulaValue::Number(
            numbers()?.into_iter().fold(f64::NEG_INFINITY, f64::max),
        )),
        "ROUND" => {
            let decimals = if values.len() > 1 {
                number(1)? as i32
            } else {
                0
            };
            let factor = 10_f64.powi(decimals);
            Ok(FormulaValue::Number((number(0)? * factor).round() / factor))
        }
        "IF" => Ok(if number(0)?.abs() > f64::EPSILON {
            values.get(1).cloned().unwrap_or(FormulaValue::Blank)
        } else {
            values.get(2).cloned().unwrap_or(FormulaValue::Blank)
        }),
        "CAGR" => Ok(FormulaValue::Number(
            (number(1)? / number(0)?).powf(1.0 / number(2)?) - 1.0,
        )),
        "NPV" => {
            let rate = number(0)?;
            let mut cash_flows = Vec::new();
            for value in &values[1..] {
                cash_flows.extend(value.flatten_numbers()?);
            }
            Ok(FormulaValue::Number(
                cash_flows
                    .iter()
                    .enumerate()
                    .map(|(index, value)| value / (1.0 + rate).powi((index + 1) as i32))
                    .sum(),
            ))
        }
        "IRR" | "XIRR" => Ok(FormulaValue::Number(irr(&numbers()?)?)),
        "WACC" => {
            let equity = number(0)?;
            let debt = number(1)?;
            let capital = equity + debt;
            if capital.abs() < f64::EPSILON {
                return Err("WACC capital base is zero".to_string());
            }
            Ok(FormulaValue::Number(
                (equity / capital) * number(2)?
                    + (debt / capital) * number(3)? * (1.0 - number(4)?),
            ))
        }
        "INDEX" => {
            let values = values
                .first()
                .ok_or_else(|| "INDEX missing array".to_string())?
                .flatten_numbers()?;
            let row = number(1)? as usize;
            values
                .get(row.saturating_sub(1))
                .copied()
                .map(FormulaValue::Number)
                .ok_or_else(|| "INDEX out of bounds".to_string())
        }
        "MATCH" => {
            let target = number(0)?;
            let values = values
                .get(1)
                .ok_or_else(|| "MATCH missing lookup array".to_string())?
                .flatten_numbers()?;
            values
                .iter()
                .position(|value| (*value - target).abs() < f64::EPSILON)
                .map(|index| FormulaValue::Number((index + 1) as f64))
                .ok_or_else(|| "MATCH value not found".to_string())
        }
        "VLOOKUP" => {
            let target = number(0)?;
            let values = values
                .get(1)
                .ok_or_else(|| "VLOOKUP missing table array".to_string())?
                .flatten_numbers()?;
            let column_index = number(2)? as usize;
            let width = column_index.max(1);
            for row in values.chunks(width) {
                if row
                    .first()
                    .is_some_and(|value| (*value - target).abs() < f64::EPSILON)
                {
                    return row
                        .get(column_index.saturating_sub(1))
                        .copied()
                        .map(FormulaValue::Number)
                        .ok_or_else(|| "VLOOKUP column out of bounds".to_string());
                }
            }
            Err("VLOOKUP value not found".to_string())
        }
        _ => Err(format!("unknown function {name}")),
    }
}

fn irr(cash_flows: &[f64]) -> Result<f64, String> {
    if cash_flows.len() < 2 {
        return Err("IRR requires at least two cash flows".to_string());
    }
    let mut rate: f64 = 0.1;
    for _ in 0..100 {
        let mut value = 0.0;
        let mut derivative = 0.0;
        for (period, cash_flow) in cash_flows.iter().enumerate() {
            let exponent = period as i32;
            value += cash_flow / (1.0 + rate).powi(exponent);
            if period > 0 {
                derivative -= period as f64 * cash_flow / (1.0 + rate).powi(exponent + 1);
            }
        }
        if derivative.abs() < f64::EPSILON {
            break;
        }
        let next = rate - value / derivative;
        if (next - rate).abs() < 1e-7 {
            return Ok(next);
        }
        rate = next;
    }
    Ok(rate)
}

fn json_to_formula_value(value: &Value) -> FormulaValue {
    match value {
        Value::Number(value) => value
            .as_f64()
            .map(FormulaValue::Number)
            .unwrap_or(FormulaValue::Blank),
        Value::String(value) => FormulaValue::Text(value.clone()),
        Value::Bool(value) => FormulaValue::Bool(*value),
        _ => FormulaValue::Blank,
    }
}

fn normalize_address(address: &str) -> String {
    address.replace('$', "").to_ascii_uppercase()
}

fn looks_like_cell_address(value: &str) -> bool {
    split_address(value).is_ok()
}

fn validate_address(value: &str) -> Result<(), String> {
    split_address(value).map(|_| ())
}

fn split_address(address: &str) -> Result<(usize, usize), String> {
    let normalized = normalize_address(address);
    let mut letters = String::new();
    let mut digits = String::new();
    for char in normalized.chars() {
        if char.is_ascii_alphabetic() && digits.is_empty() {
            letters.push(char);
        } else if char.is_ascii_digit() {
            digits.push(char);
        } else {
            return Err(format!("invalid cell address '{address}'"));
        }
    }
    if letters.is_empty() || digits.is_empty() {
        return Err(format!("invalid cell address '{address}'"));
    }
    let column = letters.chars().fold(0usize, |acc, char| {
        acc * 26 + (char as usize - 'A' as usize + 1)
    });
    Ok((
        column - 1,
        digits
            .parse::<usize>()
            .map_err(|_| "invalid row".to_string())?,
    ))
}

fn column_index_to_name(mut index: usize) -> String {
    let mut name = String::new();
    index += 1;
    while index > 0 {
        let remainder = (index - 1) % 26;
        name.insert(0, (b'A' + remainder as u8) as char);
        index = (index - 1) / 26;
    }
    name
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn evaluates_ranges_and_cross_sheet_references() {
        let workbook = json!({
            "sheets": [
                {"id": "sheet1", "name": "Sheet1", "cells": {
                    "A1": {"value": 1},
                    "A2": {"value": 2},
                    "A3": {"formula": "=SUM(A1:A2)"},
                    "B1": {"formula": "=Sheet2!A1 + 'Revenue Build'!B1"}
                }},
                {"id": "sheet2", "name": "Sheet2", "cells": {"A1": {"value": 3}}},
                {"id": "revenue", "name": "Revenue Build", "cells": {"B1": {"value": 4}}}
            ]
        });

        let result = evaluate_workbook(&workbook);
        assert!(
            result.formula_errors.is_empty(),
            "{:?}",
            result.formula_errors
        );
        assert!(result.evaluated_cells.iter().any(|cell| {
            cell.sheet_id == "sheet1" && cell.address == "A3" && cell.value == json!(3.0)
        }));
        assert!(result.evaluated_cells.iter().any(|cell| {
            cell.sheet_id == "sheet1" && cell.address == "B1" && cell.value == json!(7.0)
        }));
    }

    #[test]
    fn reports_circular_references() {
        let workbook = json!({
            "sheets": [
                {"id": "sheet1", "name": "Sheet1", "cells": {
                    "A1": {"formula": "=A2"},
                    "A2": {"formula": "=A1"}
                }}
            ]
        });

        let result = evaluate_workbook(&workbook);
        assert!(!result.formula_errors.is_empty());
        assert!(result
            .formula_errors
            .iter()
            .any(|error| error.message.contains("circular reference")));
    }
}
