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

// See also browser/base/content/test/newtab/.

const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
Cu.import("resource:///modules/RemoteNewTabUtils.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/Services.jsm");

function run_test() {
  run_next_test();
}

add_task(function validCacheMidPopulation() {
  let expectedLinks = makeLinks(0, 3, 1);

  let provider = new TestProvider(done => done(expectedLinks));
  provider.maxNumLinks = expectedLinks.length;

  RemoteNewTabUtils.initWithoutProviders();
  RemoteNewTabUtils.links.addProvider(provider);
  let promise = new Promise(resolve => RemoteNewTabUtils.links.populateCache(resolve));

  // isTopSiteGivenProvider() and getProviderLinks() should still return results
  // even when cache is empty or being populated.
  do_check_false(RemoteNewTabUtils.isTopSiteGivenProvider("example1.com", provider));
  do_check_links(RemoteNewTabUtils.getProviderLinks(provider), []);

  yield promise;

  // Once the cache is populated, we get the expected results
  do_check_true(RemoteNewTabUtils.isTopSiteGivenProvider("example1.com", provider));
  do_check_links(RemoteNewTabUtils.getProviderLinks(provider), expectedLinks);
  RemoteNewTabUtils.links.removeProvider(provider);
});

add_task(function notifyLinkDelete() {
  let expectedLinks = makeLinks(0, 3, 1);

  let provider = new TestProvider(done => done(expectedLinks));
  provider.maxNumLinks = expectedLinks.length;

  RemoteNewTabUtils.initWithoutProviders();
  RemoteNewTabUtils.links.addProvider(provider);
  yield new Promise(resolve => RemoteNewTabUtils.links.populateCache(resolve));

  do_check_links(RemoteNewTabUtils.links.getLinks(), expectedLinks);

  // Remove a link.
  let removedLink = expectedLinks[2];
  provider.notifyLinkChanged(removedLink, 2, true);
  let links = RemoteNewTabUtils.links._providers.get(provider);

  // Check that sortedLinks is correctly updated.
  do_check_links(RemoteNewTabUtils.links.getLinks(), expectedLinks.slice(0, 2));

  // Check that linkMap is accurately updated.
  do_check_eq(links.linkMap.size, 2);
  do_check_true(links.linkMap.get(expectedLinks[0].url));
  do_check_true(links.linkMap.get(expectedLinks[1].url));
  do_check_false(links.linkMap.get(removedLink.url));

  // Check that siteMap is correctly updated.
  do_check_eq(links.siteMap.size, 2);
  do_check_true(links.siteMap.has(RemoteNewTabUtils.extractSite(expectedLinks[0].url)));
  do_check_true(links.siteMap.has(RemoteNewTabUtils.extractSite(expectedLinks[1].url)));
  do_check_false(links.siteMap.has(RemoteNewTabUtils.extractSite(removedLink.url)));

  RemoteNewTabUtils.links.removeProvider(provider);
});

add_task(function populatePromise() {
  let count = 0;
  let expectedLinks = makeLinks(0, 10, 2);

  let getLinksFcn = Task.async(function* (callback) {
    //Should not be calling getLinksFcn twice
    count++;
    do_check_eq(count, 1);
    yield Promise.resolve();
    callback(expectedLinks);
  });

  let provider = new TestProvider(getLinksFcn);

  RemoteNewTabUtils.initWithoutProviders();
  RemoteNewTabUtils.links.addProvider(provider);

  RemoteNewTabUtils.links.populateProviderCache(provider, () => {});
  RemoteNewTabUtils.links.populateProviderCache(provider, () => {
    do_check_links(RemoteNewTabUtils.links.getLinks(), expectedLinks);
    RemoteNewTabUtils.links.removeProvider(provider);
  });
});

add_task(function isTopSiteGivenProvider() {
  let expectedLinks = makeLinks(0, 10, 2);

  // The lowest 2 frecencies have the same base domain.
  expectedLinks[expectedLinks.length - 2].url = expectedLinks[expectedLinks.length - 1].url + "Test";

  let provider = new TestProvider(done => done(expectedLinks));
  provider.maxNumLinks = expectedLinks.length;

  RemoteNewTabUtils.initWithoutProviders();
  RemoteNewTabUtils.links.addProvider(provider);
  yield new Promise(resolve => RemoteNewTabUtils.links.populateCache(resolve));

  do_check_eq(RemoteNewTabUtils.isTopSiteGivenProvider("example2.com", provider), true);
  do_check_eq(RemoteNewTabUtils.isTopSiteGivenProvider("example1.com", provider), false);

  // Push out frecency 2 because the maxNumLinks is reached when adding frecency 3
  let newLink = makeLink(3);
  provider.notifyLinkChanged(newLink);

  // There is still a frecent url with example2 domain, so it's still frecent.
  do_check_eq(RemoteNewTabUtils.isTopSiteGivenProvider("example3.com", provider), true);
  do_check_eq(RemoteNewTabUtils.isTopSiteGivenProvider("example2.com", provider), true);

  // Push out frecency 3
  newLink = makeLink(5);
  provider.notifyLinkChanged(newLink);

  // Push out frecency 4
  newLink = makeLink(9);
  provider.notifyLinkChanged(newLink);

  // Our count reached 0 for the example2.com domain so it's no longer a frecent site.
  do_check_eq(RemoteNewTabUtils.isTopSiteGivenProvider("example5.com", provider), true);
  do_check_eq(RemoteNewTabUtils.isTopSiteGivenProvider("example2.com", provider), false);

  RemoteNewTabUtils.links.removeProvider(provider);
});

