/// This is a practically direct rip of Manuel Ceron's awesome integration test
/// script from his Lox project, Loxido.
///
/// @see https://github.com/ceronman/loxido/blob/master/tests/integration.rs
///
/// Copyright 2020 Manuel Cerón
///
/// Permission is hereby granted, free of charge, to any person obtaining a copy of
/// this software and associated documentation files (the "Software"), to deal in
/// the Software without restriction, including without limitation the rights to
/// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
/// of the Software, and to permit persons to whom the Software is furnished to do
/// so, subject to the following conditions:
///
/// The above copyright notice and this permission notice shall be included in all
/// copies or substantial portions of the Software.
///
/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
/// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
/// SOFTWARE.
use std::path::PathBuf;
use std::{env, fs, process::Command};

use regex::Regex;

extern crate test_generator;

use test_generator::test_resources;

fn loxido_command() -> Command {
  // Create full path to binary
  let mut path = env::current_exe()
    .unwrap()
    .parent()
    .unwrap()
    .parent()
    .unwrap()
    .to_owned();
  path.push(env!("CARGO_PKG_NAME"));
  path.set_extension(env::consts::EXE_EXTENSION);
  Command::new(path.into_os_string())
}

struct RuntimeError {
  line_prefix: String,
  message: String,
}

struct Expected {
  out: Vec<String>,
  compile_err: Vec<String>,
  runtime_err: Option<RuntimeError>,
}

fn parse_comments(path: &PathBuf) -> Expected {
  let output_re = Regex::new(r"// expect: ?(.*)").unwrap();
  let error_re = Regex::new(r"// (Error.*)").unwrap();
  let error_line_re = Regex::new(r"// \[(?:c )?line (\d+)\] (Error.*)").unwrap();
  let runtime_error_re = Regex::new(r"// expect runtime error: (.+)").unwrap();

  let mut expected = Expected {
    out: vec![],
    compile_err: vec![],
    runtime_err: None,
  };

  println!("PATH: {}", path.display());
  let content = fs::read_to_string(path).unwrap();
  for (i, line) in content.lines().enumerate() {
    if let Some(m) = output_re.captures(line) {
      let s = m.get(1).unwrap().as_str().to_owned();
      expected.out.push(s);
    }
    if let Some(m) = error_line_re.captures(line) {
      let line = m.get(1).unwrap().as_str();
      let msg = m.get(2).unwrap().as_str();
      let s = format!("[line {}] {}", line, msg);
      expected.compile_err.push(s);
    }
    if let Some(m) = error_re.captures(line) {
      let msg = m.get(1).unwrap().as_str();
      let s = format!("[line {}] {}", i + 1, msg);
      expected.compile_err.push(s);
    }
    if let Some(m) = runtime_error_re.captures(line) {
      let message = m.get(1).unwrap().as_str().to_owned();
      let line_prefix = format!("[line {}]", i + 1);
      expected.runtime_err = Some(RuntimeError { line_prefix, message });
    }
  }
  expected
}

#[test_resources("blackpool/tests/integration/assignment/*.lox")]
#[test_resources("blackpool/tests/integration/block/*.lox")]
#[test_resources("blackpool/tests/integration/bool/*.lox")]
#[test_resources("blackpool/tests/integration/call/*.lox")]
#[test_resources("blackpool/tests/integration/class/*.lox")]
#[test_resources("blackpool/tests/integration/closure/*.lox")]
#[test_resources("blackpool/tests/integration/comments/*.lox")]
#[test_resources("blackpool/tests/integration/constructor/*.lox")]
#[test_resources("blackpool/tests/integration/field/*.lox")]
#[test_resources("blackpool/tests/integration/for/*.lox")]
#[test_resources("blackpool/tests/integration/function/*.lox")]
#[test_resources("blackpool/tests/integration/if/*.lox")]
#[test_resources("blackpool/tests/integration/inheritance/*.lox")]
// Skipping these for now (forever?).
// #[test_resources("blackpool/tests/integration/limit/*.lox")]
#[test_resources("blackpool/tests/integration/logical_operator/*.lox")]
#[test_resources("blackpool/tests/integration/method/*.lox")]
#[test_resources("blackpool/tests/integration/nil/*.lox")]
#[test_resources("blackpool/tests/integration/number/*.lox")]
#[test_resources("blackpool/tests/integration/operator/*.lox")]
#[test_resources("blackpool/tests/integration/print/*.lox")]
#[test_resources("blackpool/tests/integration/regression/*.lox")]
#[test_resources("blackpool/tests/integration/return/*.lox")]
#[test_resources("blackpool/tests/integration/string/*.lox")]
#[test_resources("blackpool/tests/integration/super/*.lox")]
#[test_resources("blackpool/tests/integration/this/*.lox")]
#[test_resources("blackpool/tests/integration/variable/*.lox")]
#[test_resources("blackpool/tests/integration/while/*.lox")]
fn run_file_test(filename: &str) {
  let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
  path.push(filename.replace("blackpool/", ""));
  let expected = parse_comments(&path);

  let output = loxido_command().arg(path).output().unwrap();

  let out: Vec<String> = String::from_utf8(output.stdout)
    .unwrap()
    .lines()
    .map(|x| x.to_owned())
    .collect();
  let err: Vec<String> = String::from_utf8(output.stderr)
    .unwrap()
    .lines()
    .map(|x| x.to_owned())
    .collect();

  match (expected.runtime_err.is_none(), expected.compile_err.is_empty()) {
    (true, true) => assert!(output.status.success(), "Program exited with failure, expected success"),
    (false, true) => assert_eq!(
      output.status.code().unwrap(),
      70,
      "Runtime errors should have error code 70"
    ),
    (true, false) => assert_eq!(
      output.status.code().unwrap(),
      65,
      "Compile errors should have error code 65"
    ),
    (false, false) => panic!("Simultaneous error and compile error"),
  }

  if let Some(e) = expected.runtime_err {
    assert!(
      err.len() > 0,
      "Should have a message like {}, had diddly-squat",
      e.message
    );
    assert_eq!(e.message, err[0], "Runtime error should match");
    assert_eq!(
      err[1][0..e.line_prefix.len()],
      e.line_prefix,
      "Runtime error line should match"
    );
  } else {
    if !err.is_empty() {
      println!("err: {:#?}", err);
      assert_eq!(
        output.status.code().unwrap(),
        65,
        "Compile errors should have error code 65"
      );
    }
    assert_eq!(expected.compile_err.get(0), err.get(0), "Compile error should match");
  }

  assert_eq!(expected.out, out, "Output should match");
}
