"use strict";

import {
  actionCreators as ac,
  actionTypes as at,
} from "common/Actions.sys.mjs";
import { FAKE_GLOBAL_PREFS, FakePrefs, GlobalOverrider } from "test/unit/utils";
import {
  insertPinned,
  TOP_SITES_DEFAULT_ROWS,
  TOP_SITES_MAX_SITES_PER_ROW,
} from "common/Reducers.sys.mjs";
import { getDefaultOptions } from "lib/ActivityStreamStorage.jsm";
import injector from "inject!lib/TopSitesFeed.jsm";
import { Screenshots } from "lib/Screenshots.jsm";
import { LinksCache } from "lib/LinksCache.sys.mjs";

const FAKE_FAVICON = "data987";
const FAKE_FAVICON_SIZE = 128;
const FAKE_FRECENCY = 200;
const FAKE_LINKS = new Array(2 * TOP_SITES_MAX_SITES_PER_ROW)
  .fill(null)
  .map((v, i) => ({
    frecency: FAKE_FRECENCY,
    url: `http://www.site${i}.com`,
  }));
const FAKE_SCREENSHOT = "data123";
const SEARCH_SHORTCUTS_EXPERIMENT_PREF = "improvesearch.topSiteSearchShortcuts";
const SEARCH_SHORTCUTS_SEARCH_ENGINES_PREF =
  "improvesearch.topSiteSearchShortcuts.searchEngines";
const SEARCH_SHORTCUTS_HAVE_PINNED_PREF =
  "improvesearch.topSiteSearchShortcuts.havePinned";
const SHOWN_ON_NEWTAB_PREF = "feeds.topsites";
const SHOW_SPONSORED_PREF = "showSponsoredTopSites";
const TOP_SITES_BLOCKED_SPONSORS_PREF = "browser.topsites.blockedSponsors";
const REMOTE_SETTING_DEFAULTS_PREF = "browser.topsites.useRemoteSetting";
const CONTILE_CACHE_PREF = "browser.topsites.contile.cachedTiles";
const CONTILE_CACHE_VALID_FOR_PREF = "browser.topsites.contile.cacheValidFor";
const CONTILE_CACHE_LAST_FETCH_PREF = "browser.topsites.contile.lastFetch";

function FakeTippyTopProvider() {}
FakeTippyTopProvider.prototype = {
  async init() {
    this.initialized = true;
  },
  processSite(site) {
    return site;
  },
};