add_task(function multipleProviders() {
  // Make each provider generate RemoteNewTabUtils.links.maxNumLinks links to check
  // that no more than maxNumLinks are actually returned in the merged list.
  let evenLinks = makeLinks(0, 2 * RemoteNewTabUtils.links.maxNumLinks, 2);
  let evenProvider = new TestProvider(done => done(evenLinks));
  let oddLinks = makeLinks(0, 2 * RemoteNewTabUtils.links.maxNumLinks - 1, 2);
  let oddProvider = new TestProvider(done => done(oddLinks));

  RemoteNewTabUtils.initWithoutProviders();
  RemoteNewTabUtils.links.addProvider(evenProvider);
  RemoteNewTabUtils.links.addProvider(oddProvider);

  yield new Promise(resolve => RemoteNewTabUtils.links.populateCache(resolve));

  let links = RemoteNewTabUtils.links.getLinks();
  let expectedLinks = makeLinks(RemoteNewTabUtils.links.maxNumLinks,
                                2 * RemoteNewTabUtils.links.maxNumLinks,
                                1);
  do_check_eq(links.length, RemoteNewTabUtils.links.maxNumLinks);
  do_check_links(links, expectedLinks);

  RemoteNewTabUtils.links.removeProvider(evenProvider);
  RemoteNewTabUtils.links.removeProvider(oddProvider);
});

add_task(function changeLinks() {
  let expectedLinks = makeLinks(0, 20, 2);
  let provider = new TestProvider(done => done(expectedLinks));

  RemoteNewTabUtils.initWithoutProviders();
  RemoteNewTabUtils.links.addProvider(provider);

  yield new Promise(resolve => RemoteNewTabUtils.links.populateCache(resolve));

  do_check_links(RemoteNewTabUtils.links.getLinks(), expectedLinks);

  // Notify of a new link.
  let newLink = makeLink(19);
  expectedLinks.splice(1, 0, newLink);
  provider.notifyLinkChanged(newLink);
  do_check_links(RemoteNewTabUtils.links.getLinks(), expectedLinks);

  // Notify of a link that's changed sort criteria.
  newLink.frecency = 17;
  expectedLinks.splice(1, 1);
  expectedLinks.splice(2, 0, newLink);
  provider.notifyLinkChanged({
    url: newLink.url,
    frecency: 17,
  });
  do_check_links(RemoteNewTabUtils.links.getLinks(), expectedLinks);

  // Notify of a link that's changed title.
  newLink.title = "My frecency is now 17";
  provider.notifyLinkChanged({
    url: newLink.url,
    title: newLink.title,
  });
  do_check_links(RemoteNewTabUtils.links.getLinks(), expectedLinks);

  // Notify of a new link again, but this time make it overflow maxNumLinks.
  provider.maxNumLinks = expectedLinks.length;
  newLink = makeLink(21);
  expectedLinks.unshift(newLink);
  expectedLinks.pop();
  do_check_eq(expectedLinks.length, provider.maxNumLinks); // Sanity check.
  provider.notifyLinkChanged(newLink);
  do_check_links(RemoteNewTabUtils.links.getLinks(), expectedLinks);

  // Notify of many links changed.
  expectedLinks = makeLinks(0, 3, 1);
  provider.notifyManyLinksChanged();

  // Since _populateProviderCache() is async, we must wait until the provider's
  // populate promise has been resolved.
  yield RemoteNewTabUtils.links._providers.get(provider).populatePromise;

  // RemoteNewTabUtils.links will now repopulate its cache
  do_check_links(RemoteNewTabUtils.links.getLinks(), expectedLinks);

  RemoteNewTabUtils.links.removeProvider(provider);
});

add_task(function oneProviderAlreadyCached() {
  let links1 = makeLinks(0, 10, 1);
  let provider1 = new TestProvider(done => done(links1));

  RemoteNewTabUtils.initWithoutProviders();
  RemoteNewTabUtils.links.addProvider(provider1);

  yield new Promise(resolve => RemoteNewTabUtils.links.populateCache(resolve));
  do_check_links(RemoteNewTabUtils.links.getLinks(), links1);

  let links2 = makeLinks(10, 20, 1);
  let provider2 = new TestProvider(done => done(links2));
  RemoteNewTabUtils.links.addProvider(provider2);

  yield new Promise(resolve => RemoteNewTabUtils.links.populateCache(resolve));
  do_check_links(RemoteNewTabUtils.links.getLinks(), links2.concat(links1));

  RemoteNewTabUtils.links.removeProvider(provider1);
  RemoteNewTabUtils.links.removeProvider(provider2);
});

