mod token;
mod parser;
mod remap;
mod builders;
mod ngon;
mod userfunc;
mod builtins;
mod classify;

use cord_trig::ir::{NodeId, TrigGraph, TrigOp};
use token::{tokenize, merge_ngon_tokens_with_lines, Token};
use parser::ExprParser;

pub use classify::{classify, classify_from, expr_to_sdf, ExprInfo};
#[allow(unused_imports)]
pub use classify::references_ident;

pub fn parse_expr(input: &str) -> Result<TrigGraph, String> {
    parse_expr_ctx(input, None, None)
}

pub fn parse_expr_ctx(
    input: &str,
    ref_a: Option<&TrigGraph>,
    ref_b: Option<&TrigGraph>,
) -> Result<TrigGraph, String> {
    let (mut tokens, mut token_lines) = tokenize(input)?;
    merge_ngon_tokens_with_lines(&mut tokens, &mut token_lines);
    let source_lines: Vec<&str> = input.lines().collect();
    let mut parser = ExprParser::new(&tokens, &token_lines, &source_lines, ref_a, ref_b);
    let node = parser.parse_program()?;
    parser.graph.set_output(node);
    Ok(parser.graph)
}

pub struct SceneResult {
    pub graph: TrigGraph,
    pub objects: Vec<(String, NodeId)>,
    pub all_vars: Vec<(String, NodeId)>,
    pub casts: Vec<(String, NodeId)>,
    pub cast_all: bool,
    pub plots: Vec<NodeId>,
    pub plot_all: bool,
    pub bare_exprs: Vec<NodeId>,
    pub needs_cast: bool,
    pub needs_plot: bool,
    pub warnings: Vec<String>,
}

pub fn parse_expr_scene(input: &str) -> Result<SceneResult, String> {
    parse_expr_scene_ctx(input, None, None)
}

pub fn parse_expr_scene_ctx(
    input: &str,
    ref_a: Option<&TrigGraph>,
    ref_b: Option<&TrigGraph>,
) -> Result<SceneResult, String> {
    let (mut tokens, mut token_lines) = tokenize(input)?;
    merge_ngon_tokens_with_lines(&mut tokens, &mut token_lines);
    let source_lines: Vec<&str> = input.lines().collect();
    let mut parser = ExprParser::new(&tokens, &token_lines, &source_lines, ref_a, ref_b);
    let node = parser.parse_program()?;
    parser.graph.set_output(node);

    let mut auto_plots: Vec<NodeId> = Vec::new();
    if parser.plot_all {
        let funcs_snapshot: Vec<(String, Vec<String>, Vec<Option<Vec<Token>>>, Vec<Token>)> = parser.funcs.iter()
            .filter(|(_, f)| f.params.len() == 1)
            .map(|(name, f)| (name.clone(), f.params.clone(), f.defaults.clone(), f.body.clone()))
            .collect();
        for (name, params, defaults, body) in funcs_snapshot {
            let x = parser.get_x();
            if let Ok(node) = parser.call_user_func_inner(params, defaults, body, &[x], &name) {
                auto_plots.push(node);
            }
        }
    }

    let objects: Vec<(String, NodeId)> = parser.objects.iter()
        .filter_map(|name| parser.object_nodes.get(name).map(|&id| (name.clone(), id)))
        .collect();
    let all_vars: Vec<(String, NodeId)> = parser.vars.iter()
        .map(|(name, &id)| (name.clone(), id))
        .collect();
    let bare_exprs = parser.bare_exprs.clone();
    let mut plots = parser.plot_nodes;
    if parser.plot_all {
        plots.extend(auto_plots);
    }
    Ok(SceneResult {
        graph: parser.graph,
        objects,
        all_vars,
        casts: parser.cast_nodes,
        cast_all: parser.cast_all,
        plots,
        plot_all: parser.plot_all,
        bare_exprs,
        needs_cast: parser.vars_since_last_cast > 0,
        needs_plot: parser.exprs_since_last_plot > 0,
        warnings: parser.warnings,
    })
}

