use std::{fs, path::Path, rc::Rc};

use lysp::{
    error::MachineErrorAt,
    vm::{env::Environment, machine::Machine, prelude, value::Value},
};

#[track_caller]
fn eval_str_in(code: &str, env: &Rc<Environment>) -> Result<Value, MachineErrorAt> {
    let mut machine = Machine::default();
    machine.evaluate_str(Default::default(), None, env, code)
}

#[track_caller]
fn eval_str(code: &str) -> Value {
    let env = Rc::new(Environment::default());
    prelude::load(&env);
    match eval_str_in(code, &env) {
        Ok(value) => value,
        Err(error) => {
            eprintln!("Couldn't evaluate expression:");
            eprintln!();
            eprintln!("  {code}");
            eprintln!();
            eprintln!(":: {error}");
            panic!("TEST FAILED");
        }
    }
}

#[track_caller]
fn eval_str_err(code: &str) -> MachineErrorAt {
    let env = Rc::new(Environment::default());
    prelude::load(&env);
    match eval_str_in(code, &env) {
        Ok(value) => {
            eprintln!("Expected the code to fail to evaluate, but it returned success:");
            eprintln!();
            eprintln!("  {code}");
            eprintln!();
            eprintln!("Returned");
            eprintln!();
            eprintln!(":: {value}");
            panic!("TEST FAILED");
        }
        Err(error) => error,
    }
}

#[track_caller]
fn eval_file<P: AsRef<Path>>(path: P) -> Value {
    let code = fs::read_to_string(path).expect("file read failed");
    eval_str(&code)
}

#[test]
fn test_math() {
    // math
    assert_eq!(eval_str("(+ 1 2 3)"), Value::Number(6.into()));
    assert_eq!(eval_str("(- 3 2 1)"), Value::Number(0.into()));
    assert_eq!(eval_str("(* 2 3 4)"), Value::Number(24.into()));
    assert_eq!(eval_str("(/ 16 4 2)"), Value::Number(2.into()));
    assert_eq!(eval_str("(% 35 16)"), Value::Number(3.into()));
    // TODO
    // assert_eq!(eval_str("(| 1 2 4)"), Value::Number(7.into()));
    // assert_eq!(eval_str("(& 1 2 4)"), Value::Number(0.into()));
    // assert_eq!(eval_str("(& 1 3 7)"), Value::Number(1.into()));
    // assert_eq!(eval_str("(^ 1 3 8)"), Value::Number(10.into()));
    // comparison
    assert_eq!(eval_str("(< 1 2 3 4 5)"), Value::Boolean(true.into()));
    assert_eq!(eval_str("(< 1 2 3 3 4 5)"), Value::Boolean(false.into()));
    assert_eq!(eval_str("(> 1 2 3 4 5)"), Value::Boolean(false.into()));
    assert_eq!(eval_str("(>= 5 5 4 3 2 1)"), Value::Boolean(true.into()));
    assert_eq!(eval_str("(> 5 5 4 3 2 1)"), Value::Boolean(false.into()));
    assert_eq!(eval_str("(<= 1 2 3 3 3 4)"), Value::Boolean(true.into()));
    assert_eq!(eval_str("(/= 1 2 3 4)"), Value::Boolean(true.into()));
    assert_eq!(eval_str("(/= 1 2 2 4)"), Value::Boolean(false.into()));
    assert_eq!(eval_str("(= 1 2 3 4)"), Value::Boolean(false.into()));
    assert_eq!(eval_str("(= 1 1 1 1)"), Value::Boolean(true.into()));
    // logic
    // TODO
    // assert_eq!(eval_str("(&& 1 0)"), Value::Boolean(false.into()));
    // assert_eq!(eval_str("(&& #t 1 \"yes\")"), Value::Boolean(true.into()));
    // assert_eq!(
    //     eval_str("(|| #f NIL \"\" ())"),
    //     Value::Boolean(false.into())
    // );
    // assert_eq!(
    //     eval_str("(|| #f '(1 2 3) \"\" ())"),
    //     Value::Boolean(true.into())
    // );
}