describe("Top Sites Feed", () => {
  let TopSitesFeed;
  let DEFAULT_TOP_SITES;
  let feed;
  let globals;
  let sandbox;
  let links;
  let fakeNewTabUtils;
  let fakeScreenshot;
  let filterAdultStub;
  let shortURLStub;
  let fakePageThumbs;
  let fetchStub;
  let fakeNimbusFeatures;
  let fakeSampling;

  beforeEach(() => {
    globals = new GlobalOverrider();
    sandbox = globals.sandbox;
    fakeNewTabUtils = {
      blockedLinks: {
        links: [],
        isBlocked: () => false,
        unblock: sandbox.spy(),
      },
      activityStreamLinks: {
        getTopSites: sandbox.spy(() => Promise.resolve(links)),
      },
      activityStreamProvider: {
        _addFavicons: sandbox.spy(l =>
          Promise.resolve(
            l.map(link => {
              link.favicon = FAKE_FAVICON;
              link.faviconSize = FAKE_FAVICON_SIZE;
              return link;
            })
          )
        ),
        _faviconBytesToDataURI: sandbox.spy(),
      },
      pinnedLinks: {
        links: [],
        isPinned: () => false,
        pin: sandbox.spy(),
        unpin: sandbox.spy(),
      },
    };
    fakeScreenshot = {
      getScreenshotForURL: sandbox.spy(() => Promise.resolve(FAKE_SCREENSHOT)),
      maybeCacheScreenshot: sandbox.spy(Screenshots.maybeCacheScreenshot),
      _shouldGetScreenshots: sinon.stub().returns(true),
    };
    filterAdultStub = {
      filter: sinon.stub().returnsArg(0),
    };
    shortURLStub = sinon
      .stub()
      .callsFake(site =>
        site.url.replace(/(.com|.ca)/, "").replace("https://", "")
      );
    const fakeDedupe = function () {};
    fakePageThumbs = {
      addExpirationFilter: sinon.stub(),
      removeExpirationFilter: sinon.stub(),
    };
    fakeNimbusFeatures = {
      newtab: {
        getVariable: sinon.stub(),
        onUpdate: sinon.stub(),
        offUpdate: sinon.stub(),
      },
      pocketNewtab: {
        getVariable: sinon.stub(),
      },
    };
    fakeSampling = {
      ratioSample: sinon.stub(),
    };
    globals.set({
      PageThumbs: fakePageThumbs,
      NewTabUtils: fakeNewTabUtils,
      gFilterAdultEnabled: false,
      NimbusFeatures: fakeNimbusFeatures,
      LinksCache,
      FilterAdult: filterAdultStub,
      Screenshots: fakeScreenshot,
      Sampling: fakeSampling,
    });
    sandbox.spy(global.XPCOMUtils, "defineLazyGetter");
    FAKE_GLOBAL_PREFS.set("default.sites", "https://foo.com/");
    ({ TopSitesFeed, DEFAULT_TOP_SITES } = injector({
      "lib/ActivityStreamPrefs.jsm": { Prefs: FakePrefs },
      "common/Dedupe.jsm": { Dedupe: fakeDedupe },
      "common/Reducers.jsm": {
        insertPinned,
        TOP_SITES_DEFAULT_ROWS,
        TOP_SITES_MAX_SITES_PER_ROW,
      },
      "lib/FilterAdult.jsm": { FilterAdult: filterAdultStub },
      "lib/Screenshots.jsm": { Screenshots: fakeScreenshot },
      "lib/TippyTopProvider.sys.mjs": {
        TippyTopProvider: FakeTippyTopProvider,
      },
      "lib/ShortURL.jsm": { shortURL: shortURLStub },
      "lib/ActivityStreamStorage.jsm": {
        ActivityStreamStorage: function Fake() {},
        getDefaultOptions,
      },
    }));
    feed = new TopSitesFeed();
    const storage = {
      init: sandbox.stub().resolves(),
      get: sandbox.stub().resolves(),
      set: sandbox.stub().resolves(),
    };
    // Setup for tests that don't call `init` but require feed.storage
    feed._storage = storage;
    feed.store = {
      dispatch: sinon.spy(),
      getState() {
        return this.state;
      },
      state: {
        Prefs: { values: { topSitesRows: 2 } },
        TopSites: { rows: Array(12).fill("site") },
      },
      dbStorage: { getDbTable: sandbox.stub().returns(storage) },
    };
    feed.dedupe.group = (...sites) => sites;
    links = FAKE_LINKS;
    // Turn off the search shortcuts experiment by default for other tests
    feed.store.state.Prefs.values[SEARCH_SHORTCUTS_EXPERIMENT_PREF] = false;
    feed.store.state.Prefs.values[SEARCH_SHORTCUTS_HAVE_PINNED_PREF] =
      "google,amazon";
  });
  afterEach(() => {
    globals.restore();
    sandbox.restore();
  });

  function stubFaviconsToUseScreenshots() {
    fakeNewTabUtils.activityStreamProvider._addFavicons = sandbox.stub();
  }

  describe("#constructor", () => {
    it("should defineLazyGetter for log, contextId, and _currentSearchHostname", () => {
      assert.calledThrice(global.XPCOMUtils.defineLazyGetter);

      let spyCall = global.XPCOMUtils.defineLazyGetter.getCall(0);
      assert.ok(spyCall.calledWith(sinon.match.any, "log", sinon.match.func));

      spyCall = global.XPCOMUtils.defineLazyGetter.getCall(1);
      assert.ok(
        spyCall.calledWith(sinon.match.any, "contextId", sinon.match.func)
      );

      spyCall = global.XPCOMUtils.defineLazyGetter.getCall(2);
      assert.ok(
        spyCall.calledWith(feed, "_currentSearchHostname", sinon.match.func)
      );
    });
  });

  describe("#refreshDefaults", () => {
    it("should add defaults on PREFS_INITIAL_VALUES", () => {
      feed.onAction({
        type: at.PREFS_INITIAL_VALUES,
        data: { "default.sites": "https://foo.com" },
      });

      assert.isAbove(DEFAULT_TOP_SITES.length, 0);
    });
    it("should add defaults on default.sites PREF_CHANGED", () => {
      feed.onAction({
        type: at.PREF_CHANGED,
        data: { name: "default.sites", value: "https://foo.com" },
      });

      assert.isAbove(DEFAULT_TOP_SITES.length, 0);
    });
    it("should refresh on topSiteRows PREF_CHANGED", () => {
      feed.refresh = sinon.spy();
      feed.onAction({ type: at.PREF_CHANGED, data: { name: "topSitesRows" } });

      assert.calledOnce(feed.refresh);
    });
    it("should have default sites with .isDefault = true", () => {
      feed.refreshDefaults("https://foo.com");

      DEFAULT_TOP_SITES.forEach(link =>
        assert.propertyVal(link, "isDefault", true)
      );
    });
    it("should have default sites with appropriate hostname", () => {
      feed.refreshDefaults("https://foo.com");

      DEFAULT_TOP_SITES.forEach(link =>
        assert.propertyVal(link, "hostname", shortURLStub(link))
      );
    });
    it("should add no defaults on empty pref", () => {
      feed.refreshDefaults("");

      assert.equal(DEFAULT_TOP_SITES.length, 0);
    });
    it("should clear defaults", () => {
      feed.refreshDefaults("https://foo.com");
      feed.refreshDefaults("");

      assert.equal(DEFAULT_TOP_SITES.length, 0);
    });
  });
  describe("#filterForThumbnailExpiration", () => {
    it("should pass rows.urls to the callback provided", () => {
      const rows = [
        { url: "foo.com" },
        { url: "bar.com", customScreenshotURL: "custom" },
      ];
      feed.store.state.TopSites = { rows };
      const stub = sinon.stub();

      feed.filterForThumbnailExpiration(stub);

      assert.calledOnce(stub);
      assert.calledWithExactly(stub, ["foo.com", "bar.com", "custom"]);
    });
  });
  describe("#getLinksWithDefaults", () => {
    beforeEach(() => {
      feed.refreshDefaults("https://foo.com");
    });

    describe("general", () => {
      it("should get the links from NewTabUtils", async () => {
        const result = await feed.getLinksWithDefaults();
        const reference = links.map(site =>
          Object.assign({}, site, {
            hostname: shortURLStub(site),
            typedBonus: true,
          })
        );

        assert.deepEqual(result, reference);
        assert.calledOnce(global.NewTabUtils.activityStreamLinks.getTopSites);
      });
      it("should indicate the links get typed bonus", async () => {
        const result = await feed.getLinksWithDefaults();

        assert.propertyVal(result[0], "typedBonus", true);
      });
      it("should filter out non-pinned adult sites", async () => {
        filterAdultStub.filter = sinon.stub().returns([]);
        fakeNewTabUtils.pinnedLinks.links = [{ url: "https://foo.com/" }];

        const result = await feed.getLinksWithDefaults();

        // The stub filters out everything
        assert.calledOnce(filterAdultStub.filter);
        assert.equal(result.length, 1);
        assert.equal(result[0].url, fakeNewTabUtils.pinnedLinks.links[0].url);
      });
      it("should filter out the defaults that have been blocked", async () => {
        // make sure we only have one top site, and we block the only default site we have to show
        const url = "www.myonlytopsite.com";
        const topsite = {
          frecency: FAKE_FRECENCY,
          hostname: shortURLStub({ url }),
          typedBonus: true,
          url,
        };
        const blockedDefaultSite = { url: "https://foo.com" };
        fakeNewTabUtils.activityStreamLinks.getTopSites = () => [topsite];
        fakeNewTabUtils.blockedLinks.isBlocked = site =>
          site.url === blockedDefaultSite.url;
        const result = await feed.getLinksWithDefaults();

        // what we should be left with is just the top site we added, and not the default site we blocked
        assert.lengthOf(result, 1);
        assert.deepEqual(result[0], topsite);
        assert.notInclude(result, blockedDefaultSite);
      });
      it("should call dedupe on the links", async () => {
        const stub = sinon.stub(feed.dedupe, "group").callsFake((...id) => id);

        await feed.getLinksWithDefaults();

        assert.calledOnce(stub);
      });
      it("should dedupe the links by hostname", async () => {
        const site = { url: "foo", hostname: "bar" };
        const result = feed._dedupeKey(site);

        assert.equal(result, site.hostname);
      });
      it("should add defaults if there are are not enough links", async () => {
        links = [{ frecency: FAKE_FRECENCY, url: "foo.com" }];

        const result = await feed.getLinksWithDefaults();
        const reference = [...links, ...DEFAULT_TOP_SITES].map(s =>
          Object.assign({}, s, {
            hostname: shortURLStub(s),
            typedBonus: true,
          })
        );

        assert.deepEqual(result, reference);
      });
      it("should only add defaults up to the number of visible slots", async () => {
        links = [];
        const numVisible = TOP_SITES_DEFAULT_ROWS * TOP_SITES_MAX_SITES_PER_ROW;
        for (let i = 0; i < numVisible - 1; i++) {
          links.push({ frecency: FAKE_FRECENCY, url: `foo${i}.com` });
        }
        const result = await feed.getLinksWithDefaults();
        const reference = [...links, DEFAULT_TOP_SITES[0]].map(s =>
          Object.assign({}, s, {
            hostname: shortURLStub(s),
            typedBonus: true,
          })
        );

        assert.lengthOf(result, numVisible);
        assert.deepEqual(result, reference);
      });
      it("should not throw if NewTabUtils returns null", () => {
        links = null;
        assert.doesNotThrow(() => {
          feed.getLinksWithDefaults();
        });
      });
      it("should get more if the user has asked for more", async () => {
        links = new Array(4 * TOP_SITES_MAX_SITES_PER_ROW)
          .fill(null)
          .map((v, i) => ({
            frecency: FAKE_FRECENCY,
            url: `http://www.site${i}.com`,
          }));
        feed.store.state.Prefs.values.topSitesRows = 3;

        const result = await feed.getLinksWithDefaults();

        assert.propertyVal(
          result,
          "length",
          feed.store.state.Prefs.values.topSitesRows *
            TOP_SITES_MAX_SITES_PER_ROW
        );
      });
    });
    describe("caching", () => {
      it("should reuse the cache on subsequent calls", async () => {
        await feed.getLinksWithDefaults();
        await feed.getLinksWithDefaults();

        assert.calledOnce(global.NewTabUtils.activityStreamLinks.getTopSites);
      });
      it("should ignore the cache when requesting more", async () => {
        await feed.getLinksWithDefaults();
        feed.store.state.Prefs.values.topSitesRows *= 3;

        await feed.getLinksWithDefaults();

        assert.calledTwice(global.NewTabUtils.activityStreamLinks.getTopSites);
      });
      it("should migrate frecent screenshot data without getting screenshots again", async () => {
        feed.store.state.Prefs.values[SHOWN_ON_NEWTAB_PREF] = true;
        stubFaviconsToUseScreenshots();
        await feed.getLinksWithDefaults();
        const { callCount } = fakeScreenshot.getScreenshotForURL;
        feed.frecentCache.expire();

        const result = await feed.getLinksWithDefaults();

        assert.calledTwice(global.NewTabUtils.activityStreamLinks.getTopSites);
        assert.callCount(fakeScreenshot.getScreenshotForURL, callCount);
        assert.propertyVal(result[0], "screenshot", FAKE_SCREENSHOT);
      });
      it("should migrate pinned favicon data without getting favicons again", async () => {
        fakeNewTabUtils.pinnedLinks.links = [{ url: "https://foo.com/" }];
        await feed.getLinksWithDefaults();
        const { callCount } =
          fakeNewTabUtils.activityStreamProvider._addFavicons;
        feed.pinnedCache.expire();

        const result = await feed.getLinksWithDefaults();

        assert.callCount(
          fakeNewTabUtils.activityStreamProvider._addFavicons,
          callCount
        );
        assert.propertyVal(result[0], "favicon", FAKE_FAVICON);
        assert.propertyVal(result[0], "faviconSize", FAKE_FAVICON_SIZE);
      });
      it("should not expose internal link properties", async () => {
        const result = await feed.getLinksWithDefaults();

        const internal = Object.keys(result[0]).filter(key =>
          key.startsWith("__")
        );
        assert.equal(internal.join(""), "");
      });
      it("should copy the screenshot of the frecent site if pinned site doesn't have customScreenshotURL", async () => {
        links = [{ url: "https://foo.com/", screenshot: "screenshot" }];
        fakeNewTabUtils.pinnedLinks.links = [{ url: "https://foo.com/" }];

        const result = await feed.getLinksWithDefaults();

        assert.equal(result[0].screenshot, links[0].screenshot);
      });
      it("should not copy the frecent screenshot if customScreenshotURL is set", async () => {
        links = [{ url: "https://foo.com/", screenshot: "screenshot" }];
        fakeNewTabUtils.pinnedLinks.links = [
          { url: "https://foo.com/", customScreenshotURL: "custom" },
        ];

        const result = await feed.getLinksWithDefaults();

        assert.isUndefined(result[0].screenshot);
      });
      it("should keep the same screenshot if no frecent site is found", async () => {
        links = [];
        fakeNewTabUtils.pinnedLinks.links = [
          { url: "https://foo.com/", screenshot: "custom" },
        ];

        const result = await feed.getLinksWithDefaults();

        assert.equal(result[0].screenshot, "custom");
      });
      it("should not overwrite pinned site screenshot", async () => {
        links = [{ url: "https://foo.com/", screenshot: "foo" }];
        fakeNewTabUtils.pinnedLinks.links = [
          { url: "https://foo.com/", screenshot: "bar" },
        ];

        const result = await feed.getLinksWithDefaults();

        assert.equal(result[0].screenshot, "bar");
      });
      it("should not set searchTopSite from frecent site", async () => {
        links = [
          {
            url: "https://foo.com/",
            searchTopSite: true,
            screenshot: "screenshot",
          },
        ];
        fakeNewTabUtils.pinnedLinks.links = [{ url: "https://foo.com/" }];

        const result = await feed.getLinksWithDefaults();

        assert.propertyVal(result[0], "searchTopSite", false);
        // But it should copy over other properties
        assert.propertyVal(result[0], "screenshot", "screenshot");
      });
      describe("concurrency", () => {
        beforeEach(() => {
          stubFaviconsToUseScreenshots();
          fakeScreenshot.getScreenshotForURL = sandbox
            .stub()
            .resolves(FAKE_SCREENSHOT);
        });
        afterEach(() => {
          sandbox.restore();
        });

        const getTwice = () =>
          Promise.all([
            feed.getLinksWithDefaults(),
            feed.getLinksWithDefaults(),
          ]);

        it("should call the backing data once", async () => {
          await getTwice();

          assert.calledOnce(global.NewTabUtils.activityStreamLinks.getTopSites);
        });
        it("should get screenshots once per link", async () => {
          feed.store.state.Prefs.values[SHOWN_ON_NEWTAB_PREF] = true;
          await getTwice();

          assert.callCount(
            fakeScreenshot.getScreenshotForURL,
            FAKE_LINKS.length
          );
        });
        it("should dispatch once per link screenshot fetched", async () => {
          feed.store.state.Prefs.values[SHOWN_ON_NEWTAB_PREF] = true;
          feed._requestRichIcon = sinon.stub();
          await getTwice();

          assert.callCount(feed.store.dispatch, FAKE_LINKS.length);
        });
      });
    });
    describe("deduping", () => {
      beforeEach(() => {
        ({ TopSitesFeed, DEFAULT_TOP_SITES } = injector({
          "lib/ActivityStreamPrefs.jsm": { Prefs: FakePrefs },
          "common/Reducers.jsm": {
            insertPinned,
            TOP_SITES_DEFAULT_ROWS,
            TOP_SITES_MAX_SITES_PER_ROW,
          },
          "lib/Screenshots.jsm": { Screenshots: fakeScreenshot },
        }));
        sandbox.stub(global.Services.eTLD, "getPublicSuffix").returns("com");
        feed = Object.assign(new TopSitesFeed(), { store: feed.store });
      });
      it("should not dedupe pinned sites", async () => {
        fakeNewTabUtils.pinnedLinks.links = [
          { url: "https://developer.mozilla.org/en-US/docs/Web" },
          { url: "https://developer.mozilla.org/en-US/docs/Learn" },
        ];

        const sites = await feed.getLinksWithDefaults();

        assert.lengthOf(sites, 2 * TOP_SITES_MAX_SITES_PER_ROW);
        assert.equal(sites[0].url, fakeNewTabUtils.pinnedLinks.links[0].url);
        assert.equal(sites[1].url, fakeNewTabUtils.pinnedLinks.links[1].url);
        assert.equal(sites[0].hostname, sites[1].hostname);
      });
      it("should prefer pinned sites over links", async () => {
        fakeNewTabUtils.pinnedLinks.links = [
          { url: "https://developer.mozilla.org/en-US/docs/Web" },
          { url: "https://developer.mozilla.org/en-US/docs/Learn" },
        ];
        // These will be the frecent results.
        links = [
          { frecency: FAKE_FRECENCY, url: "https://developer.mozilla.org/" },
          { frecency: FAKE_FRECENCY, url: "https://www.mozilla.org/" },
        ];

        const sites = await feed.getLinksWithDefaults();

        // Expecting 3 links where there's 2 pinned and 1 www.mozilla.org, so
        // the frecent with matching hostname as pinned is removed.
        assert.lengthOf(sites, 3);
        assert.equal(sites[0].url, fakeNewTabUtils.pinnedLinks.links[0].url);
        assert.equal(sites[1].url, fakeNewTabUtils.pinnedLinks.links[1].url);
        assert.equal(sites[2].url, links[1].url);
      });
      it("should return sites that have a title", async () => {
        // Simulate a pinned link with no title.
        fakeNewTabUtils.pinnedLinks.links = [
          { url: "https://github.com/mozilla/activity-stream" },
        ];

        const sites = await feed.getLinksWithDefaults();

        for (const site of sites) {
          assert.isDefined(site.hostname);
        }
      });
      it("should check against null entries", async () => {
        fakeNewTabUtils.pinnedLinks.links = [null];

        await feed.getLinksWithDefaults();
      });
    });
    it("should call _fetchIcon for each link", async () => {
      sinon.spy(feed, "_fetchIcon");

      const results = await feed.getLinksWithDefaults();

      assert.callCount(feed._fetchIcon, results.length);
      results.forEach(link => {
        assert.calledWith(feed._fetchIcon, link);
      });
    });
    it("should call _fetchScreenshot when customScreenshotURL is set", async () => {
      links = [];
      fakeNewTabUtils.pinnedLinks.links = [
        { url: "https://foo.com", customScreenshotURL: "custom" },
      ];
      sinon.stub(feed, "_fetchScreenshot");

      await feed.getLinksWithDefaults();

      assert.calledWith(feed._fetchScreenshot, sinon.match.object, "custom");
    });
    describe("discoverystream", () => {
      let makeStreamData = index => ({
        layout: [
          {
            components: [
              {
                placement: {
                  name: "sponsored-topsites",
                },
                spocs: {
                  positions: [{ index }],
                },
              },
            ],
          },
        ],
        spocs: {
          data: {
            "sponsored-topsites": {
              items: [{ title: "test spoc", url: "https://test-spoc.com" }],
            },
          },
        },
      });
      it("should add a sponsored topsite from discoverystream to all the valid indices", async () => {
        for (let i = 0; i < FAKE_LINKS.length; i++) {
          feed.store.state.DiscoveryStream = makeStreamData(i);
          const result = await feed.getLinksWithDefaults();
          const link = result[i];

          assert.equal(link.type, "SPOC");
          assert.equal(link.title, "test spoc");
          assert.equal(link.sponsored_position, i + 1);
          assert.equal(link.hostname, "test-spoc");
          assert.equal(link.url, "https://test-spoc.com");
        }
      });
    });
  });
  describe("#init", () => {
    it("should call refresh (broadcast:true)", async () => {
      sandbox.stub(feed, "refresh");

      await feed.init();

      assert.calledOnce(feed.refresh);
      assert.calledWithExactly(feed.refresh, {
        broadcast: true,
        isStartup: true,
      });
    });
    it("should initialise the storage", async () => {
      await feed.init();

      assert.calledOnce(feed.store.dbStorage.getDbTable);
      assert.calledWithExactly(feed.store.dbStorage.getDbTable, "sectionPrefs");
    });
    it("should call onUpdate to set up Nimbus update listener", async () => {
      await feed.init();

      assert.calledOnce(fakeNimbusFeatures.newtab.onUpdate);
    });
  });
  describe("#refresh", () => {
    beforeEach(() => {
      sandbox.stub(feed, "_fetchIcon");
      feed._startedUp = true;
    });
    it("should wait for tippytop to initialize", async () => {
      feed._tippyTopProvider.initialized = false;
      sinon.stub(feed._tippyTopProvider, "init").resolves();

      await feed.refresh();

      assert.calledOnce(feed._tippyTopProvider.init);
    });
    it("should not init the tippyTopProvider if already initialized", async () => {
      feed._tippyTopProvider.initialized = true;
      sinon.stub(feed._tippyTopProvider, "init").resolves();

      await feed.refresh();

      assert.notCalled(feed._tippyTopProvider.init);
    });
    it("should broadcast TOP_SITES_UPDATED", async () => {
      sinon.stub(feed, "getLinksWithDefaults").returns(Promise.resolve([]));

      await feed.refresh({ broadcast: true });

      assert.calledOnce(feed.store.dispatch);
      assert.calledWithExactly(
        feed.store.dispatch,
        ac.BroadcastToContent({
          type: at.TOP_SITES_UPDATED,
          data: { links: [], pref: { collapsed: false } },
        })
      );
    });
    it("should dispatch an action with the links returned", async () => {
      await feed.refresh({ broadcast: true });
      const reference = links.map(site =>
        Object.assign({}, site, {
          hostname: shortURLStub(site),
          typedBonus: true,
        })
      );

      assert.calledOnce(feed.store.dispatch);
      assert.propertyVal(
        feed.store.dispatch.firstCall.args[0],
        "type",
        at.TOP_SITES_UPDATED
      );
      assert.deepEqual(
        feed.store.dispatch.firstCall.args[0].data.links,
        reference
      );
    });
    it("should handle empty slots in the resulting top sites array", async () => {
      links = [FAKE_LINKS[0]];
      fakeNewTabUtils.pinnedLinks.links = [
        null,
        null,
        FAKE_LINKS[1],
        null,
        null,
        null,
        null,
        null,
        FAKE_LINKS[2],
      ];
      await feed.refresh({ broadcast: true });
      assert.calledOnce(feed.store.dispatch);
    });
    it("should dispatch AlsoToPreloaded when broadcast is false", async () => {
      sandbox.stub(feed, "getLinksWithDefaults").returns([]);
      await feed.refresh({ broadcast: false });

      assert.calledOnce(feed.store.dispatch);
      assert.calledWithExactly(
        feed.store.dispatch,
        ac.AlsoToPreloaded({
          type: at.TOP_SITES_UPDATED,
          data: { links: [], pref: { collapsed: false } },
        })
      );
    });
    it("should not init storage if it is already initialized", async () => {
      feed._storage.initialized = true;

      await feed.refresh({ broadcast: false });

      assert.notCalled(feed._storage.init);
    });
    it("should catch indexedDB errors", async () => {
      feed._storage.get.throws(new Error());
      globals.sandbox.spy(global.console, "error");

      try {
        await feed.refresh({ broadcast: false });
      } catch (e) {
        assert.fails();
      }

      assert.calledOnce(console.error);
    });
  });
  describe("#updateSectionPrefs", () => {
    it("should call updateSectionPrefs on UPDATE_SECTION_PREFS", () => {
      sandbox.stub(feed, "updateSectionPrefs");

      feed.onAction({
        type: at.UPDATE_SECTION_PREFS,
        data: { id: "topsites" },
      });

      assert.calledOnce(feed.updateSectionPrefs);
    });
    it("should dispatch TOP_SITES_PREFS_UPDATED", async () => {
      await feed.updateSectionPrefs({ collapsed: true });

      assert.calledOnce(feed.store.dispatch);
      assert.calledWithExactly(
        feed.store.dispatch,
        ac.BroadcastToContent({
          type: at.TOP_SITES_PREFS_UPDATED,
          data: { pref: { collapsed: true } },
        })
      );
    });
  });
  describe("#getScreenshotPreview", () => {
    it("should dispatch preview if request is succesful", async () => {
      await feed.getScreenshotPreview("custom", 1234);

      assert.calledOnce(feed.store.dispatch);
      assert.calledWithExactly(
        feed.store.dispatch,
        ac.OnlyToOneContent(
          {
            data: { preview: FAKE_SCREENSHOT, url: "custom" },
            type: at.PREVIEW_RESPONSE,
          },
          1234
        )
      );
    });
    it("should return empty string if request fails", async () => {
      fakeScreenshot.getScreenshotForURL = sandbox
        .stub()
        .returns(Promise.resolve(null));
      await feed.getScreenshotPreview("custom", 1234);

      assert.calledOnce(feed.store.dispatch);
      assert.calledWithExactly(
        feed.store.dispatch,
        ac.OnlyToOneContent(
          {
            data: { preview: "", url: "custom" },
            type: at.PREVIEW_RESPONSE,
          },
          1234
        )
      );
    });
  });
  describe("#_fetchIcon", () => {
    it("should reuse screenshot on the link", () => {
      const link = { screenshot: "reuse.png" };

      feed._fetchIcon(link);

      assert.notCalled(fakeScreenshot.getScreenshotForURL);
      assert.propertyVal(link, "screenshot", "reuse.png");
    });
    it("should reuse existing fetching screenshot on the link", async () => {
      const link = {
        __sharedCache: { fetchingScreenshot: Promise.resolve("fetching.png") },
      };

      await feed._fetchIcon(link);

      assert.notCalled(fakeScreenshot.getScreenshotForURL);
    });
    it("should get a screenshot if the link is missing it", () => {
      feed.store.state.Prefs.values[SHOWN_ON_NEWTAB_PREF] = true;
      feed._fetchIcon(Object.assign({ __sharedCache: {} }, FAKE_LINKS[0]));

      assert.calledOnce(fakeScreenshot.getScreenshotForURL);
      assert.calledWith(fakeScreenshot.getScreenshotForURL, FAKE_LINKS[0].url);
    });
    it("should not get a screenshot if the link is missing it but top sites aren't shown", () => {
      feed.store.state.Prefs.values[SHOWN_ON_NEWTAB_PREF] = false;
      feed._fetchIcon(Object.assign({ __sharedCache: {} }, FAKE_LINKS[0]));

      assert.notCalled(fakeScreenshot.getScreenshotForURL);
    });
    it("should update the link's cache with a screenshot", async () => {
      feed.store.state.Prefs.values[SHOWN_ON_NEWTAB_PREF] = true;
      const updateLink = sandbox.stub();
      const link = { __sharedCache: { updateLink } };

      await feed._fetchIcon(link);

      assert.calledOnce(updateLink);
      assert.calledWith(updateLink, "screenshot", FAKE_SCREENSHOT);
    });
    it("should skip getting a screenshot if there is a tippy top icon", () => {
      feed._tippyTopProvider.processSite = site => {
        site.tippyTopIcon = "icon.png";
        site.backgroundColor = "#fff";
        return site;
      };
      const link = { url: "example.com" };
      feed._fetchIcon(link);
      assert.propertyVal(link, "tippyTopIcon", "icon.png");
      assert.notProperty(link, "screenshot");
      assert.notCalled(fakeScreenshot.getScreenshotForURL);
    });
    it("should skip getting a screenshot if there is an icon of size greater than 96x96 and no tippy top", () => {
      const link = {
        url: "foo.com",
        favicon: "data:foo",
        faviconSize: 196,
      };
      feed._fetchIcon(link);
      assert.notProperty(link, "tippyTopIcon");
      assert.notProperty(link, "screenshot");
      assert.notCalled(fakeScreenshot.getScreenshotForURL);
    });
    it("should use the link's rich icon even if there's a tippy top", () => {
      feed._tippyTopProvider.processSite = site => {
        site.tippyTopIcon = "icon.png";
        site.backgroundColor = "#fff";
        return site;
      };
      const link = {
        url: "foo.com",
        favicon: "data:foo",
        faviconSize: 196,
      };
      feed._fetchIcon(link);
      assert.notProperty(link, "tippyTopIcon");
    });
  });
  describe("#_fetchScreenshot", () => {
    it("should call maybeCacheScreenshot", async () => {
      feed.store.state.Prefs.values[SHOWN_ON_NEWTAB_PREF] = true;
      const updateLink = sinon.stub();
      const link = {
        customScreenshotURL: "custom",
        __sharedCache: { updateLink },
      };
      await feed._fetchScreenshot(link, "custom");

      assert.calledOnce(fakeScreenshot.maybeCacheScreenshot);
      assert.calledWithExactly(
        fakeScreenshot.maybeCacheScreenshot,
        link,
        link.customScreenshotURL,
        "screenshot",
        sinon.match.func
      );
    });
    it("should not call maybeCacheScreenshot if screenshot is set", async () => {
      const updateLink = sinon.stub();
      const link = {
        customScreenshotURL: "custom",
        __sharedCache: { updateLink },
        screenshot: true,
      };
      await feed._fetchScreenshot(link, "custom");

      assert.notCalled(fakeScreenshot.maybeCacheScreenshot);
    });
  });
  describe("#onAction", () => {
    it("should call getScreenshotPreview on PREVIEW_REQUEST", () => {
      sandbox.stub(feed, "getScreenshotPreview");

      feed.onAction({
        type: at.PREVIEW_REQUEST,
        data: { url: "foo" },
        meta: { fromTarget: 1234 },
      });

      assert.calledOnce(feed.getScreenshotPreview);
      assert.calledWithExactly(feed.getScreenshotPreview, "foo", 1234);
    });
    it("should refresh on SYSTEM_TICK", async () => {
      sandbox.stub(feed, "refresh");

      feed.onAction({ type: at.SYSTEM_TICK });

      assert.calledOnce(feed.refresh);
      assert.calledWithExactly(feed.refresh, { broadcast: false });
    });
    it("should call with correct parameters on TOP_SITES_PIN", () => {
      const pinAction = {
        type: at.TOP_SITES_PIN,
        data: { site: { url: "foo.com" }, index: 7 },
      };
      feed.onAction(pinAction);
      assert.calledOnce(fakeNewTabUtils.pinnedLinks.pin);
      assert.calledWith(
        fakeNewTabUtils.pinnedLinks.pin,
        pinAction.data.site,
        pinAction.data.index
      );
    });
    it("should call pin on TOP_SITES_PIN", () => {
      sinon.stub(feed, "pin");
      const pinExistingAction = {
        type: at.TOP_SITES_PIN,
        data: { site: FAKE_LINKS[4], index: 4 },
      };

      feed.onAction(pinExistingAction);

      assert.calledOnce(feed.pin);
    });
    it("should trigger refresh on TOP_SITES_PIN", async () => {
      sinon.stub(feed, "refresh");
      const pinExistingAction = {
        type: at.TOP_SITES_PIN,
        data: { site: FAKE_LINKS[4], index: 4 },
      };

      await feed.pin(pinExistingAction);

      assert.calledOnce(feed.refresh);
    });
    it("should unblock a previously blocked top site if we are now adding it manually via 'Add a Top Site' option", async () => {
      const pinAction = {
        type: at.TOP_SITES_PIN,
        data: { site: { url: "foo.com" }, index: -1 },
      };
      feed.onAction(pinAction);
      assert.calledWith(fakeNewTabUtils.blockedLinks.unblock, {
        url: pinAction.data.site.url,
      });
    });
    it("should call insert on TOP_SITES_INSERT", async () => {
      sinon.stub(feed, "insert");
      const addAction = {
        type: at.TOP_SITES_INSERT,
        data: { site: { url: "foo.com" } },
      };

      feed.onAction(addAction);

      assert.calledOnce(feed.insert);
    });
    it("should trigger refresh on TOP_SITES_INSERT", async () => {
      sinon.stub(feed, "refresh");
      const addAction = {
        type: at.TOP_SITES_INSERT,
        data: { site: { url: "foo.com" } },
      };

      await feed.insert(addAction);

      assert.calledOnce(feed.refresh);
    });
    it("should call unpin with correct parameters on TOP_SITES_UNPIN", () => {
      fakeNewTabUtils.pinnedLinks.links = [
        null,
        null,
        { url: "foo.com" },
        null,
        null,
        null,
        null,
        null,
        FAKE_LINKS[0],
      ];
      const unpinAction = {
        type: at.TOP_SITES_UNPIN,
        data: { site: { url: "foo.com" } },
      };
      feed.onAction(unpinAction);
      assert.calledOnce(fakeNewTabUtils.pinnedLinks.unpin);
      assert.calledWith(
        fakeNewTabUtils.pinnedLinks.unpin,
        unpinAction.data.site
      );
    });
    it("should call refresh without a target if we clear history with PLACES_HISTORY_CLEARED", () => {
      sandbox.stub(feed, "refresh");

      feed.onAction({ type: at.PLACES_HISTORY_CLEARED });

      assert.calledOnce(feed.refresh);
      assert.calledWithExactly(feed.refresh, { broadcast: true });
    });
    it("should call refresh without a target if we remove a Topsite from history", () => {
      sandbox.stub(feed, "refresh");

      feed.onAction({ type: at.PLACES_LINKS_DELETED });

      assert.calledOnce(feed.refresh);
      assert.calledWithExactly(feed.refresh, { broadcast: true });
    });
    it("should still dispatch an action even if there's no target provided", async () => {
      sandbox.stub(feed, "_fetchIcon");
      feed._startedUp = true;
      await feed.refresh({ broadcast: true });
      assert.calledOnce(feed.store.dispatch);
      assert.propertyVal(
        feed.store.dispatch.firstCall.args[0],
        "type",
        at.TOP_SITES_UPDATED
      );
    });
    it("should call init on INIT action", async () => {
      sinon.stub(feed, "init");
      feed.onAction({ type: at.INIT });
      assert.calledOnce(feed.init);
    });
    it("should call refresh on PLACES_LINK_BLOCKED action", async () => {
      sinon.stub(feed, "refresh");
      await feed.onAction({ type: at.PLACES_LINK_BLOCKED });
      assert.calledOnce(feed.refresh);
      assert.calledWithExactly(feed.refresh, { broadcast: true });
    });
    it("should call refresh on PLACES_LINKS_CHANGED action", async () => {
      sinon.stub(feed, "refresh");
      await feed.onAction({ type: at.PLACES_LINKS_CHANGED });
      assert.calledOnce(feed.refresh);
      assert.calledWithExactly(feed.refresh, { broadcast: false });
    });
    it("should call pin with correct args on TOP_SITES_INSERT without an index specified", () => {
      const addAction = {
        type: at.TOP_SITES_INSERT,
        data: { site: { url: "foo.bar", label: "foo" } },
      };
      feed.onAction(addAction);
      assert.calledOnce(fakeNewTabUtils.pinnedLinks.pin);
      assert.calledWith(
        fakeNewTabUtils.pinnedLinks.pin,
        addAction.data.site,
        0
      );
    });
    it("should call pin with correct args on TOP_SITES_INSERT", () => {
      const dropAction = {
        type: at.TOP_SITES_INSERT,
        data: { site: { url: "foo.bar", label: "foo" }, index: 3 },
      };
      feed.onAction(dropAction);
      assert.calledOnce(fakeNewTabUtils.pinnedLinks.pin);
      assert.calledWith(
        fakeNewTabUtils.pinnedLinks.pin,
        dropAction.data.site,
        3
      );
    });
    it("should remove the expiration filter on UNINIT", () => {
      feed.onAction({ type: "UNINIT" });

      assert.calledOnce(fakePageThumbs.removeExpirationFilter);
    });
    it("should call updatePinnedSearchShortcuts on UPDATE_PINNED_SEARCH_SHORTCUTS action", async () => {
      sinon.stub(feed, "updatePinnedSearchShortcuts");
      const addedShortcuts = [
        {
          url: "https://google.com",
          searchVendor: "google",
          label: "google",
          searchTopSite: true,
        },
      ];
      await feed.onAction({
        type: at.UPDATE_PINNED_SEARCH_SHORTCUTS,
        data: { addedShortcuts },
      });
      assert.calledOnce(feed.updatePinnedSearchShortcuts);
    });
    it("should refresh from Contile on SHOW_SPONSORED_PREF if Contile is enabled", () => {
      sandbox.spy(feed._contile, "refresh");
      const prefChangeAction = {
        type: at.PREF_CHANGED,
        data: { name: SHOW_SPONSORED_PREF },
      };
      fakeNimbusFeatures.newtab.getVariable.returns(true);
      feed.onAction(prefChangeAction);

      assert.calledOnce(feed._contile.refresh);
    });
    it("should not refresh from Contile on SHOW_SPONSORED_PREF if Contile is disabled", () => {
      sandbox.spy(feed._contile, "refresh");
      const prefChangeAction = {
        type: at.PREF_CHANGED,
        data: { name: SHOW_SPONSORED_PREF },
      };
      fakeNimbusFeatures.newtab.getVariable.returns(false);
      feed.onAction(prefChangeAction);

      assert.notCalled(feed._contile.refresh);
    });
    it("should reset Contile cache prefs when SHOW_SPONSORED_PREF is false", () => {
      Services.prefs.setStringPref(CONTILE_CACHE_PREF, "[]");
      Services.prefs.setIntPref(CONTILE_CACHE_LAST_FETCH_PREF, 15 * 60 * 1000);
      Services.prefs.setIntPref(CONTILE_CACHE_VALID_FOR_PREF, Date.now());

      sandbox.spy(feed._contile, "refresh");
      const prefChangeAction = {
        type: at.PREF_CHANGED,
        data: { name: SHOW_SPONSORED_PREF, value: false },
      };
      fakeNimbusFeatures.newtab.getVariable.returns(true);
      feed.onAction(prefChangeAction);

      assert.calledOnce(feed._contile.refresh);

      // cached pref values should have reset
      assert.isUndefined(Services.prefs.getStringPref(CONTILE_CACHE_PREF));
      assert.isUndefined(
        Services.prefs.getIntPref(CONTILE_CACHE_LAST_FETCH_PREF)
      );
      assert.isUndefined(
        Services.prefs.getIntPref(CONTILE_CACHE_VALID_FOR_PREF)
      );
    });
  });
  describe("#add", () => {
    it("should pin site in first slot of empty pinned list", () => {
      const site = { url: "foo.bar", label: "foo" };
      feed.insert({ data: { site } });
      assert.calledOnce(fakeNewTabUtils.pinnedLinks.pin);
      assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site, 0);
    });
    it("should pin site in first slot of pinned list with empty first slot", () => {
      fakeNewTabUtils.pinnedLinks.links = [null, { url: "example.com" }];
      const site = { url: "foo.bar", label: "foo" };
      feed.insert({ data: { site } });
      assert.calledOnce(fakeNewTabUtils.pinnedLinks.pin);
      assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site, 0);
    });
    it("should move a pinned site in first slot to the next slot: part 1", () => {
      const site1 = { url: "example.com" };
      fakeNewTabUtils.pinnedLinks.links = [site1];
      const site = { url: "foo.bar", label: "foo" };
      feed.insert({ data: { site } });
      assert.calledTwice(fakeNewTabUtils.pinnedLinks.pin);
      assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site, 0);
      assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site1, 1);
    });
    it("should move a pinned site in first slot to the next slot: part 2", () => {
      const site1 = { url: "example.com" };
      const site2 = { url: "example.org" };
      fakeNewTabUtils.pinnedLinks.links = [site1, null, site2];
      const site = { url: "foo.bar", label: "foo" };
      feed.insert({ data: { site } });
      assert.calledTwice(fakeNewTabUtils.pinnedLinks.pin);
      assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site, 0);
      assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site1, 1);
    });
    it("should unpin the last site if all slots are already pinned", () => {
      const site1 = { url: "example.com" };
      const site2 = { url: "example.org" };
      const site3 = { url: "example.net" };
      const site4 = { url: "example.biz" };
      const site5 = { url: "example.info" };
      const site6 = { url: "example.news" };
      const site7 = { url: "example.lol" };
      const site8 = { url: "example.golf" };
      fakeNewTabUtils.pinnedLinks.links = [
        site1,
        site2,
        site3,
        site4,
        site5,
        site6,
        site7,
        site8,
      ];
      feed.store.state.Prefs.values.topSitesRows = 1;
      const site = { url: "foo.bar", label: "foo" };
      feed.insert({ data: { site } });
      assert.equal(fakeNewTabUtils.pinnedLinks.pin.callCount, 8);
      assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site, 0);
      assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site1, 1);
      assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site2, 2);
      assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site3, 3);
      assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site4, 4);
      assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site5, 5);
      assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site6, 6);
      assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site7, 7);
    });
  });
  describe("#pin", () => {
    it("should pin site in specified slot empty pinned list", async () => {
      const site = {
        url: "foo.bar",
        label: "foo",
        customScreenshotURL: "screenshot",
      };
      await feed.pin({ data: { index: 2, site } });
      assert.calledOnce(fakeNewTabUtils.pinnedLinks.pin);
      assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site, 2);
    });
    it("should lookup the link object to update the custom screenshot", async () => {
      const site = {
        url: "foo.bar",
        label: "foo",
        customScreenshotURL: "screenshot",
      };
      sandbox.spy(feed.pinnedCache, "request");

      await feed.pin({ data: { index: 2, site } });

      assert.calledOnce(feed.pinnedCache.request);
    });
    it("should lookup the link object to update the custom screenshot", async () => {
      const site = { url: "foo.bar", label: "foo", customScreenshotURL: null };
      sandbox.spy(feed.pinnedCache, "request");

      await feed.pin({ data: { index: 2, site } });

      assert.calledOnce(feed.pinnedCache.request);
    });
    it("should not do a link object lookup if custom screenshot field is not set", async () => {
      const site = { url: "foo.bar", label: "foo" };
      sandbox.spy(feed.pinnedCache, "request");

      await feed.pin({ data: { index: 2, site } });

      assert.notCalled(feed.pinnedCache.request);
    });
    it("should pin site in specified slot of pinned list that is free", () => {
      fakeNewTabUtils.pinnedLinks.links = [null, { url: "example.com" }];
      const site = { url: "foo.bar", label: "foo" };
      feed.pin({ data: { index: 2, site } });
      assert.calledOnce(fakeNewTabUtils.pinnedLinks.pin);
      assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site, 2);
    });
    it("should save the searchTopSite attribute if set", () => {
      fakeNewTabUtils.pinnedLinks.links = [null, { url: "example.com" }];
      const site = { url: "foo.bar", label: "foo", searchTopSite: true };
      feed.pin({ data: { index: 2, site } });
      assert.calledOnce(fakeNewTabUtils.pinnedLinks.pin);
      assert.propertyVal(
        fakeNewTabUtils.pinnedLinks.pin.firstCall.args[0],
        "searchTopSite",
        true
      );
    });
    it("should NOT move a pinned site in specified slot to the next slot", () => {
      fakeNewTabUtils.pinnedLinks.links = [null, null, { url: "example.com" }];
      const site = { url: "foo.bar", label: "foo" };
      feed.pin({ data: { index: 2, site } });
      assert.calledOnce(fakeNewTabUtils.pinnedLinks.pin);
      assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site, 2);
    });
    it("should properly update LinksCache object properties between migrations", async () => {
      fakeNewTabUtils.pinnedLinks.links = [{ url: "https://foo.com/" }];

      let pinnedLinks = await feed.pinnedCache.request();
      assert.equal(pinnedLinks.length, 1);
      feed.pinnedCache.expire();
      pinnedLinks[0].__sharedCache.updateLink("screenshot", "foo");

      pinnedLinks = await feed.pinnedCache.request();
      assert.propertyVal(pinnedLinks[0], "screenshot", "foo");

      // Force cache expiration in order to trigger a migration of objects
      feed.pinnedCache.expire();
      pinnedLinks[0].__sharedCache.updateLink("screenshot", "bar");

      pinnedLinks = await feed.pinnedCache.request();
      assert.propertyVal(pinnedLinks[0], "screenshot", "bar");
    });
    it("should call insert if index < 0", () => {
      const site = { url: "foo.bar", label: "foo" };
      const action = { data: { index: -1, site } };

      sandbox.spy(feed, "insert");
      feed.pin(action);

      assert.calledOnce(feed.insert);
      assert.calledWithExactly(feed.insert, action);
    });
    it("should not call insert if index == 0", () => {
      const site = { url: "foo.bar", label: "foo" };
      const action = { data: { index: 0, site } };

      sandbox.spy(feed, "insert");
      feed.pin(action);

      assert.notCalled(feed.insert);
    });
  });
  describe("clearLinkCustomScreenshot", () => {
    it("should remove cached screenshot if custom url changes", async () => {
      const stub = sandbox.stub();
      sandbox.stub(feed.pinnedCache, "request").returns(
        Promise.resolve([
          {
            url: "foo",
            customScreenshotURL: "old_screenshot",
            __sharedCache: { updateLink: stub },
          },
        ])
      );

      await feed._clearLinkCustomScreenshot({
        url: "foo",
        customScreenshotURL: "new_screenshot",
      });

      assert.calledOnce(stub);
      assert.calledWithExactly(stub, "screenshot", undefined);
    });
    it("should remove cached screenshot if custom url is removed", async () => {
      const stub = sandbox.stub();
      sandbox.stub(feed.pinnedCache, "request").returns(
        Promise.resolve([
          {
            url: "foo",
            customScreenshotURL: "old_screenshot",
            __sharedCache: { updateLink: stub },
          },
        ])
      );

      await feed._clearLinkCustomScreenshot({
        url: "foo",
        customScreenshotURL: "new_screenshot",
      });

      assert.calledOnce(stub);
      assert.calledWithExactly(stub, "screenshot", undefined);
    });
  });
  describe("#drop", () => {
    it("should correctly handle different index values", () => {
      let index = -1;
      const site = { url: "foo.bar", label: "foo" };
      const action = { data: { index, site } };

      feed.insert(action);

      assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site, 0);

      index = undefined;
      feed.insert(action);

      assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site, 0);
    });
    it("should pin site in specified slot that is free", () => {
      fakeNewTabUtils.pinnedLinks.links = [null, { url: "example.com" }];
      const site = { url: "foo.bar", label: "foo" };
      feed.insert({ data: { index: 2, site, draggedFromIndex: 0 } });
      assert.calledOnce(fakeNewTabUtils.pinnedLinks.pin);
      assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site, 2);
    });
    it("should move a pinned site in specified slot to the next slot", () => {
      fakeNewTabUtils.pinnedLinks.links = [null, null, { url: "example.com" }];
      const site = { url: "foo.bar", label: "foo" };
      feed.insert({ data: { index: 2, site, draggedFromIndex: 3 } });
      assert.calledTwice(fakeNewTabUtils.pinnedLinks.pin);
      assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site, 2);
      assert.calledWith(
        fakeNewTabUtils.pinnedLinks.pin,
        { url: "example.com" },
        3
      );
    });
    it("should move pinned sites in the direction of the dragged site", () => {
      const site1 = { url: "foo.bar", label: "foo" };
      const site2 = { url: "example.com", label: "example" };
      fakeNewTabUtils.pinnedLinks.links = [null, null, site2];
      feed.insert({ data: { index: 2, site: site1, draggedFromIndex: 0 } });
      assert.calledTwice(fakeNewTabUtils.pinnedLinks.pin);
      assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site1, 2);
      assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site2, 1);
      fakeNewTabUtils.pinnedLinks.pin.resetHistory();
      feed.insert({ data: { index: 2, site: site1, draggedFromIndex: 5 } });
      assert.calledTwice(fakeNewTabUtils.pinnedLinks.pin);
      assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site1, 2);
      assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site2, 3);
    });
    it("should not insert past the visible top sites", () => {
      const site1 = { url: "foo.bar", label: "foo" };
      feed.insert({ data: { index: 42, site: site1, draggedFromIndex: 0 } });
      assert.notCalled(fakeNewTabUtils.pinnedLinks.pin);
    });
  });
  describe("integration", () => {
    let resolvers = [];
    beforeEach(() => {
      feed.store.dispatch = sandbox.stub().callsFake(() => {
        resolvers.shift()();
      });
      feed._startedUp = true;
      sandbox.stub(feed, "_fetchScreenshot");
    });
    afterEach(() => {
      sandbox.restore();
    });

    const forDispatch = action =>
      new Promise(resolve => {
        resolvers.push(resolve);
        feed.onAction(action);
      });

    it("should add a pinned site and remove it", async () => {
      feed._requestRichIcon = sinon.stub();
      const url = "https://pin.me";
      fakeNewTabUtils.pinnedLinks.pin = sandbox.stub().callsFake(link => {
        fakeNewTabUtils.pinnedLinks.links.push(link);
      });

      await forDispatch({ type: at.TOP_SITES_INSERT, data: { site: { url } } });
      fakeNewTabUtils.pinnedLinks.links.pop();
      await forDispatch({ type: at.PLACES_LINK_BLOCKED });

      assert.calledTwice(feed.store.dispatch);
      assert.equal(
        feed.store.dispatch.firstCall.args[0].data.links[0].url,
        url
      );
      assert.equal(
        feed.store.dispatch.secondCall.args[0].data.links[0].url,
        FAKE_LINKS[0].url
      );
    });
  });

  describe("improvesearch.noDefaultSearchTile experiment", () => {
    const NO_DEFAULT_SEARCH_TILE_PREF = "improvesearch.noDefaultSearchTile";
    beforeEach(() => {
      global.Services.search.getDefault = async () => ({
        identifier: "google",
        searchForm: "google.com",
      });
      feed.store.state.Prefs.values[NO_DEFAULT_SEARCH_TILE_PREF] = true;
    });
    it("should filter out alexa top 5 search from the default sites", async () => {
      const TOP_5_TEST = [
        "google.com",
        "search.yahoo.com",
        "yahoo.com",
        "bing.com",
        "ask.com",
        "duckduckgo.com",
      ];
      links = [{ url: "amazon.com" }, ...TOP_5_TEST.map(url => ({ url }))];
      const urlsReturned = (await feed.getLinksWithDefaults()).map(
        link => link.url
      );
      assert.include(urlsReturned, "amazon.com");
      TOP_5_TEST.forEach(url => assert.notInclude(urlsReturned, url));
    });
    it("should not filter out alexa, default search from the query results if the experiment pref is off", async () => {
      links = [
        { url: "google.com" },
        { url: "foo.com" },
        { url: "duckduckgo" },
      ];
      feed.store.state.Prefs.values[NO_DEFAULT_SEARCH_TILE_PREF] = false;
      const urlsReturned = (await feed.getLinksWithDefaults()).map(
        link => link.url
      );
      assert.include(urlsReturned, "google.com");
    });
    it("should filter out the current default search from the default sites", async () => {
      feed._currentSearchHostname = "amazon";
      feed.onAction({
        type: at.PREFS_INITIAL_VALUES,
        data: { "default.sites": "google.com,amazon.com" },
      });
      links = [{ url: "foo.com" }];
      const urlsReturned = (await feed.getLinksWithDefaults()).map(
        link => link.url
      );
      assert.notInclude(urlsReturned, "amazon.com");
    });
    it("should not filter out current default search from pinned sites even if it matches the current default search", async () => {
      links = [{ url: "foo.com" }];
      fakeNewTabUtils.pinnedLinks.links = [{ url: "google.com" }];
      const urlsReturned = (await feed.getLinksWithDefaults()).map(
        link => link.url
      );
      assert.include(urlsReturned, "google.com");
    });
    it("should call refresh and set ._currentSearchHostname to the new engine hostname when the the default search engine has been set", () => {
      sinon.stub(feed, "refresh");
      sandbox
        .stub(global.Services.search, "defaultEngine")
        .value({ identifier: "ddg", searchForm: "duckduckgo.com" });
      feed.observe(null, "browser-search-engine-modified", "engine-default");
      assert.equal(feed._currentSearchHostname, "duckduckgo");
      assert.calledOnce(feed.refresh);
    });
    it("should call refresh when the experiment pref has changed", () => {
      sinon.stub(feed, "refresh");

      feed.onAction({
        type: at.PREF_CHANGED,
        data: { name: NO_DEFAULT_SEARCH_TILE_PREF, value: true },
      });
      assert.calledOnce(feed.refresh);

      feed.onAction({
        type: at.PREF_CHANGED,
        data: { name: NO_DEFAULT_SEARCH_TILE_PREF, value: false },
      });
      assert.calledTwice(feed.refresh);
    });
  });

  describe("improvesearch.topSitesSearchShortcuts", () => {
    beforeEach(() => {
      feed.store.state.Prefs.values[SEARCH_SHORTCUTS_EXPERIMENT_PREF] = true;
      feed.store.state.Prefs.values[SEARCH_SHORTCUTS_SEARCH_ENGINES_PREF] =
        "google,amazon";
      feed.store.state.Prefs.values[SEARCH_SHORTCUTS_HAVE_PINNED_PREF] = "";
      const searchEngines = [
        { aliases: ["@google"] },
        { aliases: ["@amazon"] },
      ];
      global.Services.search.getAppProvidedEngines = async () => searchEngines;
      fakeNewTabUtils.pinnedLinks.pin = sinon
        .stub()
        .callsFake((site, index) => {
          fakeNewTabUtils.pinnedLinks.links[index] = site;
        });
    });

    it("should properly disable search improvements if the pref is off", async () => {
      sandbox.stub(global.Services.prefs, "clearUserPref");
      sandbox.spy(feed.pinnedCache, "expire");
      sandbox.spy(feed, "refresh");

      // an actual implementation of unpin (until we can get a mochitest for search improvements)
      fakeNewTabUtils.pinnedLinks.unpin = sinon.stub().callsFake(site => {
        let index = -1;
        for (let i = 0; i < fakeNewTabUtils.pinnedLinks.links.length; i++) {
          let link = fakeNewTabUtils.pinnedLinks.links[i];
          if (link && link.url === site.url) {
            index = i;
          }
        }
        if (index > -1) {
          fakeNewTabUtils.pinnedLinks.links[index] = null;
        }
      });

      // ensure we've inserted search shorcuts + pin an additional site in space 4
      await feed._maybeInsertSearchShortcuts(fakeNewTabUtils.pinnedLinks.links);
      fakeNewTabUtils.pinnedLinks.pin({ url: "https://dontunpinme.com" }, 3);

      // turn the experiment off
      feed.onAction({
        type: at.PREF_CHANGED,
        data: { name: SEARCH_SHORTCUTS_EXPERIMENT_PREF, value: false },
      });

      // check we cleared the pref, expired the pinned cache, and refreshed the feed
      assert.calledWith(
        global.Services.prefs.clearUserPref,
        `browser.newtabpage.activity-stream.${SEARCH_SHORTCUTS_HAVE_PINNED_PREF}`
      );
      assert.calledOnce(feed.pinnedCache.expire);
      assert.calledWith(feed.refresh, { broadcast: true });

      // check that the search shortcuts were removed from the list of pinned sites
      const urlsReturned = fakeNewTabUtils.pinnedLinks.links
        .filter(s => s)
        .map(link => link.url);
      assert.notInclude(urlsReturned, "https://amazon.com");
      assert.notInclude(urlsReturned, "https://google.com");
      assert.include(urlsReturned, "https://dontunpinme.com");

      // check that the positions where the search shortcuts were null, and the additional pinned site is untouched in space 4
      assert.equal(fakeNewTabUtils.pinnedLinks.links[0], null);
      assert.equal(fakeNewTabUtils.pinnedLinks.links[1], null);
      assert.equal(fakeNewTabUtils.pinnedLinks.links[2], undefined);
      assert.deepEqual(fakeNewTabUtils.pinnedLinks.links[3], {
        url: "https://dontunpinme.com",
      });
    });

    it("should updateCustomSearchShortcuts when experiment pref is turned on", async () => {
      feed.store.state.Prefs.values[SEARCH_SHORTCUTS_EXPERIMENT_PREF] = false;
      feed.updateCustomSearchShortcuts = sinon.spy();

      // turn the experiment on
      feed.onAction({
        type: at.PREF_CHANGED,
        data: { name: SEARCH_SHORTCUTS_EXPERIMENT_PREF, value: true },
      });

      assert.calledOnce(feed.updateCustomSearchShortcuts);
    });

    it("should filter out default top sites that match a hostname of a search shortcut if previously blocked", async () => {
      feed.refreshDefaults("https://amazon.ca");
      fakeNewTabUtils.blockedLinks.links = [{ url: "https://amazon.com" }];
      fakeNewTabUtils.blockedLinks.isBlocked = site =>
        fakeNewTabUtils.blockedLinks.links[0].url === site.url;
      const urlsReturned = (await feed.getLinksWithDefaults()).map(
        link => link.url
      );
      assert.notInclude(urlsReturned, "https://amazon.ca");
    });

    it("should update frecent search topsite icon", async () => {
      feed._tippyTopProvider.processSite = site => {
        site.tippyTopIcon = "icon.png";
        site.backgroundColor = "#fff";
        return site;
      };
      links = [{ url: "google.com" }];

      const urlsReturned = await feed.getLinksWithDefaults();

      const defaultSearchTopsite = urlsReturned.find(
        s => s.url === "google.com"
      );
      assert.propertyVal(defaultSearchTopsite, "searchTopSite", true);
      assert.equal(defaultSearchTopsite.tippyTopIcon, "icon.png");
      assert.equal(defaultSearchTopsite.backgroundColor, "#fff");
    });
    it("should update default search topsite icon", async () => {
      feed._tippyTopProvider.processSite = site => {
        site.tippyTopIcon = "icon.png";
        site.backgroundColor = "#fff";
        return site;
      };
      links = [{ url: "foo.com" }];
      feed.onAction({
        type: at.PREFS_INITIAL_VALUES,
        data: { "default.sites": "google.com,amazon.com" },
      });

      const urlsReturned = await feed.getLinksWithDefaults();

      const defaultSearchTopsite = urlsReturned.find(
        s => s.url === "amazon.com"
      );
      assert.propertyVal(defaultSearchTopsite, "searchTopSite", true);
      assert.equal(defaultSearchTopsite.tippyTopIcon, "icon.png");
      assert.equal(defaultSearchTopsite.backgroundColor, "#fff");
    });
    it("should dispatch UPDATE_SEARCH_SHORTCUTS on updateCustomSearchShortcuts", async () => {
      feed.store.state.Prefs.values["improvesearch.noDefaultSearchTile"] = true;
      await feed.updateCustomSearchShortcuts();
      assert.calledOnce(feed.store.dispatch);
      assert.calledWith(feed.store.dispatch, {
        data: {
          searchShortcuts: [
            {
              keyword: "@google",
              shortURL: "google",
              url: "https://google.com",
            },
            {
              keyword: "@amazon",
              shortURL: "amazon",
              url: "https://amazon.com",
            },
          ],
        },
        meta: {
          from: "ActivityStream:Main",
          to: "ActivityStream:Content",
          isStartup: false,
        },
        type: "UPDATE_SEARCH_SHORTCUTS",
      });
    });

    describe("_maybeInsertSearchShortcuts", () => {
      beforeEach(() => {
        // Default is one row
        feed.store.state.Prefs.values.topSitesRows = TOP_SITES_DEFAULT_ROWS;
        // Eight slots per row
        fakeNewTabUtils.pinnedLinks.links = [
          { url: "" },
          { url: "" },
          { url: "" },
          null,
          { url: "" },
          { url: "" },
          null,
          { url: "" },
        ];
      });

      it("should be called on getLinksWithDefaults", async () => {
        sandbox.spy(feed, "_maybeInsertSearchShortcuts");
        await feed.getLinksWithDefaults();
        assert.calledOnce(feed._maybeInsertSearchShortcuts);
      });

      it("should do nothing and return false if the experiment is disabled", async () => {
        feed.store.state.Prefs.values[SEARCH_SHORTCUTS_EXPERIMENT_PREF] = false;
        assert.isFalse(
          await feed._maybeInsertSearchShortcuts(
            fakeNewTabUtils.pinnedLinks.links
          )
        );
        assert.notCalled(fakeNewTabUtils.pinnedLinks.pin);
      });

      it("should pin shortcuts in the correct order, into the available unpinned slots", async () => {
        await feed._maybeInsertSearchShortcuts(
          fakeNewTabUtils.pinnedLinks.links
        );
        // The shouldPin pref is "google,amazon" so expect the shortcuts in that order
        assert.deepEqual(fakeNewTabUtils.pinnedLinks.links[3], {
          url: "https://google.com",
          searchTopSite: true,
          label: "@google",
        });
        assert.deepEqual(fakeNewTabUtils.pinnedLinks.links[6], {
          url: "https://amazon.com",
          searchTopSite: true,
          label: "@amazon",
        });
      });

      it("should not pin shortcuts for the current default search engine", async () => {
        feed._currentSearchHostname = "google";
        await feed._maybeInsertSearchShortcuts(
          fakeNewTabUtils.pinnedLinks.links
        );
        assert.deepEqual(fakeNewTabUtils.pinnedLinks.links[3], {
          url: "https://amazon.com",
          searchTopSite: true,
          label: "@amazon",
        });
      });

      it("should only pin the first shortcut if there's only one available slot", async () => {
        fakeNewTabUtils.pinnedLinks.links[3] = { url: "" };
        await feed._maybeInsertSearchShortcuts(
          fakeNewTabUtils.pinnedLinks.links
        );
        // The first item in the shouldPin pref is "google" so expect only Google to be pinned
        assert.ok(
          fakeNewTabUtils.pinnedLinks.links.find(
            s => s && s.url === "https://google.com"
          )
        );
        assert.notOk(
          fakeNewTabUtils.pinnedLinks.links.find(
            s => s && s.url === "https://amazon.com"
          )
        );
      });

      it("should pin none if there's no available slot", async () => {
        fakeNewTabUtils.pinnedLinks.links[3] = { url: "" };
        fakeNewTabUtils.pinnedLinks.links[6] = { url: "" };
        await feed._maybeInsertSearchShortcuts(
          fakeNewTabUtils.pinnedLinks.links
        );
        assert.notOk(
          fakeNewTabUtils.pinnedLinks.links.find(
            s => s && s.url === "https://google.com"
          )
        );
        assert.notOk(
          fakeNewTabUtils.pinnedLinks.links.find(
            s => s && s.url === "https://amazon.com"
          )
        );
      });

      it("should not pin a shortcut if the corresponding search engine is not available", async () => {
        // Make Amazon search engine unavailable
        global.Services.search.getAppProvidedEngines = async () => [
          { aliases: ["@google"] },
        ];
        fakeNewTabUtils.pinnedLinks.links.fill(null);
        await feed._maybeInsertSearchShortcuts(
          fakeNewTabUtils.pinnedLinks.links
        );
        assert.notOk(
          fakeNewTabUtils.pinnedLinks.links.find(
            s => s && s.url === "https://amazon.com"
          )
        );
      });

      it("should not pin a search shortcut if it's been pinned before", async () => {
        fakeNewTabUtils.pinnedLinks.links.fill(null);
        feed.store.state.Prefs.values[SEARCH_SHORTCUTS_HAVE_PINNED_PREF] =
          "google,amazon";
        await feed._maybeInsertSearchShortcuts(
          fakeNewTabUtils.pinnedLinks.links
        );
        assert.notOk(
          fakeNewTabUtils.pinnedLinks.links.find(
            s => s && s.url === "https://google.com"
          )
        );
        assert.notOk(
          fakeNewTabUtils.pinnedLinks.links.find(
            s => s && s.url === "https://amazon.com"
          )
        );

        fakeNewTabUtils.pinnedLinks.links.fill(null);
        feed.store.state.Prefs.values[SEARCH_SHORTCUTS_HAVE_PINNED_PREF] =
          "amazon";
        await feed._maybeInsertSearchShortcuts(
          fakeNewTabUtils.pinnedLinks.links
        );
        assert.ok(
          fakeNewTabUtils.pinnedLinks.links.find(
            s => s && s.url === "https://google.com"
          )
        );
        assert.notOk(
          fakeNewTabUtils.pinnedLinks.links.find(
            s => s && s.url === "https://amazon.com"
          )
        );

        fakeNewTabUtils.pinnedLinks.links.fill(null);
        feed.store.state.Prefs.values[SEARCH_SHORTCUTS_HAVE_PINNED_PREF] =
          "google";
        await feed._maybeInsertSearchShortcuts(
          fakeNewTabUtils.pinnedLinks.links
        );
        assert.notOk(
          fakeNewTabUtils.pinnedLinks.links.find(
            s => s && s.url === "https://google.com"
          )
        );
        assert.ok(
          fakeNewTabUtils.pinnedLinks.links.find(
            s => s && s.url === "https://amazon.com"
          )
        );
      });

      it("should record the insertion of a search shortcut", async () => {
        feed.store.state.Prefs.values[SEARCH_SHORTCUTS_HAVE_PINNED_PREF] = "";
        // Fill up one slot, so there's only one left - to be filled by Google
        fakeNewTabUtils.pinnedLinks.links[3] = { url: "" };
        await feed._maybeInsertSearchShortcuts(
          fakeNewTabUtils.pinnedLinks.links
        );
        assert.calledWithExactly(feed.store.dispatch, {
          data: { name: SEARCH_SHORTCUTS_HAVE_PINNED_PREF, value: "google" },
          meta: { from: "ActivityStream:Content", to: "ActivityStream:Main" },
          type: "SET_PREF",
        });
      });
    });
  });

  describe("updatePinnedSearchShortcuts", () => {
    it("should unpin a shortcut in deletedShortcuts", () => {
      const deletedShortcuts = [
        {
          url: "https://google.com",
          searchVendor: "google",
          label: "google",
          searchTopSite: true,
        },
      ];
      const addedShortcuts = [];
      fakeNewTabUtils.pinnedLinks.links = [
        null,
        null,
        {
          url: "https://amazon.com",
          searchVendor: "amazon",
          label: "amazon",
          searchTopSite: true,
        },
      ];
      feed.updatePinnedSearchShortcuts({ addedShortcuts, deletedShortcuts });
      assert.notCalled(fakeNewTabUtils.pinnedLinks.pin);
      assert.calledOnce(fakeNewTabUtils.pinnedLinks.unpin);
      assert.calledWith(fakeNewTabUtils.pinnedLinks.unpin, {
        url: "https://google.com",
      });
    });

    it("should pin a shortcut in addedShortcuts", () => {
      const addedShortcuts = [
        {
          url: "https://google.com",
          searchVendor: "google",
          label: "google",
          searchTopSite: true,
        },
      ];
      const deletedShortcuts = [];
      fakeNewTabUtils.pinnedLinks.links = [
        null,
        null,
        {
          url: "https://amazon.com",
          searchVendor: "amazon",
          label: "amazon",
          searchTopSite: true,
        },
      ];
      feed.updatePinnedSearchShortcuts({ addedShortcuts, deletedShortcuts });
      assert.notCalled(fakeNewTabUtils.pinnedLinks.unpin);
      assert.calledOnce(fakeNewTabUtils.pinnedLinks.pin);
      assert.calledWith(
        fakeNewTabUtils.pinnedLinks.pin,
        {
          label: "google",
          searchTopSite: true,
          searchVendor: "google",
          url: "https://google.com",
        },
        0
      );
    });

    it("should pin and unpin in the same action", () => {
      const addedShortcuts = [
        {
          url: "https://google.com",
          searchVendor: "google",
          label: "google",
          searchTopSite: true,
        },
        {
          url: "https://ebay.com",
          searchVendor: "ebay",
          label: "ebay",
          searchTopSite: true,
        },
      ];
      const deletedShortcuts = [
        {
          url: "https://amazon.com",
          searchVendor: "amazon",
          label: "amazon",
          searchTopSite: true,
        },
      ];
      fakeNewTabUtils.pinnedLinks.links = [
        { url: "https://foo.com" },
        {
          url: "https://amazon.com",
          searchVendor: "amazon",
          label: "amazon",
          searchTopSite: true,
        },
      ];
      feed.updatePinnedSearchShortcuts({ addedShortcuts, deletedShortcuts });
      assert.calledOnce(fakeNewTabUtils.pinnedLinks.unpin);
      assert.calledTwice(fakeNewTabUtils.pinnedLinks.pin);
    });

    it("should pin a shortcut in addedShortcuts even if pinnedLinks is full", () => {
      const addedShortcuts = [
        {
          url: "https://google.com",
          searchVendor: "google",
          label: "google",
          searchTopSite: true,
        },
      ];
      const deletedShortcuts = [];
      fakeNewTabUtils.pinnedLinks.links = FAKE_LINKS;
      feed.updatePinnedSearchShortcuts({ addedShortcuts, deletedShortcuts });
      assert.notCalled(fakeNewTabUtils.pinnedLinks.unpin);
      assert.calledWith(
        fakeNewTabUtils.pinnedLinks.pin,
        { label: "google", searchTopSite: true, url: "https://google.com" },
        0
      );
    });
  });

  describe("#_attachTippyTopIconForSearchShortcut", () => {
    beforeEach(() => {
      feed._tippyTopProvider.processSite = site => {
        if (site.url === "https://www.yandex.ru/") {
          site.tippyTopIcon = "yandex-ru.png";
          site.smallFavicon = "yandex-ru.ico";
        } else if (
          site.url === "https://www.yandex.com/" ||
          site.url === "https://yandex.com"
        ) {
          site.tippyTopIcon = "yandex.png";
          site.smallFavicon = "yandex.ico";
        } else {
          site.tippyTopIcon = "google.png";
          site.smallFavicon = "google.ico";
        }
        return site;
      };
    });

    it("should choose the -ru icons for Yandex search shortcut", async () => {
      sandbox.stub(global.Services.search, "getEngineByAlias").resolves({
        wrappedJSObject: { _searchForm: "https://www.yandex.ru/" },
      });

      const link = { url: "https://yandex.com" };
      await feed._attachTippyTopIconForSearchShortcut(link, "@yandex");

      assert.equal(link.tippyTopIcon, "yandex-ru.png");
      assert.equal(link.smallFavicon, "yandex-ru.ico");
      assert.equal(link.url, "https://yandex.com");
    });

    it("should choose -com icons for Yandex search shortcut", async () => {
      sandbox.stub(global.Services.search, "getEngineByAlias").resolves({
        wrappedJSObject: { _searchForm: "https://www.yandex.com/" },
      });

      const link = { url: "https://yandex.com" };
      await feed._attachTippyTopIconForSearchShortcut(link, "@yandex");

      assert.equal(link.tippyTopIcon, "yandex.png");
      assert.equal(link.smallFavicon, "yandex.ico");
      assert.equal(link.url, "https://yandex.com");
    });

    it("should use the -com icons if can't fetch the search form URL", async () => {
      sandbox.stub(global.Services.search, "getEngineByAlias").resolves(null);

      const link = { url: "https://yandex.com" };
      await feed._attachTippyTopIconForSearchShortcut(link, "@yandex");

      assert.equal(link.tippyTopIcon, "yandex.png");
      assert.equal(link.smallFavicon, "yandex.ico");
      assert.equal(link.url, "https://yandex.com");
    });

    it("should choose the correct icon for other non-yandex search shortcut", async () => {
      sandbox.stub(global.Services.search, "getEngineByAlias").resolves({
        wrappedJSObject: { _searchForm: "https://www.google.com/" },
      });

      const link = { url: "https://google.com" };
      await feed._attachTippyTopIconForSearchShortcut(link, "@google");

      assert.equal(link.tippyTopIcon, "google.png");
      assert.equal(link.smallFavicon, "google.ico");
      assert.equal(link.url, "https://google.com");
    });
  });

  describe("#ContileIntegration", () => {
    let getStringPrefStub;
    let getIntPrefStub;
    beforeEach(() => {
      // Turn on sponsored TopSites for testing
      feed.store.state.Prefs.values[SHOW_SPONSORED_PREF] = true;
      fetchStub = sandbox.stub();
      globals.set("fetch", fetchStub);

      getStringPrefStub = sandbox.stub(global.Services.prefs, "getStringPref");
      getStringPrefStub
        .withArgs(TOP_SITES_BLOCKED_SPONSORS_PREF)
        .returns(`["foo","bar"]`);

      getIntPrefStub = sandbox.stub(global.Services.prefs, "getIntPref");

      fakeNimbusFeatures.newtab.getVariable.returns(true);
      sandbox.spy(global.Services.prefs, "setStringPref");
      sandbox.spy(global.Services.prefs, "setIntPref");
    });
    afterEach(() => {
      sandbox.restore();
    });

    it("should fetch sites from Contile", async () => {
      fetchStub.resolves({
        ok: true,
        status: 200,
        headers: new Map([
          ["cache-control", "private, max-age=859, stale-if-error=10463"],
        ]),
        json: () =>
          Promise.resolve({
            tiles: [
              {
                url: "https://www.test.com",
                image_url: "images/test-com.png",
                click_url: "https://www.test-click.com",
                impression_url: "https://www.test-impression.com",
                name: "test",
              },
              {
                url: "https://www.test1.com",
                image_url: "images/test1-com.png",
                click_url: "https://www.test1-click.com",
                impression_url: "https://www.test1-impression.com",
                name: "test1",
              },
            ],
          }),
      });

      const fetched = await feed._contile._fetchSites();

      assert.ok(fetched);
      assert.equal(feed._contile.sites.length, 2);
    });

    it("should fetch SOV (Share-of-Voice) settings from Contile", async () => {
      const sov = {
        name: "SOV-20230518215316",
        allocations: [
          {
            position: 1,
            allocation: [
              {
                partner: "foo",
                percentage: 100,
              },
              {
                partner: "bar",
                percentage: 0,
              },
            ],
          },
          {
            position: 2,
            allocation: [
              {
                partner: "foo",
                percentage: 80,
              },
              {
                partner: "bar",
                percentage: 20,
              },
            ],
          },
        ],
      };
      fetchStub.resolves({
        ok: true,
        status: 200,
        headers: new Map([
          ["cache-control", "private, max-age=859, stale-if-error=10463"],
        ]),
        json: () =>
          Promise.resolve({
            sov: btoa(JSON.stringify(sov)),
            tiles: [
              {
                url: "https://www.test.com",
                image_url: "images/test-com.png",
                click_url: "https://www.test-click.com",
                impression_url: "https://www.test-impression.com",
                name: "test",
              },
              {
                url: "https://www.test1.com",
                image_url: "images/test1-com.png",
                click_url: "https://www.test1-click.com",
                impression_url: "https://www.test1-impression.com",
                name: "test1",
              },
            ],
          }),
      });

      const fetched = await feed._contile._fetchSites();

      assert.ok(fetched);
      assert.deepEqual(feed._contile.sov, sov);
      assert.equal(feed._contile.sites.length, 2);
    });

    it("should not fetch from Contile if it's not enabled", async () => {
      fakeNimbusFeatures.newtab.getVariable.reset();
      fakeNimbusFeatures.newtab.getVariable.returns(false);
      const fetched = await feed._contile._fetchSites();

      assert.notCalled(fetchStub);
      assert.ok(!fetched);
      assert.equal(feed._contile.sites.length, 0);
    });

    it("should still return two tiles when Contile provides more than 2 tiles and filtering results in more than 2 tiles", async () => {
      fakeNimbusFeatures.newtab.getVariable.reset();
      fakeNimbusFeatures.newtab.getVariable.onCall(0).returns(true);
      fakeNimbusFeatures.newtab.getVariable.onCall(1).returns(true);

      fetchStub.resolves({
        ok: true,
        status: 200,
        headers: new Map([
          ["cache-control", "private, max-age=859, stale-if-error=10463"],
        ]),
        json: () =>
          Promise.resolve({
            tiles: [
              {
                url: "https://www.test.com",
                image_url: "images/test-com.png",
                click_url: "https://www.test-click.com",
                impression_url: "https://www.test-impression.com",
                name: "test",
              },
              {
                url: "https://foo.com",
                image_url: "images/foo-com.png",
                click_url: "https://www.foo-click.com",
                impression_url: "https://www.foo-impression.com",
                name: "foo",
              },
              {
                url: "https://bar.com",
                image_url: "images/bar-com.png",
                click_url: "https://www.bar-click.com",
                impression_url: "https://www.bar-impression.com",
                name: "bar",
              },
              {
                url: "https://test1.com",
                image_url: "images/test1-com.png",
                click_url: "https://www.test1-click.com",
                impression_url: "https://www.test1-impression.com",
                name: "test1",
              },
              {
                url: "https://test2.com",
                image_url: "images/test2-com.png",
                click_url: "https://www.test2-click.com",
                impression_url: "https://www.test2-impression.com",
                name: "test2",
              },
            ],
          }),
      });

      const fetched = await feed._contile._fetchSites();

      assert.ok(fetched);
      // Both "foo" and "bar" should be filtered
      assert.equal(feed._contile.sites.length, 2);
      assert.equal(feed._contile.sites[0].url, "https://www.test.com");
      assert.equal(feed._contile.sites[1].url, "https://test1.com");
    });

    it("should still return two tiles with replacement if the Nimbus variable was unset", async () => {
      fakeNimbusFeatures.newtab.getVariable.reset();
      fakeNimbusFeatures.newtab.getVariable.onCall(0).returns(true);
      fakeNimbusFeatures.newtab.getVariable.onCall(1).returns(undefined);
      fetchStub.resolves({
        ok: true,
        status: 200,
        headers: new Map([
          ["cache-control", "private, max-age=859, stale-if-error=10463"],
        ]),
        json: () =>
          Promise.resolve({
            tiles: [
              {
                url: "https://www.test.com",
                image_url: "images/test-com.png",
                click_url: "https://www.test-click.com",
                impression_url: "https://www.test-impression.com",
                name: "test",
              },
              {
                url: "https://foo.com",
                image_url: "images/foo-com.png",
                click_url: "https://www.foo-click.com",
                impression_url: "https://www.foo-impression.com",
                name: "foo",
              },
              {
                url: "https://test1.com",
                image_url: "images/test1-com.png",
                click_url: "https://www.test1-click.com",
                impression_url: "https://www.test1-impression.com",
                name: "test1",
              },
            ],
          }),
      });

      const fetched = await feed._contile._fetchSites();

      assert.ok(fetched);
      assert.equal(feed._contile.sites.length, 2);
      assert.equal(feed._contile.sites[0].url, "https://www.test.com");
      assert.equal(feed._contile.sites[1].url, "https://test1.com");
    });

    it("should filter the blocked sponsors", async () => {
      fetchStub.resolves({
        ok: true,
        status: 200,
        headers: new Map([
          ["cache-control", "private, max-age=859, stale-if-error=10463"],
        ]),
        json: () =>
          Promise.resolve({
            tiles: [
              {
                url: "https://www.test.com",
                image_url: "images/test-com.png",
                click_url: "https://www.test-click.com",
                impression_url: "https://www.test-impression.com",
                name: "test",
              },
              {
                url: "https://foo.com",
                image_url: "images/foo-com.png",
                click_url: "https://www.foo-click.com",
                impression_url: "https://www.foo-impression.com",
                name: "foo",
              },
              {
                url: "https://bar.com",
                image_url: "images/bar-com.png",
                click_url: "https://www.bar-click.com",
                impression_url: "https://www.bar-impression.com",
                name: "bar",
              },
            ],
          }),
      });

      const fetched = await feed._contile._fetchSites();

      assert.ok(fetched);
      // Both "foo" and "bar" should be filtered
      assert.equal(feed._contile.sites.length, 1);
      assert.equal(feed._contile.sites[0].url, "https://www.test.com");
    });

    it("should return false when Contile returns with error status and no values are stored in cache prefs", async () => {
      fetchStub.resolves({
        ok: false,
        status: 500,
      });

      const fetched = await feed._contile._fetchSites();

      assert.ok(!fetched);
      assert.ok(!feed._contile.sites.length);
    });

    it("should return false when Contile returns with error status and cached tiles are expried", async () => {
      getIntPrefStub
        .withArgs(CONTILE_CACHE_VALID_FOR_PREF)
        .returns(1000 * 60 * 15);
      getIntPrefStub
        .withArgs(CONTILE_CACHE_LAST_FETCH_PREF)
        .returns(Date.now() - 1000 * 60 * 30);

      fetchStub.resolves({
        ok: false,
        status: 500,
      });

      const fetched = await feed._contile._fetchSites();

      assert.ok(!fetched);
      assert.ok(!feed._contile.sites.length);
    });

    it("should handle invalid payload properly from Contile", async () => {
      fetchStub.resolves({
        ok: true,
        status: 200,
        json: () =>
          Promise.resolve({
            unknown: [],
          }),
      });

      const fetched = await feed._contile._fetchSites();

      assert.ok(!fetched);
      assert.ok(!feed._contile.sites.length);
    });

    it("should handle empty payload properly from Contile", async () => {
      fetchStub.resolves({
        ok: true,
        status: 200,
        headers: new Map([
          ["cache-control", "private, max-age=859, stale-if-error=10463"],
        ]),
        json: () =>
          Promise.resolve({
            tiles: [],
          }),
      });

      const fetched = await feed._contile._fetchSites();

      assert.ok(fetched);
      assert.ok(!feed._contile.sites.length);
    });

    it("should handle no content properly from Contile", async () => {
      fetchStub.resolves({ ok: true, status: 204 });

      const fetched = await feed._contile._fetchSites();

      assert.ok(!fetched);
      assert.ok(!feed._contile.sites.length);
    });

    it("should set Caching Prefs after a sucessful request", async () => {
      const tiles = [
        {
          url: "https://www.test.com",
          image_url: "images/test-com.png",
          click_url: "https://www.test-click.com",
          impression_url: "https://www.test-impression.com",
          name: "test",
        },
        {
          url: "https://www.test1.com",
          image_url: "images/test1-com.png",
          click_url: "https://www.test1-click.com",
          impression_url: "https://www.test1-impression.com",
          name: "test1",
        },
      ];
      fetchStub.resolves({
        ok: true,
        status: 200,
        headers: new Map([
          ["cache-control", "private, max-age=859, stale-if-error=10463"],
        ]),
        json: () =>
          Promise.resolve({
            tiles,
          }),
      });

      const fetched = await feed._contile._fetchSites();
      assert.ok(fetched);
      assert.calledOnce(Services.prefs.setStringPref);
      assert.calledTwice(Services.prefs.setIntPref);

      assert.calledWith(
        Services.prefs.setStringPref,
        CONTILE_CACHE_PREF,
        JSON.stringify(tiles)
      );
      assert.calledWith(
        Services.prefs.setIntPref,
        CONTILE_CACHE_VALID_FOR_PREF,
        11322
      );
    });

    it("should return cached valid tiles when Contile returns error status", async () => {
      const tiles = [
        {
          url: "https://www.test-cached.com",
          image_url: "images/test-com.png",
          click_url: "https://www.test-click.com",
          impression_url: "https://www.test-impression.com",
          name: "test",
        },
        {
          url: "https://www.test1-cached.com",
          image_url: "images/test1-com.png",
          click_url: "https://www.test1-click.com",
          impression_url: "https://www.test1-impression.com",
          name: "test1",
        },
      ];

      getStringPrefStub
        .withArgs(CONTILE_CACHE_PREF)
        .returns(JSON.stringify(tiles));

      // valid for 15 mins
      getIntPrefStub
        .withArgs(CONTILE_CACHE_VALID_FOR_PREF)
        .returns(1000 * 60 * 15);
      getIntPrefStub
        .withArgs(CONTILE_CACHE_LAST_FETCH_PREF)
        .returns(Date.now());

      fetchStub.resolves({
        status: 304,
      });

      const fetched = await feed._contile._fetchSites();
      assert.ok(fetched);
      assert.equal(feed._contile.sites.length, 2);
      assert.equal(feed._contile.sites[0].url, "https://www.test-cached.com");
      assert.equal(feed._contile.sites[1].url, "https://www.test1-cached.com");
    });

    it("should not be successful when contile returns an error and no valid tiles are cached", async () => {
      getStringPrefStub.withArgs(CONTILE_CACHE_PREF).returns("[]");

      getIntPrefStub.withArgs(CONTILE_CACHE_VALID_FOR_PREF).returns(0);
      getIntPrefStub.withArgs(CONTILE_CACHE_LAST_FETCH_PREF).returns(0);

      fetchStub.resolves({
        status: 500,
      });

      const fetched = await feed._contile._fetchSites();
      assert.ok(!fetched);
    });

    it("should return cached valid tiles filtering blocked tiles when Contile returns error status", async () => {
      const tiles = [
        {
          url: "https://foo.com",
          image_url: "images/foo-com.png",
          click_url: "https://www.foo-click.com",
          impression_url: "https://www.foo-impression.com",
          name: "foo",
        },
        {
          url: "https://www.test1-cached.com",
          image_url: "images/test1-com.png",
          click_url: "https://www.test1-click.com",
          impression_url: "https://www.test1-impression.com",
          name: "test1",
        },
      ];
      getStringPrefStub
        .withArgs(CONTILE_CACHE_PREF)
        .returns(JSON.stringify(tiles));

      // valid for 15 mins
      getIntPrefStub
        .withArgs(CONTILE_CACHE_VALID_FOR_PREF)
        .returns(1000 * 60 * 15);
      getIntPrefStub
        .withArgs(CONTILE_CACHE_LAST_FETCH_PREF)
        .returns(Date.now());

      fetchStub.resolves({
        status: 304,
      });

      const fetched = await feed._contile._fetchSites();
      assert.ok(fetched);
      assert.equal(feed._contile.sites.length, 1);
      assert.equal(feed._contile.sites[0].url, "https://www.test1-cached.com");
    });

    it("should still return 3 tiles when nimbus variable overrides max num of sponsored contile tiles", async () => {
      fakeNimbusFeatures.pocketNewtab.getVariable.returns(3);

      fetchStub.resolves({
        ok: true,
        status: 200,
        headers: new Map([
          ["cache-control", "private, max-age=859, stale-if-error=10463"],
        ]),
        json: () =>
          Promise.resolve({
            tiles: [
              {
                url: "https://www.test.com",
                image_url: "images/test-com.png",
                click_url: "https://www.test-click.com",
                impression_url: "https://www.test-impression.com",
                name: "test",
              },
              {
                url: "https://test1.com",
                image_url: "images/test1-com.png",
                click_url: "https://www.test1-click.com",
                impression_url: "https://www.test1-impression.com",
                name: "test1",
              },
              {
                url: "https://test2.com",
                image_url: "images/test2-com.png",
                click_url: "https://www.test2-click.com",
                impression_url: "https://www.test2-impression.com",
                name: "test2",
              },
            ],
          }),
      });

      const fetched = await feed._contile._fetchSites();

      assert.ok(fetched);
      assert.equal(feed._contile.sites.length, 3);
      assert.equal(feed._contile.sites[0].url, "https://www.test.com");
      assert.equal(feed._contile.sites[1].url, "https://test1.com");
      assert.equal(feed._contile.sites[2].url, "https://test2.com");
    });
  });

  describe("#_mergeSponsoredLinks", () => {
    let fakeSponsoredLinks;
    let sov;
    beforeEach(() => {
      fakeSponsoredLinks = {
        amp: [
          {
            url: "https://www.test.com",
            image_url: "images/test-com.png",
            click_url: "https://www.test-click.com",
            impression_url: "https://www.test-impression.com",
            name: "test",
            partner: "amp",
            sponsored_position: 1,
          },
          {
            url: "https://www.test1.com",
            image_url: "images/test1-com.png",
            click_url: "https://www.test1-click.com",
            impression_url: "https://www.test1-impression.com",
            name: "test1",
            partner: "amp",
            sponsored_position: 2,
          },
          {
            url: "https://www.test2.com",
            image_url: "images/test2-com.png",
            click_url: "https://www.test2-click.com",
            impression_url: "https://www.test2-impression.com",
            name: "test2",
            partner: "amp",
            sponsored_position: 2,
          },
        ],
        "moz-sales": [
          {
            url: "https://foo.com",
            image_url: "images/foo-com.png",
            click_url: "https://www.foo-click.com",
            impression_url: "https://www.foo-impression.com",
            name: "foo",
            partner: "moz-sales",
            pos: 2,
          },
        ],
      };

      sov = {
        name: "SOV-20230518215316",
        allocations: [
          {
            position: 1,
            allocation: [
              {
                partner: "amp",
                percentage: 100,
              },
              {
                partner: "moz-sales",
                percentage: 0,
              },
            ],
          },
          {
            position: 2,
            allocation: [
              {
                partner: "amp",
                percentage: 80,
              },
              {
                partner: "moz-sales",
                percentage: 20,
              },
            ],
          },
        ],
      };
    });
    afterEach(() => {
      sandbox.restore();
    });

    it("should join sponsored links if the sov object is absent", async () => {
      sandbox.stub(feed._contile, "sov").get(() => null);

      const sponsored = await feed._mergeSponsoredLinks(fakeSponsoredLinks);

      assert.deepEqual(sponsored, Object.values(fakeSponsoredLinks).flat());
    });

    it("should join sponosred links if the SOV Nimbus variable is disabled", async () => {
      fakeNimbusFeatures.pocketNewtab.getVariable.returns(false);
      const sponsored = await feed._mergeSponsoredLinks(fakeSponsoredLinks);

      assert.deepEqual(sponsored, Object.values(fakeSponsoredLinks).flat());
    });

    it("should pick sponsored links based on sov configurations", async () => {
      sandbox.stub(feed._contile, "sov").get(() => sov);
      fakeNimbusFeatures.pocketNewtab.getVariable.reset();
      fakeNimbusFeatures.pocketNewtab.getVariable.onCall(0).returns(true);
      fakeNimbusFeatures.pocketNewtab.getVariable.onCall(1).returns(undefined);
      global.Sampling.ratioSample.onCall(0).resolves(0);
      global.Sampling.ratioSample.onCall(1).resolves(1);

      const sponsored = await feed._mergeSponsoredLinks(fakeSponsoredLinks);

      assert.equal(sponsored.length, 2);
      assert.equal(sponsored[0].partner, "amp");
      assert.equal(sponsored[0].sponsored_position, 1);
      assert.equal(sponsored[1].partner, "moz-sales");
      assert.equal(sponsored[1].sponsored_position, 2);
      assert.equal(sponsored[1].pos, 1);
    });

    it("should have glean set sov partners", async () => {
      sandbox.stub(feed._contile, "sov").get(() => sov);
      fakeNimbusFeatures.pocketNewtab.getVariable.reset();
      fakeNimbusFeatures.pocketNewtab.getVariable.onCall(0).returns(true);
      fakeNimbusFeatures.pocketNewtab.getVariable.onCall(1).returns(undefined);
      global.Sampling.ratioSample.onCall(0).resolves(0);
      global.Sampling.ratioSample.onCall(1).resolves(1);
      sandbox.spy(Glean.newtab.sovAllocation, "set");

      await feed._mergeSponsoredLinks(fakeSponsoredLinks);

      assert.calledOnce(Glean.newtab.sovAllocation.set);
      assert.calledWith(Glean.newtab.sovAllocation.set, [
        '{"pos":1,"assigned":"amp","chosen":"amp"}',
        '{"pos":2,"assigned":"moz-sales","chosen":"moz-sales"}',
      ]);
    });

    it("should have glean set sov partners even with one assignment", async () => {
      const oneSov = {
        name: "SOV-20230518215316",
        allocations: [
          {
            position: 1,
            allocation: [
              {
                partner: "amp",
                percentage: 100,
              },
              {
                partner: "moz-sales",
                percentage: 0,
              },
            ],
          },
        ],
      };
      sandbox.stub(feed._contile, "sov").get(() => oneSov);
      fakeNimbusFeatures.pocketNewtab.getVariable.reset();
      fakeNimbusFeatures.pocketNewtab.getVariable.onCall(0).returns(true);
      fakeNimbusFeatures.pocketNewtab.getVariable.onCall(1).returns(undefined);
      global.Sampling.ratioSample.onCall(0).resolves(0);
      sandbox.spy(Glean.newtab.sovAllocation, "set");

      await feed._mergeSponsoredLinks(fakeSponsoredLinks);

      assert.calledOnce(Glean.newtab.sovAllocation.set);
      assert.calledWith(Glean.newtab.sovAllocation.set, [
        '{"pos":1,"assigned":"amp","chosen":"amp"}',
      ]);
    });

    it("should add remaining contile tiles when nimbus var contile max num sponsored is present", async () => {
      sandbox.stub(feed._contile, "sov").get(() => sov);
      fakeNimbusFeatures.pocketNewtab.getVariable.reset();
      fakeNimbusFeatures.pocketNewtab.getVariable.onCall(0).returns(true);
      fakeNimbusFeatures.pocketNewtab.getVariable.onCall(1).returns(3);
      global.Sampling.ratioSample.resolves(0);

      const sponsored = await feed._mergeSponsoredLinks(fakeSponsoredLinks);

      assert.equal(sponsored.length, 3);
    });

    it("should fall back to other partners if the chosen partner does not have any links", async () => {
      sandbox.stub(feed._contile, "sov").get(() => sov);
      fakeNimbusFeatures.pocketNewtab.getVariable.returns(true);
      global.Sampling.ratioSample.onCall(0).resolves(0);
      global.Sampling.ratioSample.onCall(1).resolves(0);

      fakeSponsoredLinks.amp = [];
      const sponsored = await feed._mergeSponsoredLinks(fakeSponsoredLinks);

      assert.equal(sponsored.length, 1);
      assert.equal(sponsored[0].partner, "moz-sales");
      assert.equal(sponsored[0].sponsored_position, 1);
      assert.equal(sponsored[0].pos, 0);
    });

    it("should return an empty array if none of the partners have links", async () => {
      sandbox.stub(feed._contile, "sov").get(() => sov);
      fakeNimbusFeatures.pocketNewtab.getVariable.returns(true);
      global.Sampling.ratioSample.onCall(0).resolves(0);
      global.Sampling.ratioSample.onCall(1).resolves(0);

      fakeSponsoredLinks.amp = [];
      fakeSponsoredLinks["moz-sales"] = [];
      const sponsored = await feed._mergeSponsoredLinks(fakeSponsoredLinks);

      assert.equal(sponsored.length, 0);
    });

    it("should ignore empty slots from AMP links if present", async () => {
      sandbox.stub(feed._contile, "sov").get(() => sov);
      fakeNimbusFeatures.pocketNewtab.getVariable.reset();
      fakeNimbusFeatures.pocketNewtab.getVariable.onCall(0).returns(true);
      fakeNimbusFeatures.pocketNewtab.getVariable.onCall(1).returns(undefined);
      global.Sampling.ratioSample.onCall(0).resolves(0);
      global.Sampling.ratioSample.onCall(1).resolves(1);

      // Add a few empty slots to AMP links.
      fakeSponsoredLinks.amp.unshift(undefined);
      fakeSponsoredLinks.amp.unshift(undefined);
      const sponsored = await feed._mergeSponsoredLinks(fakeSponsoredLinks);

      assert.equal(sponsored.length, 2);
      assert.equal(sponsored[0].partner, "amp");
      assert.equal(sponsored[0].sponsored_position, 1);
      assert.equal(sponsored[1].partner, "moz-sales");
      assert.equal(sponsored[1].sponsored_position, 2);
      assert.equal(sponsored[1].pos, 1);
    });
  });

  describe("#_readDefaults", () => {
    beforeEach(() => {
      // Turn on sponsored TopSites for testing
      feed.store.state.Prefs.values[SHOW_SPONSORED_PREF] = true;
      fetchStub = sandbox.stub();
      globals.set("fetch", fetchStub);
      fetchStub.resolves({ ok: true, status: 204 });
      sandbox
        .stub(global.Services.prefs, "getBoolPref")
        .withArgs(REMOTE_SETTING_DEFAULTS_PREF)
        .returns(true);

      sandbox
        .stub(global.Services.prefs, "getStringPref")
        .withArgs(TOP_SITES_BLOCKED_SPONSORS_PREF)
        .returns(`["foo","bar"]`);
      sandbox.stub(global.Services.prefs, "prefIsLocked").returns(false);
    });
    afterEach(() => {
      sandbox.restore();
    });

    it("should filter all blocked sponsored tiles from RemoteSettings when Contile is disabled", async () => {
      sandbox.stub(feed, "_getRemoteConfig").resolves([
        { url: "https://foo.com", title: "foo", sponsored_position: 1 },
        { url: "https://bar.com", title: "bar", sponsored_position: 2 },
        { url: "https://test.com", title: "test", sponsored_position: 3 },
      ]);
      fakeNimbusFeatures.newtab.getVariable.returns(false);
      await feed._readDefaults();

      assert.equal(DEFAULT_TOP_SITES.length, 1);
      assert.equal(DEFAULT_TOP_SITES[0].label, "test");
    });

    it("should also filter all blocked sponsored tiles from RemoteSettings when Contile is enabled", async () => {
      sandbox.stub(feed, "_getRemoteConfig").resolves([
        { url: "https://foo.com", title: "foo", sponsored_position: 1 },
        { url: "https://bar.com", title: "bar", sponsored_position: 2 },
        { url: "https://test.com", title: "test", sponsored_position: 3 },
      ]);
      fakeNimbusFeatures.newtab.getVariable.returns(true);

      await feed._readDefaults();

      assert.equal(DEFAULT_TOP_SITES.length, 1);
      assert.equal(DEFAULT_TOP_SITES[0].label, "test");
    });

    it("should not filter non-sponsored tiles from RemoteSettings", async () => {
      sandbox.stub(feed, "_getRemoteConfig").resolves([
        { url: "https://foo.com", title: "foo", sponsored_position: 1 },
        { url: "https://bar.com", title: "bar", sponsored_position: 2 },
        { url: "https://foo.com", title: "foo" },
      ]);

      await feed._readDefaults();

      assert.equal(DEFAULT_TOP_SITES.length, 1);
      assert.equal(DEFAULT_TOP_SITES[0].label, "foo");
    });

    it("should take the image from Contile if it's a hi-res one", async () => {
      fakeNimbusFeatures.newtab.getVariable.returns(true);
      sandbox.stub(feed, "_getRemoteConfig").resolves([]);

      sandbox.stub(feed._contile, "sites").get(() => [
        {
          url: "https://test.com",
          image_url: "https://images.test.com/test-com.png",
          image_size: 192,
          click_url: "https://www.test-click.com",
          impression_url: "https://www.test-impression.com",
          name: "test",
        },
        {
          url: "https://test1.com",
          image_url: "https://images.test1.com/test1-com.png",
          image_size: 32,
          click_url: "https://www.test1-click.com",
          impression_url: "https://www.test1-impression.com",
          name: "test1",
        },
      ]);

      await feed._readDefaults();

      const [site1, site2] = DEFAULT_TOP_SITES;
      assert.propertyVal(
        site1,
        "favicon",
        "https://images.test.com/test-com.png"
      );
      assert.propertyVal(site1, "faviconSize", 192);

      // Should not be taken as it's not hi-res
      assert.isUndefined(site2.favicon);
      assert.isUndefined(site2.faviconSize);
    });
  });

  describe("#_nimbusChangeListener", () => {
    it("should refresh on Nimbus feature updates reasons", () => {
      sandbox.spy(feed._contile, "refresh");
      feed._nimbusChangeListener(null, "experiment-updated");

      assert.calledOnce(feed._contile.refresh);
    });

    it("should not refresh on Nimbus feature loaded reasons", () => {
      sandbox.spy(feed._contile, "refresh");
      feed._nimbusChangeListener(null, "feature-experiment-loaded");
      feed._nimbusChangeListener(null, "feature-rollout-loaded");

      assert.notCalled(feed._contile.refresh);
    });
  });

  describe("#_maybeCapSponsoredLinks", () => {
    let sponsoredLinks;

    beforeEach(() => {
      sponsoredLinks = [
        {
          url: "https://www.test.com",
          name: "test",
          sponsored_position: 1,
        },
        {
          url: "https://www.test1.com",
          name: "test1",
          sponsored_position: 2,
        },
        {
          url: "https://www.test2.com",
          name: "test2",
          sponsored_position: 3,
        },
      ];
    });
    afterEach(() => {
      sandbox.restore();
    });

    it("should fall back to the default if the Nimbus variable is unspecified", () => {
      feed._maybeCapSponsoredLinks(sponsoredLinks);

      assert.equal(sponsoredLinks.length, 2);
    });
    it("should cap the links if specified by the Nimbus variable", () => {
      fakeNimbusFeatures.pocketNewtab.getVariable.returns(1);

      feed._maybeCapSponsoredLinks(sponsoredLinks);

      assert.equal(sponsoredLinks.length, 1);
    });
    it("should leave all the links if the Nimbus variable is equal to what we have", () => {
      fakeNimbusFeatures.pocketNewtab.getVariable.returns(3);

      feed._maybeCapSponsoredLinks(sponsoredLinks);

      assert.equal(sponsoredLinks.length, 3);
    });
    it("should ignore caps if they are more than what we have", () => {
      fakeNimbusFeatures.pocketNewtab.getVariable.returns(10);

      feed._maybeCapSponsoredLinks(sponsoredLinks);

      assert.equal(sponsoredLinks.length, 3);
    });
  });
});
