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

"use strict";

const { TelemetryUtils } = ChromeUtils.importESModule(
  "resource://gre/modules/TelemetryUtils.sys.mjs"
);
const { TelemetryArchiveTesting } = ChromeUtils.importESModule(
  "resource://testing-common/TelemetryArchiveTesting.sys.mjs"
);

add_task(async function test_updatePing() {
  const TEST_VERSION = "37.85";
  const TEST_BUILDID = "20150711123724";
  const XML_UPDATE = `<?xml version="1.0"?>
    <updates xmlns="http://www.mozilla.org/2005/app-update">
      <update appVersion="${Services.appinfo.version}" buildID="20080811053724"
              channel="nightly" displayVersion="Version 1.0"
              installDate="1238441400314" isCompleteUpdate="true" type="minor"
              name="Update Test 1.0" detailsURL="http://example.com/"
              previousAppVersion="${TEST_VERSION}"
              serviceURL="https://example.com/" foregroundDownload="true"
              statusText="The Update was successfully installed">
        <patch type="complete" URL="http://example.com/" size="775"
               selected="true" state="succeeded"/>
      </update>
    </updates>`;

  // Set the preferences needed for the test: they will be cleared up
  // after it runs.
  await SpecialPowers.pushPrefEnv({
    set: [
      [TelemetryUtils.Preferences.UpdatePing, true],
      ["browser.startup.homepage_override.mstone", TEST_VERSION],
      ["browser.startup.homepage_override.buildID", TEST_BUILDID],
      ["toolkit.telemetry.log.level", "Trace"],
    ],
  });

  registerCleanupFunction(async () => {
    let activeUpdateFile = getActiveUpdateFile();
    activeUpdateFile.remove(false);
    await reloadUpdateManagerData(true);
  });
  writeFile(XML_UPDATE, getActiveUpdateFile());
  writeSuccessUpdateStatusFile();
  await reloadUpdateManagerData(false);

  // Start monitoring the ping archive.
  let archiveChecker = new TelemetryArchiveTesting.Checker();
  await archiveChecker.promiseInit();

  let updatePing;
  let gleanPrevChannel;
  await GleanPings.update.testSubmission(
    reason => {
      Assert.equal("success", reason);
      Assert.equal(TEST_BUILDID, Glean.update.previousBuildId.testGetValue());
      Assert.equal(TEST_VERSION, Glean.update.previousVersion.testGetValue());
      gleanPrevChannel = Glean.update.previousChannel.testGetValue();
    },
    async () => {
      // Manually call the BrowserContentHandler: this automatically gets called
      // when the browser is started and an update was applied successfully in
      // order to display the "update" info page.
      Cc["@mozilla.org/browser/clh;1"]
        .getService(Ci.nsIBrowserHandler)
        .getFirstWindowArgs();

      // We cannot control when the ping will be generated/archived after we trigger
      // an update, so let's make sure to have one before moving on with validation.
      await TestUtils.waitForCondition(
        async function () {
          // Check that the ping made it into the Telemetry archive.
          // The test data is defined in ../data/sharedUpdateXML.js
          updatePing = await archiveChecker.promiseFindPing("update", [
            [["payload", "reason"], "success"],
            [["payload", "previousBuildId"], TEST_BUILDID],
            [["payload", "previousVersion"], TEST_VERSION],
          ]);
          return !!updatePing;
        },
        "Make sure the ping is generated before trying to validate it.",
        500,
        100
      );
    }
  );

  ok(updatePing, "The 'update' ping must be correctly sent.");

  // We have no easy way to simulate a previously applied update from toolkit/telemetry.
  // Instead of moving this test to mozapps/update as well, just test that the
  // "previousChannel" field is present and either a string or null.
  ok(
    "previousChannel" in updatePing.payload,
    "The payload must contain the 'previousChannel' field"
  );
  const channelField = updatePing.payload.previousChannel;
  if (channelField != null) {
    Assert.equal(
      typeof channelField,
      "string",
      "'previousChannel' must be a string, if available."
    );
    Assert.equal(channelField, gleanPrevChannel);
  }

  // Also make sure that the ping contains both a client id and an
  // environment section.
  ok("clientId" in updatePing, "The update ping must report a client id.");
  ok(
    "environment" in updatePing,
    "The update ping must report the environment."
  );
});

/**
 * Removes the updates.xml file and returns the nsIFile for the
 * active-update.xml file.
 *
 * @return  The nsIFile for the active-update.xml file.
 */
function getActiveUpdateFile() {
  let updateRootDir = Services.dirsvc.get("UpdRootD", Ci.nsIFile);
  let updatesFile = updateRootDir.clone();
  updatesFile.append("updates.xml");
  if (updatesFile.exists()) {
    // The following is non-fatal.
    try {
      updatesFile.remove(false);
    } catch (e) {}
  }
  let activeUpdateFile = updateRootDir.clone();
  activeUpdateFile.append("active-update.xml");
  return activeUpdateFile;
}

/**
 * Reloads the update xml files.
 *
 * @param  skipFiles (optional)
 *         If true, the update xml files will not be read and the metadata will
 *         be reset. If false (the default), the update xml files will be read
 *         to populate the update metadata.
 */
async function reloadUpdateManagerData(skipFiles = false) {
  await Cc["@mozilla.org/updates/update-manager;1"]
    .getService(Ci.nsIUpdateManager)
    .internal.reload(skipFiles);
}

/**
 * Writes the specified text to the specified file.
 *
 * @param {string} aText
 *   The string to write to the file.
 * @param {nsIFile} aFile
 *   The file to write to.
 */
function writeFile(aText, aFile) {
  const PERMS_FILE = 0o644;

  const MODE_WRONLY = 0x02;
  const MODE_CREATE = 0x08;
  const MODE_TRUNCATE = 0x20;

  if (!aFile.exists()) {
    aFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, PERMS_FILE);
  }
  let fos = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(
    Ci.nsIFileOutputStream
  );
  let flags = MODE_WRONLY | MODE_CREATE | MODE_TRUNCATE;
  fos.init(aFile, flags, PERMS_FILE, 0);
  fos.write(aText, aText.length);
  fos.close();
}

/**
 * If we want the update system to treat the update we wrote out as one that it
 * just installed, we need to make it think that the update state is
 * "succeeded".
 */
function writeSuccessUpdateStatusFile() {
  const statusFile = Services.dirsvc.get("UpdRootD", Ci.nsIFile);
  statusFile.append("updates");
  statusFile.append("0");
  statusFile.append("update.status");
  writeFile("succeeded", statusFile);
}
