/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
 * http://creativecommons.org/publicdomain/zero/1.0/ */

/**
 * Tests the DownloadList object.
 */

"use strict";

// Globals

/**
 * Returns a PRTime in the past usable to add expirable visits.
 *
 * @note Expiration ignores any visit added in the last 7 days, but it's
 *       better be safe against DST issues, by going back one day more.
 */
function getExpirablePRTime()
{
  let dateObj = new Date();
  // Normalize to midnight
  dateObj.setHours(0);
  dateObj.setMinutes(0);
  dateObj.setSeconds(0);
  dateObj.setMilliseconds(0);
  dateObj = new Date(dateObj.getTime() - 8 * 86400000);
  return dateObj.getTime() * 1000;
}

/**
 * Adds an expirable history visit for a download.
 *
 * @param aSourceUrl
 *        String containing the URI for the download source, or null to use
 *        httpUrl("source.txt").
 *
 * @return {Promise}
 * @rejects JavaScript exception.
 */
function promiseExpirableDownloadVisit(aSourceUrl)
{
  let deferred = Promise.defer();
  PlacesUtils.asyncHistory.updatePlaces(
    {
      uri: NetUtil.newURI(aSourceUrl || httpUrl("source.txt")),
      visits: [{
        transitionType: Ci.nsINavHistoryService.TRANSITION_DOWNLOAD,
        visitDate: getExpirablePRTime(),
      }]
    },
    {
      handleError: function handleError(aResultCode, aPlaceInfo) {
        let ex = new Components.Exception("Unexpected error in adding visits.",
                                          aResultCode);
        deferred.reject(ex);
      },
      handleResult: function () {},
      handleCompletion: function handleCompletion() {
        deferred.resolve();
      }
    });
  return deferred.promise;
}

// Tests

/**
 * Checks the testing mechanism used to build different download lists.
 */
add_task(function* test_construction()
{
  let downloadListOne = yield promiseNewList();
  let downloadListTwo = yield promiseNewList();
  let privateDownloadListOne = yield promiseNewList(true);
  let privateDownloadListTwo = yield promiseNewList(true);

  do_check_neq(downloadListOne, downloadListTwo);
  do_check_neq(privateDownloadListOne, privateDownloadListTwo);
  do_check_neq(downloadListOne, privateDownloadListOne);
});

/**
 * Checks the methods to add and retrieve items from the list.
 */
add_task(function* test_add_getAll()
{
  let list = yield promiseNewList();

  let downloadOne = yield promiseNewDownload();
  yield list.add(downloadOne);

  let itemsOne = yield list.getAll();
  do_check_eq(itemsOne.length, 1);
  do_check_eq(itemsOne[0], downloadOne);

  let downloadTwo = yield promiseNewDownload();
  yield list.add(downloadTwo);

  let itemsTwo = yield list.getAll();
  do_check_eq(itemsTwo.length, 2);
  do_check_eq(itemsTwo[0], downloadOne);
  do_check_eq(itemsTwo[1], downloadTwo);

  // The first snapshot should not have been modified.
  do_check_eq(itemsOne.length, 1);
});

/**
 * Checks the method to remove items from the list.
 */
add_task(function* test_remove()
{
  let list = yield promiseNewList();

  yield list.add(yield promiseNewDownload());
  yield list.add(yield promiseNewDownload());

  let items = yield list.getAll();
  yield list.remove(items[0]);

  // Removing an item that was never added should not raise an error.
  yield list.remove(yield promiseNewDownload());

  items = yield list.getAll();
  do_check_eq(items.length, 1);
});

/**
 * Tests that the "add", "remove", and "getAll" methods on the global
 * DownloadCombinedList object combine the contents of the global DownloadList
 * objects for public and private downloads.
 */
add_task(function* test_DownloadCombinedList_add_remove_getAll()
{
  let publicList = yield promiseNewList();
  let privateList = yield Downloads.getList(Downloads.PRIVATE);
  let combinedList = yield Downloads.getList(Downloads.ALL);

  let publicDownload = yield promiseNewDownload();
  let privateDownload = yield Downloads.createDownload({
    source: { url: httpUrl("source.txt"), isPrivate: true },
    target: getTempFile(TEST_TARGET_FILE_NAME).path,
  });

  yield publicList.add(publicDownload);
  yield privateList.add(privateDownload);

  do_check_eq((yield combinedList.getAll()).length, 2);

  yield combinedList.remove(publicDownload);
  yield combinedList.remove(privateDownload);

  do_check_eq((yield combinedList.getAll()).length, 0);

  yield combinedList.add(publicDownload);
  yield combinedList.add(privateDownload);

  do_check_eq((yield publicList.getAll()).length, 1);
  do_check_eq((yield privateList.getAll()).length, 1);
  do_check_eq((yield combinedList.getAll()).length, 2);

  yield publicList.remove(publicDownload);
  yield privateList.remove(privateDownload);

  do_check_eq((yield combinedList.getAll()).length, 0);
});

