use crate::common::*;

pub(crate) use pretty_assertions::assert_eq;

macro_rules! test {
  (
    name:     $name:ident,
    justfile: $justfile:expr,
    $(args:     ($($arg:tt)*),)?
    $(env:      {
      $($env_key:literal : $env_value:literal,)*
    },)?
    $(stdin:    $stdin:expr,)?
    $(stdout:   $stdout:expr,)?
    $(stderr:   $stderr:expr,)?
    $(status:   $status:expr,)?
    $(shell:    $shell:expr,)?
  ) => {
    #[test]
    fn $name() {
      #[allow(unused_mut)]
      let mut env = std::collections::BTreeMap::new();

      $($(env.insert($env_key.to_string(), $env_value.to_string());)*)?

      crate::test::Test {
        justfile: $justfile,
        $(args: &[$($arg)*],)?
        $(stdin: $stdin,)?
        $(stdout: $stdout,)?
        $(stderr: $stderr,)?
        $(status: $status,)?
        $(shell: $shell,)?
        env,
        ..crate::test::Test::default()
      }.run();
    }
  }
}

pub(crate) struct Test<'a> {
  pub(crate) justfile: &'a str,
  pub(crate) args:     &'a [&'a str],
  pub(crate) env:      BTreeMap<String, String>,
  pub(crate) stdin:    &'a str,
  pub(crate) stdout:   &'a str,
  pub(crate) stderr:   &'a str,
  pub(crate) status:   i32,
  pub(crate) shell:    bool,
}

impl<'a> Default for Test<'a> {
  fn default() -> Test<'a> {
    Test {
      justfile: "",
      args:     &[],
      env:      BTreeMap::new(),
      stdin:    "",
      stdout:   "",
      stderr:   "",
      status:   EXIT_SUCCESS,
      shell:    true,
    }
  }
}

impl<'a> Test<'a> {
  pub(crate) fn run(self) {
    let tmp = tempdir();

    let justfile = unindent(self.justfile);
    let stdout = unindent(self.stdout);
    let stderr = unindent(self.stderr);

    let mut justfile_path = tmp.path().to_path_buf();
    justfile_path.push("justfile");
    fs::write(justfile_path, justfile).unwrap();

    let mut dotenv_path = tmp.path().to_path_buf();
    dotenv_path.push(".env");
    fs::write(dotenv_path, "DOTENV_KEY=dotenv-value").unwrap();

    let mut command = Command::new(&executable_path("just"));

    if self.shell {
      command.args(&["--shell", "bash"]);
    }

    let mut child = command
      .args(self.args)
      .envs(self.env)
      .current_dir(tmp.path())
      .stdin(Stdio::piped())
      .stdout(Stdio::piped())
      .stderr(Stdio::piped())
      .spawn()
      .expect("just invocation failed");

    {
      let mut stdin_handle = child.stdin.take().expect("failed to unwrap stdin handle");

      stdin_handle
        .write_all(self.stdin.as_bytes())
        .expect("failed to write stdin to just process");
    }

    let output = child
      .wait_with_output()
      .expect("failed to wait for just process");

    let have = Output {
      status: output.status.code().unwrap(),
      stdout: str::from_utf8(&output.stdout).unwrap(),
      stderr: str::from_utf8(&output.stderr).unwrap(),
    };

    let want = Output {
      status: self.status,
      stdout: &stdout,
      stderr: &stderr,
    };

    assert_eq!(have, want, "bad output");

    if self.status == EXIT_SUCCESS {
      test_round_trip(tmp.path());
    }
  }
}

#[derive(PartialEq, Debug)]
struct Output<'a> {
  stdout: &'a str,
  stderr: &'a str,
  status: i32,
}

fn test_round_trip(tmpdir: &Path) {
  println!("Reparsing...");

  let output = Command::new(&executable_path("just"))
    .current_dir(tmpdir)
    .arg("--dump")
    .output()
    .expect("just invocation failed");

  if !output.status.success() {
    panic!("dump failed: {}", output.status);
  }

  let dumped = String::from_utf8(output.stdout).unwrap();

  let reparsed_path = tmpdir.join("reparsed.just");

  fs::write(&reparsed_path, &dumped).unwrap();

  let output = Command::new(&executable_path("just"))
    .current_dir(tmpdir)
    .arg("--justfile")
    .arg(&reparsed_path)
    .arg("--dump")
    .output()
    .expect("just invocation failed");

  if !output.status.success() {
    panic!("reparse failed: {}", output.status);
  }

  let reparsed = String::from_utf8(output.stdout).unwrap();

  assert_eq!(reparsed, dumped, "reparse mismatch");
}
