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

const TEST_PAGE_URL = "http://mochi.test:8888/browser/toolkit/components/thumbnails/test/thumbnails_background.sjs";

const imports = {};
Cu.import("resource://gre/modules/BackgroundPageThumbs.jsm", imports);
Cu.import("resource://gre/modules/PageThumbs.jsm", imports);
Cu.import("resource://gre/modules/Task.jsm", imports);
Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", imports);
registerCleanupFunction(function () {
  imports.BackgroundPageThumbs._destroy();
});

function test() {
  requestLongerTimeout(2);
  waitForExplicitFinish();
  spawnNextTest();
}

function spawnNextTest() {
  if (!tests.length) {
    finish();
    return;
  }

  let nextTest = tests.shift();
  info("starting sub-test " + nextTest.name);
  imports.Task.spawn(nextTest).then(spawnNextTest, function onError(err) {
    ok(false, err);
    spawnNextTest();
  });
}

let tests = [

  function basic() {
    let url = "http://www.example.com/";
    let file = fileForURL(url);
    ok(!file.exists(), "Thumbnail should not be cached yet.");

    let capturedURL = yield capture(url);
    is(capturedURL, url, "Captured URL should be URL passed to capture");

    ok(file.exists(), "Thumbnail should be cached after capture: " + file.path);
    file.remove(false);
  },

  function queueing() {
    let deferred = imports.Promise.defer();
    let urls = [
      "http://www.example.com/0",
      "http://www.example.com/1",
      // an item that will timeout to ensure timeouts work and we resume.
      testPageURL({ wait: 2002 }),
      "http://www.example.com/2",
    ];
    let files = urls.map(fileForURL);
    files.forEach(f => ok(!f.exists(), "Thumbnail should not be cached yet."));
    urls.forEach(function (url) {
      let isTimeoutTest = url.indexOf("wait") >= 0;
      imports.BackgroundPageThumbs.capture(url, {
        timeout: isTimeoutTest ? 100 : 30000,
        onDone: function onDone(capturedURL) {
          ok(urls.length > 0, "onDone called, so URLs should still remain");
          is(capturedURL, urls.shift(),
             "Captured URL should be currently expected URL (i.e., " +
             "capture() callbacks should be called in the correct order)");
          let file = files.shift();
          if (isTimeoutTest) {
            ok(!file.exists(),
               "Thumbnail shouldn't exist for timed out capture: " + file.path);
          } else {
            ok(file.exists(),
               "Thumbnail should be cached after capture: " + file.path);
          }
          if (!urls.length)
            deferred.resolve();
        },
      });
    });
    yield deferred.promise;
  },

  function timeout() {
    let deferred = imports.Promise.defer();
    let url = testPageURL({ wait: 30000 });
    let file = fileForURL(url);
    ok(!file.exists(), "Thumbnail should not be cached already.");
    let numCalls = 0;
    imports.BackgroundPageThumbs.capture(url, {
      timeout: 0,
      onDone: function onDone(capturedURL) {
        is(capturedURL, url, "Captured URL should be URL passed to capture");
        is(numCalls++, 0, "onDone should be called only once");
        ok(!file.exists(),
           "Capture timed out so thumbnail should not be cached: " + file.path);
        deferred.resolve();
      },
    });
    yield deferred.promise;
  },

  function redirect() {
    let finalURL = "http://example.com/redirected";
    let originalURL = testPageURL({ redirect: finalURL });

    let originalFile = fileForURL(originalURL);
    ok(!originalFile.exists(),
       "Thumbnail file for original URL should not exist yet.");

    let finalFile = fileForURL(finalURL);
    ok(!finalFile.exists(),
       "Thumbnail file for final URL should not exist yet.");

    let capturedURL = yield capture(originalURL);
    is(capturedURL, originalURL,
       "Captured URL should be URL passed to capture");
    ok(originalFile.exists(),
       "Thumbnail for original URL should be cached: " + originalFile.path);
    ok(finalFile.exists(),
       "Thumbnail for final URL should be cached: " + finalFile.path);

    originalFile.remove(false);
    finalFile.remove(false);
  },

  function destroyBrowserTimeout() {
    let url1 = "http://example.com/1";
    let file1 = fileForURL(url1);
    ok(!file1.exists(), "First file should not exist yet.");

    let url2 = "http://example.com/2";
    let file2 = fileForURL(url2);
    ok(!file2.exists(), "Second file should not exist yet.");

    let defaultTimeout = imports.BackgroundPageThumbs._destroyBrowserTimeout;
    imports.BackgroundPageThumbs._destroyBrowserTimeout = 1000;

    yield capture(url1);
    ok(file1.exists(), "First file should exist after capture.");
    file1.remove(false);

    yield wait(2000);
    is(imports.BackgroundPageThumbs._thumbBrowser, undefined,
       "Thumb browser should be destroyed after timeout.");
    imports.BackgroundPageThumbs._destroyBrowserTimeout = defaultTimeout;

    yield capture(url2);
    ok(file2.exists(), "Second file should exist after capture.");
    file2.remove(false);

    isnot(imports.BackgroundPageThumbs._thumbBrowser, undefined,
          "Thumb browser should exist immediately after capture.");
  },

  function privateBrowsingActive() {
    let url = "http://example.com/";
    let file = fileForURL(url);
    ok(!file.exists(), "Thumbnail file should not already exist.");

    let win = yield openPrivateWindow();
    let capturedURL = yield capture(url);
    is(capturedURL, url, "Captured URL should be URL passed to capture.");
    ok(file.exists(),
       "Thumbnail file should be created even when a private window is open.");
    file.remove(false);

    win.close();
  },

  function openPrivateWindowDuringCapture() {
    let url = "http://example.com/";
    let file = fileForURL(url);
    ok(!file.exists(), "Thumbnail file should not already exist.");

    let deferred = imports.Promise.defer();

    let waitCount = 0;
    function maybeFinish() {
      if (++waitCount == 2)
        deferred.resolve();
    }

    imports.BackgroundPageThumbs.capture(url, {
      onDone: function (capturedURL) {
        is(capturedURL, url, "Captured URL should be URL passed to capture.");
        ok(file.exists(),
           "Thumbnail file should be created even though a private window " +
           "was opened during the capture.");
        file.remove(false);
        maybeFinish();
      },
    });

    // Opening the private window at this point relies on a couple of
    // implementation details: (1) The capture will start immediately and
    // synchronously (since at this point in the test, the service is
    // initialized and its queue is empty), and (2) when it starts the capture
    // registers with the window watcher.
    openPrivateWindow().then(function (win) {
      win.close();
      maybeFinish();
    });

    yield deferred.promise;
  },

  function noCookiesSent() {
    // Visit the test page in the browser and tell it to set a cookie.
    let url = testPageURL({ setGreenCookie: true });
    let tab = gBrowser.loadOneTab(url, { inBackground: false });
    let browser = tab.linkedBrowser;
    yield onPageLoad(browser);

    // The root element of the page shouldn't be green yet.
    let greenStr = "rgb(0, 255, 0)";
    isnot(browser.contentDocument.documentElement.style.backgroundColor,
          greenStr,
          "The page shouldn't be green yet.");

    // Cookie should be set now.  Reload the page to verify.  Its root element
    // will be green if the cookie's set.
    browser.reload();
    yield onPageLoad(browser);
    is(browser.contentDocument.documentElement.style.backgroundColor,
       greenStr,
       "The page should be green now.");

    // Capture the page.  Get the image data of the capture and verify it's not
    // green.  (Checking only the first pixel suffices.)
    yield capture(url);
    let file = fileForURL(url);
    ok(file.exists(), "Thumbnail file should exist after capture.");

    let deferred = imports.Promise.defer();
    retrieveImageDataForURL(url, function ([r, g, b]) {
      isnot([r, g, b].toString(), [0, 255, 0].toString(),
            "The captured page should not be green.");
      gBrowser.removeTab(tab);
      file.remove(false);
      deferred.resolve();
    });
    yield deferred.promise;
  },

  // check that if a page captured in the background attempts to set a cookie,
  // that cookie is not saved for subsequent requests.
  function noCookiesStored() {
    let url = testPageURL({ setRedCookie: true });
    let file = fileForURL(url);
    ok(!file.exists(), "Thumbnail file should not exist before capture.");
    yield capture(url);
    ok(file.exists(), "Thumbnail file should exist after capture.");
    file.remove(false);
    // now load it up in a browser - it should *not* be red, otherwise the
    // cookie above was saved.
    let tab = gBrowser.loadOneTab(url, { inBackground: false });
    let browser = tab.linkedBrowser;
    yield onPageLoad(browser);

    // The root element of the page shouldn't be red.
    let redStr = "rgb(255, 0, 0)";
    isnot(browser.contentDocument.documentElement.style.backgroundColor,
          redStr,
          "The page shouldn't be red.");
    gBrowser.removeTab(tab);
  },

  // the following tests attempt to display modal dialogs.  The test just
  // relies on the fact that if the dialog was displayed the test will hang
  // and timeout.  IOW - the tests would pass if the dialogs appear and are
  // manually closed by the user - so don't do that :)  (obviously there is
  // noone available to do that when run via tbpl etc, so this should be safe,
  // and it's tricky to use the window-watcher to check a window *does not*
  // appear - how long should the watcher be active before assuming it's not
  // going to appear?)
  function noAuthPrompt() {
    let url = "http://mochi.test:8888/browser/browser/base/content/test/authenticate.sjs?user=anyone";
    let file = fileForURL(url);
    ok(!file.exists(), "Thumbnail file should not already exist.");

    let capturedURL = yield capture(url);
    is(capturedURL, url, "Captured URL should be URL passed to capture.");
    ok(file.exists(),
       "Thumbnail file should exist even though it requires auth.");
    file.remove(false);
  },

  function noAlert() {
    let url = "data:text/html,<script>alert('yo!');</script>";
    let file = fileForURL(url);
    ok(!file.exists(), "Thumbnail file should not already exist.");

    let capturedURL = yield capture(url);
    is(capturedURL, url, "Captured URL should be URL passed to capture.");
    ok(file.exists(),
       "Thumbnail file should exist even though it alerted.");
    file.remove(false);
  },

  function noDuplicates() {
    let deferred = imports.Promise.defer();
    let url = "http://example.com/1";
    let file = fileForURL(url);
    ok(!file.exists(), "Thumbnail file should not already exist.");
    let numCallbacks = 0;
    let doneCallback = function(doneUrl) {
      is(doneUrl, url, "called back with correct url");
      numCallbacks += 1;
      // We will delete the file after the first callback, then check it
      // still doesn't exist on the second callback, which should give us
      // confidence that we didn't end up with 2 different captures happening
      // for the same url...
      if (numCallbacks == 1) {
        ok(file.exists(), "Thumbnail file should now exist.");
        file.remove(false);
        return;
      }
      if (numCallbacks == 2) {
        ok(!file.exists(), "Thumbnail file should still be deleted.");
        // and that's all we expect, so we are done...
        deferred.resolve();
        return;
      }
      ok(false, "only expecting 2 callbacks");
    }
    imports.BackgroundPageThumbs.capture(url, {onDone: doneCallback});
    imports.BackgroundPageThumbs.capture(url, {onDone: doneCallback});
    yield deferred.promise;
  },

  function capIfMissing() {
    let url = "http://example.com/";
    let file = fileForURL(url);
    ok(!file.exists(), "Thumbnail file should not already exist.");

    let capturedURL = yield captureIfMissing(url);
    is(capturedURL, url, "Captured URL should be URL passed to capture");
    ok(file.exists(), "Thumbnail should be cached after capture: " + file.path);

    let past = Date.now() - 1000000000;
    let pastFudge = past + 30000;
    file.lastModifiedTime = past;
    ok(file.lastModifiedTime < pastFudge, "Last modified time should stick!");
    capturedURL = yield captureIfMissing(url);
    is(capturedURL, url, "Captured URL should be URL passed to second capture");
    ok(file.exists(), "Thumbnail should remain cached after second capture: " +
                      file.path);
    ok(file.lastModifiedTime < pastFudge,
       "File should not have been overwritten");

    file.remove(false);
  },
];

