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

let { MockFilePicker } = SpecialPowers;
MockFilePicker.init(window);
registerCleanupFunction(() => MockFilePicker.cleanup());

/**
 * Check that the downloads panel opens when a download is spoofed.
 */
async function checkPanelOpens() {
  info("Waiting for panel to open.");
  let promise = promisePanelOpened();
  DownloadsCommon.getData(window)._notifyDownloadEvent("start");
  is(
    DownloadsPanel.isPanelShowing,
    true,
    "Panel state should indicate a preparation to be opened."
  );
  await promise;

  is(DownloadsPanel.panel.state, "open", "Panel should be opened.");

  DownloadsPanel.hidePanel();
}

/**
 * Start a download and check that the downloads panel opens correctly according
 * to the download parameter, openDownloadsListOnStart
 * @param {boolean} [openDownloadsListOnStart]
 *        true (default) - open downloads panel when download starts
 *        false - no downloads panel; update indicator attention state
 */
async function downloadAndCheckPanel({ openDownloadsListOnStart = true } = {}) {
  info("creating a download and setting it to in progress");
  await task_addDownloads([
    {
      state: DownloadsCommon.DOWNLOAD_DOWNLOADING,
      openDownloadsListOnStart,
    },
  ]);
  let publicList = await Downloads.getList(Downloads.PUBLIC);
  let downloads = await publicList.getAll();
  downloads[0].stopped = false;

  // Make sure we remove that download at the end of the test.
  let oldShowEventNotification = DownloadsIndicatorView.showEventNotification;
  registerCleanupFunction(async () => {
    for (let download of downloads) {
      await publicList.remove(download);
    }
    DownloadsIndicatorView.showEventNotification = oldShowEventNotification;
  });

  // Instead of the panel opening, the download notification should be shown.
  let promiseDownloadStartedNotification = new Promise(resolve => {
    DownloadsIndicatorView.showEventNotification = aType => {
      if (aType == "start") {
        resolve();
      }
    };
  });

  DownloadsCommon.getData(window)._notifyDownloadEvent("start", {
    openDownloadsListOnStart,
  });
  is(
    DownloadsPanel.isPanelShowing,
    false,
    "Panel state should indicate it is not preparing to be opened"
  );

  info("waiting for download to start");
  await promiseDownloadStartedNotification;

  DownloadsIndicatorView.showEventNotification = oldShowEventNotification;
  is(DownloadsPanel.panel.state, "closed", "Panel should be closed");
}

function clickCheckbox(checkbox) {
  // Clicking a checkbox toggles its checkedness first.
  if (checkbox.getAttribute("checked") == "true") {
    checkbox.removeAttribute("checked");
  } else {
    checkbox.setAttribute("checked", "true");
  }
  // Then it runs the command and closes the popup.
  checkbox.doCommand();
  checkbox.parentElement.hidePopup();
}

/**
 * Test that the downloads panel correctly opens or doesn't open based on
 * whether the download triggered a dialog already. If askWhereToSave is true,
 * we should get a file picker dialog. If preferredAction is alwaysAsk, we
 * should get an unknown content type dialog. If neither of those is true, we
 * should get no dialog at all, and expect the downloads panel to open.
 * @param {boolean} [expectPanelToOpen] true - fail if panel doesn't open
 *                                      false (default) - fail if it opens
 * @param {number}  [preferredAction]   Default download action:
 *                                      0 (default) - save download to disk
 *                                      1 - open UCT dialog first
 * @param {boolean} [askWhereToSave]    true - open file picker dialog
 *                                      false (default) - use download dir
 */
