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

/*
 * Test cancellation of a download in order to test edge-cases related to
 * channel diversion.  Channel diversion occurs in cases of file (and PSM cert)
 * downloads where we realize in the child that we really want to consume the
 * channel data in the parent.  For data "sourced" by the parent, like network
 * data, data streaming to the child is suspended and the parent waits for the
 * child to send back the data it already received, then the channel is resumed.
 * For data generated by the child, such as (the current, to be mooted by
 * parent-intercept) child-side intercept, the data (currently) stream is
 * continually pumped up to the parent.
 *
 * In particular, we want to reproduce the circumstances of Bug 1418795 where
 * the child-side input-stream pump attempts to send data to the parent process
 * but the parent has canceled the channel and so the IPC Actor has been torn
 * down.  Diversion begins once the nsURILoader receives the OnStartRequest
 * notification with the headers, so there are two ways to produce
 */

ChromeUtils.import('resource://gre/modules/Services.jsm');
const { Downloads } = ChromeUtils.import("resource://gre/modules/Downloads.jsm", {});

/**
 * Clear the downloads list so other tests don't see our byproducts.
 */
async function clearDownloads() {
  const downloads = await Downloads.getList(Downloads.ALL);
  downloads.removeFinished();
}

/**
 * Returns a Promise that will be resolved once the download dialog shows up and
 * we have clicked the given button.
 */
function promiseClickDownloadDialogButton(buttonAction) {
  const uri = "chrome://mozapps/content/downloads/unknownContentType.xul";
  BrowserTestUtils.promiseAlertDialogOpen(buttonAction, uri, async win => {
    // nsHelperAppDlg.js currently uses an eval-based setTimeout(0) to invoke
    // its postShowCallback that results in a misleading error to the console
    // if we close the dialog before it gets a chance to run.  Just a
    // setTimeout is not sufficient because it appears we get our "load"
    // listener before the document's, so we use TestUtils.waitForTick() to
    // defer until after its load handler runs, then use setTimeout(0) to end
    // up after its eval.
    await TestUtils.waitForTick();

    await new Promise(resolve => setTimeout(resolve, 0));

    const button = win.document.documentElement.getButton(buttonAction);
    button.disabled = false;
    info(`clicking ${buttonAction} button`);
    button.click();
  });
}

async function performCanceledDownload(tab, path) {
  // Start waiting for the download dialog before triggering the download.
  info("watching for download popup");
  const cancelDownload = promiseClickDownloadDialogButton("cancel");

  // Trigger the download.
  info(`triggering download of "${path}"`);
  await ContentTask.spawn(
    tab.linkedBrowser,
    path,
    function(path) {
      // Put a Promise in place that we can wait on for stream closure.
      content.wrappedJSObject.trackStreamClosure(path);
      // Create the link and trigger the download.
      const link = content.document.createElement('a');
      link.href = path;
      link.download = path;
      content.document.body.appendChild(link);
      link.click();
    });

  // Wait for the cancelation to have been triggered.
  info("waiting for download popup");
  await cancelDownload;
  ok(true, "canceled download");

  // Wait for confirmation that the stream stopped.
  info(`wait for the ${path} stream to close.`);
  const why = await ContentTask.spawn(
    tab.linkedBrowser,
    path,
    function(path) {
      return content.wrappedJSObject.streamClosed[path].promise;
    });
  is(why.why, "canceled", "Ensure the stream canceled instead of timing out.");
  // Note that for the "sw-stream-download" case, we end up with a bogus
  // reason of "'close' may only be called on a stream in the 'readable' state."
  // Since we aren't actually invoking close(), I'm assuming this is an
  // implementation bug that will be corrected in the web platform tests.
  info(`Cancellation reason: ${why.message} after ${why.ticks} ticks`);
}

const gTestRoot = getRootDirectory(gTestPath)
  .replace("chrome://mochitests/content/", "http://mochi.test:8888/");


const PAGE_URL = `${gTestRoot}download_canceled/page_download_canceled.html`;

add_task(async function interruptedDownloads() {
  await SpecialPowers.pushPrefEnv({'set': [
    ['dom.serviceWorkers.enabled', true],
    ['dom.serviceWorkers.exemptFromPerDomainMax', true],
    ['dom.serviceWorkers.testing.enabled', true],
    ["javascript.options.streams", true],
    ["dom.streams.enabled", true],
  ]});

  // Open the tab
  const tab = await BrowserTestUtils.openNewForegroundTab({
    gBrowser,
    opening: PAGE_URL
  });

  // Wait for it to become controlled.  Check that it was a promise that
  // resolved as expected rather than undefined by checking the return value.
  const controlled = await ContentTask.spawn(
    tab.linkedBrowser,
    null,
    function() {
      // This is a promise set up by the page during load, and we are post-load.
      return content.wrappedJSObject.controlled;
    });
  is(controlled, "controlled", "page became controlled");

  // Download a pass-through fetch stream.
  await performCanceledDownload(tab, "sw-passthrough-download");

  // Download a SW-generated stream
  await performCanceledDownload(tab, "sw-stream-download");

  // Cleanup
  await ContentTask.spawn(
    tab.linkedBrowser,
    null,
    function() {
      return content.wrappedJSObject.registration.unregister();
    });
  await BrowserTestUtils.removeTab(tab);
  await clearDownloads();
});