add_task(function newLowRankedLink() {
  // Init a provider with 10 links and make its maximum number also 10.
  let links = makeLinks(0, 10, 1);
  let provider = new TestProvider(done => done(links));
  provider.maxNumLinks = links.length;

  RemoteNewTabUtils.initWithoutProviders();
  RemoteNewTabUtils.links.addProvider(provider);

  yield new Promise(resolve => RemoteNewTabUtils.links.populateCache(resolve));
  do_check_links(RemoteNewTabUtils.links.getLinks(), links);

  // Notify of a new link that's low-ranked enough not to make the list.
  let newLink = makeLink(0);
  provider.notifyLinkChanged(newLink);
  do_check_links(RemoteNewTabUtils.links.getLinks(), links);

  // Notify about the new link's title change.
  provider.notifyLinkChanged({
    url: newLink.url,
    title: "a new title",
  });
  do_check_links(RemoteNewTabUtils.links.getLinks(), links);

  RemoteNewTabUtils.links.removeProvider(provider);
});

add_task(function extractSite() {
  // All these should extract to the same site
  [ "mozilla.org",
    "m.mozilla.org",
    "mobile.mozilla.org",
    "www.mozilla.org",
    "www3.mozilla.org",
  ].forEach(host => {
    let url = "http://" + host;
    do_check_eq(RemoteNewTabUtils.extractSite(url), "mozilla.org", "extracted same " + host);
  });

  // All these should extract to the same subdomain
  [ "bugzilla.mozilla.org",
    "www.bugzilla.mozilla.org",
  ].forEach(host => {
    let url = "http://" + host;
    do_check_eq(RemoteNewTabUtils.extractSite(url), "bugzilla.mozilla.org", "extracted eTLD+2 " + host);
  });

  // All these should not extract to the same site
  [ "bugzilla.mozilla.org",
    "bug123.bugzilla.mozilla.org",
    "too.many.levels.bugzilla.mozilla.org",
    "m2.mozilla.org",
    "mobile30.mozilla.org",
    "ww.mozilla.org",
    "ww2.mozilla.org",
    "wwwww.mozilla.org",
    "wwwww50.mozilla.org",
    "wwws.mozilla.org",
    "secure.mozilla.org",
    "secure10.mozilla.org",
    "many.levels.deep.mozilla.org",
    "just.check.in",
    "192.168.0.1",
    "localhost",
  ].forEach(host => {
    let url = "http://" + host;
    do_check_neq(RemoteNewTabUtils.extractSite(url), "mozilla.org", "extracted diff " + host);
  });

  // All these should not extract to the same site
  [ "about:blank",
    "file:///Users/user/file",
    "chrome://browser/something",
    "ftp://ftp.mozilla.org/",
  ].forEach(url => {
    do_check_neq(RemoteNewTabUtils.extractSite(url), "mozilla.org", "extracted diff url " + url);
  });
});

function TestProvider(getLinksFn) {
  this.getLinks = getLinksFn;
  this._observers = new Set();
}

TestProvider.prototype = {
  addObserver: function (observer) {
    this._observers.add(observer);
  },
  notifyLinkChanged: function (link, index=-1, deleted=false) {
    this._notifyObservers("onLinkChanged", link, index, deleted);
  },
  notifyManyLinksChanged: function () {
    this._notifyObservers("onManyLinksChanged");
  },
  _notifyObservers: function () {
    let observerMethodName = arguments[0];
    let args = Array.prototype.slice.call(arguments, 1);
    args.unshift(this);
    for (let obs of this._observers) {
      if (obs[observerMethodName])
        obs[observerMethodName].apply(RemoteNewTabUtils.links, args);
    }
  },
};

function do_check_links(actualLinks, expectedLinks) {
  do_check_true(Array.isArray(actualLinks));
  do_check_eq(actualLinks.length, expectedLinks.length);
  for (let i = 0; i < expectedLinks.length; i++) {
    let expected = expectedLinks[i];
    let actual = actualLinks[i];
    do_check_eq(actual.url, expected.url);
    do_check_eq(actual.title, expected.title);
    do_check_eq(actual.frecency, expected.frecency);
    do_check_eq(actual.lastVisitDate, expected.lastVisitDate);
  }
}

function makeLinks(frecRangeStart, frecRangeEnd, step) {
  let links = [];
  // Remember, links are ordered by frecency descending.
  for (let i = frecRangeEnd; i > frecRangeStart; i -= step) {
    links.push(makeLink(i));
  }
  return links;
}

function makeLink(frecency) {
  return {
    url: "http://example" + frecency + ".com/",
    title: "My frecency is " + frecency,
    frecency: frecency,
    lastVisitDate: 0,
  };
}