async function testDownloadsPanelAfterDialog({
  expectPanelToOpen = false,
  preferredAction,
  askWhereToSave = false,
} = {}) {
  const { saveToDisk, alwaysAsk } = Ci.nsIHandlerInfo;
  if (![saveToDisk, alwaysAsk].includes(preferredAction)) {
    preferredAction = saveToDisk;
  }
  const openUCT = preferredAction === alwaysAsk;
  const TEST_PATH = getRootDirectory(gTestPath).replace(
    "chrome://mochitests/content",
    "https://example.com"
  );
  const MimeSvc = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
  const HandlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService(
    Ci.nsIHandlerService
  );
  let publicList = await Downloads.getList(Downloads.PUBLIC);

  for (let download of await publicList.getAll()) {
    await publicList.remove(download);
  }

  // We need to test the changes from bug 1739348, where the helper app service
  // sets a flag based on whether a file picker dialog was opened, and this flag
  // determines whether the downloads panel will be opened as the download
  // starts. We need to actually hit "Save" for the download to start, but we
  // can't interact with the real file picker dialog. So this temporarily
  // replaces it with a barebones component that plugs into the helper app
  // service and tells it to start saving the file to the default path.
  if (askWhereToSave) {
    MockFilePicker.returnValue = MockFilePicker.returnOK;
    MockFilePicker.showCallback = function (fp) {
      // Get the default location from the helper app service.
      let testFile = MockFilePicker.displayDirectory.clone();
      testFile.append(fp.defaultString);
      info("File picker download path: " + testFile.path);
      MockFilePicker.setFiles([testFile]);
      MockFilePicker.filterIndex = 0; // kSaveAsType_Complete
      MockFilePicker.showCallback = null;
      // Confirm that saving should proceed. The helper app service uses this
      // value to determine whether to invoke launcher.saveDestinationAvailable
      return MockFilePicker.returnOK;
    };
  }

  await SpecialPowers.pushPrefEnv({
    set: [
      ["browser.download.useDownloadDir", !askWhereToSave],
      ["browser.download.always_ask_before_handling_new_types", openUCT],
      ["security.dialog_enable_delay", 0],
    ],
  });

  // Configure the handler for the file according to parameters.
  let mimeInfo = MimeSvc.getFromTypeAndExtension("text/plain", "txt");
  let existed = HandlerSvc.exists(mimeInfo);
  mimeInfo.alwaysAskBeforeHandling = openUCT;
  mimeInfo.preferredAction = preferredAction;
  HandlerSvc.store(mimeInfo);
  registerCleanupFunction(async () => {
    // Reset the handler to its original state.
    if (existed) {
      HandlerSvc.store(mimeInfo);
    } else {
      HandlerSvc.remove(mimeInfo);
    }
    await publicList.removeFinished();
    BrowserTestUtils.removeTab(loadingTab);
  });

  let dialogWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded();
  let downloadFinishedPromise = new Promise(resolve => {
    publicList.addView({
      onDownloadChanged(download) {
        info("Download changed!");
        if (download.succeeded || download.error) {
          info("Download succeeded or failed.");
          publicList.removeView(this);
          resolve(download);
        }
      },
    });
  });
  let panelOpenedPromise = expectPanelToOpen ? promisePanelOpened() : null;

  // Open the tab that will trigger the download.
  let loadingTab = await BrowserTestUtils.openNewForegroundTab({
    gBrowser,
    opening: TEST_PATH + "foo.txt",
    waitForLoad: false,
    waitForStateStop: true,
  });

  // Wait for a UCT dialog if the handler was set up to open one.
  if (openUCT) {
    let dialogWindow = await dialogWindowPromise;
    is(
      dialogWindow.location.href,
      "chrome://mozapps/content/downloads/unknownContentType.xhtml",
      "Should have seen the unknown content dialogWindow."
    );
    let doc = dialogWindow.document;
    let dialog = doc.getElementById("unknownContentType");
    let radio = doc.getElementById("save");
    let button = dialog.getButton("accept");

    await TestUtils.waitForCondition(
      () => !button.disabled,
      "Waiting for the UCT dialog's Accept button to be enabled."
    );
    ok(!radio.hidden, "The Save option should be visible");
    // Make sure we aren't opening the file.
    radio.click();
    ok(radio.selected, "The Save option should be selected");
    button.disabled = false;
    dialog.acceptDialog();
  }

  info("Waiting for download to finish.");
  let download = await downloadFinishedPromise;
  ok(!download.error, "There should be no error.");
  is(
    DownloadsPanel.isPanelShowing,
    expectPanelToOpen,
    `Panel should${expectPanelToOpen ? " " : " not "}be showing.`
  );
  if (DownloadsPanel.isPanelShowing) {
    await panelOpenedPromise;
    let hiddenPromise = BrowserTestUtils.waitForPopupEvent(
      DownloadsPanel.panel,
      "hidden"
    );
    DownloadsPanel.hidePanel();
    await hiddenPromise;
  }
  if (download?.target.exists) {
    try {
      info("Removing test file: " + download.target.path);
      if (Services.appinfo.OS === "WINNT") {
        await IOUtils.setPermissions(download.target.path, 0o600);
      }
      await IOUtils.remove(download.target.path);
    } catch (ex) {
      /* ignore */
    }
  }
  for (let dl of await publicList.getAll()) {
    await publicList.remove(dl);
  }
  BrowserTestUtils.removeTab(loadingTab);
}

