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

"use strict";

/* import-globals-from ../../../../../toolkit/crashreporter/test/browser/head.js */
load("head.js");

const { RemoteSettingsCrashPull } = ChromeUtils.importESModule(
  "resource://gre/modules/RemoteSettingsCrashPull.sys.mjs"
);

const { CrashServiceUtils } = ChromeUtils.importESModule(
  "resource://gre/modules/CrashService.sys.mjs"
);

const { RemoteSettings } = ChromeUtils.importESModule(
  "resource://services-settings/remote-settings.sys.mjs"
);

const { TestUtils } = ChromeUtils.importESModule(
  "resource://testing-common/TestUtils.sys.mjs"
);

const kRemoteSettingsCollectionName = "crash-reports-ondemand";

let rscp = undefined;
let crD = undefined;

async function generatePending(content) {
  const pending = crD.clone();
  pending.append("pending");
  const pendingCrash = addPendingCrashreport(
    crD,
    Date.now(),
    content,
    JSON.stringify(content)
  );
  const pendingDmp = PathUtils.join(pending.path, `${pendingCrash.id}.dmp`);
  return {
    id: pendingCrash.id,
    sha: await CrashServiceUtils.computeMinidumpHash(pendingDmp),
  };
}

async function deletePending(id) {
  const pending = crD.clone();
  await pending.append("pending");

  let dmp = pending.clone();
  await dmp.append(`${id}.dmp`);
  await dmp.remove(false);

  let extra = pending.clone();
  await extra.append(`${id}.extra`);
  await extra.remove(false);

  let memory_json_gz = pending.clone();
  await memory_json_gz.append(`${id}.memory.json.gz`);
  await memory_json_gz.remove(false);
}

add_setup(async function setup_test() {
  do_get_profile();

  const appD = make_fake_appdir();
  crD = appD.clone();
  crD.append("Crash Reports");

  rscp = RemoteSettingsCrashPull;
  await rscp.collection();
});

add_task(function test_verify_valid() {
  Assert.ok(rscp, "RemoteSettingsCrashPull obtained.");
});

add_task(async function test_no_crash_pending_found() {
  let dateLimit = new Date();
  dateLimit.setDate(dateLimit.getDate() - 1);

  let crashes = await rscp.getPendingSha256(dateLimit);
  Assert.equal(crashes.length, 0, "No pending crash");
});

add_task(async function test_one_crash_pending_found() {
  let dateLimit = new Date();
  dateLimit.setDate(dateLimit.getDate() - 1);

  const generatedSha = await generatePending({ foo: "bar" });

  let crashes = await rscp.getPendingSha256(dateLimit);
  Assert.equal(crashes.length, 1, "One pending crash");
  Assert.ok(!!crashes[0].sha256.length, "Generated crash SHA256 exists");
  Assert.equal(
    crashes[0].sha256,
    generatedSha.sha,
    "Generated crash SHA256 match"
  );

  await deletePending(crashes[0].id);
  crashes = await rscp.getPendingSha256(dateLimit);
  Assert.equal(crashes.length, 0, "No more pending crash");
});

add_task(async function test_more_crash_pending_found() {
  let dateLimit = new Date();
  dateLimit.setDate(dateLimit.getDate() - 1);

  const NB_CRASHES = 10;

  let generatedShas = [];
  for (let i = 0; i < NB_CRASHES; ++i) {
    generatedShas[i] = await generatePending({ foo: "bar", val: i });
  }

  let crashes = await rscp.getPendingSha256(dateLimit);
  Assert.equal(crashes.length, generatedShas.length, "More pending crash");

  const generatedShaValues = generatedShas.map(e => e.sha);
  const crashesShaValues = crashes.map(e => e.sha256);

  for (let i = 0; i < NB_CRASHES; ++i) {
    Assert.ok(!!crashesShaValues.length, `Generated crash ${i} SHA256 exists`);
    Assert.ok(
      crashesShaValues.includes(generatedShaValues[i]),
      `Generated crash ${i} SHA256 match`
    );
  }

  for (let i = 0; i < NB_CRASHES; ++i) {
    await deletePending(crashes[i].id);
  }

  crashes = await rscp.getPendingSha256(dateLimit);
  Assert.equal(crashes.length, 0, "No more pending crash");
});

add_task(async function test_no_interesting_crash_at_all() {
  let matches = await rscp.checkForInterestingUnsubmittedCrash([]);
  Assert.equal(matches.length, 0, "No matching crash");
});

add_task(
  async function test_no_interesting_crash_with_no_crash_pending_one_record() {
    let matches = await rscp.checkForInterestingUnsubmittedCrash([
      {
        hashes: [
          "eec354db7cec5508f75c2dbb07b780386b0022806f164a75c230f988c4779f65",
        ],
      },
    ]);
    Assert.equal(matches.length, 0, "No matching crash");
  }
);

