const { makeFakeAppDir } = ChromeUtils.importESModule(
  "resource://testing-common/AppData.sys.mjs"
);
var { AppConstants } = ChromeUtils.importESModule(
  "resource://gre/modules/AppConstants.sys.mjs"
);

function getEventDir() {
  return PathUtils.join(do_get_tempdir().path, "crash-events");
}

function sendCommandAsync(command) {
  return new Promise(resolve => {
    sendCommand(command, resolve);
  });
}

/**
 * Run an xpcshell subprocess and crash it.
 *
 * @param setup
 *        A string of JavaScript code to execute in the subprocess
 *        before crashing. If this is a function and not a string,
 *        it will have .toSource() called on it, and turned into
 *        a call to itself. (for programmer convenience)
 *        This code will be evaluted between crasher_subprocess_head.js
 *        and crasher_subprocess_tail.js, so it will have access
 *        to everything defined in crasher_subprocess_head.js,
 *        which includes "crashReporter", a variable holding
 *        the crash reporter service.
 *
 * @param callback
 *        A JavaScript function to be called after the subprocess
 *        crashes. It will be passed (minidump, extra, extrafile), where
 *         - minidump is an nsIFile of the minidump file produced,
 *         - extra is an object containing the key,value pairs from
 *           the .extra file.
 *         - extrafile is an nsIFile of the extra file
 *
 * @param canReturnZero
 *       If true, the subprocess may return with a zero exit code.
 *       Certain types of crashes may not cause the process to
 *       exit with an error.
 */
async function do_crash(setup, callback, canReturnZero) {
  // get current process filename (xpcshell)
  let bin = Services.dirsvc.get("XREExeF", Ci.nsIFile);
  if (!bin.exists()) {
    // weird, can't find xpcshell binary?
    do_throw("Can't find xpcshell binary!");
  }
  // get Gre dir (GreD)
  let greD = Services.dirsvc.get("GreD", Ci.nsIFile);
  let headfile = do_get_file("crasher_subprocess_head.js");
  let tailfile = do_get_file("crasher_subprocess_tail.js");
  // run xpcshell -g GreD -f head -e "some setup code" -f tail
  let process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);
  process.init(bin);
  let args = ["-g", greD.path, "-f", headfile.path];
  if (setup) {
    if (typeof setup == "function") {
      // funky, but convenient
      setup = "(" + setup.toSource() + ")();";
    }
    args.push("-e", setup);
  }
  args.push("-f", tailfile.path);

  let crashD = do_get_tempdir();
  crashD.append("crash-events");
  if (!crashD.exists()) {
    crashD.create(crashD.DIRECTORY_TYPE, 0o700);
  }

  Services.env.set("CRASHES_EVENTS_DIR", crashD.path);

  try {
    process.run(true, args, args.length);
  } catch (ex) {
    // on Windows we exit with a -1 status when crashing.
  } finally {
    Services.env.set("CRASHES_EVENTS_DIR", "");
  }

  if (!canReturnZero) {
    // should exit with an error (should have crashed)
    Assert.notEqual(process.exitValue, 0);
  }

  await handleMinidump(callback);
}

function getMinidump() {
  let en = do_get_tempdir().directoryEntries;
  while (en.hasMoreElements()) {
    let f = en.nextFile;
    if (f.leafName.substr(-4) == ".dmp") {
      return f;
    }
  }

  return null;
}

function getCrashReporterPath() {
  const binPrefix =
    AppConstants.platform === "macosx"
      ? "crashreporter.app/Contents/MacOS/"
      : "";
  const binSuffix = AppConstants.platform === "win" ? ".exe" : "";
  const exeName = binPrefix + "crashreporter" + binSuffix;

  let exe = Services.dirsvc.get("GreBinD", Ci.nsIFile);
  exe.appendRelativePath(exeName);

  return exe;
}

function runMinidumpAnalyzer(dumpFile) {
  let bin = getCrashReporterPath();
  let process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);
  process.init(bin);
  let args = ["--analyze"];
  args.push(dumpFile.path);
  process.run(true /* blocking */, args, args.length);
}

async function handleMinidump(callback) {
  // find minidump
  let minidump = getMinidump();

  if (minidump == null) {
    do_throw("No minidump found!");
  }

  let extrafile = minidump.clone();
  extrafile.leafName = extrafile.leafName.slice(0, -4) + ".extra";

  let memoryfile = minidump.clone();
  memoryfile.leafName = memoryfile.leafName.slice(0, -4) + ".memory.json.gz";

  let cleanup = async function () {
    for (let file of [minidump, extrafile, memoryfile]) {
      while (file.exists()) {
        try {
          file.remove(false);
        } catch (e) {
          // On Windows the file may be locked, wait briefly and try again
          await new Promise(resolve => do_timeout(50, resolve));
        }
      }
    }
  };

  // Just in case, don't let these files linger.
  registerCleanupFunction(cleanup);

  Assert.ok(extrafile.exists());
  let extra = await IOUtils.readJSON(extrafile.path);
  // No matter how we crash, there should always be a crash id
  Assert.ok("CrashEventID" in extra);

  if (callback) {
    await callback(minidump, extra, extrafile, memoryfile);
  }

  await cleanup();
}

function spinEventLoop() {
  return new Promise(resolve => {
    executeSoon(resolve);
  });
}

/**
 * Helper for testing a content process crash.
 *
 * This variant accepts a setup function which runs in the content process
 * to set data as needed _before_ the crash.  The tail file triggers a generic
 * crash after setup.
 */