/**
 * Make sure the downloads panel opens automatically with a new download.
 */
add_task(async function test_downloads_panel_opens() {
  await SpecialPowers.pushPrefEnv({
    set: [
      ["browser.download.always_ask_before_handling_new_types", false],
      ["browser.download.alwaysOpenPanel", true],
    ],
  });
  await checkPanelOpens();
});

add_task(async function test_customizemode_doesnt_wreck_things() {
  await SpecialPowers.pushPrefEnv({
    set: [
      ["browser.download.always_ask_before_handling_new_types", false],
      ["browser.download.alwaysOpenPanel", true],
    ],
  });

  // Enter customize mode:
  let customizationReadyPromise = BrowserTestUtils.waitForEvent(
    gNavToolbox,
    "customizationready"
  );
  gCustomizeMode.enter();
  await customizationReadyPromise;

  info("Try to open the panel (will not work, in customize mode)");
  let promise = promisePanelOpened();
  DownloadsCommon.getData(window)._notifyDownloadEvent("start");
  // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
  await new Promise(r => setTimeout(r, 100));
  is(
    DownloadsPanel.panel.state,
    "closed",
    "Should not start opening the panel."
  );

  let afterCustomizationPromise = BrowserTestUtils.waitForEvent(
    gNavToolbox,
    "aftercustomization"
  );
  gCustomizeMode.exit();
  await afterCustomizationPromise;

  // Avoid a failure on Linux where the window isn't active for some reason,
  // which prevents the window's downloads panel from opening.
  if (Services.focus.activeWindow != window) {
    info("Main window is not active, trying to focus.");
    await SimpleTest.promiseFocus(window);
    is(Services.focus.activeWindow, window, "Main window should be active.");
  }
  DownloadsCommon.getData(window)._notifyDownloadEvent("start");
  await TestUtils.waitForCondition(
    () => DownloadsPanel.isPanelShowing,
    "Panel state should indicate a preparation to be opened"
  );
  await promise;

  is(DownloadsPanel.panel.state, "open", "Panel should be opened");

  DownloadsPanel.hidePanel();
});

/**
 * Make sure the downloads panel _does not_ open automatically if we set the
 * pref telling it not to do that.
 */
add_task(async function test_downloads_panel_opening_pref() {
  await SpecialPowers.pushPrefEnv({
    set: [
      ["browser.download.always_ask_before_handling_new_types", false],
      ["browser.download.alwaysOpenPanel", false],
    ],
  });
  registerCleanupFunction(async () => {
    await SpecialPowers.popPrefEnv();
  });
  await downloadAndCheckPanel();
  await SpecialPowers.popPrefEnv();
});

/**
 * Make sure the downloads panel _does not_ open automatically if we pass the
 * parameter telling it not to do that to the download constructor.
 */
add_task(async function test_downloads_openDownloadsListOnStart_param() {
  await SpecialPowers.pushPrefEnv({
    set: [
      ["browser.download.always_ask_before_handling_new_types", false],
      ["browser.download.alwaysOpenPanel", true],
    ],
  });
  registerCleanupFunction(async () => {
    await SpecialPowers.popPrefEnv();
  });
  await downloadAndCheckPanel({ openDownloadsListOnStart: false });
  await SpecialPowers.popPrefEnv();
});

/**
 * Make sure the downloads panel _does not_ open automatically when an
 * extension calls the browser.downloads.download API method while it is
 * not handling user input, but that we do open it automatically when
 * the same WebExtensions API is called while handling user input
 * (See Bug 1759231)
 */