/**
 * Checks that views receive the download add and remove notifications, and that
 * adding and removing views works as expected, both for a normal and a combined
 * list.
 */
add_task(function* test_notifications_add_remove()
{
  for (let isCombined of [false, true]) {
    // Force creating a new list for both the public and combined cases.
    let list = yield promiseNewList();
    if (isCombined) {
      list = yield Downloads.getList(Downloads.ALL);
    }

    let downloadOne = yield promiseNewDownload();
    let downloadTwo = yield Downloads.createDownload({
      source: { url: httpUrl("source.txt"), isPrivate: true },
      target: getTempFile(TEST_TARGET_FILE_NAME).path,
    });
    yield list.add(downloadOne);
    yield list.add(downloadTwo);

    // Check that we receive add notifications for existing elements.
    let addNotifications = 0;
    let viewOne = {
      onDownloadAdded: function (aDownload) {
        // The first download to be notified should be the first that was added.
        if (addNotifications == 0) {
          do_check_eq(aDownload, downloadOne);
        } else if (addNotifications == 1) {
          do_check_eq(aDownload, downloadTwo);
        }
        addNotifications++;
      },
    };
    yield list.addView(viewOne);
    do_check_eq(addNotifications, 2);

    // Check that we receive add notifications for new elements.
    yield list.add(yield promiseNewDownload());
    do_check_eq(addNotifications, 3);

    // Check that we receive remove notifications.
    let removeNotifications = 0;
    let viewTwo = {
      onDownloadRemoved: function (aDownload) {
        do_check_eq(aDownload, downloadOne);
        removeNotifications++;
      },
    };
    yield list.addView(viewTwo);
    yield list.remove(downloadOne);
    do_check_eq(removeNotifications, 1);

    // We should not receive remove notifications after the view is removed.
    yield list.removeView(viewTwo);
    yield list.remove(downloadTwo);
    do_check_eq(removeNotifications, 1);

    // We should not receive add notifications after the view is removed.
    yield list.removeView(viewOne);
    yield list.add(yield promiseNewDownload());
    do_check_eq(addNotifications, 3);
  }
});

/**
 * Checks that views receive the download change notifications, both for a
 * normal and a combined list.
 */
add_task(function* test_notifications_change()
{
  for (let isCombined of [false, true]) {
    // Force creating a new list for both the public and combined cases.
    let list = yield promiseNewList();
    if (isCombined) {
      list = yield Downloads.getList(Downloads.ALL);
    }

    let downloadOne = yield promiseNewDownload();
    let downloadTwo = yield Downloads.createDownload({
      source: { url: httpUrl("source.txt"), isPrivate: true },
      target: getTempFile(TEST_TARGET_FILE_NAME).path,
    });
    yield list.add(downloadOne);
    yield list.add(downloadTwo);

    // Check that we receive change notifications.
    let receivedOnDownloadChanged = false;
    yield list.addView({
      onDownloadChanged: function (aDownload) {
        do_check_eq(aDownload, downloadOne);
        receivedOnDownloadChanged = true;
      },
    });
    yield downloadOne.start();
    do_check_true(receivedOnDownloadChanged);

    // We should not receive change notifications after a download is removed.
    receivedOnDownloadChanged = false;
    yield list.remove(downloadTwo);
    yield downloadTwo.start();
    do_check_false(receivedOnDownloadChanged);
  }
});

/**
 * Checks that the reference to "this" is correct in the view callbacks.
 */
add_task(function* test_notifications_this()
{
  let list = yield promiseNewList();

  // Check that we receive change notifications.
  let receivedOnDownloadAdded = false;
  let receivedOnDownloadChanged = false;
  let receivedOnDownloadRemoved = false;
  let view = {
    onDownloadAdded: function () {
      do_check_eq(this, view);
      receivedOnDownloadAdded = true;
    },
    onDownloadChanged: function () {
      // Only do this check once.
      if (!receivedOnDownloadChanged) {
        do_check_eq(this, view);
        receivedOnDownloadChanged = true;
      }
    },
    onDownloadRemoved: function () {
      do_check_eq(this, view);
      receivedOnDownloadRemoved = true;
    },
  };
  yield list.addView(view);

  let download = yield promiseNewDownload();
  yield list.add(download);
  yield download.start();
  yield list.remove(download);

  // Verify that we executed the checks.
  do_check_true(receivedOnDownloadAdded);
  do_check_true(receivedOnDownloadChanged);
  do_check_true(receivedOnDownloadRemoved);
});

