use crate::engine::{CycleConfig, Engine, EvalConfig};
use crate::test_workbook::TestWorkbook;
use formualizer_common::{ExcelErrorKind, LiteralValue};
use formualizer_parse::parser::parse;
fn iterate_engine(max_iterations: u32, max_change: f64) -> Engine<TestWorkbook> {
Engine::new(
TestWorkbook::new(),
EvalConfig::default().with_cycle(CycleConfig::iterate(max_iterations, max_change)),
)
}
fn set_formula(engine: &mut Engine<TestWorkbook>, sheet: &str, row: u32, col: u32, f: &str) {
engine
.set_cell_formula(sheet, row, col, parse(f).expect("parse"))
.expect("set formula");
}
fn set_value(engine: &mut Engine<TestWorkbook>, sheet: &str, row: u32, col: u32, v: LiteralValue) {
engine
.set_cell_value(sheet, row, col, v)
.expect("set value");
}
fn text(engine: &Engine<TestWorkbook>, sheet: &str, row: u32, col: u32) -> String {
match engine.get_cell_value(sheet, row, col) {
Some(LiteralValue::Text(s)) => s,
other => panic!("expected text at {sheet} r{row}c{col}, got {other:?}"),
}
}
fn num(engine: &Engine<TestWorkbook>, sheet: &str, row: u32, col: u32) -> f64 {
match engine.get_cell_value(sheet, row, col) {
Some(LiteralValue::Number(n)) => n,
Some(LiteralValue::Int(i)) => i as f64,
other => panic!("expected number at {sheet} r{row}c{col}, got {other:?}"),
}
}
#[test]
fn self_concat_grows_one_char_per_pass_and_always_caps() {
let mut engine = iterate_engine(10, 0.001);
set_formula(&mut engine, "Sheet1", 1, 1, "=A1&\"x\"");
engine.evaluate_all().unwrap();
assert_eq!(text(&engine, "Sheet1", 1, 1), "x".repeat(10));
let t = engine.last_cycle_telemetry();
assert_eq!(t.iterated_sccs, 1);
assert_eq!(t.converged_sccs, 0);
assert_eq!(t.capped_sccs, 1);
assert_eq!(t.settle_passes_total, 10);
engine.evaluate_all().unwrap();
assert_eq!(text(&engine, "Sheet1", 1, 1), "x".repeat(20));
assert_eq!(engine.last_cycle_telemetry().capped_sccs, 1);
}
#[test]
fn self_concat_with_long_seed_allocates_linearly_per_pass_and_survives() {
let seed = "s".repeat(8 * 1024);
let cap = 64u32;
let mut engine = iterate_engine(cap, 0.001);
set_value(
&mut engine,
"Sheet1",
1,
2,
LiteralValue::Text(seed.clone()),
);
set_formula(&mut engine, "Sheet1", 1, 1, "=A1&B1");
engine.evaluate_all().unwrap();
let got = text(&engine, "Sheet1", 1, 1);
assert_eq!(got.len(), cap as usize * seed.len());
assert!(got.starts_with(&seed) && got.ends_with(&seed));
let t = engine.last_cycle_telemetry();
assert_eq!(t.capped_sccs, 1);
assert_eq!(t.settle_passes_total, cap as usize);
}
#[test]
fn concat_growth_feeds_downstream_len_with_final_value_only() {
let mut engine = iterate_engine(5, 0.001);
set_formula(&mut engine, "Sheet1", 1, 1, "=CONCAT(A1,\"ab\")");
set_formula(&mut engine, "Sheet1", 1, 2, "=LEN(A1)");
engine.evaluate_all().unwrap();
assert_eq!(
num(&engine, "Sheet1", 1, 2),
10.0,
"LEN sees the capped final value (5 passes × 2 chars)"
);
engine.evaluate_all().unwrap();
assert_eq!(
num(&engine, "Sheet1", 1, 2),
20.0,
"second recalc: downstream stays fresh via the iterative redirty"
);
}
#[test]
fn three_way_number_text_error_oscillation_caps_without_panic() {
for cap in [3u32, 4, 5, 6, 7] {
let mut engine = iterate_engine(cap, f64::MAX / 2.0);
set_formula(
&mut engine,
"Sheet1",
1,
1,
"=IF(ISERROR(B1),1,IF(ISNUMBER(B1),\"t\",1/0))",
);
set_formula(&mut engine, "Sheet1", 1, 2, "=A1");
engine.evaluate_all().unwrap();
let t = engine.last_cycle_telemetry();
assert_eq!(t.iterated_sccs, 1, "cap {cap}");
assert_eq!(t.converged_sccs, 0, "cap {cap}: must never converge");
assert_eq!(t.capped_sccs, 1, "cap {cap}");
assert_eq!(t.settle_passes_total, cap as usize, "cap {cap}");
let v = engine.get_cell_value("Sheet1", 1, 1).unwrap();
let phase_ok = matches!(
&v,
LiteralValue::Int(1) | LiteralValue::Number(_) | LiteralValue::Text(_)
) || matches!(&v, LiteralValue::Error(e) if e.kind == ExcelErrorKind::Div);
assert!(phase_ok, "cap {cap}: unexpected phase value {v:?}");
}
}
#[test]
fn boolean_not_cycle_oscillates_and_stops_on_cap_parity() {
for (cap, expected) in [(1u32, true), (2, false), (3, true), (4, false)] {
let mut engine = iterate_engine(cap, 0.001);
set_formula(&mut engine, "Sheet1", 1, 1, "=NOT(B1)");
set_formula(&mut engine, "Sheet1", 1, 2, "=A1");
engine.evaluate_all().unwrap();
assert_eq!(
engine.get_cell_value("Sheet1", 1, 1),
Some(LiteralValue::Boolean(expected)),
"cap {cap}"
);
let t = engine.last_cycle_telemetry();
assert_eq!(t.capped_sccs, 1, "cap {cap}");
assert_eq!(t.converged_sccs, 0, "cap {cap}");
}
}
#[test]
fn empty_producing_cycle_member_converges_on_empty_vs_empty() {
let mut engine = iterate_engine(100, 0.001);
set_formula(&mut engine, "Sheet1", 1, 2, "=IF(C1=0,\"\",C1)");
set_formula(&mut engine, "Sheet1", 1, 3, "=B1*1");
engine.evaluate_all().unwrap();
let t = engine.last_cycle_telemetry();
assert_eq!(t.iterated_sccs, 1);
assert_eq!(
t.converged_sccs, 1,
"stable mixed-type fixed point must converge"
);
assert_eq!(t.capped_sccs, 0);
}