async function do_content_crash(setup, callback) {
  do_load_child_test_harness();

  // Setting the minidump path won't work in the child, so we need to do
  // that here.
  Services.appinfo.minidumpPath = do_get_tempdir();

  /* import-globals-from ../unit/crasher_subprocess_head.js */
  /* import-globals-from ../unit/crasher_subprocess_tail.js */

  let headfile = do_get_file("../unit/crasher_subprocess_head.js");
  let tailfile = do_get_file("../unit/crasher_subprocess_tail.js");
  if (setup) {
    if (typeof setup == "function") {
      // funky, but convenient
      setup = "(" + setup.toSource() + ")();";
    }
  }

  do_get_profile();
  await makeFakeAppDir();
  await sendCommandAsync('load("' + headfile.path.replace(/\\/g, "/") + '");');
  if (setup) {
    await sendCommandAsync(setup);
  }
  await sendCommandAsync('load("' + tailfile.path.replace(/\\/g, "/") + '");');
  await spinEventLoop();

  let minidump = getMinidump();
  let id = minidump.leafName.slice(0, -4);
  await Services.crashmanager.ensureCrashIsPresent(id);
  try {
    await handleMinidump(callback);
  } catch (x) {
    do_report_unexpected_exception(x);
  }
}

/**
 * Helper for testing a content process crash.
 *
 * This variant accepts a trigger function which runs in the content process
 * and does something to _trigger_ the crash.
 */
async function do_triggered_content_crash(trigger, callback) {
  do_load_child_test_harness();

  // Setting the minidump path won't work in the child, so we need to do
  // that here.
  Services.appinfo.minidumpPath = do_get_tempdir();

  /* import-globals-from ../unit/crasher_subprocess_head.js */

  let headfile = do_get_file("../unit/crasher_subprocess_head.js");
  if (trigger) {
    if (typeof trigger == "function") {
      // funky, but convenient
      trigger = "(" + trigger.toSource() + ")();";
    }
  }

  do_get_profile();
  await makeFakeAppDir();
  await sendCommandAsync('load("' + headfile.path.replace(/\\/g, "/") + '");');
  await sendCommandAsync(trigger);
  await spinEventLoop();
  let id = getMinidump().leafName.slice(0, -4);
  await Services.crashmanager.ensureCrashIsPresent(id);
  try {
    await handleMinidump(callback);
  } catch (x) {
    do_report_unexpected_exception(x);
  }
}

/**
 * Run the `crash` backgroundtask subprocess, crashing it in the
 * specified manner.
 *
 * @param crashType Integer `CrashTestUtils.CRASH_...` code.
 * @param crashExtras Dictionary of key-value pairs to include in
 *                    minidump extras.
 *
 * @param callback
 *        A JavaScript function to be called after the subprocess
 *        crashes. It will be passed (minidump, extra, extrafile), where
 *         - minidump is an nsIFile of the minidump file produced,
 *         - extra is an object containing the key,value pairs from
 *           the .extra file.
 *         - extrafile is an nsIFile of the extra file
 *
 * @param canReturnZero
 *       If true, the subprocess may return with a zero exit code.
 *       Certain types of crashes may not cause the process to
 *       exit with an error.
 */
async function do_backgroundtask_crash(
  crashType,
  crashExtras,
  callback,
  canReturnZero
) {
  Assert.ok(AppConstants.MOZ_BACKGROUNDTASKS);

  // Get full path to application (not xpcshell)
  let bin = Services.dirsvc.get("GreBinD", Ci.nsIFile);
  if (AppConstants.platform === "win") {
    bin.append(AppConstants.MOZ_APP_NAME + ".exe");
  } else {
    bin.append(AppConstants.MOZ_APP_NAME);
  }

  // run `application --backgroundtask crash ...`.
  let process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);
  process.init(bin);

  let args = ["--backgroundtask", "crash"];
  args.push(crashType.toString());

  // Sorted to be deterministic.
  let sorted = Object.entries(crashExtras).sort((a, b) => a[0] < b[0]);
  for (let [key, value] of sorted) {
    args.push(key);
    args.push(value);
  }

  let crashD = do_get_tempdir();
  crashD.append("crash-events");
  if (!crashD.exists()) {
    crashD.create(crashD.DIRECTORY_TYPE, 0o700);
  }

  Services.env.set("CRASHES_EVENTS_DIR", crashD.path);

  // Ensure `resource://testing-common` gets mapped.
  let protocolHandler = Services.io
    .getProtocolHandler("resource")
    .QueryInterface(Ci.nsIResProtocolHandler);

  let uri = protocolHandler.getSubstitution("testing-common");
  Assert.ok(uri, "resource://testing-common is not substituted");

  // The equivalent of _TESTING_MODULES_DIR in xpcshell.
  Services.env.set("XPCSHELL_TESTING_MODULES_URI", uri.spec);

  try {
    process.run(true, args, args.length);
  } catch (ex) {
    // on Windows we exit with a -1 status when crashing.
  } finally {
    Services.env.set("CRASHES_EVENTS_DIR", "");
    Services.env.set("XPCSHELL_TESTING_MODULES_URI", "");
  }

  if (!canReturnZero) {
    // should exit with an error (should have crashed)
    Assert.notEqual(process.exitValue, 0);
  }

  await handleMinidump(callback);
}

// Import binary APIs via js-ctypes.
var { CrashTestUtils } = ChromeUtils.importESModule(
  "resource://test/CrashTestUtils.sys.mjs"
);
