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

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

const { NewTabUtils } = ChromeUtils.importESModule(
  "resource://gre/modules/NewTabUtils.sys.mjs"
);
const { PlacesTestUtils } = ChromeUtils.importESModule(
  "resource://testing-common/PlacesTestUtils.sys.mjs"
);
const { PlacesUtils } = ChromeUtils.importESModule(
  "resource://gre/modules/PlacesUtils.sys.mjs"
);

// const SEARCH_SHORTCUTS_EXPERIMENT_PREF =
//   "browser.newtabpage.activity-stream.improvesearch.topSiteSearchShortcuts";
// Services.prefs.setBoolPref(SEARCH_SHORTCUTS_EXPERIMENT_PREF, false);

// use time at the start of the tests, changing it inside timeDaysAgo()
// may cause tiny time differences, which break expected sql ordering
const TIME_NOW = new Date().getTime();

// utility function to compute past timestamp in microseconds
function timeDaysAgo(numDays) {
  return (TIME_NOW - numDays * 24 * 60 * 60 * 1000) * 1000;
}

// tests that timestamp falls within 10 days of now
function isVisitDateOK(timestampMS) {
  let range = 10 * 24 * 60 * 60 * 1000;
  return Math.abs(Date.now() - timestampMS) < range;
}

// a set up function to prep the activity stream provider
function setUpActivityStreamTest() {
  return (async function () {
    await PlacesUtils.history.clear();
    await PlacesUtils.bookmarks.eraseEverything();
    let faviconExpiredPromise = new Promise(resolve => {
      Services.obs.addObserver(resolve, "places-favicons-expired");
    });
    PlacesUtils.favicons.expireAllFavicons();
    await faviconExpiredPromise;
  })();
}