add_task(
  async function test_no_interesting_crash_with_crash_pending_one_record_not_matching() {
    const generatedSha = await generatePending({ foo: "bar", val: "toto" });
    Assert.equal(
      generatedSha.sha,
      "97d33a98fe06d7b652a678c2e8cc2fc6f317e7ea6544b353490e97143db76f34",
      "Verify generated crash sha"
    );

    let matches = await rscp.checkForInterestingUnsubmittedCrash([
      {
        hashes: [
          "eec354db7cec5508f75c2dbb07b780386b0022806f164a75c230f988c4779f65",
        ],
      },
    ]);
    Assert.equal(matches.length, 0, "No matching crash");

    await deletePending(generatedSha.id);
  }
);

add_task(
  async function test_one_interesting_crash_with_crash_pending_one_record_matching() {
    const generatedSha = await generatePending({ foo: "bar", val: "toto" });
    Assert.equal(
      generatedSha.sha,
      "97d33a98fe06d7b652a678c2e8cc2fc6f317e7ea6544b353490e97143db76f34",
      "Verify generated crash sha"
    );

    let matches = await rscp.checkForInterestingUnsubmittedCrash([
      {
        hashes: [
          "97d33a98fe06d7b652a678c2e8cc2fc6f317e7ea6544b353490e97143db76f34",
        ],
      },
    ]);
    Assert.equal(matches.length, 1, "One matching crash");
    Assert.equal(matches[0], generatedSha.id, "IDs of crash matches");

    await deletePending(generatedSha.id);
  }
);

add_task(
  async function test_no_interesting_crash_at_all_from_remoteSettings_sync() {
    const payload = {
      current: [],
      created: [
        {
          hashes: ["1", "2", "3"],
        },
        {
          hashes: ["4", "5", "6"],
        },
      ],
      updated: [],
      deleted: [],
    };

    let callbackCalled = false;
    rscp._showCallback = function (_) {
      callbackCalled = true;
    };

    await RemoteSettings(kRemoteSettingsCollectionName).emit("sync", {
      data: payload,
    });

    try {
      await TestUtils.waitForCondition(
        () => callbackCalled,
        "Waiting for callback to have been called (should not happen"
      );
    } catch (ex) {
      if (!ex.includes("timed out after 50 tries")) {
        throw ex;
      }
    }

    Assert.ok(!callbackCalled, "Show callback should not have been called");
    rscp._showCallback = undefined;
  }
);

add_task(
  async function test_one_interesting_crash_from_remoteSettings_one_record() {
    const generatedSha = await generatePending({ foo: "bar", val: "coucou" });
    Assert.equal(
      generatedSha.sha,
      "2435191bfd64cf0c8cbf0397f1cb5654f778388a3be72cb01502196896f5a0e9",
      "Verify generated crash sha"
    );

    const payload = {
      current: [],
      created: [
        {
          hashes: [
            "2435191bfd64cf0c8cbf0397f1cb5654f778388a3be72cb01502196896f5a0e9",
          ],
        },
      ],
      updated: [],
      deleted: [],
    };

    let callbackCalled = false;
    rscp._showCallback = function (reportIDs) {
      Assert.equal(reportIDs.length, 1, "Should have one match");
      Assert.equal(reportIDs[0], generatedSha.id, "ID of report should match");
      callbackCalled = true;
    };

    await RemoteSettings(kRemoteSettingsCollectionName).emit("sync", {
      data: payload,
    });
    await TestUtils.waitForCondition(
      () => callbackCalled,
      "Waiting for callback to have been called"
    );
    rscp._showCallback = undefined;
    await deletePending(generatedSha.id);
  }
);

add_task(
  async function test_one_interesting_crash_from_remoteSettings_more_records() {
    const generatedSha = await generatePending({ foo: "bar", val: "coucou2" });
    Assert.equal(
      generatedSha.sha,
      "93211f02bf1bd5316e85ff3738c3edcf8569e11e4d1a5bd23e9596cd0f7c3520",
      "Verify generated crash sha"
    );

    const payload = {
      current: [],
      created: [
        {
          hashes: ["1", "2"],
        },
        {
          hashes: [
            "93211f02bf1bd5316e85ff3738c3edcf8569e11e4d1a5bd23e9596cd0f7c3520",
            "3",
          ],
        },
      ],
      updated: [],
      deleted: [],
    };

    let callbackCalled = false;
    rscp._showCallback = function (reportIDs) {
      Assert.equal(reportIDs.length, 1, "Should have one match");
      Assert.equal(reportIDs[0], generatedSha.id, "ID of report should match");
      callbackCalled = true;
    };

    await RemoteSettings(kRemoteSettingsCollectionName).emit("sync", {
      data: payload,
    });
    await TestUtils.waitForCondition(
      () => callbackCalled,
      "Waiting for callback to have been called"
    );
    rscp._showCallback = undefined;
    await deletePending(generatedSha.id);
  }
);

