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

"use strict";

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

const MAX_NAME_LENGTH = 64;

// The following libraries (except libxul) are all built from the
// toolkit/components/telemetry/tests/modules-test.cpp file, which contains
// instructions on how to build them.
const libModules = ctypes.libraryName("modules-test");
const libUnicode = ctypes.libraryName("modμles-test");
const libLongName =
  "lorem_ipsum_dolor_sit_amet_consectetur_adipiscing_elit_Fusce_sit_amet_tellus_non_magna_euismod_vestibulum_Vivamus_turpis_duis.dll";

function chooseDLL(x86, x64, aarch64) {
  let xpcomabi = Services.appinfo.XPCOMABI;
  let cpu = xpcomabi.split("-")[0];
  switch (cpu) {
    case "aarch64":
      return aarch64;
    case "x86_64":
      return x64;
    case "x86":
      return x86;
    // This case only happens on Android, which gets skipped below. The previous
    // code was returning the x86 version when testing for arm.
    case "arm":
      return x86;
    default:
      Assert.ok(false, "unexpected CPU type: " + cpu);
      return x86;
  }
}

const libUnicodePDB = chooseDLL(
  "testUnicodePDB32.dll",
  "testUnicodePDB64.dll",
  "testUnicodePDBAArch64.dll"
);
const libNoPDB = chooseDLL(
  "testNoPDB32.dll",
  "testNoPDB64.dll",
  "testNoPDBAArch64.dll"
);
const libxul = PathUtils.filename(PathUtils.xulLibraryPath);

const libModulesFile = do_get_file(libModules).path;
const libUnicodeFile = PathUtils.join(
  PathUtils.parent(libModulesFile),
  libUnicode
);
const libLongNameFile = PathUtils.join(
  PathUtils.parent(libModulesFile),
  libLongName
);
const libUnicodePDBFile = do_get_file(libUnicodePDB).path;
const libNoPDBFile = do_get_file(libNoPDB).path;

let libModulesHandle,
  libUnicodeHandle,
  libLongNameHandle,
  libUnicodePDBHandle,
  libNoPDBHandle;

let expectedLibs;
if (AppConstants.platform === "win") {
  const version = AppConstants.MOZ_APP_VERSION.substring(
    0,
    AppConstants.MOZ_APP_VERSION.indexOf(".") + 2
  );

  expectedLibs = [
    {
      name: libxul,
      debugName: libxul.replace(".dll", ".pdb"),
      version,
    },
    {
      name: libModules,
      debugName: libModules.replace(".dll", ".pdb"),
      version,
    },
    {
      name: libUnicode,
      debugName: libModules.replace(".dll", ".pdb"),
      version,
    },
    {
      name: libLongName.substring(0, MAX_NAME_LENGTH - 1) + "…",
      debugName: libModules.replace(".dll", ".pdb"),
      version,
    },
    {
      name: libUnicodePDB,
      debugName: "libmodμles.pdb",
      version: null,
    },
    {
      name: libNoPDB,
      debugName: null,
      version: null,
    },
    {
      // We choose this DLL because it's guaranteed to exist in our process and
      // be signed on all Windows versions that we support.
      name: "ntdll.dll",
      // debugName changes depending on OS version and is irrelevant to this test
      // version changes depending on OS version and is irrelevant to this test
      certSubject: "Microsoft Windows",
    },
  ];
} else {
  expectedLibs = [
    {
      name: libxul,
      debugName: libxul,
      version: null,
    },
    {
      name: libModules,
      debugName: libModules,
      version: null,
    },
    {
      name: libUnicode,
      debugName: libUnicode,
      version: null,
    },
    {
      name: libLongName.substring(0, MAX_NAME_LENGTH - 1) + "…",
      debugName: libLongName.substring(0, MAX_NAME_LENGTH - 1) + "…",
      version: null,
    },
  ];
}