function do_check_links(actualLinks, expectedLinks) {
  Assert.ok(Array.isArray(actualLinks));
  Assert.equal(actualLinks.length, expectedLinks.length);
  for (let i = 0; i < expectedLinks.length; i++) {
    let expected = expectedLinks[i];
    let actual = actualLinks[i];
    Assert.equal(actual.url, expected.url);
    Assert.equal(actual.title, expected.title);
    Assert.equal(actual.frecency, expected.frecency);
    Assert.equal(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,
    lastVisitDate: 0,
  };
}

// A small 1x1 test png
const image1x1 =
  "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg==";

function getBookmarksSize() {
  return NewTabUtils.activityStreamProvider.executePlacesQuery(
    "SELECT count(*) FROM moz_bookmarks WHERE type = :type",
    { params: { type: PlacesUtils.bookmarks.TYPE_BOOKMARK } }
  );
}

function getHistorySize() {
  return NewTabUtils.activityStreamProvider.executePlacesQuery(
    "SELECT count(*) FROM moz_places WHERE hidden = 0 AND last_visit_date NOT NULL"
  );
}

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

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

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

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

  await promise;

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

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

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

  NewTabUtils.initWithoutProviders();
  NewTabUtils.links.addProvider(provider);
  await new Promise(resolve => NewTabUtils.links.populateCache(resolve));

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

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

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

  // Check that linkMap is accurately updated.
  Assert.equal(links.linkMap.size, 2);
  Assert.ok(links.linkMap.get(expectedLinks[0].url));
  Assert.ok(links.linkMap.get(expectedLinks[1].url));
  Assert.ok(!links.linkMap.get(removedLink.url));

  // Check that siteMap is correctly updated.
  Assert.equal(links.siteMap.size, 2);
  Assert.ok(links.siteMap.has(NewTabUtils.extractSite(expectedLinks[0].url)));
  Assert.ok(links.siteMap.has(NewTabUtils.extractSite(expectedLinks[1].url)));
  Assert.ok(!links.siteMap.has(NewTabUtils.extractSite(removedLink.url)));

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

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

  let getLinksFcn = async function (callback) {
    // Should not be calling getLinksFcn twice
    count++;
    Assert.equal(count, 1);
    await Promise.resolve();
    callback(expectedLinks);
  };

  let provider = new TestProvider(getLinksFcn);

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

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

add_task(async 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;

  NewTabUtils.initWithoutProviders();
  NewTabUtils.links.addProvider(provider);
  await new Promise(resolve => NewTabUtils.links.populateCache(resolve));

  Assert.equal(
    NewTabUtils.isTopSiteGivenProvider("example2.com", provider),
    true
  );
  Assert.equal(
    NewTabUtils.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.
  Assert.equal(
    NewTabUtils.isTopSiteGivenProvider("example3.com", provider),
    true
  );
  Assert.equal(
    NewTabUtils.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.
  Assert.equal(
    NewTabUtils.isTopSiteGivenProvider("example5.com", provider),
    true
  );
  Assert.equal(
    NewTabUtils.isTopSiteGivenProvider("example2.com", provider),
    false
  );

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

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

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

  await new Promise(resolve => NewTabUtils.links.populateCache(resolve));

  let links = NewTabUtils.links.getLinks();
  let expectedLinks = makeLinks(
    NewTabUtils.links.maxNumLinks,
    2 * NewTabUtils.links.maxNumLinks,
    1
  );
  Assert.equal(links.length, NewTabUtils.links.maxNumLinks);
  do_check_links(links, expectedLinks);

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

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

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

  await new Promise(resolve => NewTabUtils.links.populateCache(resolve));

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

  // Notify of a new link.
  let newLink = makeLink(19);
  expectedLinks.splice(1, 0, newLink);
  provider.notifyLinkChanged(newLink);
  do_check_links(NewTabUtils.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(NewTabUtils.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(NewTabUtils.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();
  Assert.equal(expectedLinks.length, provider.maxNumLinks); // Sanity check.
  provider.notifyLinkChanged(newLink);
  do_check_links(NewTabUtils.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.
  await NewTabUtils.links._providers.get(provider).populatePromise;

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

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

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

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

  await new Promise(resolve => NewTabUtils.links.populateCache(resolve));
  do_check_links(NewTabUtils.links.getLinks(), links1);

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

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

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

add_task(async 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;

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

  await new Promise(resolve => NewTabUtils.links.populateCache(resolve));
  do_check_links(NewTabUtils.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(NewTabUtils.links.getLinks(), links);

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

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

add_task(async 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;
    Assert.equal(
      NewTabUtils.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;
    Assert.equal(
      NewTabUtils.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;
    Assert.notEqual(
      NewTabUtils.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 => {
    Assert.notEqual(
      NewTabUtils.extractSite(url),
      "mozilla.org",
      "extracted diff url " + url
    );
  });
});

add_task(async function faviconBytesToDataURI() {
  let tests = [
    [{ favicon: "bar".split("").map(s => s.charCodeAt(0)), mimeType: "foo" }],
    [
      {
        favicon: "bar".split("").map(s => s.charCodeAt(0)),
        mimeType: "foo",
        xxyy: "quz",
      },
    ],
  ];
  let provider = NewTabUtils.activityStreamProvider;

  for (let test of tests) {
    let clone = JSON.parse(JSON.stringify(test));
    delete clone[0].mimeType;
    clone[0].favicon = `data:foo;base64,${btoa("bar")}`;
    let result = provider._faviconBytesToDataURI(test);
    Assert.deepEqual(
      JSON.stringify(clone),
      JSON.stringify(result),
      "favicon converted to data uri"
    );
  }
});

add_task(async function addFavicons() {
  await setUpActivityStreamTest();
  let provider = NewTabUtils.activityStreamProvider;

  // start by passing in a bad uri and check that we get a null favicon back
  let links = [{ url: "mozilla.com" }];
  await provider._addFavicons(links);
  Assert.equal(
    links[0].favicon,
    null,
    "Got a null favicon because we passed in a bad url"
  );
  Assert.equal(
    links[0].mimeType,
    null,
    "Got a null mime type because we passed in a bad url"
  );
  Assert.equal(
    links[0].faviconSize,
    null,
    "Got a null favicon size because we passed in a bad url"
  );

  // now fix the url and try again - this time we get good favicon data back
  // a 1x1 favicon as a data URI of mime type image/png
  const base64URL = image1x1;
  links[0].url = "https://mozilla.com";

  let visit = [
    {
      uri: links[0].url,
      visitDate: timeDaysAgo(0),
      transition: PlacesUtils.history.TRANSITION_TYPED,
    },
  ];
  await PlacesTestUtils.addVisits(visit);

  let faviconData = new Map();
  faviconData.set("https://mozilla.com", `${base64URL}#tippytop`);
  await PlacesTestUtils.addFavicons(faviconData);

  await provider._addFavicons(links);
  Assert.equal(
    links[0].mimeType,
    "image/png",
    "Got the right mime type before deleting it"
  );
  Assert.equal(
    links[0].faviconLength,
    links[0].favicon.length,
    "Got the right length for the byte array"
  );
  Assert.equal(
    provider._faviconBytesToDataURI(links)[0].favicon,
    base64URL,
    "Got the right favicon"
  );
  Assert.equal(
    links[0].faviconSize,
    1,
    "Got the right favicon size (width and height of favicon)"
  );
  Assert.equal(links[0].faviconRef, "tippytop", "Got the favicon url ref");

  // Check with http version of the link that doesn't have its own
  const nonHttps = [{ url: links[0].url.replace("https", "http") }];
  await provider._addFavicons(nonHttps);
  Assert.equal(
    provider._faviconBytesToDataURI(nonHttps)[0].favicon,
    base64URL,
    "Got the same favicon"
  );
  Assert.equal(
    nonHttps[0].faviconLength,
    links[0].faviconLength,
    "Got the same favicon length"
  );
  Assert.equal(
    nonHttps[0].faviconSize,
    links[0].faviconSize,
    "Got the same favicon size"
  );
  Assert.equal(
    nonHttps[0].mimeType,
    links[0].mimeType,
    "Got the same mime type"
  );

  // Check that we do not collect favicons for pocket items
  const pocketItems = [
    { url: links[0].url },
    { url: "https://mozilla1.com", type: "pocket" },
  ];
  await provider._addFavicons(pocketItems);
  Assert.equal(
    provider._faviconBytesToDataURI(pocketItems)[0].favicon,
    base64URL,
    "Added favicon data only to the non-pocket item"
  );
  Assert.equal(
    pocketItems[1].favicon,
    null,
    "Did not add a favicon to the pocket item"
  );
  Assert.equal(
    pocketItems[1].mimeType,
    null,
    "Did not add mimeType to the pocket item"
  );
  Assert.equal(
    pocketItems[1].faviconSize,
    null,
    "Did not add a faviconSize to the pocket item"
  );
});

add_task(async function getHighlightsWithoutPocket() {
  const addMetadata = url =>
    PlacesUtils.history.update({
      description: "desc",
      previewImageURL: "https://image/",
      url,
    });

  await setUpActivityStreamTest();

  let provider = NewTabUtils.activityStreamLinks;
  let links = await provider.getHighlights();
  Assert.equal(links.length, 0, "empty history yields empty links");

  // Add bookmarks
  const now = Date.now();
  const oldSeconds = 24 * 60 * 60; // 1 day old
  let bookmarks = [
    {
      dateAdded: new Date(now - oldSeconds * 1000),
      parentGuid: PlacesUtils.bookmarks.unfiledGuid,
      title: "foo",
      url: "https://mozilla1.com/dayOld",
    },
    {
      parentGuid: PlacesUtils.bookmarks.unfiledGuid,
      title: "foo",
      url: "https://mozilla1.com/nowNew",
    },
  ];
  for (let placeInfo of bookmarks) {
    await PlacesUtils.bookmarks.insert(placeInfo);
  }

  links = await provider.getHighlights();
  Assert.equal(
    links.length,
    0,
    "adding bookmarks without visits doesn't yield more links"
  );

  // Add a history visit
  let testURI = "http://mozilla.com/";
  await PlacesTestUtils.addVisits(testURI);

  links = await provider.getHighlights();
  Assert.equal(
    links.length,
    0,
    "adding visits without metadata doesn't yield more links"
  );

  // Add bookmark visits
  for (let placeInfo of bookmarks) {
    await PlacesTestUtils.addVisits(placeInfo.url);
  }

  links = await provider.getHighlights();
  Assert.equal(links.length, 2, "adding visits to bookmarks yields more links");
  Assert.equal(
    links[0].url,
    bookmarks[1].url,
    "first bookmark is younger bookmark"
  );
  Assert.equal(links[0].type, "bookmark", "first bookmark is bookmark");
  Assert.ok(links[0].date_added, "got a date_added for the bookmark");
  Assert.equal(
    links[1].url,
    bookmarks[0].url,
    "second bookmark is older bookmark"
  );
  Assert.equal(links[1].type, "bookmark", "second bookmark is bookmark");
  Assert.ok(links[1].date_added, "got a date_added for the bookmark");

  // Add metadata to history
  await addMetadata(testURI);

  links = await provider.getHighlights();
  Assert.equal(links.length, 3, "adding metadata yield more links");
  Assert.equal(links[0].url, bookmarks[1].url, "still have younger bookmark");
  Assert.equal(links[1].url, bookmarks[0].url, "still have older bookmark");
  Assert.equal(links[2].url, testURI, "added visit corresponds to added url");
  Assert.equal(links[2].type, "history", "added visit is history");

  links = await provider.getHighlights({ numItems: 2 });
  Assert.equal(links.length, 2, "limited to 2 items");
  Assert.equal(links[0].url, bookmarks[1].url, "still have younger bookmark");
  Assert.equal(links[1].url, bookmarks[0].url, "still have older bookmark");

  links = await provider.getHighlights({ excludeHistory: true });
  Assert.equal(links.length, 2, "only have bookmarks");
  Assert.equal(links[0].url, bookmarks[1].url, "still have younger bookmark");
  Assert.equal(links[1].url, bookmarks[0].url, "still have older bookmark");

  links = await provider.getHighlights({ excludeBookmarks: true });
  Assert.equal(links.length, 1, "only have history");
  Assert.equal(links[0].url, testURI, "only have the history now");

  links = await provider.getHighlights({
    excludeBookmarks: true,
    excludeHistory: true,
  });
  Assert.equal(links.length, 0, "requested nothing, get nothing");

  links = await provider.getHighlights({ bookmarkSecondsAgo: oldSeconds / 2 });
  Assert.equal(links.length, 2, "old bookmark filtered out with");
  Assert.equal(links[0].url, bookmarks[1].url, "still have newer bookmark");
  Assert.equal(links[1].url, testURI, "still have the history");

  // Add a visit and metadata to the older bookmark
  await PlacesTestUtils.addVisits(bookmarks[0].url);
  await addMetadata(bookmarks[0].url);

  links = await provider.getHighlights({ bookmarkSecondsAgo: oldSeconds / 2 });
  Assert.equal(links.length, 3, "old bookmark returns as history");
  Assert.equal(links[0].url, bookmarks[1].url, "still have newer bookmark");
  Assert.equal(
    links[1].url,
    bookmarks[0].url,
    "old bookmark now is newer history"
  );
  Assert.equal(links[1].type, "history", "old bookmark now is history");
  Assert.equal(links[2].url, testURI, "still have the history");

  // Bookmark the history item
  await PlacesUtils.bookmarks.insert({
    parentGuid: PlacesUtils.bookmarks.unfiledGuid,
    title: "now a bookmark",
    url: testURI,
  });

  links = await provider.getHighlights();
  Assert.equal(
    links.length,
    3,
    "a visited bookmark doesn't appear as bookmark and history"
  );
  Assert.equal(
    links[0].url,
    testURI,
    "history is now the first, i.e., most recent, bookmark"
  );
  Assert.equal(links[0].type, "bookmark", "was history now bookmark");
  Assert.ok(links[0].date_added, "got a date_added for the now bookmark");
  Assert.equal(
    links[1].url,
    bookmarks[1].url,
    "still have younger bookmark now second"
  );
  Assert.equal(
    links[2].url,
    bookmarks[0].url,
    "still have older bookmark now third"
  );

  // Test the `withFavicons` option.
  await PlacesTestUtils.addFavicons(new Map([[testURI, image1x1]]));
  links = await provider.getHighlights({ withFavicons: true });
  Assert.equal(links.length, 3, "We're not expecting a change in links");
  Assert.equal(links[0].favicon, image1x1, "Link 1 should contain a favicon");
  Assert.equal(links[1].favicon, null, "Link 2 has no favicon data");
  Assert.equal(links[2].favicon, null, "Link 3 has no favicon data");
});

add_task(async function getHighlightsWithPocketSuccess() {
  await setUpActivityStreamTest();

  // Add a bookmark
  let bookmark = {
    parentGuid: PlacesUtils.bookmarks.unfiledGuid,
    title: "foo",
    description: "desc",
    preview_image_url: "foo.com/img.png",
    url: "https://mozilla1.com/",
  };

  const fakeResponse = {
    list: {
      123: {
        time_added: "123",
        image: { src: "foo.com/img.png" },
        excerpt: "A description for foo",
        resolved_title: "A title for foo",
        resolved_url: "http://www.foo.com",
        item_id: "123",
        open_url: "http://www.getpocket.com/itemID",
        status: "0",
      },
      456: {
        item_id: "456",
        status: "2",
      },
    },
  };

  await PlacesUtils.bookmarks.insert(bookmark);
  await PlacesTestUtils.addVisits(bookmark.url);

  NewTabUtils.activityStreamProvider.fetchSavedPocketItems = () => fakeResponse;
  let provider = NewTabUtils.activityStreamLinks;

  // Force a cache invalidation
  NewTabUtils.activityStreamLinks._pocketLastUpdated =
    Date.now() - 70 * 60 * 1000;
  NewTabUtils.activityStreamLinks._pocketLastLatest = -1;
  let links = await provider.getHighlights();

  // We should have 1 bookmark followed by 1 pocket story in highlights
  // We should not have stored the second pocket item since it was deleted
  Assert.equal(links.length, 2, "Should have 2 links in highlights");

  // First highlight should be a bookmark
  Assert.equal(links[0].url, bookmark.url, "The first link is the bookmark");

  // Second highlight should be a Pocket item with the correct fields to display
  let pocketItem = fakeResponse.list["123"];
  let currentLink = links[1];
  Assert.equal(currentLink.url, pocketItem.resolved_url, "Correct Pocket item");
  Assert.equal(currentLink.type, "pocket", "Attached the correct type");
  Assert.equal(
    currentLink.preview_image_url,
    pocketItem.image.src,
    "Correct preview image was added"
  );
  Assert.equal(
    currentLink.title,
    pocketItem.resolved_title,
    "Correct title was added"
  );
  Assert.equal(
    currentLink.description,
    pocketItem.excerpt,
    "Correct description was added"
  );
  Assert.equal(
    currentLink.pocket_id,
    pocketItem.item_id,
    "item_id was preserved"
  );
  Assert.equal(
    currentLink.open_url,
    `${pocketItem.open_url}?src=fx_new_tab`,
    "open_url was preserved"
  );
  Assert.equal(
    currentLink.date_added,
    pocketItem.time_added * 1000,
    "date_added was added to pocket item"
  );

  NewTabUtils.activityStreamLinks._savedPocketStories = null;
});

add_task(async function getHighlightsWithPocketCached() {
  await setUpActivityStreamTest();

  let fakeResponse = {
    list: {
      123: {
        time_added: "123",
        image: { src: "foo.com/img.png" },
        excerpt: "A description for foo",
        resolved_title: "A title for foo",
        resolved_url: "http://www.foo.com",
        item_id: "123",
        open_url: "http://www.getpocket.com/itemID",
        status: "0",
      },
      456: {
        item_id: "456",
        status: "2",
      },
    },
  };

  NewTabUtils.activityStreamProvider.fetchSavedPocketItems = () => fakeResponse;
  let provider = NewTabUtils.activityStreamLinks;

  let links = await provider.getHighlights();
  Assert.equal(
    links.length,
    1,
    "Sanity check that we got 1 link back for highlights"
  );
  Assert.equal(
    links[0].url,
    fakeResponse.list["123"].resolved_url,
    "Sanity check that it was the pocket story"
  );

  // Update what the response would be
  fakeResponse.list["789"] = {
    time_added: "123",
    image: { src: "bar.com/img.png" },
    excerpt: "A description for bar",
    resolved_title: "A title for bar",
    resolved_url: "http://www.bar.com",
    item_id: "789",
    open_url: "http://www.getpocket.com/itemID",
    status: "0",
  };

  // Call getHighlights again - this time we should get the cached links since we just updated
  links = await provider.getHighlights();
  Assert.equal(links.length, 1, "We still got 1 link back for highlights");
  Assert.equal(
    links[0].url,
    fakeResponse.list["123"].resolved_url,
    "It was still the same pocket story"
  );

  // Now force a cache invalidation and call getHighlights again
  NewTabUtils.activityStreamLinks._pocketLastUpdated =
    Date.now() - 70 * 60 * 1000;
  NewTabUtils.activityStreamLinks._pocketLastLatest = -1;
  links = await provider.getHighlights();
  Assert.equal(
    links.length,
    2,
    "This time we got fresh links with the new response"
  );
  Assert.equal(
    links[0].url,
    fakeResponse.list["123"].resolved_url,
    "First link is unchanged"
  );
  Assert.equal(
    links[1].url,
    fakeResponse.list["789"].resolved_url,
    "Second link is the new link"
  );

  NewTabUtils.activityStreamLinks._savedPocketStories = null;
});

add_task(async function getHighlightsWithPocketFailure() {
  await setUpActivityStreamTest();

  NewTabUtils.activityStreamProvider.fetchSavedPocketItems = function () {
    throw new Error();
  };
  let provider = NewTabUtils.activityStreamLinks;

  // Force a cache invalidation
  NewTabUtils.activityStreamLinks._pocketLastUpdated =
    Date.now() - 70 * 60 * 1000;
  NewTabUtils.activityStreamLinks._pocketLastLatest = -1;
  let links = await provider.getHighlights();
  Assert.equal(links.length, 0, "Return empty links if we reject the promise");
});

add_task(async function getHighlightsWithPocketNoData() {
  await setUpActivityStreamTest();

  NewTabUtils.activityStreamProvider.fetchSavedPocketItems = () => {};

  let provider = NewTabUtils.activityStreamLinks;

  // Force a cache invalidation
  NewTabUtils.activityStreamLinks._pocketLastUpdated =
    Date.now() - 70 * 60 * 1000;
  NewTabUtils.activityStreamLinks._pocketLastLatest = -1;
  let links = await provider.getHighlights();
  Assert.equal(
    links.length,
    0,
    "Return empty links if we got no data back from the response"
  );
});

add_task(async function getTopFrecentSites() {
  await setUpActivityStreamTest();

  let provider = NewTabUtils.activityStreamLinks;
  let links = await provider.getTopSites({ topsiteFrecency: 100 });
  Assert.equal(links.length, 0, "empty history yields empty links");

  // add a visit
  let testURI = "http://mozilla.com/";
  await PlacesTestUtils.addVisits(testURI);

  links = await provider.getTopSites();
  Assert.equal(
    links.length,
    0,
    "adding a single visit doesn't exceed default threshold"
  );

  links = await provider.getTopSites({ topsiteFrecency: 100 });
  Assert.equal(links.length, 1, "adding a visit yields a link");
  Assert.equal(links[0].url, testURI, "added visit corresponds to added url");
});

add_task(
  {
    skip_if: () =>
      AppConstants.MOZ_APP_NAME == "thunderbird" ||
      Services.prefs.getBoolPref(
        "browser.topsites.useRemoteSetting"
      ) /* see bug 1664502 */,
  },
  async function getTopFrecentSites_improveSearch() {
    await setUpActivityStreamTest();
    const SEARCH_SHORTCUTS_EXPERIMENT_PREF =
      "browser.newtabpage.activity-stream.improvesearch.topSiteSearchShortcuts";
    Services.prefs.setBoolPref(SEARCH_SHORTCUTS_EXPERIMENT_PREF, true);

    let testURI = "https://www.amazon.com?search=tv";
    await PlacesTestUtils.addVisits(testURI);

    let provider = NewTabUtils.activityStreamLinks;
    let links = await provider.getTopSites({ topsiteFrecency: 100 });
    Assert.equal(
      links.length,
      1,
      "sanity check that we got the link from top sites"
    );
    Assert.equal(
      links[0].url,
      "https://amazon.com",
      "the amazon site was converted to generic search shortcut site"
    );

    Services.prefs.setBoolPref(SEARCH_SHORTCUTS_EXPERIMENT_PREF, false);
  }
);

add_task(async function getTopFrecentSites_no_dedup() {
  await setUpActivityStreamTest();

  let provider = NewTabUtils.activityStreamLinks;
  let links = await provider.getTopSites({ topsiteFrecency: 100 });
  Assert.equal(links.length, 0, "empty history yields empty links");

  // Add a visits in reverse order they will be returned in when not deduped.
  let testURIs = [
    { uri: "http://www.mozilla.com/" },
    { uri: "http://mozilla.com/" },
  ];
  await PlacesTestUtils.addVisits(testURIs);

  links = await provider.getTopSites();
  Assert.equal(
    links.length,
    0,
    "adding a single visit doesn't exceed default threshold"
  );

  links = await provider.getTopSites({ topsiteFrecency: 100 });
  Assert.equal(links.length, 1, "adding a visit yields a link");
  // Plain domain is returned when deduped.
  Assert.equal(
    links[0].url,
    testURIs[1].uri,
    "added visit corresponds to added url"
  );

  links = await provider.getTopSites({
    topsiteFrecency: 100,
    onePerDomain: false,
  });
  Assert.equal(links.length, 2, "adding a visit yields a link");
  Assert.equal(
    links[0].url,
    testURIs[1].uri,
    "added visit corresponds to added url"
  );
  Assert.equal(
    links[1].url,
    testURIs[0].uri,
    "added visit corresponds to added url"
  );
});

add_task(async function getTopFrecentSites_dedupeWWW() {
  await setUpActivityStreamTest();

  let provider = NewTabUtils.activityStreamLinks;

  let links = await provider.getTopSites({ topsiteFrecency: 100 });
  Assert.equal(links.length, 0, "empty history yields empty links");

  // add a visit without www
  let testURI = "http://mozilla.com";
  await PlacesTestUtils.addVisits(testURI);

  // add a visit with www
  testURI = "http://www.mozilla.com";
  await PlacesTestUtils.addVisits(testURI);

  // Test combined frecency score
  links = await provider.getTopSites({ topsiteFrecency: 100 });
  Assert.equal(links.length, 1, "adding both www. and no-www. yields one link");
  Assert.equal(links[0].frecency, 200, "frecency scores are combined");

  // add another page visit with www and without www
  let noWWW = "http://mozilla.com/page";
  await PlacesTestUtils.addVisits(noWWW);
  let withWWW = "http://www.mozilla.com/page";
  await PlacesTestUtils.addVisits(withWWW);
  links = await provider.getTopSites({ topsiteFrecency: 100 });
  Assert.equal(links.length, 1, "adding both www. and no-www. yields one link");
  Assert.equal(
    links[0].frecency,
    200,
    "frecency scores are combined ignoring extra pages"
  );

  // add another visit with www
  await PlacesTestUtils.addVisits(withWWW);
  links = await provider.getTopSites({ topsiteFrecency: 100 });
  Assert.equal(links.length, 1, "still yields one link");
  Assert.equal(links[0].url, withWWW, "more frecent www link is used");
  Assert.equal(
    links[0].frecency,
    300,
    "frecency scores are combined ignoring extra pages"
  );

  // add a couple more visits to the no-www page
  await PlacesTestUtils.addVisits(noWWW);
  await PlacesTestUtils.addVisits(noWWW);
  links = await provider.getTopSites({ topsiteFrecency: 100 });
  Assert.equal(links.length, 1, "still yields one link");
  Assert.equal(links[0].url, noWWW, "now more frecent no-www link is used");
  Assert.equal(
    links[0].frecency,
    500,
    "frecency scores are combined ignoring extra pages"
  );
});

add_task(async function getTopFrencentSites_maxLimit() {
  await setUpActivityStreamTest();

  let provider = NewTabUtils.activityStreamLinks;

  // add many visits
  const MANY_LINKS = 20;
  for (let i = 0; i < MANY_LINKS; i++) {
    let testURI = `http://mozilla${i}.com`;
    await PlacesTestUtils.addVisits(testURI);
  }

  let links = await provider.getTopSites({ topsiteFrecency: 100 });
  Assert.less(
    links.length,
    MANY_LINKS,
    "query default limited to less than many"
  );
  Assert.greater(links.length, 6, "query default to more than visible count");
});

add_task(async function getTopFrencentSites_allowedProtocols() {
  await setUpActivityStreamTest();

  let provider = NewTabUtils.activityStreamLinks;

  // add a visit from a file:// site
  let testURI = "file:///some/file/path.png";
  await PlacesTestUtils.addVisits(testURI);

  let links = await provider.getTopSites({ topsiteFrecency: 100 });
  Assert.equal(links.length, 0, "don't get sites with the file:// protocol");

  // now add a site with an allowed protocol
  testURI = "http://www.mozilla.com";
  await PlacesTestUtils.addVisits(testURI);

  links = await provider.getTopSites({ topsiteFrecency: 100 });
  Assert.equal(links.length, 1, "http:// is an allowed protocol");

  // and just to be sure, add a visit to a site with ftp:// protocol
  testURI = "ftp://bad/example";
  await PlacesTestUtils.addVisits(testURI);

  links = await provider.getTopSites({ topsiteFrecency: 100 });
  Assert.equal(
    links.length,
    1,
    "we still only accept http:// and https:// for top sites"
  );

  // add a different allowed protocol
  testURI = "https://https";
  await PlacesTestUtils.addVisits(testURI);

  links = await provider.getTopSites({ topsiteFrecency: 100 });
  Assert.equal(
    links.length,
    2,
    "we now accept both http:// and https:// for top sites"
  );
});

add_task(async function getTopFrecentSites_order() {
  await setUpActivityStreamTest();

  let provider = NewTabUtils.activityStreamLinks;
  let { TRANSITION_TYPED } = PlacesUtils.history;

  let timeEarlier = timeDaysAgo(0);
  let timeLater = timeDaysAgo(2);

  let visits = [
    // frecency 200
    {
      uri: "https://mozilla1.com/0",
      visitDate: timeEarlier,
      transition: TRANSITION_TYPED,
    },
    // sort by url, frecency 200
    {
      uri: "https://mozilla2.com/1",
      visitDate: timeEarlier,
      transition: TRANSITION_TYPED,
    },
    // sort by last visit date, frecency 200
    {
      uri: "https://mozilla3.com/2",
      visitDate: timeLater,
      transition: TRANSITION_TYPED,
    },
    // sort by frecency, frecency 10
    { uri: "https://mozilla0.com/", visitDate: timeLater },
  ];

  let links = await provider.getTopSites({ topsiteFrecency: 0 });
  Assert.equal(links.length, 0, "empty history yields empty links");

  // map of page url to favicon url
  let faviconData = new Map();
  faviconData.set("https://mozilla3.com/2", image1x1);

  await PlacesTestUtils.addVisits(visits);
  await PlacesTestUtils.addFavicons(faviconData);

  links = await provider.getTopSites({ topsiteFrecency: 0 });
  Assert.equal(
    links.length,
    visits.length,
    "number of links added is the same as obtain by getTopFrecentSites"
  );

  // first link doesn't have a favicon
  Assert.equal(
    links[0].url,
    visits[0].uri,
    "links are obtained in the expected order"
  );
  Assert.equal(null, links[0].favicon, "favicon data is stored as expected");
  Assert.ok(
    isVisitDateOK(links[0].lastVisitDate),
    "visit date within expected range"
  );

  // second link doesn't have a favicon
  Assert.equal(
    links[1].url,
    visits[1].uri,
    "links are obtained in the expected order"
  );
  Assert.equal(null, links[1].favicon, "favicon data is stored as expected");
  Assert.ok(
    isVisitDateOK(links[1].lastVisitDate),
    "visit date within expected range"
  );

  // third link should have the favicon data that we added
  Assert.equal(
    links[2].url,
    visits[2].uri,
    "links are obtained in the expected order"
  );
  Assert.equal(
    faviconData.get(links[2].url),
    links[2].favicon,
    "favicon data is stored as expected"
  );
  Assert.ok(
    isVisitDateOK(links[2].lastVisitDate),
    "visit date within expected range"
  );

  // fourth link doesn't have a favicon
  Assert.equal(
    links[3].url,
    visits[3].uri,
    "links are obtained in the expected order"
  );
  Assert.equal(null, links[3].favicon, "favicon data is stored as expected");
  Assert.ok(
    isVisitDateOK(links[3].lastVisitDate),
    "visit date within expected range"
  );
});

add_task(async function getTopFrecentSites_hideWithSearchParam() {
  await setUpActivityStreamTest();

  let pref = "browser.newtabpage.activity-stream.hideTopSitesWithSearchParam";
  let provider = NewTabUtils.activityStreamLinks;

  // This maps URL search params to objects describing whether a URL with those
  // params is expected to be included in the returned links. Each object maps
  // from effective hide-with search params to whether the URL is expected to be
  // included.
  let tests = {
    "": {
      "": true,
      test: true,
      "test=": true,
      "test=hide": true,
      nomatch: true,
      "nomatch=": true,
      "nomatch=hide": true,
    },
    test: {
      "": true,
      test: false,
      "test=": false,
      "test=hide": true,
      nomatch: true,
      "nomatch=": true,
      "nomatch=hide": true,
    },
    "test=hide": {
      "": true,
      test: false,
      "test=": true,
      "test=hide": false,
      nomatch: true,
      "nomatch=": true,
      "nomatch=hide": true,
    },
    "test=foo&test=hide": {
      "": true,
      test: false,
      "test=": true,
      "test=hide": false,
      nomatch: true,
      "nomatch=": true,
      "nomatch=hide": true,
    },
  };

  for (let [urlParams, expected] of Object.entries(tests)) {
    for (let prefValue of Object.keys(expected)) {
      info(
        "Running test: " + JSON.stringify({ urlParams, prefValue, expected })
      );

      // Add a visit to a URL with search params `urlParams`.
      let url = new URL("http://example.com/");
      url.search = urlParams;
      await PlacesTestUtils.addVisits(url);

      // Set the pref to `prefValue`.
      Services.prefs.setCharPref(pref, prefValue);

      // Call `getTopSites()` with all the test values for `hideWithSearchParam`
      // plus undefined. When `hideWithSearchParam` is undefined, the pref value
      // should be used. Otherwise it should override the pref.
      for (let hideWithSearchParam of [undefined, ...Object.keys(expected)]) {
        info(
          "Calling getTopSites() with hideWithSearchParam: " +
            JSON.stringify(hideWithSearchParam)
        );

        let options = { topsiteFrecency: 100 };
        if (hideWithSearchParam !== undefined) {
          options = { ...options, hideWithSearchParam };
        }
        let links = await provider.getTopSites(options);

        let effectiveHideWithParam =
          hideWithSearchParam === undefined ? prefValue : hideWithSearchParam;
        if (expected[effectiveHideWithParam]) {
          Assert.equal(links.length, 1, "One link returned");
          Assert.equal(links[0].url, url.toString(), "Expected link returned");
        } else {
          Assert.equal(links.length, 0, "No links returned");
        }
      }

      await PlacesUtils.history.clear();
    }
  }

  Services.prefs.clearUserPref(pref);
});

add_task(async function activitySteamProvider_deleteHistoryLink() {
  await setUpActivityStreamTest();

  let provider = NewTabUtils.activityStreamLinks;

  let { TRANSITION_TYPED } = PlacesUtils.history;

  let visits = [
    // frecency 200
    {
      uri: "https://mozilla1.com/0",
      visitDate: timeDaysAgo(1),
      transition: TRANSITION_TYPED,
    },
    // sort by url, frecency 200
    { uri: "https://mozilla2.com/1", visitDate: timeDaysAgo(0) },
  ];

  let size = await getHistorySize();
  Assert.equal(size, 0, "empty history has size 0");

  await PlacesTestUtils.addVisits(visits);

  size = await getHistorySize();
  Assert.equal(size, 2, "expected history size");

  // delete a link
  let deleted = await provider.deleteHistoryEntry("https://mozilla2.com/1");
  Assert.equal(deleted, true, "link is deleted");

  // ensure that there's only one link left
  size = await getHistorySize();
  Assert.equal(size, 1, "expected history size");

  // pin the link and delete it
  const linkToPin = { url: "https://mozilla1.com/0" };
  NewTabUtils.pinnedLinks.pin(linkToPin, 0);

  // sanity check that the correct link was pinned
  Assert.equal(
    NewTabUtils.pinnedLinks.links.length,
    1,
    "added a link to pinned sites"
  );
  Assert.equal(
    NewTabUtils.pinnedLinks.isPinned(linkToPin),
    true,
    "pinned the correct link"
  );

  // delete the pinned link and ensure it was both deleted from history and unpinned
  deleted = await provider.deleteHistoryEntry("https://mozilla1.com/0");
  size = await getHistorySize();
  Assert.equal(deleted, true, "link is deleted");
  Assert.equal(size, 0, "expected history size");
  Assert.equal(
    NewTabUtils.pinnedLinks.links.length,
    0,
    "unpinned the deleted link"
  );
});

add_task(async function activityStream_deleteBookmark() {
  await setUpActivityStreamTest();

  let provider = NewTabUtils.activityStreamLinks;
  let bookmarks = [
    {
      url: "https://mozilla1.com/0",
      parentGuid: PlacesUtils.bookmarks.unfiledGuid,
    },
    {
      url: "https://mozilla1.com/1",
      parentGuid: PlacesUtils.bookmarks.unfiledGuid,
    },
  ];

  let bookmarksSize = await getBookmarksSize();
  Assert.equal(bookmarksSize, 0, "empty bookmarks yields 0 size");

  for (let placeInfo of bookmarks) {
    await PlacesUtils.bookmarks.insert(placeInfo);
  }

  bookmarksSize = await getBookmarksSize();
  Assert.equal(bookmarksSize, 2, "size 2 for 2 bookmarks added");

  let bookmarkGuid = await new Promise(resolve =>
    PlacesUtils.bookmarks.fetch({ url: bookmarks[0].url }, bookmark =>
      resolve(bookmark.guid)
    )
  );
  await provider.deleteBookmark(bookmarkGuid);
  Assert.strictEqual(
    await PlacesUtils.bookmarks.fetch(bookmarkGuid),
    null,
    "the bookmark should no longer be found"
  );
  bookmarksSize = await getBookmarksSize();
  Assert.equal(bookmarksSize, 1, "size 1 after deleting");
});

add_task(async function activityStream_blockedURLs() {
  await setUpActivityStreamTest();

  let provider = NewTabUtils.activityStreamLinks;
  NewTabUtils.blockedLinks.addObserver(provider);

  let { TRANSITION_TYPED } = PlacesUtils.history;

  let timeToday = timeDaysAgo(0);
  let timeEarlier = timeDaysAgo(2);

  let visits = [
    {
      uri: "https://example1.com/",
      visitDate: timeToday,
      transition: TRANSITION_TYPED,
    },
    {
      uri: "https://example2.com/",
      visitDate: timeToday,
      transition: TRANSITION_TYPED,
    },
    {
      uri: "https://example3.com/",
      visitDate: timeEarlier,
      transition: TRANSITION_TYPED,
    },
    {
      uri: "https://example4.com/",
      visitDate: timeEarlier,
      transition: TRANSITION_TYPED,
    },
  ];
  await PlacesTestUtils.addVisits(visits);
  await PlacesUtils.bookmarks.insert({
    url: "https://example5.com/",
    parentGuid: PlacesUtils.bookmarks.unfiledGuid,
  });

  let sizeQueryResult;

  // bookmarks
  sizeQueryResult = await getBookmarksSize();
  Assert.equal(sizeQueryResult, 1, "got the correct bookmark size");
});

add_task(async function activityStream_getTotalBookmarksCount() {
  await setUpActivityStreamTest();

  let provider = NewTabUtils.activityStreamProvider;
  let bookmarks = [
    {
      url: "https://mozilla1.com/0",
      parentGuid: PlacesUtils.bookmarks.unfiledGuid,
    },
    {
      url: "https://mozilla1.com/1",
      parentGuid: PlacesUtils.bookmarks.unfiledGuid,
    },
  ];

  let bookmarksSize = await provider.getTotalBookmarksCount();
  Assert.equal(
    bookmarksSize,
    0,
    ".getTotalBookmarksCount() returns 0 for an empty bookmarks table"
  );

  for (const bookmark of bookmarks) {
    await PlacesUtils.bookmarks.insert(bookmark);
  }

  bookmarksSize = await provider.getTotalBookmarksCount();
  Assert.equal(
    bookmarksSize,
    2,
    ".getTotalBookmarksCount() returns 2 after 2 bookmarks are inserted"
  );
});

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

TestProvider.prototype = {
  addObserver(observer) {
    this._observers.add(observer);
  },
  notifyLinkChanged(link, index = -1, deleted = false) {
    this._notifyObservers("onLinkChanged", link, index, deleted);
  },
  notifyManyLinksChanged() {
    this._notifyObservers("onManyLinksChanged");
  },
  _notifyObservers() {
    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(NewTabUtils.links, args);
      }
    }
  },
};

const puny = "xn--kpry57d";
const idn = "台灣";

add_task(async function test_shortURL() {
  Assert.equal(
    NewTabUtils.shortURL({ url: false }),
    "",
    "Should return a blank string if url is falsey"
  );
  Assert.equal(
    NewTabUtils.shortURL({ url: "" }),
    "",
    "Should return a blank string if url is empty"
  );
  Assert.equal(
    NewTabUtils.shortURL({}),
    "",
    "Should return a blank string if url is missing"
  );

  const invalidURLs = [
    true,
    "something",
    "http:",
    "http::double",
    "http://badport:65536/",
  ];
  invalidURLs.forEach(url =>
    Assert.equal(
      NewTabUtils.shortURL({ url }),
      url,
      `Should return the invalid url as-is: ${url}`
    )
  );

  Assert.equal(
    NewTabUtils.shortURL({ url: "http://com.blah.com" }),
    "com.blah",
    "Should remove the eTLD"
  );

  Assert.equal(
    NewTabUtils.shortURL({ url: `http://${puny}.blah.com` }),
    `${idn}.blah`,
    "Should convert punycode to IDN"
  );

  Assert.equal(
    NewTabUtils.shortURL({ url: "http://bar.com" }),
    "bar",
    "Should get the hostname from .url"
  );

  Assert.equal(
    NewTabUtils.shortURL({ url: "http://foo.www.com" }),
    "foo.www",
    "Should not strip out www if not the first subdomain"
  );

  Assert.equal(
    NewTabUtils.shortURL({ url: "HTTP://FOO.COM" }),
    "foo",
    "Should convert to lowercase"
  );

  Assert.equal(
    NewTabUtils.shortURL({ url: "http://foo.com:8888" }),
    "foo",
    "Should not include the port"
  );

  Assert.equal(
    NewTabUtils.shortURL({ url: "file:///foo/bar.txt" }),
    "/foo/bar.txt",
    "Should return the path for file: URLs"
  );

  Assert.equal(
    NewTabUtils.shortURL({ url: "about:newtab" }),
    "newtab",
    "Should return the content for about: URLs"
  );

  Assert.equal(
    NewTabUtils.shortURL({ url: "about:" }),
    "about:",
    "Should fall back to full URL as a last resort"
  );
});

add_task(async function test_shortHostname() {
  Assert.equal(
    NewTabUtils.shortHostname(""),
    "",
    "Should return a blank string if hostname is empty"
  );

  const invalidHostnames = ["something/something", "http:", "http::double"];
  invalidHostnames.forEach(host =>
    Assert.equal(
      NewTabUtils.shortHostname(host),
      host,
      `Should return the input for invalid hostnames: ${host}`
    )
  );

  Assert.equal(
    NewTabUtils.shortHostname("com.blah.com"),
    "com.blah",
    "Should remove the eTLD"
  );

  Assert.equal(
    NewTabUtils.shortHostname("foo.com/bar"),
    "foo",
    "Should remove eTLD and path"
  );

  Assert.equal(
    NewTabUtils.shortHostname("http://foo.com/"),
    "http://foo",
    "Should remove eTLD and keep protocol"
  );

  Assert.equal(
    NewTabUtils.shortHostname(`${puny}.blah.com`),
    `${idn}.blah`,
    "Should convert punycode to IDN"
  );

  Assert.equal(
    NewTabUtils.shortHostname("foo.www.com"),
    "foo.www",
    "Should not strip out www if not the first subdomain"
  );

  Assert.equal(
    NewTabUtils.shortHostname("FOO.COM"),
    "foo",
    "Should convert to lowercase"
  );

  Assert.equal(
    NewTabUtils.shortHostname("127.0.0.1"),
    "127.0.0.1",
    "Should return hostname for IP address"
  );

  Assert.equal(
    NewTabUtils.shortHostname("www.gov.uk"),
    "gov.uk",
    "Should return the eTLD for www-only non-eTLD"
  );

  Assert.equal(
    NewTabUtils.shortHostname(`www.${puny}`),
    idn,
    "Should return IDN for www-only non-eTLD"
  );
});