add_task(async function test_downloads_panel_on_webext_download_api() {
  await SpecialPowers.pushPrefEnv({
    set: [
      ["browser.download.always_ask_before_handling_new_types", false],
      ["browser.download.alwaysOpenPanel", true],
    ],
  });
  registerCleanupFunction(async () => {
    await SpecialPowers.popPrefEnv();
  });

  const extension = ExtensionTestUtils.loadExtension({
    manifest: {
      permissions: ["downloads"],
    },
    background() {
      async function startDownload(downloadOptions) {
        /* globals browser */
        const downloadId = await browser.downloads.download(downloadOptions);
        const downloadDone = new Promise(resolve => {
          browser.downloads.onChanged.addListener(function listener(delta) {
            browser.test.log(`downloads.onChanged = ${JSON.stringify(delta)}`);
            if (
              delta.id == downloadId &&
              delta.state?.current !== "in_progress"
            ) {
              browser.downloads.onChanged.removeListener(listener);
              resolve();
            }
          });
        });

        browser.test.sendMessage("start-download:done");
        await downloadDone;
        await browser.downloads.removeFile(downloadId);
        browser.test.sendMessage("removed-download-file");
      }

      browser.test.onMessage.addListener(
        (msg, { withHandlingUserInput, downloadOptions }) => {
          if (msg !== "start-download") {
            browser.test.fail(`Got unexpected test message: ${msg}`);
            return;
          }

          if (withHandlingUserInput) {
            browser.test.withHandlingUserInput(() =>
              startDownload(downloadOptions)
            );
          } else {
            startDownload(downloadOptions);
          }
        }
      );
    },
  });

  await extension.startup();

  startServer();

  async function testExtensionDownloadCall({ withHandlingUserInput }) {
    mustInterruptResponses();
    let rnd = Math.random();
    let url = httpUrl(`interruptible.txt?q=${rnd}`);

    extension.sendMessage("start-download", {
      withHandlingUserInput,
      downloadOptions: { url },
    });
    await extension.awaitMessage("start-download:done");

    let publicList = await Downloads.getList(Downloads.PUBLIC);
    let downloads = await publicList.getAll();

    let download = downloads.find(d => d.source.url === url);
    is(download.source.url, url, "download has the expected url");
    is(
      download.openDownloadsListOnStart,
      withHandlingUserInput,
      `download panel should ${withHandlingUserInput ? "open" : "stay closed"}`
    );

    continueResponses();
    await extension.awaitMessage("removed-download-file");
  }

  info(
    "Test extension downloads.download API method call without handling user input"
  );
  await testExtensionDownloadCall({ withHandlingUserInput: true });

  info(
    "Test extension downloads.download API method call while handling user input"
  );
  await testExtensionDownloadCall({ withHandlingUserInput: false });

  await extension.unload();
  await SpecialPowers.popPrefEnv();
});

/**
 * Make sure the downloads panel opens automatically with new download, only if
 * no other downloads are in progress.
 */
add_task(async function test_downloads_panel_remains_closed() {
  await SpecialPowers.pushPrefEnv({
    set: [["browser.download.always_ask_before_handling_new_types", false]],
  });
  await task_addDownloads([
    { state: DownloadsCommon.DOWNLOAD_DOWNLOADING },
    { state: DownloadsCommon.DOWNLOAD_DOWNLOADING },
  ]);

  let publicList = await Downloads.getList(Downloads.PUBLIC);
  let downloads = await publicList.getAll();

  info("setting 2 downloads to be in progress");
  downloads[0].stopped = false;
  downloads[1].stopped = false;

  let oldShowEventNotification = DownloadsIndicatorView.showEventNotification;

  registerCleanupFunction(async () => {
    // Remove all downloads created during the test.
    for (let download of downloads) {
      await publicList.remove(download);
    }
    DownloadsIndicatorView.showEventNotification = oldShowEventNotification;
  });

  let promiseDownloadStartedNotification = new Promise(resolve => {
    // Instead of downloads panel opening, download notification should be shown.
    DownloadsIndicatorView.showEventNotification = aType => {
      if (aType == "start") {
        DownloadsIndicatorView.showEventNotification = oldShowEventNotification;
        resolve();
      }
    };
  });

  DownloadsCommon.getData(window)._notifyDownloadEvent("start");

  is(
    DownloadsPanel.isPanelShowing,
    false,
    "Panel state should NOT indicate a preparation to be opened"
  );

  await promiseDownloadStartedNotification;

  is(DownloadsPanel.panel.state, "closed", "Panel should be closed");

  for (let download of downloads) {
    await publicList.remove(download);
  }
  is((await publicList.getAll()).length, 0, "Should have no downloads left.");
});

/**
 * Make sure the downloads panel doesn't open if the window isn't in the
 * foreground.
 */