/**
 * Checks that download is removed on history expiration.
 */
add_task(function* test_history_expiration()
{
  mustInterruptResponses();

  function cleanup() {
    Services.prefs.clearUserPref("places.history.expiration.max_pages");
  }
  do_register_cleanup(cleanup);

  // Set max pages to 0 to make the download expire.
  Services.prefs.setIntPref("places.history.expiration.max_pages", 0);

  let list = yield promiseNewList();
  let downloadOne = yield promiseNewDownload();
  let downloadTwo = yield promiseNewDownload(httpUrl("interruptible.txt"));

  let deferred = Promise.defer();
  let removeNotifications = 0;
  let downloadView = {
    onDownloadRemoved: function (aDownload) {
      if (++removeNotifications == 2) {
        deferred.resolve();
      }
    },
  };
  yield list.addView(downloadView);

  // Work with one finished download and one canceled download.
  yield downloadOne.start();
  downloadTwo.start().catch(() => {});
  yield downloadTwo.cancel();

  // We must replace the visits added while executing the downloads with visits
  // that are older than 7 days, otherwise they will not be expired.
  yield PlacesTestUtils.clearHistory();
  yield promiseExpirableDownloadVisit();
  yield promiseExpirableDownloadVisit(httpUrl("interruptible.txt"));

  // After clearing history, we can add the downloads to be removed to the list.
  yield list.add(downloadOne);
  yield list.add(downloadTwo);

  // Force a history expiration.
  Cc["@mozilla.org/places/expiration;1"]
    .getService(Ci.nsIObserver).observe(null, "places-debug-start-expiration", -1);

  // Wait for both downloads to be removed.
  yield deferred.promise;

  cleanup();
});

/**
 * Checks all downloads are removed after clearing history.
 */
add_task(function* test_history_clear()
{
  let list = yield promiseNewList();
  let downloadOne = yield promiseNewDownload();
  let downloadTwo = yield promiseNewDownload();
  yield list.add(downloadOne);
  yield list.add(downloadTwo);

  let deferred = Promise.defer();
  let removeNotifications = 0;
  let downloadView = {
    onDownloadRemoved: function (aDownload) {
      if (++removeNotifications == 2) {
        deferred.resolve();
      }
    },
  };
  yield list.addView(downloadView);

  yield downloadOne.start();
  yield downloadTwo.start();

  yield PlacesTestUtils.clearHistory();

  // Wait for the removal notifications that may still be pending.
  yield deferred.promise;
});

/**
 * Tests the removeFinished method to ensure that it only removes
 * finished downloads.
 */
add_task(function* test_removeFinished()
{
  let list = yield promiseNewList();
  let downloadOne = yield promiseNewDownload();
  let downloadTwo = yield promiseNewDownload();
  let downloadThree = yield promiseNewDownload();
  let downloadFour = yield promiseNewDownload();
  yield list.add(downloadOne);
  yield list.add(downloadTwo);
  yield list.add(downloadThree);
  yield list.add(downloadFour);

  let deferred = Promise.defer();
  let removeNotifications = 0;
  let downloadView = {
    onDownloadRemoved: function (aDownload) {
      do_check_true(aDownload == downloadOne ||
                    aDownload == downloadTwo ||
                    aDownload == downloadThree);
      do_check_true(removeNotifications < 3);
      if (++removeNotifications == 3) {
        deferred.resolve();
      }
    },
  };
  yield list.addView(downloadView);

  // Start three of the downloads, but don't start downloadTwo, then set
  // downloadFour to have partial data. All downloads except downloadFour
  // should be removed.
  yield downloadOne.start();
  yield downloadThree.start();
  yield downloadFour.start();
  downloadFour.hasPartialData = true;

  list.removeFinished();
  yield deferred.promise;

  let downloads = yield list.getAll()
  do_check_eq(downloads.length, 1);
});

/**
 * Tests the global DownloadSummary objects for the public, private, and
 * combined download lists.
 */
