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

"use strict";

SimpleTest.requestCompleteLog();

// Wait until the child process with the given PID has indeed been terminated.
//
// Note that `checkUtilityExists`, and other functions deriving from the output
// of `ChromeUtils.requestProcInfo()`, do not suffice for this purpose! It is an
// attested failure mode that the file-dialog utility process has been removed
// from the proc-info list, but is still live with the file-picker dialog still
// displayed.
function untilChildProcessDead(pid) {
  return utilityProcessTest().untilChildProcessDead(pid);
}

async function fileDialogProcessExists() {
  return !!(await tryGetUtilityPid("windowsFileDialog"));
}

// Poll for the creation of a file dialog process.
function untilFileDialogProcessExists(options = { maxTime: 2000 }) {
  // milliseconds
  const maxTime = options.maxTime ?? 2000,
    pollTime = options.pollTime ?? 100;
  const count = maxTime / pollTime;

  return TestUtils.waitForCondition(
    () => tryGetUtilityPid("windowsFileDialog", { quiet: true }),
    "waiting for file dialog process",
    pollTime, // interval
    count // maxTries
  );
}

function openFileDialog() {
  const process = (async () => {
    await untilFileDialogProcessExists();
    let pid = await tryGetUtilityPid("windowsFileDialog");
    ok(pid, `pid should be acquired in openFileDialog::process (got ${pid})`);
    // HACK: Wait briefly for the file dialog to open.
    //
    // If this is not done, we may attempt to crash the process while it's in
    // the middle of creating and showing the file dialog window. There _should_
    // be no problem with this, but `::MiniDumpWriteDump()` occasionally fails
    // with mysterious errors (`ERROR_BAD_LENGTH`) if we crashed the process
    // while that was happening, yielding no minidump and therefore a failing
    // test.
    //
    // Use of an arbitrary timeout could presumably be avoided by setting a
    // window hook for the file dialog being shown and `await`ing on that.
    //
    // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
    await new Promise(res => setTimeout(res, 1000));
    return pid;
  })();

  const file = new Promise(resolve => {
    info("Opening Windows file dialog");
    let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
    fp.init(
      window.browsingContext,
      "Test: browser_utility_filepicker_crashed.js",
      fp.modeOpen
    );
    fp.open(result => {
      Assert.equal(
        result,
        fp.returnCancel,
        "filepicker should resolve to cancellation"
      );
      resolve();
    });
  });

  return { process, file };
}

add_setup(async function () {
  await SpecialPowers.pushPrefEnv({
    set: [
      // remote, no fallback
      ["widget.windows.utility_process_file_picker", 2],
    ],
  });
});

function makeTask(description, Describe, action) {
  let task = async function () {
    if (await fileDialogProcessExists()) {
      // If this test proceeds, it will probably cause whatever other test has a
      // file dialog open to fail.
      //
      // (We shouldn't be running two such tests in parallel on the same Fx
      // instance, but it's not obvious at this level that we're not.)
      ok(false, "another test has a file dialog open; aborting");
      return;
    }

    // the file-picker-crashed notification should be shown after the process
    // dies, but we must set up the listener before the notification is shown
    let notificationAppeared = BrowserTestUtils.waitForNotificationBar(
      gBrowser,
      undefined,
      "file-picker-crashed"
    );

    const { process, file } = openFileDialog();
    const pid = await process;
    const untilDead = untilChildProcessDead(pid);

    info(Describe + " the file-dialog utility process");
    await action();

    // the file-picker's callback should have been promptly cancelled
    const _before = Date.now();
    await file;
    const _after = Date.now();
    const delta = _after - _before;
    info(`file callback resolved after ${description} after ${delta}ms`);

    let notification = await notificationAppeared;
    ok(notification, "file-picker notification should appear");
    notification.close();

    // depending on the test configuration, this may take some time while
    // cleanup occurs
    await untilDead;
  };

  // give this task a legible name
  Object.defineProperty(task, "name", {
    value: "testFileDialogProcess-" + Describe.replace(" ", ""),
  });

  return task;
}

for (let [description, Describe, action] of [
  ["crash", "Crash", () => crashSomeUtilityActor("windowsFileDialog")],
  [
    "being killed",
    "Kill",
    () => cleanUtilityProcessShutdown("windowsFileDialog", true),
  ],
  // Unfortunately, a controlled shutdown doesn't actually terminate the utility
  // process; the file dialog remains open. (This is expected to be resolved with
  // bug 1837008.)
  /* [
    "shutdown",
    "Shut down",
    () => cleanUtilityProcessShutdown("windowsFileDialog"),
  ] */
]) {
  add_task(makeTask(description, Describe, action));
  add_task(testCleanup);
}

async function testCleanup() {
  const killFileDialogProcess = async () => {
    if (await tryGetUtilityPid("windowsFileDialog", { quiet: true })) {
      await cleanUtilityProcessShutdown("windowsFileDialog", true);
      return true;
    }
    return false;
  };

  // If a test failure occurred, the file dialog process may or may not already
  // exist...
  if (await killFileDialogProcess()) {
    console.warn("File dialog process found and killed");
    return;
  }

  // ... and if not, may or may not be pending creation.
  info("Active file dialog process not found; waiting...");
  try {
    await untilFileDialogProcessExists({ maxTime: 1000 });
  } catch (e) {
    info("File dialog process not found during cleanup (as expected)");
    return;
  }
  await killFileDialogProcess();
  console.warn("Delayed file dialog process found and killed");
}