add_task(async function test_downloads_panel_inactive_window() {
  await SpecialPowers.pushPrefEnv({
    set: [["browser.download.always_ask_before_handling_new_types", false]],
  });

  let oldShowEventNotification = DownloadsIndicatorView.showEventNotification;

  registerCleanupFunction(async () => {
    DownloadsIndicatorView.showEventNotification = oldShowEventNotification;
  });

  let promiseDownloadStartedNotification = new Promise(resolve => {
    // Instead of downloads panel opening, download notification should be shown.
    DownloadsIndicatorView.showEventNotification = aType => {
      if (aType == "start") {
        DownloadsIndicatorView.showEventNotification = oldShowEventNotification;
        resolve();
      }
    };
  });

  let testRunnerWindow = Array.from(Services.wm.getEnumerator("")).find(
    someWin => someWin != window
  );

  await SimpleTest.promiseFocus(testRunnerWindow);

  DownloadsCommon.getData(window)._notifyDownloadEvent("start");

  is(
    DownloadsPanel.isPanelShowing,
    false,
    "Panel state should NOT indicate a preparation to be opened"
  );

  await promiseDownloadStartedNotification;
  await SimpleTest.promiseFocus(window);

  is(DownloadsPanel.panel.state, "closed", "Panel should be closed");

  testRunnerWindow = null;
});

/**
 * When right-clicking the downloads toolbar button, there should be a menuitem
 * for toggling alwaysOpenPanel. Check that it works correctly.
 */
add_task(async function test_alwaysOpenPanel_menuitem() {
  const alwaysOpenPanelPref = "browser.download.alwaysOpenPanel";
  let checkbox = document.getElementById(
    "toolbar-context-always-open-downloads-panel"
  );
  let button = document.getElementById("downloads-button");

  Services.prefs.clearUserPref(alwaysOpenPanelPref);
  await SpecialPowers.pushPrefEnv({
    set: [["browser.download.autohideButton", false]],
  });
  registerCleanupFunction(async () => {
    await SpecialPowers.popPrefEnv();
    Services.prefs.clearUserPref(alwaysOpenPanelPref);
  });

  is(button.hidden, false, "Downloads button should not be hidden.");

  info("Check context menu for downloads button.");
  await openContextMenu(button);
  is(checkbox.hidden, false, "Always Open checkbox is visible.");
  is(checkbox.getAttribute("checked"), "true", "Always Open is enabled.");

  info("Disable Always Open via context menu.");
  clickCheckbox(checkbox);
  is(
    Services.prefs.getBoolPref(alwaysOpenPanelPref),
    false,
    "Always Open pref has been set to false."
  );

  await downloadAndCheckPanel();

  await openContextMenu(button);
  is(checkbox.hidden, false, "Always Open checkbox is visible.");
  isnot(checkbox.getAttribute("checked"), "true", "Always Open is disabled.");

  info("Enable Always Open via context menu");
  clickCheckbox(checkbox);
  is(
    Services.prefs.getBoolPref(alwaysOpenPanelPref),
    true,
    "Pref has been set to true"
  );

  await checkPanelOpens();
});

/**
 * Verify that the downloads panel opens if the download did not open a file
 * picker or UCT dialog
 */
add_task(async function test_downloads_panel_after_no_dialogs() {
  await testDownloadsPanelAfterDialog({ expectPanelToOpen: true });
  ok(true, "Downloads panel opened because no dialogs were opened.");
});

/**
 * Verify that the downloads panel doesn't open if the download opened an
 * unknown content type dialog (e.g. action = always ask)
 */
add_task(async function test_downloads_panel_after_UCT_dialog() {
  await testDownloadsPanelAfterDialog({
    expectPanelToOpen: false,
    preferredAction: Ci.nsIHandlerInfo.alwaysAsk,
  });
  ok(true, "Downloads panel suppressed after UCT dialog.");
});

/**
 * Verify that the downloads panel doesn't open if the download opened a file
 * picker dialog (e.g. useDownloadDir = false)
 */
add_task(async function test_downloads_panel_after_file_picker_dialog() {
  await testDownloadsPanelAfterDialog({
    expectPanelToOpen: false,
    preferredAction: Ci.nsIHandlerInfo.saveToDisk,
    askWhereToSave: true,
  });
  ok(true, "Downloads panel suppressed after file picker dialog.");
});

/**
 * Verify that the downloads panel doesn't open if the download opened both
 * dialogs (e.g. default action = always ask AND useDownloadDir = false)
 */
add_task(async function test_downloads_panel_after_both_dialogs() {
  await testDownloadsPanelAfterDialog({
    expectPanelToOpen: false,
    preferredAction: Ci.nsIHandlerInfo.alwaysAsk,
    askWhereToSave: true,
  });
  ok(true, "Downloads panel suppressed after UCT and file picker dialogs.");
});