add_task(function* test_DownloadSummary()
{
  mustInterruptResponses();

  let publicList = yield promiseNewList();
  let privateList = yield Downloads.getList(Downloads.PRIVATE);

  let publicSummary = yield Downloads.getSummary(Downloads.PUBLIC);
  let privateSummary = yield Downloads.getSummary(Downloads.PRIVATE);
  let combinedSummary = yield Downloads.getSummary(Downloads.ALL);

  // Add a public download that has succeeded.
  let succeededPublicDownload = yield promiseNewDownload();
  yield succeededPublicDownload.start();
  yield publicList.add(succeededPublicDownload);

  // Add a public download that has been canceled midway.
  let canceledPublicDownload =
      yield promiseNewDownload(httpUrl("interruptible.txt"));
  canceledPublicDownload.start().catch(() => {});
  yield promiseDownloadMidway(canceledPublicDownload);
  yield canceledPublicDownload.cancel();
  yield publicList.add(canceledPublicDownload);

  // Add a public download that is in progress.
  let inProgressPublicDownload =
      yield promiseNewDownload(httpUrl("interruptible.txt"));
  inProgressPublicDownload.start().catch(() => {});
  yield promiseDownloadMidway(inProgressPublicDownload);
  yield publicList.add(inProgressPublicDownload);

  // Add a private download that is in progress.
  let inProgressPrivateDownload = yield Downloads.createDownload({
    source: { url: httpUrl("interruptible.txt"), isPrivate: true },
    target: getTempFile(TEST_TARGET_FILE_NAME).path,
  });
  inProgressPrivateDownload.start().catch(() => {});
  yield promiseDownloadMidway(inProgressPrivateDownload);
  yield privateList.add(inProgressPrivateDownload);

  // Verify that the summary includes the total number of bytes and the
  // currently transferred bytes only for the downloads that are not stopped.
  // For simplicity, we assume that after a download is added to the list, its
  // current state is immediately propagated to the summary object, which is
  // true in the current implementation, though it is not guaranteed as all the
  // download operations may happen asynchronously.
  do_check_false(publicSummary.allHaveStopped);
  do_check_eq(publicSummary.progressTotalBytes, TEST_DATA_SHORT.length * 2);
  do_check_eq(publicSummary.progressCurrentBytes, TEST_DATA_SHORT.length);

  do_check_false(privateSummary.allHaveStopped);
  do_check_eq(privateSummary.progressTotalBytes, TEST_DATA_SHORT.length * 2);
  do_check_eq(privateSummary.progressCurrentBytes, TEST_DATA_SHORT.length);

  do_check_false(combinedSummary.allHaveStopped);
  do_check_eq(combinedSummary.progressTotalBytes, TEST_DATA_SHORT.length * 4);
  do_check_eq(combinedSummary.progressCurrentBytes, TEST_DATA_SHORT.length * 2);

  yield inProgressPublicDownload.cancel();

  // Stopping the download should have excluded it from the summary.
  do_check_true(publicSummary.allHaveStopped);
  do_check_eq(publicSummary.progressTotalBytes, 0);
  do_check_eq(publicSummary.progressCurrentBytes, 0);

  do_check_false(privateSummary.allHaveStopped);
  do_check_eq(privateSummary.progressTotalBytes, TEST_DATA_SHORT.length * 2);
  do_check_eq(privateSummary.progressCurrentBytes, TEST_DATA_SHORT.length);

  do_check_false(combinedSummary.allHaveStopped);
  do_check_eq(combinedSummary.progressTotalBytes, TEST_DATA_SHORT.length * 2);
  do_check_eq(combinedSummary.progressCurrentBytes, TEST_DATA_SHORT.length);

  yield inProgressPrivateDownload.cancel();

  // All the downloads should be stopped now.
  do_check_true(publicSummary.allHaveStopped);
  do_check_eq(publicSummary.progressTotalBytes, 0);
  do_check_eq(publicSummary.progressCurrentBytes, 0);

  do_check_true(privateSummary.allHaveStopped);
  do_check_eq(privateSummary.progressTotalBytes, 0);
  do_check_eq(privateSummary.progressCurrentBytes, 0);

  do_check_true(combinedSummary.allHaveStopped);
  do_check_eq(combinedSummary.progressTotalBytes, 0);
  do_check_eq(combinedSummary.progressCurrentBytes, 0);
});

/**
 * Checks that views receive the summary change notification.  This is tested on
 * the combined summary when adding a public download, as we assume that if we
 * pass the test in this case we will also pass it in the others.
 */
add_task(function* test_DownloadSummary_notifications()
{
  let list = yield promiseNewList();
  let summary = yield Downloads.getSummary(Downloads.ALL);

  let download = yield promiseNewDownload();
  yield list.add(download);

  // Check that we receive change notifications.
  let receivedOnSummaryChanged = false;
  yield summary.addView({
    onSummaryChanged: function () {
      receivedOnSummaryChanged = true;
    },
  });
  yield download.start();
  do_check_true(receivedOnSummaryChanged);
});