#[test]
fn test_abort() {
    // let err = eval_str_err("(assert (= 1 (+ 1 1)))");
    // let EvalError::Machine(MachineError {
    //     error: MachineErrorKind::Aborted(message),
    //     ..
    // }) = err
    // else {
    //     panic!("Invalid error returned")
    // };
    // assert_eq!(&message, "<undefined>: assertion failed: (= 1 (+ 1 1))");

    // let err = eval_str_err("(abort 1234)");
    // let EvalError::Machine(MachineError {
    //     error: MachineErrorKind::Aborted(message),
    //     ..
    // }) = err
    // else {
    //     panic!("Invalid error returned")
    // };
    // assert_eq!(&message, "1234");
}

#[test]
fn test_lambda() {
    assert_eq!(
        eval_str("((lambda (x) (+ x 1)) 1)"),
        Value::Number(2.into())
    );
    assert_eq!(
        eval_str("((lambda (x) (+ ((lambda () 2)) 1)) 1)"),
        Value::Number(3.into())
    );
}

#[test]
fn test_math_behaves_the_same_inside_apply() {
    assert_eq!(eval_str("(apply + '(1 2 3 4))"), eval_str("(+ 1 2 3 4)"));
    assert_eq!(eval_str("(apply * '(1 2 3 4))"), eval_str("(* 1 2 3 4)"));
    assert_eq!(eval_str("(apply - '(1 2 3 4))"), eval_str("(- 1 2 3 4)"));
    assert_eq!(eval_str("(apply / '(100 25 2))"), eval_str("(/ 100 25 2)"));
    assert_eq!(eval_str("(apply % '(90 37))"), eval_str("(% 90 37)"));
    // TODO
    // assert_eq!(eval_str("(apply | '(1 2 4))"), eval_str("(| 1 2 4)"));
    // assert_eq!(eval_str("(apply & '(1 3 7))"), eval_str("(& 1 3 7)"));
    // assert_eq!(eval_str("(apply ^ '(1 3 7))"), eval_str("(^ 1 3 7)"));
    // assert_eq!(eval_str("(apply < '(1 2 3 4))"), eval_str("(< 1 2 3 4)"));
    // assert_eq!(eval_str("(apply > '(1 2 3 4))"), eval_str("(> 1 2 3 4)"));
}

#[test]
fn test_setq() {
    assert_eq!(eval_str("(setq a 1234) a\n"), Value::Number(1234.into()));
}

#[test]
fn test_let() {
    // Should fail
    eval_str_err("(let (a 1234 b (+ a 1)) NIL)");
    eval_str_err("(let (a 1234) (let (a 9999 b (+ a 4321)) b))");

    assert_eq!(
        eval_str("(let (a 1234 b 4321) (+ a b))"),
        Value::Number(5555.into())
    );
    assert_eq!(
        eval_str("(let* (a 1234 b (+ a 4321)) (+ b 4444))"),
        Value::Number(9999.into())
    );

    // Nested let
    assert_eq!(
        eval_str("(let (a 1234) (let (b 4321) (+ a b)))"),
        Value::Number(5555.into())
    );
    // Does shadow
    assert_eq!(
        eval_str("(let (a 1234) (let* (a 9999 b (+ a 4321)) b))"),
        Value::Number(14320.into())
    );
}

#[test]
fn test_macro() {
    assert_eq!(
        eval_str("(defmacro stringify (x) (cons 'list `,x)) (stringify (1 2 3 4))"),
        Value::list_or_nil([
            Value::Number(1.into()),
            Value::Number(2.into()),
            Value::Number(3.into()),
            Value::Number(4.into())
        ])
    );
}

#[test]
fn test_examples_work() {
    const EXCLUDE: &[&str] = &["repl.lysp", "echo.lysp", "io.lysp", "import.lysp"];

    // None of them should crash at least
    for file in fs::read_dir("examples").unwrap() {
        let entry = file.unwrap();
        let filename = entry.file_name();
        let filename = filename.to_str().unwrap();

        if !filename.ends_with(".lysp") || EXCLUDE.contains(&filename) {
            continue;
        }

        eprintln!("Eval {}", entry.path().display());
        eval_file(entry.path());
    }
}