add_task(
  async function test_one_interesting_crash_from_remoteSettings_more_records_dupes() {
    const generatedSha = await generatePending({ foo: "bar", val: "coucou3" });
    Assert.equal(
      generatedSha.sha,
      "7f004093f5821cbf1ab055d07d53759052acca5a03a978ba3d96ed121e2b8266",
      "Verify generated crash sha"
    );

    const payload = {
      current: [],
      created: [
        {
          hashes: ["1", "2"],
        },
        {
          hashes: [
            "7f004093f5821cbf1ab055d07d53759052acca5a03a978ba3d96ed121e2b8266",
            "3",
          ],
        },
        {
          hashes: [
            "4",
            "5",
            "7f004093f5821cbf1ab055d07d53759052acca5a03a978ba3d96ed121e2b8266",
            "7",
          ],
        },
      ],
      updated: [],
      deleted: [],
    };

    let callbackCalled = false;
    rscp._showCallback = function (reportIDs) {
      Assert.equal(reportIDs.length, 1, "Should have one match");
      Assert.equal(reportIDs[0], generatedSha.id, "ID of report should match");
      callbackCalled = true;
    };

    await RemoteSettings(kRemoteSettingsCollectionName).emit("sync", {
      data: payload,
    });
    await TestUtils.waitForCondition(
      () => callbackCalled,
      "Waiting for callback to have been called"
    );
    rscp._showCallback = undefined;
    await deletePending(generatedSha.id);
  }
);

add_task(
  async function test_many_interesting_crash_from_remoteSettings_many_records() {
    const NB_CRASHES = 10;

    let generatedShas = [];
    for (let i = 0; i < NB_CRASHES; ++i) {
      generatedShas[i] = await generatePending({
        foo: "bar",
        val: "coucou",
        idx: i,
      });
    }

    const payload = {
      current: [],
      created: [
        {
          hashes: ["1", generatedShas[9].sha, "2", generatedShas[0].sha],
        },
        {
          hashes: [generatedShas[1].sha, "3"],
        },
        {
          hashes: ["4", generatedShas[6].sha, "5", generatedShas[3].sha, "7"],
        },
      ],
      updated: [],
      deleted: [],
    };

    let callbackCalled = false;
    rscp._showCallback = function (reportIDs) {
      Assert.equal(reportIDs.length, 5, "Should have one match");

      Assert.notEqual(
        reportIDs[0],
        reportIDs[1],
        "ID of report #0 should be different from #1"
      );
      Assert.notEqual(
        reportIDs[1],
        reportIDs[2],
        "ID of report #1 should be different from #2"
      );
      Assert.notEqual(
        reportIDs[2],
        reportIDs[3],
        "ID of report #2 should be different from #3"
      );
      Assert.notEqual(
        reportIDs[3],
        reportIDs[4],
        "ID of report #3 should be different from #4"
      );
      for (let i of [0, 1, 3, 6, 9]) {
        Assert.ok(
          reportIDs.includes(generatedShas[i].id),
          `ID of sha ${i} found in reports`
        );
      }

      callbackCalled = true;
    };

    await RemoteSettings(kRemoteSettingsCollectionName).emit("sync", {
      data: payload,
    });
    await TestUtils.waitForCondition(
      () => callbackCalled,
      "Waiting for callback to have been called"
    );
    rscp._showCallback = undefined;
    for (let i = 0; i < NB_CRASHES; ++i) {
      await deletePending(generatedShas[i].id);
    }
  }
);

add_task(function test_pref_disabled() {
  const originalPref = Services.prefs.getBoolPref(
    "browser.crashReports.onDemand"
  );
  Services.prefs.setBoolPref("browser.crashReports.onDemand", false);

  const originalCollection = RemoteSettingsCrashPull.collection;
  let collectionCalled = false;
  RemoteSettingsCrashPull.collection = function () {
    console.debug("Calling collection()");
    collectionCalled = true;
  };

  RemoteSettingsCrashPull.start();

  Assert.ok(RemoteSettingsCrashPull, "RemoteSettingsCrashPull obtained.");
  Assert.ok(
    !collectionCalled,
    "Method collection() should not have been called"
  );

  RemoteSettingsCrashPull.collection = originalCollection;
  Services.prefs.setBoolPref("browser.crashReports.onDemand", originalPref);
});

add_task(function test_pref_enabled() {
  const originalPref = Services.prefs.getBoolPref(
    "browser.crashReports.onDemand"
  );
  Services.prefs.setBoolPref("browser.crashReports.onDemand", true);

  const originalCollection = RemoteSettingsCrashPull.collection;
  let collectionCalled = false;
  RemoteSettingsCrashPull.collection = function () {
    console.debug("Calling collection()");
    collectionCalled = true;
  };

  RemoteSettingsCrashPull.start();

  Assert.ok(RemoteSettingsCrashPull, "RemoteSettingsCrashPull obtained.");
  Assert.ok(collectionCalled, "Method collection() should have been called");

  RemoteSettingsCrashPull.collection = originalCollection;
  Services.prefs.setBoolPref("browser.crashReports.onDemand", originalPref);
});

add_task(function teardown_test() {
  cleanup_fake_appdir();
});
