mod common;

use common::assert_values_close;
use gonidium::opt::optimize;
use gonidium::{Value, interp_eval, lower, parse_dsl};

fn eval_expr(src: &str, inputs: &[Value]) -> Vec<Value> {
    let func = parse_dsl(src).expect("parse should succeed");
    let dag = lower(&func).expect("lower should succeed").dag;
    interp_eval(&dag, inputs).expect("interp eval should succeed")
}

#[test]
fn optimize_keeps_semantics_add_zero() {
    let src = r#"
    |x: f64|
    x + 0
    "#;
    let func = parse_dsl(src).expect("parse should succeed");
    let dag = lower(&func).expect("lower should succeed").dag;
    let base = interp_eval(&dag, &[Value::F64(3.5)]).expect("eval should succeed");
    let opt_dag = optimize(&dag).expect("optimize should succeed");
    let optimized = interp_eval(&opt_dag, &[Value::F64(3.5)]).expect("eval should succeed");
    assert_values_close(&base, &optimized);
}

#[test]
fn optimize_keeps_semantics_builtin_chain() {
    let src = r#"
    |x: f64|
    log(exp(x))
    "#;
    let func = parse_dsl(src).expect("parse should succeed");
    let dag = lower(&func).expect("lower should succeed").dag;
    let base = interp_eval(&dag, &[Value::F64(0.7)]).expect("eval should succeed");
    let opt_dag = optimize(&dag).expect("optimize should succeed");
    let optimized = interp_eval(&opt_dag, &[Value::F64(0.7)]).expect("eval should succeed");
    assert_values_close(&base, &optimized);
}

#[test]
fn optimize_keeps_semantics_floor_ceil_chain() {
    let src = r#"
    |x: f64|
    floor(x) + ceil(x)
    "#;
    let func = parse_dsl(src).expect("parse should succeed");
    let dag = lower(&func).expect("lower should succeed").dag;
    let base = interp_eval(&dag, &[Value::F64(3.9)]).expect("eval should succeed");
    let opt_dag = optimize(&dag).expect("optimize should succeed");
    let optimized = interp_eval(&opt_dag, &[Value::F64(3.9)]).expect("eval should succeed");
    assert_values_close(&base, &optimized);
}

#[test]
fn optimize_identity_check() {
    let src = r#"
    |a: f64, b: f64|
    (a + b) + 0
    "#;
    let baseline = eval_expr(src, &[Value::F64(1.2), Value::F64(2.3)]);
    let func = parse_dsl(src).expect("parse should succeed");
    let dag = lower(&func).expect("lower should succeed").dag;
    let opt_dag = optimize(&dag).expect("optimize should succeed");
    let after =
        interp_eval(&opt_dag, &[Value::F64(1.2), Value::F64(2.3)]).expect("eval should succeed");
    assert_values_close(&baseline, &after);
}

/// Verify that `x - x` with NaN input still produces NaN after optimisation,
/// i.e. the (unsafe) `sub-self => 0` rule is not applied.
#[test]
fn optimize_sub_self_nan_is_nan() {
    let src = r#"
    |x: f64|
    x - x
    "#;
    let func = parse_dsl(src).expect("parse should succeed");
    let dag = lower(&func).expect("lower should succeed").dag;
    let opt_dag = optimize(&dag).expect("optimize should succeed");
    let result = interp_eval(&opt_dag, &[Value::F64(f64::NAN)]).expect("eval should succeed");
    let Value::F64(v) = result[0] else {
        panic!("expected f64")
    };
    assert!(
        v.is_nan(),
        "NaN - NaN must remain NaN after optimisation, got {v}"
    );
}

/// Verify that `x / x` with NaN input still produces NaN after optimisation,
/// i.e. the (unsafe) `div-self => 1` rule is not applied.
#[test]
fn optimize_div_self_nan_is_nan() {
    let src = r#"
    |x: f64|
    x / x
    "#;
    let func = parse_dsl(src).expect("parse should succeed");
    let dag = lower(&func).expect("lower should succeed").dag;
    let opt_dag = optimize(&dag).expect("optimize should succeed");
    let result = interp_eval(&opt_dag, &[Value::F64(f64::NAN)]).expect("eval should succeed");
    let Value::F64(v) = result[0] else {
        panic!("expected f64")
    };
    assert!(
        v.is_nan(),
        "NaN / NaN must remain NaN after optimisation, got {v}"
    );
}

/// Verify that `x * 0` with NaN input still produces NaN after optimisation,
/// i.e. the (unsafe) `mul-zero => 0` rule is not applied.
#[test]
fn optimize_mul_zero_nan_is_nan() {
    let src = r#"
    |x: f64|
    x * 0
    "#;
    let func = parse_dsl(src).expect("parse should succeed");
    let dag = lower(&func).expect("lower should succeed").dag;
    let opt_dag = optimize(&dag).expect("optimize should succeed");
    let result = interp_eval(&opt_dag, &[Value::F64(f64::NAN)]).expect("eval should succeed");
    let Value::F64(v) = result[0] else {
        panic!("expected f64")
    };
    assert!(
        v.is_nan(),
        "NaN * 0 must remain NaN after optimisation, got {v}"
    );
}
