/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";

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

function backgroundScript() {
  let complete = new Map();

  function waitForComplete(id) {
    if (complete.has(id)) {
      return complete.get(id).promise;
    }

    let promise = new Promise(resolve => {
      complete.set(id, {resolve});
    });
    complete.get(id).promise = promise;
    return promise;
  }

  browser.downloads.onChanged.addListener(change => {
    if (change.state && change.state.current == "complete") {
      // Make sure we have a promise.
      waitForComplete(change.id);
      complete.get(change.id).resolve();
    }
  });

  browser.test.onMessage.addListener(async (msg, ...args) => {
    if (msg == "download.request") {
      try {
        let id = await browser.downloads.download(args[0]);
        browser.test.sendMessage("download.done", {status: "success", id});
      } catch (error) {
        browser.test.sendMessage("download.done", {status: "error", errmsg: error.message});
      }
    } else if (msg == "search.request") {
      try {
        let downloads = await browser.downloads.search(args[0]);
        browser.test.sendMessage("search.done", {status: "success", downloads});
      } catch (error) {
        browser.test.sendMessage("search.done", {status: "error", errmsg: error.message});
      }
    } else if (msg == "waitForComplete.request") {
      await waitForComplete(args[0]);
      browser.test.sendMessage("waitForComplete.done");
    }
  });

  browser.test.sendMessage("ready");
}

async function clearDownloads(callback) {
  let list = await Downloads.getList(Downloads.ALL);
  let downloads = await list.getAll();

  await Promise.all(downloads.map(download => list.remove(download)));

  return downloads;
}

add_task(async function test_decoded_filename_download() {
  const server = createHttpServer();
  server.registerPrefixHandler("/data/", (_, res) => res.write("length=8"));

  const BASE = `http://localhost:${server.identity.primaryPort}/data`;
  const FILE_NAME_ENCODED_1 = "file%2Fencode.txt";
  const FILE_NAME_DECODED_1 = "file_encode.txt";
  const FILE_NAME_ENCODED_URL_1 = BASE + "/" + FILE_NAME_ENCODED_1;
  const FILE_NAME_ENCODED_2 = "file%F0%9F%9A%B2encoded.txt";
  const FILE_NAME_DECODED_2 = "file\u{0001F6B2}encoded.txt";
  const FILE_NAME_ENCODED_URL_2 = BASE + "/" + FILE_NAME_ENCODED_2;
  const FILE_NAME_ENCODED_3 = "file%X%20encode.txt";
  const FILE_NAME_DECODED_3 = "file%X encode.txt";
  const FILE_NAME_ENCODED_URL_3 = BASE + "/" + FILE_NAME_ENCODED_3;
  const FILE_ENCODED_LEN = 8;

  const nsIFile = Ci.nsIFile;
  let downloadDir = FileUtils.getDir("TmpD", ["downloads"]);
  downloadDir.createUnique(nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
  info(`downloadDir ${downloadDir.path}`);

  function downloadPath(filename) {
    let path = downloadDir.clone();
    path.append(filename);
    return path.path;
  }

  Services.prefs.setIntPref("browser.download.folderList", 2);
  Services.prefs.setComplexValue("browser.download.dir", nsIFile, downloadDir);

  registerCleanupFunction(async () => {
    Services.prefs.clearUserPref("browser.download.folderList");
    Services.prefs.clearUserPref("browser.download.dir");
    await cleanupDir(downloadDir);
    await clearDownloads();
  });

  await clearDownloads().then(downloads => {
    info(`removed ${downloads.length} pre-existing downloads from history`);
  });

  let extension = ExtensionTestUtils.loadExtension({
    background: backgroundScript,
    manifest: {
      permissions: ["downloads"],
    },
  });

  async function download(options) {
    extension.sendMessage("download.request", options);
    let result = await extension.awaitMessage("download.done");

    if (result.status == "success") {
      info(`wait for onChanged event to indicate ${result.id} is complete`);
      extension.sendMessage("waitForComplete.request", result.id);

      await extension.awaitMessage("waitForComplete.done");
    }

    return result;
  }

  function search(query) {
    extension.sendMessage("search.request", query);
    return extension.awaitMessage("search.done");
  }

  await extension.startup();
  await extension.awaitMessage("ready");

  let downloadIds = {};
  let msg = await download({url: FILE_NAME_ENCODED_URL_1});
  equal(msg.status, "success", "download() succeeded");
  downloadIds.fileEncoded1 = msg.id;

  msg = await download({url: FILE_NAME_ENCODED_URL_2});
  equal(msg.status, "success", "download() succeeded");
  downloadIds.fileEncoded2 = msg.id;

  msg = await download({url: FILE_NAME_ENCODED_URL_3});
  equal(msg.status, "success", "download() succeeded");
  downloadIds.fileEncoded3 = msg.id;

  // Search for each individual download and check
  // the corresponding DownloadItem.
  async function checkDownloadItem(id, expect) {
    let item = await search({id});
    equal(item.status, "success", "search() succeeded");
    equal(item.downloads.length, 1, "search() found exactly 1 download");
    Object.keys(expect).forEach(function(field) {
      equal(item.downloads[0][field], expect[field], `DownloadItem.${field} is correct"`);
    });
  }

  await checkDownloadItem(downloadIds.fileEncoded1, {
    url: FILE_NAME_ENCODED_URL_1,
    filename: downloadPath(FILE_NAME_DECODED_1),
    state: "complete",
    bytesReceived: FILE_ENCODED_LEN,
    totalBytes: FILE_ENCODED_LEN,
    fileSize: FILE_ENCODED_LEN,
    exists: true,
  });

  await checkDownloadItem(downloadIds.fileEncoded2, {
    url: FILE_NAME_ENCODED_URL_2,
    filename: downloadPath(FILE_NAME_DECODED_2),
    state: "complete",
    bytesReceived: FILE_ENCODED_LEN,
    totalBytes: FILE_ENCODED_LEN,
    fileSize: FILE_ENCODED_LEN,
    exists: true,
  });

  await checkDownloadItem(downloadIds.fileEncoded3, {
    url: FILE_NAME_ENCODED_URL_3,
    filename: downloadPath(FILE_NAME_DECODED_3),
    state: "complete",
    bytesReceived: FILE_ENCODED_LEN,
    totalBytes: FILE_ENCODED_LEN,
    fileSize: FILE_ENCODED_LEN,
    exists: true,
  });

  // Searching for downloads by the decoded filename works correctly.
  async function checkSearch(query, expected, description) {
    let item = await search(query);
    equal(item.status, "success", "search() succeeded");
    equal(item.downloads.length, expected.length, `search() for ${description} found exactly ${expected.length} downloads`);
    equal(item.downloads[0].id, downloadIds[expected[0]], `search() for ${description} returned ${expected[0]} in position ${0}`);
  }

  await checkSearch({filename: downloadPath(FILE_NAME_DECODED_1)}, ["fileEncoded1"], "filename");
  await checkSearch({filename: downloadPath(FILE_NAME_DECODED_2)}, ["fileEncoded2"], "filename");
  await checkSearch({filename: downloadPath(FILE_NAME_DECODED_3)}, ["fileEncoded3"], "filename");

  await extension.unload();
});
