use pretty_assertions::assert_eq as assert_eq_pretty;

macro_rules! assert_eq_display {
    ($left:expr, $right:expr) => {{
        match (&$left, &$right) {
            (left_val, right_val) => {
                if !(*left_val == *right_val) {
                    panic!(
                        r#"assertion failed: `(left == right)`
 left: `{}`,
right: `{}`"#,
                        left_val, right_val
                    )
                }
            }
        }
    }};
}

#[macro_export]
macro_rules! make_spec_test {
    ($type:ident, $status:ident, $name:ident, $path:expr) => {
        #[test]
        #[allow(non_snake_case)]
        fn $name() {
            use crate::tests::*;
            match run_test_stringy_error($path, Feature::$type, Status::$status)
            {
                Ok(_) => {}
                Err(s) => panic!(s),
            }
        }
    };
}

use crate::error::{Error, Result};
use crate::phase::Parsed;
use std::path::PathBuf;

#[derive(Copy, Clone)]
pub enum Feature {
    Parser,
    Import,
    Normalization,
    AlphaNormalization,
    Typecheck,
    TypeInference,
}

#[derive(Copy, Clone)]
pub enum Status {
    Success,
    Failure,
}

fn parse_file_str<'i>(file_path: &str) -> Result<Parsed> {
    Parsed::parse_file(&PathBuf::from(file_path))
}

fn parse_binary_file_str<'i>(file_path: &str) -> Result<Parsed> {
    Parsed::parse_binary_file(&PathBuf::from(file_path))
}

pub fn run_test_stringy_error(
    base_path: &str,
    feature: Feature,
    status: Status,
) -> std::result::Result<(), String> {
    let base_path: String = base_path.to_string();
    run_test(&base_path, feature, status)
        .map_err(|e| e.to_string())
        .map(|_| ())
}

pub fn run_test(
    base_path: &str,
    feature: Feature,
    status: Status,
) -> Result<()> {
    use self::Feature::*;
    use self::Status::*;
    let feature_prefix = match feature {
        Parser => "parser/",
        Import => "import/",
        Normalization => "normalization/",
        AlphaNormalization => "alpha-normalization/",
        Typecheck => "typecheck/",
        TypeInference => "type-inference/",
    };
    let status_prefix = match status {
        Success => "success/",
        Failure => "failure/",
    };
    let base_path = "../dhall-lang/tests/".to_owned()
        + feature_prefix
        + status_prefix
        + base_path;
    match status {
        Success => {
            let expr_file_path = base_path.clone() + "A.dhall";
            let expr = parse_file_str(&expr_file_path)?;

            if let Parser = feature {
                let expected_file_path = base_path + "B.dhallb";
                let expected = parse_binary_file_str(&expected_file_path)?;

                assert_eq_pretty!(expr, expected);

                // Round-trip pretty-printer
                let expr_string = expr.to_string();
                let expr: Parsed = crate::de::from_str(&expr_string, None)?;
                assert_eq!(expr, expected);

                return Ok(());
            }

            let expr = expr.resolve()?;

            let expected_file_path = base_path + "B.dhall";
            let expected = parse_file_str(&expected_file_path)?
                .resolve()?
                .skip_typecheck()
                .normalize();

            match feature {
                Parser => unreachable!(),
                Import => {
                    let expr = expr.skip_typecheck().normalize();
                    assert_eq_display!(expr, expected);
                }
                Typecheck => {
                    expr.typecheck_with(&expected.to_type())?;
                }
                TypeInference => {
                    let expr = expr.typecheck()?;
                    let ty = expr.get_type()?.into_owned();
                    assert_eq_display!(ty.to_normalized(), expected);
                }
                Normalization => {
                    let expr = expr.skip_typecheck().normalize();
                    assert_eq_display!(expr, expected);
                }
                AlphaNormalization => {
                    let expr =
                        expr.skip_typecheck().normalize().to_expr_alpha();
                    assert_eq_display!(expr, expected.to_expr());
                }
            }
        }
        Failure => {
            let file_path = base_path + ".dhall";
            match feature {
                Parser => {
                    let err = parse_file_str(&file_path).unwrap_err();
                    match err {
                        Error::Parse(_) => {}
                        e => panic!("Expected parse error, got: {:?}", e),
                    }
                }
                Import => {
                    parse_file_str(&file_path)?.resolve().unwrap_err();
                }
                Normalization | AlphaNormalization => unreachable!(),
                Typecheck | TypeInference => {
                    parse_file_str(&file_path)?
                        .skip_resolve()?
                        .typecheck()
                        .unwrap_err();
                }
            }
        }
    }
    Ok(())
}