add_task(async function setup() {
  do_get_profile();

  await IOUtils.copy(libModulesFile, libUnicodeFile);
  await IOUtils.copy(libModulesFile, libLongNameFile);

  libModulesHandle = ctypes.open(libModulesFile);
  libUnicodeHandle = ctypes.open(libUnicodeFile);
  libLongNameHandle = ctypes.open(libLongNameFile);
  if (AppConstants.platform === "win") {
    libUnicodePDBHandle = ctypes.open(libUnicodePDBFile);
    libNoPDBHandle = ctypes.open(libNoPDBFile);
  }

  // Pretend the untrustedmodules ping has already been sent now to get it out
  // of the way and avoid confusing the test with our PingServer receiving two
  // pings during our test.
  Services.prefs.setIntPref(
    "app.update.lastUpdateTime.telemetry_untrustedmodules_ping",
    Math.round(Date.now() / 1000)
  );

  // Force the timer to fire (using a small interval).
  Cc["@mozilla.org/updates/timer-manager;1"]
    .getService(Ci.nsIObserver)
    .observe(null, "utm-test-init", "");
  Preferences.set("toolkit.telemetry.modulesPing.interval", 0);
  Preferences.set("app.update.url", "http://localhost");

  // Start the local ping server and setup Telemetry to use it during the tests.
  PingServer.start();
  Preferences.set(
    TelemetryUtils.Preferences.Server,
    "http://localhost:" + PingServer.port
  );
});

registerCleanupFunction(function () {
  if (libModulesHandle) {
    libModulesHandle.close();
  }
  if (libUnicodeHandle) {
    libUnicodeHandle.close();
  }
  if (libLongNameHandle) {
    libLongNameHandle.close();
  }
  if (libUnicodePDBHandle) {
    libUnicodePDBHandle.close();
  }
  if (libNoPDBHandle) {
    libNoPDBHandle.close();
  }

  return IOUtils.remove(libUnicodeFile)
    .then(() => IOUtils.remove(libLongNameFile))
    .then(() => PingServer.stop());
});

add_task(
  {
    skip_if: () => !AppConstants.MOZ_GECKO_PROFILER,
  },
  async function test_send_ping() {
    await TelemetryController.testSetup();

    let found = await PingServer.promiseNextPing();
    Assert.ok(!!found, "Telemetry ping submitted.");
    Assert.strictEqual(found.type, "modules", "Ping type is 'modules'");
    Assert.ok(found.environment, "'modules' ping has an environment.");
    Assert.ok(!!found.clientId, "'modules' ping has a client ID.");
    Assert.ok(
      !!found.payload.modules,
      "Telemetry ping payload contains the 'modules' array."
    );

    let nameComparator;
    if (AppConstants.platform === "win") {
      // Do case-insensitive checking of file/module names on Windows
      nameComparator = function (a, b) {
        if (typeof a === "string" && typeof b === "string") {
          return a.toLowerCase() === b.toLowerCase();
        }

        return a === b;
      };
    } else {
      nameComparator = function (a, b) {
        return a === b;
      };
    }

    for (let lib of expectedLibs) {
      let test_lib = found.payload.modules.find(module =>
        nameComparator(module.name, lib.name)
      );

      Assert.ok(!!test_lib, "There is a '" + lib.name + "' module.");

      if ("version" in lib) {
        if (lib.version !== null) {
          Assert.ok(
            test_lib.version.startsWith(lib.version),
            "The version of the " +
              lib.name +
              " module (" +
              test_lib.version +
              ") is correct (it starts with '" +
              lib.version +
              "')."
          );
        } else {
          Assert.strictEqual(
            test_lib.version,
            null,
            "The version of the " + lib.name + " module is null."
          );
        }
      }

      if ("debugName" in lib) {
        Assert.ok(
          nameComparator(test_lib.debugName, lib.debugName),
          "The " + lib.name + " module has the correct debug name."
        );
      }

      if (lib.debugName === null) {
        Assert.strictEqual(
          test_lib.debugID,
          null,
          "The " + lib.name + " module doesn't have a debug ID."
        );
      } else {
        Assert.greater(
          test_lib.debugID.length,
          0,
          "The " + lib.name + " module has a debug ID."
        );
      }

      if ("certSubject" in lib) {
        Assert.strictEqual(
          test_lib.certSubject,
          lib.certSubject,
          "The " + lib.name + " module has the expected cert subject."
        );
      }
    }

    let test_lib = found.payload.modules.find(
      module => module.name === libLongName
    );
    Assert.ok(!test_lib, "There isn't a '" + libLongName + "' module.");
  }
);
