/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

add_task(async () => {
  info(
    "Test that off-main thread fileio is captured for a profiled thread, " +
      "and that it will be sent to the main thread."
  );
  const filename = "test_marker_fileio";
  const profile = await startProfilerAndTriggerFileIO({
    features: ["fileioall"],
    threadsFilter: ["GeckoMain", "BgIOThreadPool"],
    filename,
  });

  const threads = getThreads(profile);
  const mainThread = threads.find(thread => thread.name === "GeckoMain");
  const mainThreadFileIO = getInflatedFileIOMarkers(mainThread, filename);
  let backgroundThread;
  let backgroundThreadFileIO;
  for (const thread of threads) {
    // Check for FileIO in any of the background threads.
    if (thread.name.startsWith("BgIOThreadPool")) {
      const markers = getInflatedFileIOMarkers(thread, filename);
      if (markers.length) {
        backgroundThread = thread;
        backgroundThreadFileIO = markers;
        break;
      }
    }
  }

  info("Check all of the main thread FileIO markers.");
  checkInflatedFileIOMarkers(mainThreadFileIO, filename);
  for (const { data, name } of mainThreadFileIO) {
    equal(
      name,
      "FileIO (non-main thread)",
      "The markers from off main thread are labeled as such."
    );
    equal(
      data.threadId,
      backgroundThread.tid,
      "The main thread FileIO markers were all sent from the background thread."
    );
  }

  info("Check all of the background thread FileIO markers.");
  checkInflatedFileIOMarkers(backgroundThreadFileIO, filename);
  for (const { data, name } of backgroundThreadFileIO) {
    equal(
      name,
      "FileIO",
      "The markers on the thread where they were generated just say FileIO"
    );
    equal(
      data.threadId,
      undefined,
      "The background thread FileIO correctly excludes the threadId."
    );
  }
});

add_task(async () => {
  info(
    "Test that off-main thread fileio is captured for a thread that is not profiled, " +
      "and that it will be sent to the main thread."
  );
  const filename = "test_marker_fileio";
  const profile = await startProfilerAndTriggerFileIO({
    features: ["fileioall"],
    threadsFilter: ["GeckoMain"],
    filename,
  });

  const threads = getThreads(profile);
  const mainThread = threads.find(thread => thread.name === "GeckoMain");
  const mainThreadFileIO = getInflatedFileIOMarkers(mainThread, filename);

  info("Check all of the main thread FileIO markers.");
  checkInflatedFileIOMarkers(mainThreadFileIO, filename);
  for (const { data, name } of mainThreadFileIO) {
    equal(
      name,
      "FileIO (non-profiled thread)",
      "The markers from off main thread are labeled as such."
    );
    equal(typeof data.threadId, "number", "A thread ID is captured.");
  }
});

/**
 * @typedef {Object} TestConfig
 * @prop {Array} features The list of profiler features
 * @prop {string[]} threadsFilter The list of threads to profile
 * @prop {string} filename A filename to trigger a write operation
 */

/**
 * Start the profiler and get FileIO markers.
 * @param {TestConfig}
 * @returns {Profile}
 */
async function startProfilerAndTriggerFileIO({
  features,
  threadsFilter,
  filename,
}) {
  const entries = 10000;
  const interval = 10;
  await Services.profiler.StartProfiler(
    entries,
    interval,
    features,
    threadsFilter
  );

  const path = PathUtils.join(PathUtils.tempDir, filename);

  info(`Using a temporary file to test FileIO: ${path}`);

  if (fileExists(path)) {
    console.warn(
      "This test is triggering FileIO by writing to a file. However, the test found an " +
        "existing file at the location it was trying to write to. This could happen " +
        "because a previous run of the test failed to clean up after itself. This test " +
        " will now clean up that file before running the test again."
    );
    await removeFile(path);
  }

  info("Write to the file, but do so using a background thread.");

  // IOUtils handles file operations using a background thread.
  await IOUtils.write(path, new TextEncoder().encode("Test data."));
  const exists = await fileExists(path);
  ok(exists, `Created temporary file at: ${path}`);

  info("Remove the file");
  await removeFile(path);

  return stopNowAndGetProfile();
}

async function fileExists(file) {
  try {
    let { type } = await IOUtils.stat(file);
    return type === "regular";
  } catch (_error) {
    return false;
  }
}

async function removeFile(file) {
  await IOUtils.remove(file);
  const exists = await fileExists(file);
  ok(!exists, `Removed temporary file: ${file}`);
}
