/* Any copyright is dedicated to the Public Domain.
 * http://creativecommons.org/publicdomain/zero/1.0/ */

"use strict";

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

add_task(async function test_instantiation() {
  Assert.ok(
    !getCrashManagerNoCreate(),
    "CrashManager global instance not initially defined."
  );

  do_get_profile();
  await makeFakeAppDir();

  // Fake profile creation.
  Cc["@mozilla.org/crashservice;1"]
    .getService(Ci.nsIObserver)
    .observe(null, "profile-after-change", null);

  Assert.ok(getCrashManagerNoCreate(), "Profile creation makes it available.");
  Assert.ok(Services.crashmanager, "CrashManager available via Services.");
  Assert.strictEqual(
    getCrashManagerNoCreate(),
    Services.crashmanager,
    "The objects are the same."
  );
});

var gMinidumpDir = do_get_tempdir();

// Ensure that the nsICrashReporter methods can find the dump
Services.appinfo.minidumpPath = gMinidumpDir;

var gDumpFile;
var gExtraFile;

// Sets up a fake crash dump and sets up the crashreporter so that it will be
// able to find it.
async function setup(crashId) {
  const cwd = Services.dirsvc.get("CurWorkD", Ci.nsIFile).path;
  const minidump = PathUtils.join(cwd, "crash.dmp");
  const extra = PathUtils.join(cwd, "crash.extra");

  // Make a copy of the files because the .extra file will be modified
  gDumpFile = PathUtils.join(gMinidumpDir.path, `${crashId}.dmp`);
  await IOUtils.copy(minidump, gDumpFile);
  gExtraFile = PathUtils.join(gMinidumpDir.path, `${crashId}.extra`);
  await IOUtils.copy(extra, gExtraFile);
}

// Cleans up the fake crash dump and resets the minidump path
async function teardown() {
  await IOUtils.remove(gDumpFile);
  await IOUtils.remove(gExtraFile);
}

async function addCrash(id, type = Ci.nsICrashService.CRASH_TYPE_CRASH) {
  let cs = Cc["@mozilla.org/crashservice;1"].getService(Ci.nsICrashService);
  return cs.addCrash(Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT, type, id);
}

async function getCrash(crashId) {
  let crashes = await Services.crashmanager.getCrashes();
  return crashes.find(c => {
    return c.id === crashId;
  });
}

async function test_addCrashBase(crashId, allThreads) {
  await setup(crashId);

  let crashType = Ci.nsICrashService.CRASH_TYPE_CRASH;
  if (allThreads) {
    crashType = Ci.nsICrashService.CRASH_TYPE_HANG;
  }
  await addCrash(crashId, crashType);
  let crash = await getCrash(crashId);
  Assert.ok(crash, "Crash " + crashId + " has been stored successfully.");
  Assert.equal(crash.metadata.ProcessType, "content");
  Assert.equal(
    crash.metadata.MinidumpSha256Hash,
    "c8ad56a2096310f40c8a4b46c890625a740fdd72e409f412933011ff947c5a40"
  );
  Assert.ok(crash.metadata.StackTraces, "The StackTraces field is present.\n");
  Assert.ok(crash.metadata.CrashEventID, "The CrashEventID field is present.");

  try {
    let stackTraces = JSON.parse(crash.metadata.StackTraces);
    Assert.equal("error" in stackTraces, false);
    Assert.ok(stackTraces.crash_type, "The crash_type field is populated.");
    Assert.ok(
      stackTraces.crash_address,
      "The crash_address field is populated."
    );
    Assert.ok(
      stackTraces.modules && !!stackTraces.modules.length,
      "The module list is populated."
    );
    Assert.ok(
      stackTraces.threads && !!stackTraces.threads.length,
      "The thread list is populated."
    );

    if (allThreads) {
      Assert.greater(
        stackTraces.threads.length,
        1,
        "The stack trace contains more than one thread."
      );
    } else {
      Assert.equal(
        stackTraces.threads.length,
        1,
        "The stack trace contains exactly one thread."
      );
    }

    let frames = stackTraces.threads[0].frames;
    Assert.ok(frames && !!frames.length, "The stack trace is present.\n");
  } catch (e) {
    Assert.ok(false, "StackTraces does not contain valid JSON.");
  }

  try {
    let telemetryEnvironment = JSON.parse(crash.metadata.TelemetryEnvironment);
    Assert.equal(telemetryEnvironment.EscapedField, "EscapedData\n\nfoo");
  } catch (e) {
    Assert.ok(
      false,
      "TelemetryEnvironment contents were not properly escaped\n"
    );
  }

  await teardown();
}

add_task(async function test_addCrash() {
  await test_addCrashBase("56cd87bc-bb26-339b-3a8e-f00c0f11380e", false);
});

add_task(async function test_addCrashAllThreads() {
  await test_addCrashBase("071843c4-da89-4447-af9f-965163e0b253", true);
});

add_task(async function test_addCrash_shutdownOnCrash() {
  const crashId = "de7f63dd-7516-4525-a44b-6d2f2bd3934a";
  await setup(crashId);

  // Set the MOZ_CRASHREPORTER_SHUTDOWN environment variable
  Services.env.set("MOZ_CRASHREPORTER_SHUTDOWN", "1");

  await addCrash(crashId);

  let crash = await getCrash(crashId);
  Assert.ok(crash, "Crash " + crashId + " has been stored successfully.");
  Assert.strictEqual(
    crash.metadata.StackTraces,
    undefined,
    "The StackTraces field is not present because the minidump " +
      "analyzer did not start.\n"
  );

  Services.env.set("MOZ_CRASHREPORTER_SHUTDOWN", ""); // Unset the environment variable
  await teardown();
});

add_task(async function test_addCrash_quitting() {
  const firstCrashId = "0e578a74-a887-48cb-b270-d4775d01e715";
  const secondCrashId = "208379e5-1979-430d-a066-f6e57a8130ce";

  await setup(firstCrashId);

  let minidumpAnalyzerKilledPromise = new Promise((resolve, reject) => {
    Services.obs.addObserver((subject, topic) => {
      if (topic === "test-minidump-analyzer-killed") {
        resolve();
      }

      reject();
    }, "test-minidump-analyzer-killed");
  });

  let addCrashPromise = addCrash(firstCrashId);

  // Spin the event loop so that the minidump analyzer is launched
  await new Promise(resolve => {
    executeSoon(resolve);
  });

  // Pretend we're quitting
  let cs = Cc["@mozilla.org/crashservice;1"].getService(Ci.nsICrashService);
  let obs = cs.QueryInterface(Ci.nsIObserver);
  obs.observe(null, "quit-application", null);

  // Wait for the minidump analyzer to be killed
  await minidumpAnalyzerKilledPromise;

  // Now wait for the crash to be recorded
  await addCrashPromise;
  let crash = await getCrash(firstCrashId);
  Assert.ok(crash, "Crash " + firstCrashId + " has been stored successfully.");

  // Cleanup the fake crash and generate a new one
  await teardown();
  await setup(secondCrashId);

  await addCrash(secondCrashId);
  crash = await getCrash(secondCrashId);
  Assert.ok(crash, "Crash " + secondCrashId + " has been stored successfully.");
  Assert.strictEqual(
    crash.metadata.StackTraces,
    undefined,
    "The StackTraces field is not present because the minidump " +
      "analyzer did not start.\n"
  );
  await teardown();
});