function capture(url, options) {
  return captureWithMethod("capture", url, options);
}

function captureIfMissing(url, options) {
  return captureWithMethod("captureIfMissing", url, options);
}

function captureWithMethod(methodName, url, options={}) {
  let deferred = imports.Promise.defer();
  options.onDone = function onDone(capturedURL) {
    deferred.resolve(capturedURL);
  };
  imports.BackgroundPageThumbs[methodName](url, options);
  return deferred.promise;
}

function testPageURL(opts) {
  return TEST_PAGE_URL + "?" + encodeURIComponent(JSON.stringify(opts || {}));
}

function fileForURL(url) {
  let path = imports.PageThumbsStorage.getFilePathForURL(url);
  let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
  file.initWithPath(path);
  return file;
}

function wait(ms) {
  let deferred = imports.Promise.defer();
  setTimeout(function onTimeout() {
    deferred.resolve();
  }, ms);
  return deferred.promise;
}

function openPrivateWindow() {
  let deferred = imports.Promise.defer();
  // from OpenBrowserWindow in browser.js
  let win = window.openDialog("chrome://browser/content/", "_blank",
                              "chrome,all,dialog=no,private",
                              "about:privatebrowsing");
  win.addEventListener("load", function load(event) {
    if (event.target == win.document) {
      win.removeEventListener("load", load);
      deferred.resolve(win);
    }
  });
  return deferred.promise;
}

function onPageLoad(browser) {
  let deferred = imports.Promise.defer();
  browser.addEventListener("load", function load(event) {
    if (event.target == browser.contentWindow.document) {
      browser.removeEventListener("load", load, true);
      deferred.resolve();
    }
  }, true);
  return deferred.promise;
}