pub fn resolve_scene(scene: SceneResult) -> TrigGraph {
    let mut graph = scene.graph;

    let targets: Vec<NodeId> = if scene.cast_all {
        if scene.objects.is_empty() {
            scene.all_vars.iter().map(|(_, id)| *id).collect()
        } else {
            scene.objects.iter().map(|(_, id)| *id).collect()
        }
    } else if !scene.casts.is_empty() {
        scene.casts.iter().map(|(_, id)| *id).collect()
    } else {
        vec![]
    };

    if targets.len() > 1 {
        let mut u = targets[0];
        for &t in &targets[1..] {
            u = graph.push(TrigOp::Min(u, t));
        }
        graph.set_output(u);
    } else if targets.len() == 1 {
        graph.set_output(targets[0]);
    }

    graph
}

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

    #[test]
    fn parse_simple() {
        let g = parse_expr("x + y").unwrap();
        assert!((evaluate(&g, 3.0, 4.0, 0.0) - 7.0).abs() < 1e-10);
    }

    #[test]
    fn parse_trig() {
        let g = parse_expr("sin(x)").unwrap();
        let val = evaluate(&g, std::f64::consts::FRAC_PI_2, 0.0, 0.0);
        assert!((val - 1.0).abs() < 1e-10);
    }

    #[test]
    fn parse_sphere() {
        let g = parse_expr("sphere(5)").unwrap();
        assert!((evaluate(&g, 5.0, 0.0, 0.0) - 0.0).abs() < 1e-10);
        assert!((evaluate(&g, 0.0, 0.0, 0.0) - -5.0).abs() < 1e-10);
    }

    #[test]
    fn parse_nested() {
        let g = parse_expr("sin(x) * cos(y) + z").unwrap();
        let val = evaluate(&g, 0.0, 0.0, 7.0);
        assert!((val - 7.0).abs() < 1e-10);
    }

    #[test]
    fn classify_expr() {
        let g = parse_expr("sin(x) + y * z").unwrap();
        let info = classify(&g);
        assert_eq!(info.dimensions, 3);
        assert!(info.has_trig);
        assert!(info.has_multiply);
    }

    #[test]
    fn cross_ref_a() {
        let a = parse_expr("x + 1").unwrap();
        let b = parse_expr_ctx("A * 2", Some(&a), None).unwrap();
        assert!((evaluate(&b, 3.0, 0.0, 0.0) - 8.0).abs() < 1e-10);
    }

    #[test]
    fn box_sdf() {
        let g = parse_expr("box(1, 1, 1)").unwrap();
        assert!((evaluate(&g, 1.0, 0.0, 0.0) - 0.0).abs() < 1e-6);
        assert!((evaluate(&g, 0.0, 0.0, 0.0) - -1.0).abs() < 1e-6);
    }

    #[test]
    fn cylinder_sdf() {
        let g = parse_expr("cylinder(2, 3)").unwrap();
        assert!((evaluate(&g, 2.0, 0.0, 0.0) - 0.0).abs() < 1e-6);
    }

    #[test]
    fn references_ident_check() {
        assert!(references_ident("A + 3", "A"));
        assert!(references_ident("sqrt(A)", "A"));
        assert!(!references_ident("abs(x)", "A"));
        assert!(!references_ident("max(x, y)", "A"));
    }

    #[test]
    fn line_comment() {
        let g = parse_expr("// a sphere\nsphere(3)").unwrap();
        assert!((evaluate(&g, 3.0, 0.0, 0.0)).abs() < 1e-6);
    }

    #[test]
    fn block_comment() {
        let g = parse_expr("sphere(/* radius */ 3)").unwrap();
        assert!((evaluate(&g, 3.0, 0.0, 0.0)).abs() < 1e-6);
    }

    #[test]
    fn reassignment() {
        let g = parse_expr("let a = 5\na = a + 1\na").unwrap();
        assert!((evaluate(&g, 0.0, 0.0, 0.0) - 6.0).abs() < 1e-10);
    }

    #[test]
    fn reassignment_chain() {
        let g = parse_expr("let a = box(2,2,2)\nlet b = a/pi\nb = 1/b*pi").unwrap();
        let val = evaluate(&g, 0.0, 0.0, 0.0);
        let expected = -std::f64::consts::PI.powi(2) / 2.0;
        assert!((val - expected).abs() < 1e-6, "got {val}, expected {expected}");
    }

    #[test]
    fn type_annotation() {
        let g = parse_expr("let a: f64 = 3.0\na + 1").unwrap();
        assert!((evaluate(&g, 0.0, 0.0, 0.0) - 4.0).abs() < 1e-10);
    }

    #[test]
    fn type_annotation_sdf() {
        let g = parse_expr("let s: sdf = sphere(3)\ns").unwrap();
        assert!((evaluate(&g, 3.0, 0.0, 0.0) - 0.0).abs() < 1e-6);
    }

    #[test]
    fn clip_alias() {
        let g = parse_expr("clip(x, 0, 1)").unwrap();
        assert!((evaluate(&g, 0.5, 0.0, 0.0) - 0.5).abs() < 1e-10);
        assert!((evaluate(&g, -1.0, 0.0, 0.0) - 0.0).abs() < 1e-10);
        assert!((evaluate(&g, 5.0, 0.0, 0.0) - 1.0).abs() < 1e-10);
    }

    #[test]
    fn translate_sphere() {
        let g = parse_expr("translate(sphere(3), 5, 0, 0)").unwrap();
        assert!((evaluate(&g, 5.0, 0.0, 0.0) - -3.0).abs() < 1e-6);
        assert!((evaluate(&g, 8.0, 0.0, 0.0) - 0.0).abs() < 1e-6);
        assert!((evaluate(&g, 0.0, 0.0, 0.0) - 2.0).abs() < 1e-6);
    }

    #[test]
    fn rotate_z_sphere() {
        let g = parse_expr("rotate_z(sphere(3), 1.0)").unwrap();
        assert!((evaluate(&g, 3.0, 0.0, 0.0) - 0.0).abs() < 1e-6);
        assert!((evaluate(&g, 0.0, 0.0, 0.0) - -3.0).abs() < 1e-6);
    }

    #[test]
    fn rotate_z_box() {
        let g = parse_expr("let b = box(1,2,1); rotate_z(b, pi/2)").unwrap();
        assert!((evaluate(&g, 2.0, 0.0, 0.0)).abs() < 0.1);
    }

    #[test]
    fn scale_sphere() {
        let g = parse_expr("scale(sphere(1), 3)").unwrap();
        assert!((evaluate(&g, 3.0, 0.0, 0.0) - 0.0).abs() < 1e-6);
        assert!((evaluate(&g, 0.0, 0.0, 0.0) - -3.0).abs() < 1e-6);
    }

    #[test]
    fn mirror_x_sphere() {
        let g = parse_expr("mirror_x(sphere(3))").unwrap();
        assert!((evaluate(&g, 3.0, 0.0, 0.0) - 0.0).abs() < 1e-6);
        assert!((evaluate(&g, -3.0, 0.0, 0.0) - 0.0).abs() < 1e-6);
    }

    #[test]
    fn mirror_x_translated() {
        let g = parse_expr("mirror_x(translate(sphere(1), 5, 0, 0))").unwrap();
        assert!((evaluate(&g, 5.0, 0.0, 0.0) - -1.0).abs() < 1e-6);
        assert!((evaluate(&g, -5.0, 0.0, 0.0) - -1.0).abs() < 1e-6);
    }

    #[test]
    fn union_two_spheres() {
        let g = parse_expr("let a = sphere(1); let b = translate(sphere(1), 5, 0, 0); union(a, b)").unwrap();
        assert!(evaluate(&g, 0.0, 0.0, 0.0) < 0.0);
        assert!(evaluate(&g, 5.0, 0.0, 0.0) < 0.0);
        assert!(evaluate(&g, 2.5, 0.0, 0.0) > 0.0);
    }

    #[test]
    fn diff_two_spheres() {
        let g = parse_expr("let a = sphere(3); let b = sphere(2); diff(a, b)").unwrap();
        assert!(evaluate(&g, 2.5, 0.0, 0.0) < 0.0);
        assert!(evaluate(&g, 1.0, 0.0, 0.0) > 0.0);
    }

    #[test]
    fn intersect_two() {
        let g = parse_expr("let a = sphere(3); let b = translate(sphere(3), 2, 0, 0); intersect(a, b)").unwrap();
        assert!(evaluate(&g, 0.0, 0.0, 0.0) < 0.0);
        assert!(evaluate(&g, 4.0, 0.0, 0.0) > 0.0);
    }

    #[test]
    fn multi_object_scene() {
        let scene = parse_expr_scene(
            "let a: Obj = sphere(1)\nlet b: Obj = translate(sphere(1), 5, 0, 0)"
        ).unwrap();
        assert_eq!(scene.objects.len(), 2);
        assert_eq!(scene.objects[0].0, "a");
        assert_eq!(scene.objects[1].0, "b");
        let b_val = evaluate(&scene.graph, 5.0, 0.0, 0.0);
        assert!(b_val < 0.0);
    }

    #[test]
    fn multi_object_with_bare_expr() {
        let scene = parse_expr_scene(
            "let a: Obj = sphere(1)\nlet b: Obj = sphere(2)\na"
        ).unwrap();
        assert_eq!(scene.objects.len(), 2);
        assert!((evaluate(&scene.graph, 1.0, 0.0, 0.0) - 0.0).abs() < 1e-6);
    }

    #[test]
    fn single_object_no_union() {
        let scene = parse_expr_scene("let a: Obj = sphere(3)").unwrap();
        assert_eq!(scene.objects.len(), 1);
        assert_eq!(scene.objects[0].0, "a");
        assert!((evaluate(&scene.graph, 3.0, 0.0, 0.0) - 0.0).abs() < 1e-6);
    }

    #[test]
    fn obj_type_case_insensitive() {
        let scene = parse_expr_scene("let a: obj = sphere(1)").unwrap();
        assert_eq!(scene.objects.len(), 1);
    }

    #[test]
    fn objects_preserve_node_ids() {
        let scene = parse_expr_scene(
            "let a: Obj = sphere(1)\nlet b: Obj = sphere(5)"
        ).unwrap();
        let a_id = scene.objects[0].1;
        let b_id = scene.objects[1].1;
        assert_ne!(a_id, b_id);
    }

    #[test]
    fn cast_specific_object() {
        let scene = parse_expr_scene(
            "let a: Obj = sphere(1)\nlet b: Obj = sphere(2)\ncast(a)"
        ).unwrap();
        assert_eq!(scene.objects.len(), 2);
        assert_eq!(scene.casts.len(), 1);
        assert_eq!(scene.casts[0].0, "a");
        assert!(!scene.cast_all);
    }

    #[test]
    fn cast_all_objects() {
        let scene = parse_expr_scene(
            "let a: Obj = sphere(1)\nlet b: Obj = sphere(2)\ncast()"
        ).unwrap();
        assert_eq!(scene.objects.len(), 2);
        assert!(scene.cast_all);
    }

    #[test]
    fn dot_cast_syntax() {
        let scene = parse_expr_scene(
            "let a: Obj = sphere(1)\nlet b: Obj = sphere(2)\na.cast()"
        ).unwrap();
        assert_eq!(scene.casts.len(), 1);
        assert_eq!(scene.casts[0].0, "a");
    }

    #[test]
    fn dot_cast_non_obj_errors() {
        let result = parse_expr_scene("let a = 5\na.cast()");
        match result {
            Err(e) => assert!(e.contains("not an Obj"), "got: {e}"),
            Ok(_) => panic!("expected error"),
        }
    }

    #[test]
    fn cast_undefined_errors() {
        let result = parse_expr_scene("cast(z_obj)");
        match result {
            Err(e) => assert!(e.contains("not defined"), "got: {e}"),
            Ok(_) => panic!("expected error"),
        }
    }

    #[test]
    fn no_cast_no_render() {
        let scene = parse_expr_scene("sphere(3)").unwrap();
        assert!(scene.casts.is_empty());
        assert!(!scene.cast_all);
        assert!(scene.needs_plot);
        assert!(!scene.needs_cast);
    }

    #[test]
    fn cast_all_renders_all_vars() {
        let scene = parse_expr_scene(
            "let a = sphere(1)\nlet b = sphere(2)\ncast()"
        ).unwrap();
        assert!(scene.cast_all);
        assert_eq!(scene.all_vars.len(), 2);
        assert!(!scene.needs_cast);
    }

    #[test]
    fn cast_multiple() {
        let scene = parse_expr_scene(
            "let a: Obj = sphere(1)\nlet b: Obj = sphere(2)\nlet c: Obj = sphere(3)\ncast(a)\ncast(c)"
        ).unwrap();
        assert_eq!(scene.casts.len(), 2);
        assert_eq!(scene.casts[0].0, "a");
        assert_eq!(scene.casts[1].0, "c");
    }

    #[test]
    fn plot_expr() {
        let scene = parse_expr_scene(
            "let a: Obj = sphere(1)\nplot(sin(x))"
        ).unwrap();
        assert_eq!(scene.plots.len(), 1);
        assert!(!scene.plot_all);
    }

    #[test]
    fn plot_all() {
        let scene = parse_expr_scene(
            "f(a) = a^2\nplot()"
        ).unwrap();
        assert!(scene.plot_all);
    }

    #[test]
    fn cast_only_program() {
        let scene = parse_expr_scene(
            "let a: Obj = sphere(1)\ncast(a)"
        ).unwrap();
        assert_eq!(scene.casts.len(), 1);
        assert!(!scene.needs_cast);
    }

    #[test]
    fn needs_cast_after_new_var() {
        let scene = parse_expr_scene(
            "let a: Obj = sphere(1)\ncast(a)\nlet b = sphere(2)"
        ).unwrap();
        assert!(scene.needs_cast);
    }

    #[test]
    fn needs_cast_no_cast_calls() {
        let scene = parse_expr_scene(
            "let a = sphere(1)\nlet b = sphere(2)"
        ).unwrap();
        assert!(scene.needs_cast);
    }

    #[test]
    fn no_needs_cast_empty() {
        let scene = parse_expr_scene("sin(x) + cos(y)").unwrap();
        assert!(!scene.needs_cast);
        assert!(scene.needs_plot);
    }

    #[test]
    fn decimal_dot_still_works() {
        let g = parse_expr(".5 + .5").unwrap();
        assert!((evaluate(&g, 0.0, 0.0, 0.0) - 1.0).abs() < 1e-10);
    }

    #[test]
    fn classify_from_scoped() {
        let scene = parse_expr_scene("let a = sin(x)\nlet b = x + y + z").unwrap();
        let a_id = scene.all_vars.iter().find(|(n, _)| n == "a").unwrap().1;
        let info = classify_from(&scene.graph, a_id);
        assert_eq!(info.dimensions, 1);
        assert!(info.uses_x);
        assert!(!info.uses_y);
        assert!(!info.uses_z);
    }

    #[test]
    fn expr_to_sdf_1d_tube() {
        let mut g = parse_expr("sin(x)").unwrap();
        let expr_node = g.output;
        let sdf = expr_to_sdf(&mut g, expr_node, 1, 0.05, 10.0);
        g.set_output(sdf);
        let val = evaluate(&g, 0.0, 0.0, 0.0);
        assert!(val.abs() < 0.1, "expected near-zero on curve, got {val}");
        let val_far = evaluate(&g, 0.0, 5.0, 0.0);
        assert!(val_far > 1.0, "expected positive far from curve, got {val_far}");
    }

    #[test]
    fn expr_to_sdf_2d_surface() {
        let mut g = parse_expr("x * x + y * y").unwrap();
        let expr_node = g.output;
        let sdf = expr_to_sdf(&mut g, expr_node, 2, 0.05, 10.0);
        g.set_output(sdf);
        let val = evaluate(&g, 0.0, 0.0, 0.0);
        assert!(val.abs() < 0.1, "expected near-zero on surface, got {val}");
        let val_far = evaluate(&g, 1.0, 1.0, 10.0);
        assert!(val_far > 1.0, "expected positive far from surface, got {val_far}");
    }

    #[test]
    fn expr_to_sdf_bounded() {
        let mut g = parse_expr("sin(x)").unwrap();
        let expr_node = g.output;
        let sdf = expr_to_sdf(&mut g, expr_node, 1, 0.05, 10.0);
        g.set_output(sdf);
        let val = evaluate(&g, 20.0, 0.0, 0.0);
        assert!(val > 5.0, "expected positive outside domain, got {val}");
    }

    #[test]
    fn cone_sdf() {
        let g = parse_expr("cone(3, 1, 5)").unwrap();
        assert!(evaluate(&g, 0.0, 0.0, 0.0) < 0.0);
        assert!(evaluate(&g, 2.0, 0.0, 0.0).abs() < 0.1);
        assert!(evaluate(&g, 3.0, 0.0, 0.0) > 0.0);
    }

    #[test]
    fn smooth_union_sdf() {
        let g = parse_expr("smooth_union(sphere(2), translate(sphere(2), 3, 0, 0), 1)").unwrap();
        assert!(evaluate(&g, 0.0, 0.0, 0.0) < 0.0);
        assert!(evaluate(&g, 3.0, 0.0, 0.0) < 0.0);
        let mid = evaluate(&g, 1.5, 0.0, 0.0);
        assert!(mid < 0.0, "smooth_union should blend between shapes, got {mid}");
    }

    #[test]
    fn smooth_union_alias() {
        let g = parse_expr("smin(sphere(2), translate(sphere(2), 3, 0, 0), 1)").unwrap();
        assert!(evaluate(&g, 0.0, 0.0, 0.0) < 0.0);
    }

    #[test]
    fn auto_infer_obj_sphere() {
        let scene = parse_expr_scene("let s = sphere(3)\ncast()").unwrap();
        assert!(!scene.objects.is_empty(), "sphere should auto-infer as Obj");
        assert_eq!(scene.objects[0].0, "s");
    }

    #[test]
    fn auto_infer_obj_through_transform() {
        let scene = parse_expr_scene("let s = translate(sphere(3), 1, 0, 0)\ncast()").unwrap();
        assert!(!scene.objects.is_empty(), "translated sphere should auto-infer as Obj");
    }

    #[test]
    fn auto_infer_obj_through_csg() {
        let scene = parse_expr_scene(
            "let a = sphere(3)\nlet b = sphere(1)\nlet c = diff(a, b)\ncast()"
        ).unwrap();
        assert!(scene.objects.iter().any(|(n, _)| n == "c"), "CSG result should auto-infer as Obj");
    }

    #[test]
    fn auto_infer_obj_through_sch() {
        let scene = parse_expr_scene(
            "sch Part(r) { sphere(r) }\nlet s = Part(3)\ncast()"
        ).unwrap();
        assert!(!scene.objects.is_empty(), "schematic returning Obj should auto-infer");
    }

    #[test]
    fn auto_infer_obj_through_map() {
        let scene = parse_expr_scene(
            "let row = map(i, 0..3) { translate(sphere(1), i * 3, 0, 0) }\ncast()"
        ).unwrap();
        assert!(!scene.objects.is_empty(), "map of Obj should auto-infer as Obj");
    }

    #[test]
    fn no_auto_infer_for_scalars() {
        let scene = parse_expr_scene("let a = sin(x)\nlet b: Obj = sphere(3)\ncast()").unwrap();
        assert!(scene.objects.iter().any(|(n, _)| n == "b"));
        assert!(!scene.objects.iter().any(|(n, _)| n == "a"), "sin(x) should not be Obj");
    }

    #[test]
    fn auto_infer_dot_cast_works() {
        let scene = parse_expr_scene("let s = sphere(3)\ns.cast()").unwrap();
        assert_eq!(scene.casts.len(), 1);
        assert_eq!(scene.casts[0].0, "s");
    }

    #[test]
    fn tau_constant() {
        let g = parse_expr("tau").unwrap();
        let val = cord_trig::eval::evaluate(&g, 0.0, 0.0, 0.0);
        assert!((val - 2.0 * std::f64::consts::PI).abs() < 1e-15);
    }

    #[test]
    fn tau_equals_two_pi() {
        let g = parse_expr("tau - 2 * pi").unwrap();
        let val = cord_trig::eval::evaluate(&g, 0.0, 0.0, 0.0);
        assert!(val.abs() < 1e-15);
    }

    #[test]
    fn tau_uppercase() {
        let g = parse_expr("TAU").unwrap();
        let val = cord_trig::eval::evaluate(&g, 0.0, 0.0, 0.0);
        assert!((val - 2.0 * std::f64::consts::PI).abs() < 1e-15);
    }

    #[test]
    fn type_error_obj_plus_num() {
        let result = parse_expr("sphere(3) + 5");
        match result {
            Err(e) => assert!(e.contains("cannot add Obj and Num"), "got: {e}"),
            Ok(_) => panic!("expected error"),
        }
    }

    #[test]
    fn type_error_num_plus_obj() {
        let result = parse_expr("5 + sphere(3)");
        match result {
            Err(e) => assert!(e.contains("cannot add Obj and Num"), "got: {e}"),
            Ok(_) => panic!("expected error"),
        }
    }

    #[test]
    fn unused_variable_warning() {
        let scene = parse_expr_scene("let a = sin(x)\nlet b = cos(x)\nb").unwrap();
        assert!(scene.warnings.iter().any(|w| w.contains("unused variable: a")),
            "expected unused warning for 'a', got: {:?}", scene.warnings);
        assert!(!scene.warnings.iter().any(|w| w.contains("unused variable: b")),
            "b is used, should not warn");
    }

    #[test]
    fn no_unused_warning_for_cast_vars() {
        let scene = parse_expr_scene("let a: Obj = sphere(1)\ncast(a)").unwrap();
        assert!(scene.warnings.is_empty(), "cast var should not warn: {:?}", scene.warnings);
    }

    #[test]
    fn parse_converted_retainer() {
        let src = r#"let d0 = translate(box(22.8044, 3.0784, 0.1600), 0.0000, 0.0000, 0.1600)
let p1 = 0.0000*x + 0.0000*y + -1.0000*z - 0.0000
let p2 = 0.0000*x + 0.0000*y + 1.0000*z - 3.7742
let p3 = intersect(p1, p2)
let p4 = 0.0000*x + -0.9353*y + 0.3538*z - 2.3929
let p5 = intersect(p3, p4)
let p6 = 0.0000*x + 0.9353*y + 0.3538*z - 2.3929
let p7 = intersect(p5, p6)
let p8 = -0.9216*x + 0.0000*y + 0.3881*z - 20.5374
let p9 = intersect(p7, p8)
let p10 = 0.9216*x + -0.0000*y + 0.3881*z - 20.5374
let p11 = intersect(p9, p10)
let o12 = p11 - 0.3000
let d13 = diff(d0, translate(o12, 0.0000, 0.0000, 0.2200))
let u14 = translate(box(0.6000, 0.1100, 0.1100), 0.6000, 0.1100, 0.1100)
let u15 = translate(box(0.8500, 0.1100, 0.2500), 0.3500, 0.1100, -0.0300)
let u16 = union(u14, u15)
let u17 = union(d13, translate(u16, 23.0844, -0.1100, 0.3200))
let u18 = translate(box(0.6000, 0.1100, 0.1100), 0.6000, 0.1100, 0.1100)
let u19 = translate(box(0.8500, 0.1100, 0.2500), 0.3500, 0.1100, -0.0300)
let u20 = union(u18, u19)
let t21 = rotate_z(translate(u20, 23.0844, -0.1100, 0.0000), 3.141593)
let u22 = union(u17, translate(t21, 0.0000, 0.0000, 0.3200))
let scene: Obj = u22"#;
        let scene = parse_expr_scene(src).unwrap();
        let val = evaluate(&scene.graph, 0.0, 0.0, 0.0);
        assert!(val.is_finite(), "expected finite SDF value, got {val}");
    }
}
