import {
  actionCreators as ac,
  actionTypes as at,
  actionUtils as au,
} from "common/Actions.sys.mjs";
import { combineReducers, createStore } from "redux";
import { GlobalOverrider } from "test/unit/utils";
import { DiscoveryStreamFeed } from "lib/DiscoveryStreamFeed.jsm";
import { RecommendationProvider } from "lib/RecommendationProvider.jsm";
import { reducers } from "common/Reducers.sys.mjs";

import { PersistentCache } from "lib/PersistentCache.sys.mjs";
import { PersonalityProvider } from "lib/PersonalityProvider/PersonalityProvider.jsm";

const CONFIG_PREF_NAME = "discoverystream.config";
const DUMMY_ENDPOINT = "https://getpocket.cdn.mozilla.net/dummy";
const ENDPOINTS_PREF_NAME = "discoverystream.endpoints";
const SPOC_IMPRESSION_TRACKING_PREF = "discoverystream.spoc.impressions";
const REC_IMPRESSION_TRACKING_PREF = "discoverystream.rec.impressions";
const THIRTY_MINUTES = 30 * 60 * 1000;
const ONE_WEEK = 7 * 24 * 60 * 60 * 1000; // 1 week

const FAKE_UUID = "{foo-123-foo}";

// eslint-disable-next-line max-statements
describe("DiscoveryStreamFeed", () => {
  let feed;
  let feeds;
  let recommendationProvider;
  let sandbox;
  let fetchStub;
  let clock;
  let fakeNewTabUtils;
  let fakePktApi;
  let globals;

  const setPref = (name, value) => {
    const action = {
      type: at.PREF_CHANGED,
      data: {
        name,
        value: typeof value === "object" ? JSON.stringify(value) : value,
      },
    };
    feed.store.dispatch(action);
    feed.onAction(action);
  };

  beforeEach(() => {
    sandbox = sinon.createSandbox();

    // Fetch
    fetchStub = sandbox.stub(global, "fetch");

    // Time
    clock = sinon.useFakeTimers();

    globals = new GlobalOverrider();
    globals.set({
      gUUIDGenerator: { generateUUID: () => FAKE_UUID },
      PersistentCache,
      PersonalityProvider,
    });

    sandbox
      .stub(global.Services.prefs, "getBoolPref")
      .withArgs("browser.newtabpage.activity-stream.discoverystream.enabled")
      .returns(true);

    recommendationProvider = new RecommendationProvider();
    recommendationProvider.store = createStore(combineReducers(reducers), {});
    feeds = {
      "feeds.recommendationprovider": recommendationProvider,
    };

    // Feed
    feed = new DiscoveryStreamFeed();
    feed.store = createStore(combineReducers(reducers), {
      Prefs: {
        values: {
          [CONFIG_PREF_NAME]: JSON.stringify({
            enabled: false,
            show_spocs: false,
            layout_endpoint: DUMMY_ENDPOINT,
          }),
          [ENDPOINTS_PREF_NAME]: DUMMY_ENDPOINT,
          "discoverystream.enabled": true,
          "feeds.section.topstories": true,
          "feeds.system.topstories": true,
          "discoverystream.spocs.personalized": true,
          "discoverystream.recs.personalized": true,
        },
      },
    });
    feed.store.feeds = {
      get: name => feeds[name],
    };
    global.fetch.resetHistory();

    sandbox.stub(feed, "_maybeUpdateCachedData").resolves();

    globals.set("setTimeout", callback => {
      callback();
    });

    fakeNewTabUtils = {
      blockedLinks: {
        links: [],
        isBlocked: () => false,
      },
    };
    globals.set("NewTabUtils", fakeNewTabUtils);

    fakePktApi = {
      isUserLoggedIn: () => false,
      getRecentSavesCache: () => null,
      getRecentSaves: () => null,
    };
    globals.set("pktApi", fakePktApi);
  });

  afterEach(() => {
    clock.restore();
    sandbox.restore();
    globals.restore();
  });

  describe("#fetchFromEndpoint", () => {
    beforeEach(() => {
      feed._prefCache = {
        config: {
          api_key_pref: "",
        },
      };
      fetchStub.resolves({
        json: () => Promise.resolve("hi"),
        ok: true,
      });
    });
    it("should get a response", async () => {
      const response = await feed.fetchFromEndpoint(DUMMY_ENDPOINT);

      assert.equal(response, "hi");
    });
    it("should not send cookies", async () => {
      await feed.fetchFromEndpoint(DUMMY_ENDPOINT);

      assert.propertyVal(fetchStub.firstCall.args[1], "credentials", "omit");
    });
    it("should allow unexpected response", async () => {
      fetchStub.resolves({ ok: false });

      const response = await feed.fetchFromEndpoint(DUMMY_ENDPOINT);

      assert.equal(response, null);
    });
    it("should disallow unexpected endpoints", async () => {
      feed.store.getState = () => ({
        Prefs: { values: { [ENDPOINTS_PREF_NAME]: "https://other.site" } },
      });

      const response = await feed.fetchFromEndpoint(DUMMY_ENDPOINT);

      assert.equal(response, null);
    });
    it("should allow multiple endpoints", async () => {
      feed.store.getState = () => ({
        Prefs: {
          values: {
            [ENDPOINTS_PREF_NAME]: `https://other.site,${DUMMY_ENDPOINT}`,
          },
        },
      });

      const response = await feed.fetchFromEndpoint(DUMMY_ENDPOINT);

      assert.equal(response, "hi");
    });
    it("should replace urls with $apiKey", async () => {
      sandbox.stub(global.Services.prefs, "getCharPref").returns("replaced");

      await feed.fetchFromEndpoint(
        "https://getpocket.cdn.mozilla.net/dummy?consumer_key=$apiKey"
      );

      assert.calledWithMatch(
        fetchStub,
        "https://getpocket.cdn.mozilla.net/dummy?consumer_key=replaced",
        { credentials: "omit" }
      );
    });
    it("should replace locales with $locale", async () => {
      feed.locale = "replaced";
      await feed.fetchFromEndpoint(
        "https://getpocket.cdn.mozilla.net/dummy?locale_lang=$locale"
      );

      assert.calledWithMatch(
        fetchStub,
        "https://getpocket.cdn.mozilla.net/dummy?locale_lang=replaced",
        { credentials: "omit" }
      );
    });
    it("should allow POST and with other options", async () => {
      await feed.fetchFromEndpoint("https://getpocket.cdn.mozilla.net/dummy", {
        method: "POST",
        body: "{}",
      });

      assert.calledWithMatch(
        fetchStub,
        "https://getpocket.cdn.mozilla.net/dummy",
        {
          credentials: "omit",
          method: "POST",
          body: "{}",
        }
      );
    });
  });

  describe("#setupPocketState", () => {
    it("should setup logged in state and recent saves with cache", async () => {
      fakePktApi.isUserLoggedIn = () => true;
      fakePktApi.getRecentSavesCache = () => [1, 2, 3];
      sandbox.spy(feed.store, "dispatch");
      await feed.setupPocketState({});
      assert.calledTwice(feed.store.dispatch);
      assert.calledWith(
        feed.store.dispatch.firstCall,
        ac.OnlyToOneContent(
          {
            type: at.DISCOVERY_STREAM_POCKET_STATE_SET,
            data: { isUserLoggedIn: true },
          },
          {}
        )
      );
      assert.calledWith(
        feed.store.dispatch.secondCall,
        ac.OnlyToOneContent(
          {
            type: at.DISCOVERY_STREAM_RECENT_SAVES,
            data: { recentSaves: [1, 2, 3] },
          },
          {}
        )
      );
    });
    it("should setup logged in state and recent saves without cache", async () => {
      fakePktApi.isUserLoggedIn = () => true;
      fakePktApi.getRecentSaves = ({ success }) => success([1, 2, 3]);
      sandbox.spy(feed.store, "dispatch");
      await feed.setupPocketState({});
      assert.calledTwice(feed.store.dispatch);
      assert.calledWith(
        feed.store.dispatch.firstCall,
        ac.OnlyToOneContent(
          {
            type: at.DISCOVERY_STREAM_POCKET_STATE_SET,
            data: { isUserLoggedIn: true },
          },
          {}
        )
      );
      assert.calledWith(
        feed.store.dispatch.secondCall,
        ac.OnlyToOneContent(
          {
            type: at.DISCOVERY_STREAM_RECENT_SAVES,
            data: { recentSaves: [1, 2, 3] },
          },
          {}
        )
      );
    });
  });

  describe("#getOrCreateImpressionId", () => {
    it("should create impression id in constructor", async () => {
      assert.equal(feed._impressionId, FAKE_UUID);
    });
    it("should create impression id if none exists", async () => {
      sandbox.stub(global.Services.prefs, "getCharPref").returns("");
      sandbox.stub(global.Services.prefs, "setCharPref").returns();

      const result = feed.getOrCreateImpressionId();

      assert.equal(result, FAKE_UUID);
      assert.calledOnce(global.Services.prefs.setCharPref);
    });
    it("should use impression id if exists", async () => {
      sandbox.stub(global.Services.prefs, "getCharPref").returns("from get");

      const result = feed.getOrCreateImpressionId();

      assert.equal(result, "from get");
      assert.calledOnce(global.Services.prefs.getCharPref);
    });
  });

  describe("#parseGridPositions", () => {
    it("should return an equivalent array for an array of non negative integers", async () => {
      assert.deepEqual(feed.parseGridPositions([0, 2, 3]), [0, 2, 3]);
    });
    it("should return undefined for an array containing negative integers", async () => {
      assert.equal(feed.parseGridPositions([-2, 2, 3]), undefined);
    });
    it("should return undefined for an undefined input", async () => {
      assert.equal(feed.parseGridPositions(undefined), undefined);
    });
  });

  describe("#loadLayout", () => {
    it("should fetch data and populate the cache if it is empty", async () => {
      const resp = { layout: ["foo", "bar"] };
      const fakeCache = {};
      sandbox.stub(feed.cache, "get").returns(Promise.resolve(fakeCache));
      sandbox.stub(feed.cache, "set").returns(Promise.resolve());

      fetchStub.resolves({ ok: true, json: () => Promise.resolve(resp) });

      await feed.loadLayout(feed.store.dispatch);

      assert.calledOnce(fetchStub);
      assert.equal(feed.cache.set.firstCall.args[0], "layout");
      assert.deepEqual(feed.cache.set.firstCall.args[1].layout, resp.layout);
    });
    it("should fetch data and populate the cache if the cached data is older than 30 mins", async () => {
      const resp = { layout: ["foo", "bar"] };
      const fakeCache = {
        layout: { layout: ["hello"], lastUpdated: Date.now() },
      };

      sandbox.stub(feed.cache, "get").returns(Promise.resolve(fakeCache));
      sandbox.stub(feed.cache, "set").returns(Promise.resolve());

      fetchStub.resolves({ ok: true, json: () => Promise.resolve(resp) });

      clock.tick(THIRTY_MINUTES + 1);
      await feed.loadLayout(feed.store.dispatch);

      assert.calledOnce(fetchStub);
      assert.equal(feed.cache.set.firstCall.args[0], "layout");
      assert.deepEqual(feed.cache.set.firstCall.args[1].layout, resp.layout);
    });
    it("should use the cached data and not fetch if the cached data is less than 30 mins old", async () => {
      const fakeCache = {
        layout: { layout: ["hello"], lastUpdated: Date.now() },
      };

      sandbox.stub(feed.cache, "get").returns(Promise.resolve(fakeCache));
      sandbox.stub(feed.cache, "set").returns(Promise.resolve());

      clock.tick(THIRTY_MINUTES - 1);
      await feed.loadLayout(feed.store.dispatch);

      assert.notCalled(fetchStub);
      assert.notCalled(feed.cache.set);
    });
    it("should set spocs_endpoint from layout", async () => {
      const resp = { layout: ["foo", "bar"], spocs: { url: "foo.com" } };
      const fakeCache = {};
      sandbox.stub(feed.cache, "get").returns(Promise.resolve(fakeCache));
      sandbox.stub(feed.cache, "set").returns(Promise.resolve());

      fetchStub.resolves({ ok: true, json: () => Promise.resolve(resp) });

      await feed.loadLayout(feed.store.dispatch);

      assert.equal(
        feed.store.getState().DiscoveryStream.spocs.spocs_endpoint,
        "foo.com"
      );
    });
    it("should use local layout with hardcoded_layout being true", async () => {
      feed.config.hardcoded_layout = true;
      sandbox.stub(feed, "fetchLayout").returns(Promise.resolve(""));

      await feed.loadLayout(feed.store.dispatch);

      assert.notCalled(feed.fetchLayout);
      assert.equal(
        feed.store.getState().DiscoveryStream.spocs.spocs_endpoint,
        "https://spocs.getpocket.com/spocs"
      );
    });
    it("should use local basic layout with hardcoded_layout and hardcoded_basic_layout being true", async () => {
      feed.config.hardcoded_layout = true;
      feed.config.hardcoded_basic_layout = true;
      sandbox.stub(feed, "fetchLayout").returns(Promise.resolve(""));

      await feed.loadLayout(feed.store.dispatch);

      assert.notCalled(feed.fetchLayout);
      assert.equal(
        feed.store.getState().DiscoveryStream.spocs.spocs_endpoint,
        "https://spocs.getpocket.com/spocs"
      );
      const { layout } = feed.store.getState().DiscoveryStream;
      assert.equal(layout[0].components[2].properties.items, 3);
    });
    it("should use 1 row layout if specified", async () => {
      feed.config.hardcoded_layout = true;
      feed.store = createStore(combineReducers(reducers), {
        Prefs: {
          values: {
            [CONFIG_PREF_NAME]: JSON.stringify({
              enabled: true,
              show_spocs: false,
              layout_endpoint: DUMMY_ENDPOINT,
            }),
            [ENDPOINTS_PREF_NAME]: DUMMY_ENDPOINT,
            "discoverystream.enabled": true,
            "discoverystream.region-basic-layout": true,
          },
        },
      });
      sandbox.stub(feed, "fetchLayout").returns(Promise.resolve(""));

      await feed.loadLayout(feed.store.dispatch);

      const { layout } = feed.store.getState().DiscoveryStream;
      assert.equal(layout[0].components[2].properties.items, 3);
    });
    it("should use 7 row layout if specified", async () => {
      feed.config.hardcoded_layout = true;
      feed.store = createStore(combineReducers(reducers), {
        Prefs: {
          values: {
            [CONFIG_PREF_NAME]: JSON.stringify({
              enabled: true,
              show_spocs: false,
              layout_endpoint: DUMMY_ENDPOINT,
            }),
            [ENDPOINTS_PREF_NAME]: DUMMY_ENDPOINT,
            "discoverystream.enabled": true,
            "discoverystream.region-basic-layout": false,
          },
        },
      });
      sandbox.stub(feed, "fetchLayout").returns(Promise.resolve(""));

      await feed.loadLayout(feed.store.dispatch);

      const { layout } = feed.store.getState().DiscoveryStream;
      assert.equal(layout[0].components[2].properties.items, 21);
    });
    it("should use new spocs endpoint if in the config", async () => {
      feed.config.spocs_endpoint = "https://spocs.getpocket.com/spocs2";

      await feed.loadLayout(feed.store.dispatch);

      assert.equal(
        feed.store.getState().DiscoveryStream.spocs.spocs_endpoint,
        "https://spocs.getpocket.com/spocs2"
      );
    });
    it("should use local basic layout with hardcoded_layout and FF pref hardcoded_basic_layout", async () => {
      feed.config.hardcoded_layout = true;
      feed.store = createStore(combineReducers(reducers), {
        Prefs: {
          values: {
            [CONFIG_PREF_NAME]: JSON.stringify({
              enabled: false,
              show_spocs: false,
              layout_endpoint: DUMMY_ENDPOINT,
            }),
            [ENDPOINTS_PREF_NAME]: DUMMY_ENDPOINT,
            "discoverystream.enabled": true,
            "discoverystream.hardcoded-basic-layout": true,
          },
        },
      });

      sandbox.stub(feed, "fetchLayout").returns(Promise.resolve(""));

      await feed.loadLayout(feed.store.dispatch);

      assert.notCalled(feed.fetchLayout);
      assert.equal(
        feed.store.getState().DiscoveryStream.spocs.spocs_endpoint,
        "https://spocs.getpocket.com/spocs"
      );
      const { layout } = feed.store.getState().DiscoveryStream;
      assert.equal(layout[0].components[2].properties.items, 3);
    });
    it("should use new spocs endpoint if in a FF pref", async () => {
      feed.store = createStore(combineReducers(reducers), {
        Prefs: {
          values: {
            [CONFIG_PREF_NAME]: JSON.stringify({
              enabled: false,
              show_spocs: false,
              layout_endpoint: DUMMY_ENDPOINT,
            }),
            [ENDPOINTS_PREF_NAME]: DUMMY_ENDPOINT,
            "discoverystream.enabled": true,
            "discoverystream.spocs-endpoint":
              "https://spocs.getpocket.com/spocs2",
          },
        },
      });

      await feed.loadLayout(feed.store.dispatch);

      assert.equal(
        feed.store.getState().DiscoveryStream.spocs.spocs_endpoint,
        "https://spocs.getpocket.com/spocs2"
      );
    });
    it("should fetch local layout for invalid layout endpoint or when fetch layout fails", async () => {
      feed.config.hardcoded_layout = false;
      fetchStub.resolves({ ok: false });

      await feed.loadLayout(feed.store.dispatch, true);

      assert.calledOnce(fetchStub);
      assert.equal(
        feed.store.getState().DiscoveryStream.spocs.spocs_endpoint,
        "https://spocs.getpocket.com/spocs"
      );
    });
    it("should return enough stories to fill a four card layout", async () => {
      feed.config.hardcoded_layout = true;

      feed.store = createStore(combineReducers(reducers), {
        Prefs: {
          values: {
            pocketConfig: { fourCardLayout: true },
          },
        },
      });

      sandbox.stub(feed, "fetchLayout").returns(Promise.resolve(""));

      await feed.loadLayout(feed.store.dispatch);

      const { layout } = feed.store.getState().DiscoveryStream;
      assert.equal(layout[0].components[2].properties.items, 24);
    });
    it("should create a layout with spoc and widget positions", async () => {
      feed.config.hardcoded_layout = true;
      feed.store = createStore(combineReducers(reducers), {
        Prefs: {
          values: {
            pocketConfig: {
              spocPositions: "1, 2",
              widgetPositions: "3, 4",
            },
          },
        },
      });

      await feed.loadLayout(feed.store.dispatch);

      const { layout } = feed.store.getState().DiscoveryStream;
      assert.deepEqual(layout[0].components[2].spocs.positions, [
        { index: 1 },
        { index: 2 },
      ]);
      assert.deepEqual(layout[0].components[2].widgets.positions, [
        { index: 3 },
        { index: 4 },
      ]);
    });
    it("should create a layout with spoc position data", async () => {
      feed.config.hardcoded_layout = true;
      feed.store = createStore(combineReducers(reducers), {
        Prefs: {
          values: {
            pocketConfig: {
              spocAdTypes: "1230",
              spocZoneIds: "4560, 7890",
            },
          },
        },
      });

      await feed.loadLayout(feed.store.dispatch);

      const { layout } = feed.store.getState().DiscoveryStream;
      assert.deepEqual(layout[0].components[2].placement.ad_types, [1230]);
      assert.deepEqual(
        layout[0].components[2].placement.zone_ids,
        [4560, 7890]
      );
    });
    it("should create a layout with spoc topsite position data", async () => {
      feed.config.hardcoded_layout = true;
      feed.store = createStore(combineReducers(reducers), {
        Prefs: {
          values: {
            pocketConfig: {
              spocTopsitesAdTypes: "1230",
              spocTopsitesZoneIds: "4560, 7890",
            },
          },
        },
      });

      await feed.loadLayout(feed.store.dispatch);

      const { layout } = feed.store.getState().DiscoveryStream;
      assert.deepEqual(layout[0].components[0].placement.ad_types, [1230]);
      assert.deepEqual(
        layout[0].components[0].placement.zone_ids,
        [4560, 7890]
      );
    });
    it("should create a layout with proper spoc url with a site id", async () => {
      feed.config.hardcoded_layout = true;
      feed.store = createStore(combineReducers(reducers), {
        Prefs: {
          values: {
            pocketConfig: {
              spocSiteId: "1234",
            },
          },
        },
      });

      await feed.loadLayout(feed.store.dispatch);
      const { spocs } = feed.store.getState().DiscoveryStream;
      assert.deepEqual(
        spocs.spocs_endpoint,
        "https://spocs.getpocket.com/spocs?site=1234"
      );
    });
  });

  describe("#updatePlacements", () => {
    it("should dispatch DISCOVERY_STREAM_SPOCS_PLACEMENTS", () => {
      sandbox.spy(feed.store, "dispatch");
      feed.store.getState = () => ({
        Prefs: { values: { showSponsored: true } },
      });
      Object.defineProperty(feed, "config", {
        get: () => ({ show_spocs: true }),
      });
      const fakeComponents = {
        components: [
          { placement: { name: "first" }, spocs: {} },
          { placement: { name: "second" }, spocs: {} },
        ],
      };
      const fakeLayout = [fakeComponents];

      feed.updatePlacements(feed.store.dispatch, fakeLayout);

      assert.calledOnce(feed.store.dispatch);
      assert.calledWith(feed.store.dispatch, {
        type: "DISCOVERY_STREAM_SPOCS_PLACEMENTS",
        data: { placements: [{ name: "first" }, { name: "second" }] },
        meta: { isStartup: false },
      });
    });
    it("should dispatch DISCOVERY_STREAM_SPOCS_PLACEMENTS with prefs array", () => {
      sandbox.spy(feed.store, "dispatch");
      feed.store.getState = () => ({
        Prefs: { values: { showSponsored: true, withPref: true } },
      });
      Object.defineProperty(feed, "config", {
        get: () => ({ show_spocs: true }),
      });
      const fakeComponents = {
        components: [
          { placement: { name: "withPref" }, spocs: { prefs: ["withPref"] } },
          { placement: { name: "withoutPref1" }, spocs: {} },
          {
            placement: { name: "withoutPref2" },
            spocs: { prefs: ["whatever"] },
          },
          { placement: { name: "withoutPref3" }, spocs: { prefs: [] } },
        ],
      };
      const fakeLayout = [fakeComponents];

      feed.updatePlacements(feed.store.dispatch, fakeLayout);

      assert.calledOnce(feed.store.dispatch);
      assert.calledWith(feed.store.dispatch, {
        type: "DISCOVERY_STREAM_SPOCS_PLACEMENTS",
        data: { placements: [{ name: "withPref" }, { name: "withoutPref1" }] },
        meta: { isStartup: false },
      });
    });
    it("should fire update placements from loadLayout", async () => {
      sandbox.spy(feed, "updatePlacements");

      await feed.loadLayout(feed.store.dispatch);

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

  describe("#placementsForEach", () => {
    it("should forEach through placements", () => {
      feed.store.getState = () => ({
        DiscoveryStream: {
          spocs: {
            placements: [{ name: "first" }, { name: "second" }],
          },
        },
      });

      let items = [];

      feed.placementsForEach(item => items.push(item.name));

      assert.deepEqual(items, ["first", "second"]);
    });
  });

  describe("#loadLayoutEndPointUsingPref", () => {
    it("should return endpoint if valid key", async () => {
      const endpoint = feed.finalLayoutEndpoint(
        "https://somedomain.org/stories?consumer_key=$apiKey",
        "test_key_val"
      );
      assert.equal(
        "https://somedomain.org/stories?consumer_key=test_key_val",
        endpoint
      );
    });

    it("should throw error if key is empty", async () => {
      assert.throws(() => {
        feed.finalLayoutEndpoint(
          "https://somedomain.org/stories?consumer_key=$apiKey",
          ""
        );
      });
    });

    it("should return url if $apiKey is missing in layout_endpoint", async () => {
      const endpoint = feed.finalLayoutEndpoint(
        "https://somedomain.org/stories?consumer_key=",
        "test_key_val"
      );
      assert.equal("https://somedomain.org/stories?consumer_key=", endpoint);
    });

    it("should update config layout_endpoint based on api_key_pref value", async () => {
      feed.store.getState = () => ({
        Prefs: {
          values: {
            [CONFIG_PREF_NAME]: JSON.stringify({
              api_key_pref: "test_api_key_pref",
              enabled: true,
              layout_endpoint:
                "https://somedomain.org/stories?consumer_key=$apiKey",
            }),
          },
        },
      });
      sandbox
        .stub(global.Services.prefs, "getCharPref")
        .returns("test_api_key_val");
      assert.equal(
        "https://somedomain.org/stories?consumer_key=test_api_key_val",
        feed.config.layout_endpoint
      );
    });

    it("should not update config layout_endpoint if api_key_pref missing", async () => {
      feed.store.getState = () => ({
        Prefs: {
          values: {
            [CONFIG_PREF_NAME]: JSON.stringify({
              enabled: true,
              layout_endpoint:
                "https://somedomain.org/stories?consumer_key=1234",
            }),
          },
        },
      });
      sandbox
        .stub(global.Services.prefs, "getCharPref")
        .returns("test_api_key_val");
      assert.notCalled(global.Services.prefs.getCharPref);
      assert.equal(
        "https://somedomain.org/stories?consumer_key=1234",
        feed.config.layout_endpoint
      );
    });

    it("should not set config layout_endpoint if layout_endpoint missing in prefs", async () => {
      feed.store.getState = () => ({
        Prefs: {
          values: {
            [CONFIG_PREF_NAME]: JSON.stringify({
              enabled: true,
            }),
          },
        },
      });
      sandbox
        .stub(global.Services.prefs, "getCharPref")
        .returns("test_api_key_val");
      assert.notCalled(global.Services.prefs.getCharPref);
      assert.isUndefined(feed.config.layout_endpoint);
    });
  });

  describe("#loadComponentFeeds", () => {
    let fakeCache;
    let fakeDiscoveryStream;
    beforeEach(() => {
      fakeDiscoveryStream = {
        Prefs: {},
        DiscoveryStream: {
          layout: [
            { components: [{ feed: { url: "foo.com" } }] },
            { components: [{}] },
            {},
          ],
        },
      };
      fakeCache = {};
      sandbox.stub(feed.store, "getState").returns(fakeDiscoveryStream);
      sandbox.stub(feed.cache, "set").returns(Promise.resolve());
    });

    afterEach(() => {
      sandbox.restore();
    });

    it("should not dispatch updates when layout is not defined", async () => {
      fakeDiscoveryStream = {
        DiscoveryStream: {},
      };
      feed.store.getState.returns(fakeDiscoveryStream);
      sandbox.spy(feed.store, "dispatch");

      await feed.loadComponentFeeds(feed.store.dispatch);

      assert.notCalled(feed.store.dispatch);
    });

    it("should populate feeds cache", async () => {
      fakeCache = {
        feeds: { "foo.com": { lastUpdated: Date.now(), data: "data" } },
      };
      sandbox.stub(feed.cache, "get").returns(Promise.resolve(fakeCache));

      await feed.loadComponentFeeds(feed.store.dispatch);

      assert.calledWith(feed.cache.set, "feeds", {
        "foo.com": { data: "data", lastUpdated: 0 },
      });
    });

    it("should send feed update events with new feed data", async () => {
      sandbox.stub(feed.cache, "get").returns(Promise.resolve(fakeCache));
      sandbox.spy(feed.store, "dispatch");
      feed._prefCache = {
        config: {
          api_key_pref: "",
        },
      };

      await feed.loadComponentFeeds(feed.store.dispatch);

      assert.calledWith(feed.store.dispatch.firstCall, {
        type: at.DISCOVERY_STREAM_FEED_UPDATE,
        data: { feed: { data: { status: "failed" } }, url: "foo.com" },
        meta: { isStartup: false },
      });
      assert.calledWith(feed.store.dispatch.secondCall, {
        type: at.DISCOVERY_STREAM_FEEDS_UPDATE,
        meta: { isStartup: false },
      });
    });

    it("should return number of promises equal to unique urls", async () => {
      sandbox.stub(feed.cache, "get").returns(Promise.resolve(fakeCache));
      sandbox.stub(global.Promise, "all").resolves();
      fakeDiscoveryStream = {
        DiscoveryStream: {
          layout: [
            {
              components: [
                { feed: { url: "foo.com" } },
                { feed: { url: "bar.com" } },
              ],
            },
            { components: [{ feed: { url: "foo.com" } }] },
            {},
            { components: [{ feed: { url: "baz.com" } }] },
          ],
        },
      };
      feed.store.getState.returns(fakeDiscoveryStream);

      await feed.loadComponentFeeds(feed.store.dispatch);

      assert.calledOnce(global.Promise.all);
      const { args } = global.Promise.all.firstCall;
      assert.equal(args[0].length, 3);
    });
  });

  describe("#getComponentFeed", () => {
    it("should fetch fresh feed data if cache is empty", async () => {
      const fakeCache = {};
      sandbox.stub(feed.cache, "get").returns(Promise.resolve(fakeCache));
      sandbox.stub(feed, "rotate").callsFake(val => val);
      sandbox
        .stub(feed, "scoreItems")
        .callsFake(val => ({ data: val, filtered: [] }));
      sandbox.stub(feed, "fetchFromEndpoint").resolves({
        recommendations: "data",
        settings: {
          recsExpireTime: 1,
        },
      });

      const feedResp = await feed.getComponentFeed("foo.com");

      assert.equal(feedResp.data.recommendations, "data");
    });
    it("should fetch fresh feed data if cache is old", async () => {
      const fakeCache = { feeds: { "foo.com": { lastUpdated: Date.now() } } };
      sandbox.stub(feed.cache, "get").returns(Promise.resolve(fakeCache));
      sandbox.stub(feed, "fetchFromEndpoint").resolves({
        recommendations: "data",
        settings: {
          recsExpireTime: 1,
        },
      });
      sandbox.stub(feed, "rotate").callsFake(val => val);
      sandbox
        .stub(feed, "scoreItems")
        .callsFake(val => ({ data: val, filtered: [] }));
      clock.tick(THIRTY_MINUTES + 1);

      const feedResp = await feed.getComponentFeed("foo.com");

      assert.equal(feedResp.data.recommendations, "data");
    });
    it("should return feed data from cache if it is fresh", async () => {
      const fakeCache = {
        feeds: { "foo.com": { lastUpdated: Date.now(), data: "data" } },
      };
      sandbox.stub(feed.cache, "get").resolves(fakeCache);
      sandbox.stub(feed, "fetchFromEndpoint").resolves("old data");
      clock.tick(THIRTY_MINUTES - 1);

      const feedResp = await feed.getComponentFeed("foo.com");

      assert.equal(feedResp.data, "data");
    });
    it("should return null if no response was received", async () => {
      sandbox.stub(feed, "fetchFromEndpoint").resolves(null);

      const feedResp = await feed.getComponentFeed("foo.com");

      assert.deepEqual(feedResp, { data: { status: "failed" } });
    });
  });

  describe("#personalizationOverride", () => {
    it("should dispatch setPref", async () => {
      sandbox.spy(feed.store, "dispatch");
      feed.store.getState = () => ({
        Prefs: {
          values: {
            "discoverystream.personalization.enabled": true,
          },
        },
      });

      feed.personalizationOverride(true);

      assert.calledWithMatch(feed.store.dispatch, {
        data: {
          name: "discoverystream.personalization.override",
          value: true,
        },
        type: at.SET_PREF,
      });
    });
    it("should dispatch CLEAR_PREF", async () => {
      sandbox.spy(feed.store, "dispatch");
      feed.store.getState = () => ({
        Prefs: {
          values: {
            "discoverystream.personalization.enabled": true,
            "discoverystream.personalization.override": true,
          },
        },
      });

      feed.personalizationOverride(false);

      assert.calledWithMatch(feed.store.dispatch, {
        data: {
          name: "discoverystream.personalization.override",
        },
        type: at.CLEAR_PREF,
      });
    });
  });

  describe("#loadSpocs", () => {
    beforeEach(() => {
      feed._prefCache = {
        config: {
          api_key_pref: "",
        },
      };

      sandbox.stub(feed, "getPlacements").returns([{ name: "spocs" }]);
      Object.defineProperty(feed, "showSpocs", { get: () => true });
    });
    it("should not fetch or update cache if no spocs endpoint is defined", async () => {
      feed.store.dispatch(
        ac.BroadcastToContent({
          type: at.DISCOVERY_STREAM_SPOCS_ENDPOINT,
          data: "",
        })
      );

      sandbox.spy(feed.cache, "set");

      await feed.loadSpocs(feed.store.dispatch);

      assert.notCalled(global.fetch);
      assert.notCalled(feed.cache.set);
    });
    it("should fetch fresh spocs data if cache is empty", async () => {
      sandbox.stub(feed.cache, "get").returns(Promise.resolve());
      sandbox.stub(feed, "fetchFromEndpoint").resolves({ placement: "data" });
      sandbox.stub(feed.cache, "set").returns(Promise.resolve());

      await feed.loadSpocs(feed.store.dispatch);

      assert.calledWith(feed.cache.set, "spocs", {
        spocs: { placement: "data" },
        lastUpdated: 0,
      });
      assert.equal(
        feed.store.getState().DiscoveryStream.spocs.data.placement,
        "data"
      );
    });
    it("should fetch fresh data if cache is old", async () => {
      const cachedSpoc = {
        spocs: { placement: "old" },
        lastUpdated: Date.now(),
      };
      const cachedData = { spocs: cachedSpoc };
      sandbox.stub(feed.cache, "get").returns(Promise.resolve(cachedData));
      sandbox.stub(feed, "fetchFromEndpoint").resolves({ placement: "new" });
      sandbox.stub(feed.cache, "set").returns(Promise.resolve());
      clock.tick(THIRTY_MINUTES + 1);

      await feed.loadSpocs(feed.store.dispatch);

      assert.equal(
        feed.store.getState().DiscoveryStream.spocs.data.placement,
        "new"
      );
    });
    it("should return spoc data from cache if it is fresh", async () => {
      const cachedSpoc = {
        spocs: { placement: "old" },
        lastUpdated: Date.now(),
      };
      const cachedData = { spocs: cachedSpoc };
      sandbox.stub(feed.cache, "get").returns(Promise.resolve(cachedData));
      sandbox.stub(feed, "fetchFromEndpoint").resolves({ placement: "new" });
      sandbox.stub(feed.cache, "set").returns(Promise.resolve());
      clock.tick(THIRTY_MINUTES - 1);

      await feed.loadSpocs(feed.store.dispatch);

      assert.equal(
        feed.store.getState().DiscoveryStream.spocs.data.placement,
        "old"
      );
    });
    it("should properly transform spocs using placements", async () => {
      sandbox.stub(feed.cache, "get").returns(Promise.resolve());
      sandbox
        .stub(feed, "fetchFromEndpoint")
        .resolves({ spocs: { items: [{ id: "data" }] } });
      sandbox.stub(feed.cache, "set").returns(Promise.resolve());

      await feed.loadSpocs(feed.store.dispatch);

      assert.calledWith(feed.cache.set, "spocs", {
        spocs: {
          spocs: {
            context: "",
            title: "",
            sponsor: "",
            sponsored_by_override: undefined,
            items: [{ id: "data", score: 1 }],
          },
        },
        lastUpdated: 0,
      });

      assert.deepEqual(
        feed.store.getState().DiscoveryStream.spocs.data.spocs.items[0],
        { id: "data", score: 1 }
      );
    });
    it("should normalizeSpocsItems for older spoc data", async () => {
      sandbox.stub(feed.cache, "get").returns(Promise.resolve());
      sandbox
        .stub(feed, "fetchFromEndpoint")
        .resolves({ spocs: [{ id: "data" }] });
      sandbox.stub(feed.cache, "set").returns(Promise.resolve());

      await feed.loadSpocs(feed.store.dispatch);

      assert.deepEqual(
        feed.store.getState().DiscoveryStream.spocs.data.spocs.items[0],
        { id: "data", score: 1 }
      );
    });
    it("should call personalizationVersionOverride with feature_flags", async () => {
      sandbox.stub(feed.cache, "get").returns(Promise.resolve());
      sandbox.stub(feed, "personalizationOverride").returns();
      sandbox
        .stub(feed, "fetchFromEndpoint")
        .resolves({ settings: { feature_flags: {} }, spocs: [{ id: "data" }] });
      sandbox.stub(feed.cache, "set").returns(Promise.resolve());

      await feed.loadSpocs(feed.store.dispatch);

      assert.calledOnce(feed.personalizationOverride);
    });
    it("should return expected data if normalizeSpocsItems returns no spoc data", async () => {
      // We don't need this for just this test, we are setting placements manually.
      feed.getPlacements.restore();
      Object.defineProperty(feed, "showSponsoredStories", {
        get: () => true,
      });

      sandbox.stub(feed.cache, "get").returns(Promise.resolve());
      sandbox
        .stub(feed, "fetchFromEndpoint")
        .resolves({ placement1: [{ id: "data" }], placement2: [] });
      sandbox.stub(feed.cache, "set").returns(Promise.resolve());

      const fakeComponents = {
        components: [
          { placement: { name: "placement1" }, spocs: {} },
          { placement: { name: "placement2" }, spocs: {} },
        ],
      };
      feed.updatePlacements(feed.store.dispatch, [fakeComponents]);

      await feed.loadSpocs(feed.store.dispatch);

      assert.deepEqual(feed.store.getState().DiscoveryStream.spocs.data, {
        placement1: {
          title: "",
          context: "",
          sponsor: "",
          sponsored_by_override: undefined,
          items: [{ id: "data", score: 1 }],
        },
        placement2: {
          title: "",
          context: "",
          items: [],
        },
      });
    });
    it("should use title and context on spoc data", async () => {
      // We don't need this for just this test, we are setting placements manually.
      feed.getPlacements.restore();
      Object.defineProperty(feed, "showSponsoredStories", {
        get: () => true,
      });
      sandbox.stub(feed.cache, "get").returns(Promise.resolve());
      sandbox.stub(feed, "fetchFromEndpoint").resolves({
        placement1: {
          title: "title",
          context: "context",
          sponsor: "",
          sponsored_by_override: undefined,
          items: [{ id: "data" }],
        },
      });
      sandbox.stub(feed.cache, "set").returns(Promise.resolve());

      const fakeComponents = {
        components: [{ placement: { name: "placement1" }, spocs: {} }],
      };
      feed.updatePlacements(feed.store.dispatch, [fakeComponents]);

      await feed.loadSpocs(feed.store.dispatch);

      assert.deepEqual(feed.store.getState().DiscoveryStream.spocs.data, {
        placement1: {
          title: "title",
          context: "context",
          sponsor: "",
          sponsored_by_override: undefined,
          items: [{ id: "data", score: 1 }],
        },
      });
    });
  });

  describe("#normalizeSpocsItems", () => {
    it("should return correct data if new data passed in", async () => {
      const spocs = {
        title: "title",
        context: "context",
        sponsor: "sponsor",
        sponsored_by_override: "override",
        items: [{ id: "id" }],
      };
      const result = feed.normalizeSpocsItems(spocs);
      assert.deepEqual(result, spocs);
    });
    it("should return normalized data if new data passed in without title or context", async () => {
      const spocs = {
        items: [{ id: "id" }],
      };
      const result = feed.normalizeSpocsItems(spocs);
      assert.deepEqual(result, {
        title: "",
        context: "",
        sponsor: "",
        sponsored_by_override: undefined,
        items: [{ id: "id" }],
      });
    });
    it("should return normalized data if old data passed in", async () => {
      const spocs = [{ id: "id" }];
      const result = feed.normalizeSpocsItems(spocs);
      assert.deepEqual(result, {
        title: "",
        context: "",
        sponsor: "",
        sponsored_by_override: undefined,
        items: [{ id: "id" }],
      });
    });
  });

  describe("#showSpocs", () => {
    it("should return true from showSpocs if showSponsoredStories is false", async () => {
      Object.defineProperty(feed, "showSponsoredStories", {
        get: () => false,
      });
      Object.defineProperty(feed, "showSponsoredTopsites", {
        get: () => true,
      });
      assert.isTrue(feed.showSpocs);
    });
    it("should return true from showSpocs if showSponsoredTopsites is false", async () => {
      Object.defineProperty(feed, "showSponsoredStories", {
        get: () => true,
      });
      Object.defineProperty(feed, "showSponsoredTopsites", {
        get: () => false,
      });
      assert.isTrue(feed.showSpocs);
    });
    it("should return true from showSpocs if both are true", async () => {
      Object.defineProperty(feed, "showSponsoredStories", {
        get: () => true,
      });
      Object.defineProperty(feed, "showSponsoredTopsites", {
        get: () => true,
      });
      assert.isTrue(feed.showSpocs);
    });
    it("should return false from showSpocs if both are false", async () => {
      Object.defineProperty(feed, "showSponsoredStories", {
        get: () => false,
      });
      Object.defineProperty(feed, "showSponsoredTopsites", {
        get: () => false,
      });
      assert.isFalse(feed.showSpocs);
    });
  });

  describe("#showSponsoredStories", () => {
    it("should return false from showSponsoredStories if user pref showSponsored is false", async () => {
      feed.store.getState = () => ({
        Prefs: { values: { showSponsored: false } },
      });
      Object.defineProperty(feed, "config", {
        get: () => ({ show_spocs: true }),
      });

      assert.isFalse(feed.showSponsoredStories);
    });
    it("should return false from showSponsoredStories if DiscoveryStream pref show_spocs is false", async () => {
      feed.store.getState = () => ({
        Prefs: { values: { showSponsored: true } },
      });
      Object.defineProperty(feed, "config", {
        get: () => ({ show_spocs: false }),
      });

      assert.isFalse(feed.showSponsoredStories);
    });
    it("should return true from showSponsoredStories if both prefs are true", async () => {
      feed.store.getState = () => ({
        Prefs: { values: { showSponsored: true } },
      });
      Object.defineProperty(feed, "config", {
        get: () => ({ show_spocs: true }),
      });

      assert.isTrue(feed.showSponsoredStories);
    });
  });

  describe("#showSponsoredTopsites", () => {
    it("should return false from showSponsoredTopsites if user pref showSponsoredTopSites is false", async () => {
      feed.store.getState = () => ({
        Prefs: { values: { showSponsoredTopSites: false } },
        DiscoveryStream: {
          spocs: {
            placements: [{ name: "sponsored-topsites" }],
          },
        },
      });
      assert.isFalse(feed.showSponsoredTopsites);
    });
    it("should return true from showSponsoredTopsites if user pref showSponsoredTopSites is true", async () => {
      feed.store.getState = () => ({
        Prefs: { values: { showSponsoredTopSites: true } },
        DiscoveryStream: {
          spocs: {
            placements: [{ name: "sponsored-topsites" }],
          },
        },
      });
      assert.isTrue(feed.showSponsoredTopsites);
    });
  });

  describe("#showStories", () => {
    it("should return false from showStories if user pref is false", async () => {
      feed.store.getState = () => ({
        Prefs: {
          values: {
            "feeds.section.topstories": false,
            "feeds.system.topstories": true,
          },
        },
      });
      assert.isFalse(feed.showStories);
    });
    it("should return false from showStories if system pref is false", async () => {
      feed.store.getState = () => ({
        Prefs: {
          values: {
            "feeds.section.topstories": true,
            "feeds.system.topstories": false,
          },
        },
      });
      assert.isFalse(feed.showStories);
    });
    it("should return true from showStories if both prefs are true", async () => {
      feed.store.getState = () => ({
        Prefs: {
          values: {
            "feeds.section.topstories": true,
            "feeds.system.topstories": true,
          },
        },
      });
      assert.isTrue(feed.showStories);
    });
  });

  describe("#showTopsites", () => {
    it("should return false from showTopsites if user pref is false", async () => {
      feed.store.getState = () => ({
        Prefs: {
          values: {
            "feeds.topsites": false,
            "feeds.system.topsites": true,
          },
        },
      });
      assert.isFalse(feed.showTopsites);
    });
    it("should return false from showTopsites if system pref is false", async () => {
      feed.store.getState = () => ({
        Prefs: {
          values: {
            "feeds.topsites": true,
            "feeds.system.topsites": false,
          },
        },
      });
      assert.isFalse(feed.showTopsites);
    });
    it("should return true from showTopsites if both prefs are true", async () => {
      feed.store.getState = () => ({
        Prefs: {
          values: {
            "feeds.topsites": true,
            "feeds.system.topsites": true,
          },
        },
      });
      assert.isTrue(feed.showTopsites);
    });
  });

  describe("#clearSpocs", () => {
    let defaultState;
    let DiscoveryStream;
    let Prefs;
    beforeEach(() => {
      Object.defineProperty(feed, "config", {
        get: () => ({ show_spocs: true }),
      });
      DiscoveryStream = {
        layout: [],
        spocs: {
          placements: [{ name: "sponsored-topsites" }],
        },
      };
      Prefs = {
        values: {
          "feeds.section.topstories": true,
          "feeds.system.topstories": true,
          "feeds.topsites": true,
          "feeds.system.topsites": true,
          showSponsoredTopSites: true,
          showSponsored: true,
        },
      };
      defaultState = {
        DiscoveryStream,
        Prefs,
      };
      feed.store.getState = () => defaultState;
    });
    it("should not fail with no endpoint", async () => {
      sandbox.stub(feed.store, "getState").returns({
        Prefs: {
          values: { "discoverystream.endpointSpocsClear": null },
        },
      });
      sandbox.stub(feed, "fetchFromEndpoint").resolves(null);

      await feed.clearSpocs();

      assert.notCalled(feed.fetchFromEndpoint);
    });
    it("should call DELETE with endpoint", async () => {
      sandbox.stub(feed.store, "getState").returns({
        Prefs: {
          values: {
            "discoverystream.endpointSpocsClear": "https://spocs/user",
          },
        },
      });
      sandbox.stub(feed, "fetchFromEndpoint").resolves(null);
      feed._impressionId = "1234";

      await feed.clearSpocs();

      assert.equal(
        feed.fetchFromEndpoint.firstCall.args[0],
        "https://spocs/user"
      );
      assert.equal(feed.fetchFromEndpoint.firstCall.args[1].method, "DELETE");
      assert.equal(
        feed.fetchFromEndpoint.firstCall.args[1].body,
        '{"pocket_id":"1234"}'
      );
    });
    it("should properly call clearSpocs when sponsored content is changed", async () => {
      sandbox.stub(feed, "clearSpocs").returns(Promise.resolve());
      //sandbox.stub(feed, "updatePlacements").returns();
      sandbox.stub(feed, "loadSpocs").returns();

      await feed.onAction({
        type: at.PREF_CHANGED,
        data: { name: "showSponsored" },
      });

      assert.notCalled(feed.clearSpocs);

      Prefs.values.showSponsoredTopSites = false;
      Prefs.values.showSponsored = false;

      await feed.onAction({
        type: at.PREF_CHANGED,
        data: { name: "showSponsored" },
      });

      assert.calledOnce(feed.clearSpocs);
    });
    it("should call clearSpocs when top stories and top sites is turned off", async () => {
      sandbox.stub(feed, "clearSpocs").returns(Promise.resolve());
      Prefs.values["feeds.section.topstories"] = false;
      Prefs.values["feeds.topsites"] = false;

      await feed.onAction({
        type: at.PREF_CHANGED,
        data: { name: "feeds.section.topstories" },
      });

      assert.calledOnce(feed.clearSpocs);

      await feed.onAction({
        type: at.PREF_CHANGED,
        data: { name: "feeds.topsites" },
      });

      assert.calledTwice(feed.clearSpocs);
    });
  });

  describe("#rotate", () => {
    it("should move seen first story to the back of the response", () => {
      const recsExpireTime = 5600;
      const feedResponse = {
        recommendations: [
          {
            id: "first",
          },
          {
            id: "second",
          },
          {
            id: "third",
          },
          {
            id: "fourth",
          },
        ],
        settings: {
          recsExpireTime,
        },
      };
      const fakeImpressions = {
        first: Date.now() - recsExpireTime * 1000,
        third: Date.now(),
      };
      sandbox.stub(feed, "readDataPref").returns(fakeImpressions);

      const result = feed.rotate(
        feedResponse.recommendations,
        feedResponse.settings.recsExpireTime
      );

      assert.equal(result[3].id, "first");
    });
  });

  describe("#reset", () => {
    it("should fire all reset based functions", async () => {
      sandbox.stub(global.Services.obs, "removeObserver").returns();

      sandbox.stub(feed, "resetDataPrefs").returns();
      sandbox.stub(feed, "resetCache").returns(Promise.resolve());
      sandbox.stub(feed, "resetState").returns();

      feed.loaded = true;

      await feed.reset();

      assert.calledOnce(feed.resetDataPrefs);
      assert.calledOnce(feed.resetCache);
      assert.calledOnce(feed.resetState);
      assert.calledOnce(global.Services.obs.removeObserver);
    });
  });

  describe("#resetCache", () => {
    it("should set .layout, .feeds .spocs and .personalization to {}", async () => {
      sandbox.stub(feed.cache, "set").returns(Promise.resolve());

      await feed.resetCache();

      assert.callCount(feed.cache.set, 4);
      const firstCall = feed.cache.set.getCall(0);
      const secondCall = feed.cache.set.getCall(1);
      const thirdCall = feed.cache.set.getCall(2);
      const fourthCall = feed.cache.set.getCall(3);
      assert.deepEqual(firstCall.args, ["layout", {}]);
      assert.deepEqual(secondCall.args, ["feeds", {}]);
      assert.deepEqual(thirdCall.args, ["spocs", {}]);
      assert.deepEqual(fourthCall.args, ["personalization", {}]);
    });
  });

  describe("#scoreItems", () => {
    it("should return initial data if spocs are empty", async () => {
      const { data: result } = await feed.scoreItems([]);

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

    it("should sort based on item_score", async () => {
      const { data: result } = await feed.scoreItems([
        { id: 2, flight_id: 2, item_score: 0.8 },
        { id: 4, flight_id: 4, item_score: 0.5 },
        { id: 3, flight_id: 3, item_score: 0.7 },
        { id: 1, flight_id: 1, item_score: 0.9 },
      ]);

      assert.deepEqual(result, [
        { id: 1, flight_id: 1, item_score: 0.9, score: 0.9 },
        { id: 2, flight_id: 2, item_score: 0.8, score: 0.8 },
        { id: 3, flight_id: 3, item_score: 0.7, score: 0.7 },
        { id: 4, flight_id: 4, item_score: 0.5, score: 0.5 },
      ]);
    });

    it("should sort based on priority", async () => {
      const { data: result } = await feed.scoreItems([
        { id: 6, flight_id: 6, priority: 2, item_score: 0.7 },
        { id: 2, flight_id: 3, priority: 1, item_score: 0.2 },
        { id: 4, flight_id: 4, item_score: 0.6 },
        { id: 5, flight_id: 5, priority: 2, item_score: 0.8 },
        { id: 3, flight_id: 3, item_score: 0.8 },
        { id: 1, flight_id: 1, priority: 1, item_score: 0.3 },
      ]);

      assert.deepEqual(result, [
        {
          id: 1,
          flight_id: 1,
          priority: 1,
          score: 0.3,
          item_score: 0.3,
        },
        {
          id: 2,
          flight_id: 3,
          priority: 1,
          score: 0.2,
          item_score: 0.2,
        },
        {
          id: 5,
          flight_id: 5,
          priority: 2,
          score: 0.8,
          item_score: 0.8,
        },
        {
          id: 6,
          flight_id: 6,
          priority: 2,
          score: 0.7,
          item_score: 0.7,
        },
        { id: 3, flight_id: 3, item_score: 0.8, score: 0.8 },
        { id: 4, flight_id: 4, item_score: 0.6, score: 0.6 },
      ]);
    });

    it("should add a score prop to spocs", async () => {
      const { data: result } = await feed.scoreItems([
        { flight_id: 1, item_score: 0.9 },
      ]);

      assert.equal(result[0].score, 0.9);
    });
  });

  describe("#filterBlocked", () => {
    it("should return initial data if spocs are empty", () => {
      const { data: result } = feed.filterBlocked([]);

      assert.equal(result.length, 0);
    });
    it("should return initial data if links are not blocked", () => {
      const { data: result } = feed.filterBlocked([
        { url: "https://foo.com" },
        { url: "test.com" },
      ]);
      assert.equal(result.length, 2);
    });
    it("should return initial recommendations data if links are not blocked", () => {
      const { data: result } = feed.filterBlocked([
        { url: "https://foo.com" },
        { url: "test.com" },
      ]);
      assert.equal(result.length, 2);
    });
    it("filterRecommendations based on blockedlist by passing feed data", () => {
      fakeNewTabUtils.blockedLinks.links = [{ url: "https://foo.com" }];
      fakeNewTabUtils.blockedLinks.isBlocked = site =>
        fakeNewTabUtils.blockedLinks.links[0].url === site.url;

      const result = feed.filterRecommendations({
        lastUpdated: 4,
        data: {
          recommendations: [{ url: "https://foo.com" }, { url: "test.com" }],
        },
      });

      assert.equal(result.lastUpdated, 4);
      assert.lengthOf(result.data.recommendations, 1);
      assert.equal(result.data.recommendations[0].url, "test.com");
      assert.notInclude(
        result.data.recommendations,
        fakeNewTabUtils.blockedLinks.links[0]
      );
    });
  });

  describe("#frequencyCapSpocs", () => {
    it("should return filtered out spocs based on frequency caps", () => {
      const fakeSpocs = [
        {
          id: 1,
          flight_id: "seen",
          caps: {
            lifetime: 3,
            flight: {
              count: 1,
              period: 1,
            },
          },
        },
        {
          id: 2,
          flight_id: "not-seen",
          caps: {
            lifetime: 3,
            flight: {
              count: 1,
              period: 1,
            },
          },
        },
      ];
      const fakeImpressions = {
        seen: [Date.now() - 1],
      };
      sandbox.stub(feed, "readDataPref").returns(fakeImpressions);

      const { data: result, filtered } = feed.frequencyCapSpocs(fakeSpocs);

      assert.equal(result.length, 1);
      assert.equal(result[0].flight_id, "not-seen");
      assert.deepEqual(filtered, [fakeSpocs[0]]);
    });
    it("should return simple structure and do nothing with no spocs", () => {
      const { data: result, filtered } = feed.frequencyCapSpocs([]);

      assert.equal(result.length, 0);
      assert.equal(filtered.length, 0);
    });
  });

  describe("#migrateFlightId", () => {
    it("should migrate campaign to flight if no flight exists", () => {
      const fakeSpocs = [
        {
          id: 1,
          campaign_id: "campaign",
          caps: {
            lifetime: 3,
            campaign: {
              count: 1,
              period: 1,
            },
          },
        },
      ];
      const { data: result } = feed.migrateFlightId(fakeSpocs);

      assert.deepEqual(result[0], {
        id: 1,
        flight_id: "campaign",
        campaign_id: "campaign",
        caps: {
          lifetime: 3,
          flight: {
            count: 1,
            period: 1,
          },
          campaign: {
            count: 1,
            period: 1,
          },
        },
      });
    });
    it("should not migrate campaign to flight if caps or id don't exist", () => {
      const fakeSpocs = [{ id: 1 }];
      const { data: result } = feed.migrateFlightId(fakeSpocs);

      assert.deepEqual(result[0], { id: 1 });
    });
    it("should return simple structure and do nothing with no spocs", () => {
      const { data: result } = feed.migrateFlightId([]);

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

  describe("#isBelowFrequencyCap", () => {
    it("should return true if there are no flight impressions", () => {
      const fakeImpressions = {
        seen: [Date.now() - 1],
      };
      const fakeSpoc = {
        flight_id: "not-seen",
        caps: {
          lifetime: 3,
          flight: {
            count: 1,
            period: 1,
          },
        },
      };

      const result = feed.isBelowFrequencyCap(fakeImpressions, fakeSpoc);

      assert.isTrue(result);
    });
    it("should return true if there are no flight caps", () => {
      const fakeImpressions = {
        seen: [Date.now() - 1],
      };
      const fakeSpoc = {
        flight_id: "seen",
        caps: {
          lifetime: 3,
        },
      };

      const result = feed.isBelowFrequencyCap(fakeImpressions, fakeSpoc);

      assert.isTrue(result);
    });

    it("should return false if lifetime cap is hit", () => {
      const fakeImpressions = {
        seen: [Date.now() - 1],
      };
      const fakeSpoc = {
        flight_id: "seen",
        caps: {
          lifetime: 1,
          flight: {
            count: 3,
            period: 1,
          },
        },
      };

      const result = feed.isBelowFrequencyCap(fakeImpressions, fakeSpoc);

      assert.isFalse(result);
    });

    it("should return false if time based cap is hit", () => {
      const fakeImpressions = {
        seen: [Date.now() - 1],
      };
      const fakeSpoc = {
        flight_id: "seen",
        caps: {
          lifetime: 3,
          flight: {
            count: 1,
            period: 1,
          },
        },
      };

      const result = feed.isBelowFrequencyCap(fakeImpressions, fakeSpoc);

      assert.isFalse(result);
    });
  });

  describe("#retryFeed", () => {
    it("should retry a feed fetch", async () => {
      sandbox.stub(feed, "getComponentFeed").returns(Promise.resolve({}));
      sandbox.stub(feed, "filterRecommendations").returns({});
      sandbox.spy(feed.store, "dispatch");

      await feed.retryFeed({ url: "https://feed.com" });

      assert.calledOnce(feed.getComponentFeed);
      assert.calledOnce(feed.filterRecommendations);
      assert.calledOnce(feed.store.dispatch);
      assert.equal(
        feed.store.dispatch.firstCall.args[0].type,
        "DISCOVERY_STREAM_FEED_UPDATE"
      );
      assert.deepEqual(feed.store.dispatch.firstCall.args[0].data, {
        feed: {},
        url: "https://feed.com",
      });
    });
  });

  describe("#recordFlightImpression", () => {
    it("should return false if time based cap is hit", () => {
      sandbox.stub(feed, "readDataPref").returns({});
      sandbox.stub(feed, "writeDataPref").returns();

      feed.recordFlightImpression("seen");

      assert.calledWith(feed.writeDataPref, SPOC_IMPRESSION_TRACKING_PREF, {
        seen: [0],
      });
    });
  });

  describe("#recordBlockFlightId", () => {
    it("should call writeDataPref with new flight id added", () => {
      sandbox.stub(feed, "readDataPref").returns({ 1234: 1 });
      sandbox.stub(feed, "writeDataPref").returns();

      feed.recordBlockFlightId("5678");

      assert.calledOnce(feed.readDataPref);
      assert.calledWith(feed.writeDataPref, "discoverystream.flight.blocks", {
        1234: 1,
        5678: 1,
      });
    });
  });

  describe("#cleanUpFlightImpressionPref", () => {
    it("should remove flight-3 because it is no longer being used", async () => {
      const fakeSpocs = {
        spocs: {
          items: [
            {
              flight_id: "flight-1",
              caps: {
                lifetime: 3,
                flight: {
                  count: 1,
                  period: 1,
                },
              },
            },
            {
              flight_id: "flight-2",
              caps: {
                lifetime: 3,
                flight: {
                  count: 1,
                  period: 1,
                },
              },
            },
          ],
        },
      };
      const fakeImpressions = {
        "flight-2": [Date.now() - 1],
        "flight-3": [Date.now() - 1],
      };
      sandbox.stub(feed, "getPlacements").returns([{ name: "spocs" }]);
      sandbox.stub(feed, "readDataPref").returns(fakeImpressions);
      sandbox.stub(feed, "writeDataPref").returns();

      feed.cleanUpFlightImpressionPref(fakeSpocs);

      assert.calledWith(feed.writeDataPref, SPOC_IMPRESSION_TRACKING_PREF, {
        "flight-2": [-1],
      });
    });
  });

  describe("#recordTopRecImpressions", () => {
    it("should add a rec id to the rec impression pref", () => {
      sandbox.stub(feed, "readDataPref").returns({});
      sandbox.stub(feed, "writeDataPref");

      feed.recordTopRecImpressions("rec");

      assert.calledWith(feed.writeDataPref, REC_IMPRESSION_TRACKING_PREF, {
        rec: 0,
      });
    });
    it("should not add an impression if it already exists", () => {
      sandbox.stub(feed, "readDataPref").returns({ rec: 4 });
      sandbox.stub(feed, "writeDataPref");

      feed.recordTopRecImpressions("rec");

      assert.notCalled(feed.writeDataPref);
    });
  });

  describe("#cleanUpTopRecImpressionPref", () => {
    it("should remove recs no longer being used", () => {
      const newFeeds = {
        "https://foo.com": {
          data: {
            recommendations: [
              {
                id: "rec1",
              },
              {
                id: "rec2",
              },
            ],
          },
        },
        "https://bar.com": {
          data: {
            recommendations: [
              {
                id: "rec3",
              },
              {
                id: "rec4",
              },
            ],
          },
        },
      };
      const fakeImpressions = {
        rec2: Date.now() - 1,
        rec3: Date.now() - 1,
        rec5: Date.now() - 1,
      };
      sandbox.stub(feed, "readDataPref").returns(fakeImpressions);
      sandbox.stub(feed, "writeDataPref").returns();

      feed.cleanUpTopRecImpressionPref(newFeeds);

      assert.calledWith(feed.writeDataPref, REC_IMPRESSION_TRACKING_PREF, {
        rec2: -1,
        rec3: -1,
      });
    });
  });

  describe("#writeDataPref", () => {
    it("should call Services.prefs.setStringPref", () => {
      sandbox.spy(feed.store, "dispatch");
      const fakeImpressions = {
        foo: [Date.now() - 1],
        bar: [Date.now() - 1],
      };

      feed.writeDataPref(SPOC_IMPRESSION_TRACKING_PREF, fakeImpressions);

      assert.calledWithMatch(feed.store.dispatch, {
        data: {
          name: SPOC_IMPRESSION_TRACKING_PREF,
          value: JSON.stringify(fakeImpressions),
        },
        type: at.SET_PREF,
      });
    });
  });

  describe("#addEndpointQuery", () => {
    const url = "https://spocs.getpocket.com/spocs";

    it("should return same url with no query", () => {
      const result = feed.addEndpointQuery(url, "");
      assert.equal(result, url);
    });

    it("should add multiple query params to standard url", () => {
      const params = "?first=first&second=second";
      const result = feed.addEndpointQuery(url, params);
      assert.equal(result, url + params);
    });

    it("should add multiple query params to url with a query already", () => {
      const params = "first=first&second=second";
      const initialParams = "?zero=zero";
      const result = feed.addEndpointQuery(
        `${url}${initialParams}`,
        `?${params}`
      );
      assert.equal(result, `${url}${initialParams}&${params}`);
    });
  });

  describe("#readDataPref", () => {
    it("should return what's in Services.prefs.getStringPref", () => {
      const fakeImpressions = {
        foo: [Date.now() - 1],
        bar: [Date.now() - 1],
      };
      setPref(SPOC_IMPRESSION_TRACKING_PREF, fakeImpressions);

      const result = feed.readDataPref(SPOC_IMPRESSION_TRACKING_PREF);

      assert.deepEqual(result, fakeImpressions);
    });
  });

  describe("#setupPrefs", () => {
    it("should call setupPrefs", async () => {
      sandbox.spy(feed, "setupPrefs");
      feed.onAction({
        type: at.INIT,
      });
      assert.calledOnce(feed.setupPrefs);
    });
    it("should dispatch to at.DISCOVERY_STREAM_PREFS_SETUP with proper data", async () => {
      sandbox.spy(feed.store, "dispatch");
      globals.set("ExperimentAPI", {
        getExperimentMetaData: () => ({
          slug: "experimentId",
          branch: {
            slug: "branchId",
          },
        }),
        getRolloutMetaData: () => ({}),
      });
      global.Services.prefs.getBoolPref
        .withArgs("extensions.pocket.enabled")
        .returns(true);
      feed.store.getState = () => ({
        Prefs: {
          values: {
            region: "CA",
            pocketConfig: {
              recentSavesEnabled: true,
              hideDescriptions: false,
              hideDescriptionsRegions: "US,CA,GB",
              compactImages: true,
              imageGradient: true,
              newSponsoredLabel: true,
              titleLines: "1",
              descLines: "1",
              readTime: true,
              saveToPocketCard: false,
              saveToPocketCardRegions: "US,CA,GB",
            },
          },
        },
      });
      feed.setupPrefs();
      assert.deepEqual(feed.store.dispatch.firstCall.args[0].data, {
        utmSource: "pocket-newtab",
        utmCampaign: "experimentId",
        utmContent: "branchId",
      });
      assert.deepEqual(feed.store.dispatch.secondCall.args[0].data, {
        recentSavesEnabled: true,
        pocketButtonEnabled: true,
        saveToPocketCard: true,
        hideDescriptions: true,
        compactImages: true,
        imageGradient: true,
        newSponsoredLabel: true,
        titleLines: "1",
        descLines: "1",
        readTime: true,
      });
    });
  });

  describe("#onAction: DISCOVERY_STREAM_IMPRESSION_STATS", () => {
    it("should call recordTopRecImpressions from DISCOVERY_STREAM_IMPRESSION_STATS", async () => {
      sandbox.stub(feed, "recordTopRecImpressions").returns();
      await feed.onAction({
        type: at.DISCOVERY_STREAM_IMPRESSION_STATS,
        data: { tiles: [{ id: "seen" }] },
      });

      assert.calledWith(feed.recordTopRecImpressions, "seen");
    });
  });

  describe("#onAction: DISCOVERY_STREAM_SPOC_IMPRESSION", () => {
    beforeEach(() => {
      const data = {
        spocs: {
          items: [
            {
              id: 1,
              flight_id: "seen",
              caps: {
                lifetime: 3,
                flight: {
                  count: 1,
                  period: 1,
                },
              },
            },
            {
              id: 2,
              flight_id: "not-seen",
              caps: {
                lifetime: 3,
                flight: {
                  count: 1,
                  period: 1,
                },
              },
            },
          ],
        },
      };
      sandbox.stub(feed.store, "getState").returns({
        DiscoveryStream: {
          spocs: {
            data,
          },
        },
      });
    });

    it("should call dispatch to ac.AlsoToPreloaded with filtered spoc data", async () => {
      sandbox.stub(feed, "getPlacements").returns([{ name: "spocs" }]);
      Object.defineProperty(feed, "showSpocs", { get: () => true });
      const fakeImpressions = {
        seen: [Date.now() - 1],
      };
      const result = {
        spocs: {
          items: [
            {
              id: 2,
              flight_id: "not-seen",
              caps: {
                lifetime: 3,
                flight: {
                  count: 1,
                  period: 1,
                },
              },
            },
          ],
        },
      };
      sandbox.stub(feed, "recordFlightImpression").returns();
      sandbox.stub(feed, "readDataPref").returns(fakeImpressions);
      sandbox.spy(feed.store, "dispatch");

      await feed.onAction({
        type: at.DISCOVERY_STREAM_SPOC_IMPRESSION,
        data: { flightId: "seen" },
      });

      assert.deepEqual(
        feed.store.dispatch.secondCall.args[0].data.spocs,
        result
      );
    });
    it("should not call dispatch to ac.AlsoToPreloaded if spocs were not changed by frequency capping", async () => {
      sandbox.stub(feed, "getPlacements").returns([{ name: "spocs" }]);
      Object.defineProperty(feed, "showSpocs", { get: () => true });
      const fakeImpressions = {};
      sandbox.stub(feed, "recordFlightImpression").returns();
      sandbox.stub(feed, "readDataPref").returns(fakeImpressions);
      sandbox.spy(feed.store, "dispatch");

      await feed.onAction({
        type: at.DISCOVERY_STREAM_SPOC_IMPRESSION,
        data: { flight_id: "seen" },
      });

      assert.notCalled(feed.store.dispatch);
    });
    it("should attempt feq cap on valid spocs with placements on impression", async () => {
      sandbox.restore();
      Object.defineProperty(feed, "showSpocs", { get: () => true });
      const fakeImpressions = {};
      sandbox.stub(feed, "recordFlightImpression").returns();
      sandbox.stub(feed, "readDataPref").returns(fakeImpressions);
      sandbox.spy(feed.store, "dispatch");
      sandbox.spy(feed, "frequencyCapSpocs");

      const data = {
        spocs: {
          items: [
            {
              id: 2,
              flight_id: "seen-2",
              caps: {
                lifetime: 3,
                flight: {
                  count: 1,
                  period: 1,
                },
              },
            },
          ],
        },
      };
      sandbox.stub(feed.store, "getState").returns({
        DiscoveryStream: {
          spocs: {
            data,
            placements: [{ name: "spocs" }, { name: "notSpocs" }],
          },
        },
      });

      await feed.onAction({
        type: at.DISCOVERY_STREAM_SPOC_IMPRESSION,
        data: { flight_id: "doesn't matter" },
      });

      assert.calledOnce(feed.frequencyCapSpocs);
      assert.calledWith(feed.frequencyCapSpocs, data.spocs.items);
    });
  });

  describe("#onAction: PLACES_LINK_BLOCKED", () => {
    beforeEach(() => {
      const data = {
        spocs: {
          items: [
            {
              id: 1,
              flight_id: "foo",
              url: "foo.com",
            },
            {
              id: 2,
              flight_id: "bar",
              url: "bar.com",
            },
          ],
        },
      };
      sandbox.stub(feed.store, "getState").returns({
        DiscoveryStream: {
          spocs: {
            data,
            placements: [{ name: "spocs" }],
          },
        },
      });
    });

    it("should call dispatch if found a blocked spoc", async () => {
      Object.defineProperty(feed, "showSpocs", { get: () => true });

      sandbox.spy(feed.store, "dispatch");

      await feed.onAction({
        type: at.PLACES_LINK_BLOCKED,
        data: { url: "foo.com" },
      });

      assert.deepEqual(
        feed.store.dispatch.firstCall.args[0].data.url,
        "foo.com"
      );
    });
    it("should dispatch once if the blocked is not a SPOC", async () => {
      Object.defineProperty(feed, "showSpocs", { get: () => true });
      sandbox.spy(feed.store, "dispatch");

      await feed.onAction({
        type: at.PLACES_LINK_BLOCKED,
        data: { url: "not_a_spoc.com" },
      });

      assert.calledOnce(feed.store.dispatch);
      assert.deepEqual(
        feed.store.dispatch.firstCall.args[0].data.url,
        "not_a_spoc.com"
      );
    });
    it("should dispatch a DISCOVERY_STREAM_SPOC_BLOCKED for a blocked spoc", async () => {
      Object.defineProperty(feed, "showSpocs", { get: () => true });
      sandbox.spy(feed.store, "dispatch");

      await feed.onAction({
        type: at.PLACES_LINK_BLOCKED,
        data: { url: "foo.com" },
      });

      assert.equal(
        feed.store.dispatch.secondCall.args[0].type,
        "DISCOVERY_STREAM_SPOC_BLOCKED"
      );
    });
  });

  describe("#onAction: BLOCK_URL", () => {
    it("should call recordBlockFlightId whith BLOCK_URL", async () => {
      sandbox.stub(feed, "recordBlockFlightId").returns();

      await feed.onAction({
        type: at.BLOCK_URL,
        data: [
          {
            flight_id: "1234",
          },
        ],
      });

      assert.calledWith(feed.recordBlockFlightId, "1234");
    });
  });

  describe("#onAction: INIT", () => {
    it("should be .loaded=false before initialization", () => {
      assert.isFalse(feed.loaded);
    });
    it("should load data and set .loaded=true if config.enabled is true", async () => {
      sandbox.stub(feed.cache, "set").returns(Promise.resolve());
      setPref(CONFIG_PREF_NAME, { enabled: true });
      sandbox.stub(feed, "loadLayout").returns(Promise.resolve());

      await feed.onAction({ type: at.INIT });

      assert.calledOnce(feed.loadLayout);
      assert.isTrue(feed.loaded);
    });
  });

  describe("#onAction: DISCOVERY_STREAM_CONFIG_SET_VALUE", async () => {
    it("should add the new value to the pref without changing the existing values", async () => {
      sandbox.spy(feed.store, "dispatch");
      setPref(CONFIG_PREF_NAME, { enabled: true, other: "value" });

      await feed.onAction({
        type: at.DISCOVERY_STREAM_CONFIG_SET_VALUE,
        data: { name: "layout_endpoint", value: "foo.com" },
      });

      assert.calledWithMatch(feed.store.dispatch, {
        data: {
          name: CONFIG_PREF_NAME,
          value: JSON.stringify({
            enabled: true,
            other: "value",
            layout_endpoint: "foo.com",
          }),
        },
        type: at.SET_PREF,
      });
    });
  });

  describe("#onAction: DISCOVERY_STREAM_POCKET_STATE_INIT", async () => {
    it("should call setupPocketState", async () => {
      sandbox.spy(feed, "setupPocketState");
      feed.onAction({
        type: at.DISCOVERY_STREAM_POCKET_STATE_INIT,
        meta: { fromTarget: {} },
      });
      assert.calledOnce(feed.setupPocketState);
    });
  });

  describe("#onAction: DISCOVERY_STREAM_CONFIG_RESET", async () => {
    it("should call configReset", async () => {
      sandbox.spy(feed, "configReset");
      feed.onAction({
        type: at.DISCOVERY_STREAM_CONFIG_RESET,
      });
      assert.calledOnce(feed.configReset);
    });
  });

  describe("#onAction: DISCOVERY_STREAM_CONFIG_RESET_DEFAULTS", async () => {
    it("Should dispatch CLEAR_PREF with pref name", async () => {
      sandbox.spy(feed.store, "dispatch");
      await feed.onAction({
        type: at.DISCOVERY_STREAM_CONFIG_RESET_DEFAULTS,
      });

      assert.calledWithMatch(feed.store.dispatch, {
        data: {
          name: CONFIG_PREF_NAME,
        },
        type: at.CLEAR_PREF,
      });
    });
  });

  describe("#onAction: DISCOVERY_STREAM_RETRY_FEED", async () => {
    it("should call retryFeed", async () => {
      sandbox.spy(feed, "retryFeed");
      feed.onAction({
        type: at.DISCOVERY_STREAM_RETRY_FEED,
        data: { feed: { url: "https://feed.com" } },
      });
      assert.calledOnce(feed.retryFeed);
      assert.calledWith(feed.retryFeed, { url: "https://feed.com" });
    });
  });

  describe("#onAction: DISCOVERY_STREAM_CONFIG_CHANGE", () => {
    it("should call this.loadLayout if config.enabled changes to true ", async () => {
      sandbox.stub(feed.cache, "set").returns(Promise.resolve());
      // First initialize
      await feed.onAction({ type: at.INIT });
      assert.isFalse(feed.loaded);

      // force clear cached pref value
      feed._prefCache = {};
      setPref(CONFIG_PREF_NAME, { enabled: true });

      sandbox.stub(feed, "resetCache").returns(Promise.resolve());
      sandbox.stub(feed, "loadLayout").returns(Promise.resolve());
      await feed.onAction({ type: at.DISCOVERY_STREAM_CONFIG_CHANGE });

      assert.calledOnce(feed.loadLayout);
      assert.calledOnce(feed.resetCache);
      assert.isTrue(feed.loaded);
    });
    it("should clear the cache if a config change happens and config.enabled is true", async () => {
      sandbox.stub(feed.cache, "set").returns(Promise.resolve());
      // force clear cached pref value
      feed._prefCache = {};
      setPref(CONFIG_PREF_NAME, { enabled: true });

      sandbox.stub(feed, "resetCache").returns(Promise.resolve());
      await feed.onAction({ type: at.DISCOVERY_STREAM_CONFIG_CHANGE });

      assert.calledOnce(feed.resetCache);
    });
    it("should dispatch DISCOVERY_STREAM_LAYOUT_RESET from DISCOVERY_STREAM_CONFIG_CHANGE", async () => {
      sandbox.stub(feed, "resetDataPrefs");
      sandbox.stub(feed, "resetCache").resolves();
      sandbox.stub(feed, "enable").resolves();
      setPref(CONFIG_PREF_NAME, { enabled: true });
      sandbox.spy(feed.store, "dispatch");

      await feed.onAction({ type: at.DISCOVERY_STREAM_CONFIG_CHANGE });

      assert.calledWithMatch(feed.store.dispatch, {
        type: at.DISCOVERY_STREAM_LAYOUT_RESET,
      });
    });
    it("should not call this.loadLayout if config.enabled changes to false", async () => {
      sandbox.stub(feed.cache, "set").returns(Promise.resolve());
      // force clear cached pref value
      feed._prefCache = {};
      setPref(CONFIG_PREF_NAME, { enabled: true });

      await feed.onAction({ type: at.INIT });
      assert.isTrue(feed.loaded);

      feed._prefCache = {};
      setPref(CONFIG_PREF_NAME, { enabled: false });
      sandbox.stub(feed, "resetCache").returns(Promise.resolve());
      sandbox.stub(feed, "loadLayout").returns(Promise.resolve());
      await feed.onAction({ type: at.DISCOVERY_STREAM_CONFIG_CHANGE });

      assert.notCalled(feed.loadLayout);
      assert.calledOnce(feed.resetCache);
      assert.isFalse(feed.loaded);
    });
  });

  describe("#onAction: UNINIT", () => {
    it("should reset pref cache", async () => {
      feed._prefCache = { cached: "value" };

      await feed.onAction({ type: at.UNINIT });

      assert.deepEqual(feed._prefCache, {});
    });
  });

  describe("#onAction: PREF_CHANGED", () => {
    it("should update state.DiscoveryStream.config when the pref changes", async () => {
      setPref(CONFIG_PREF_NAME, {
        enabled: true,
        show_spocs: false,
        layout_endpoint: "foo",
      });

      assert.deepEqual(feed.store.getState().DiscoveryStream.config, {
        enabled: true,
        show_spocs: false,
        layout_endpoint: "foo",
      });
    });
    it("should fire loadSpocs is showSponsored pref changes", async () => {
      sandbox.stub(feed, "loadSpocs").returns(Promise.resolve());

      await feed.onAction({
        type: at.PREF_CHANGED,
        data: { name: "showSponsored" },
      });

      assert.calledOnce(feed.loadSpocs);
    });
    it("should fire onPrefChange when pocketConfig pref changes", async () => {
      sandbox.stub(feed, "onPrefChange").returns(Promise.resolve());

      await feed.onAction({
        type: at.PREF_CHANGED,
        data: { name: "pocketConfig", value: false },
      });

      assert.calledOnce(feed.onPrefChange);
    });
    it("should fire onCollectionsChanged when collections pref changes", async () => {
      sandbox.stub(feed, "onCollectionsChanged").returns(Promise.resolve());

      await feed.onAction({
        type: at.PREF_CHANGED,
        data: { name: "discoverystream.sponsored-collections.enabled" },
      });

      assert.calledOnce(feed.onCollectionsChanged);
    });
    it("should re enable stories when top stories is turned on", async () => {
      sandbox.stub(feed, "refreshAll").returns(Promise.resolve());
      feed.loaded = true;
      setPref(CONFIG_PREF_NAME, {
        enabled: true,
      });

      await feed.onAction({
        type: at.PREF_CHANGED,
        data: { name: "feeds.section.topstories", value: true },
      });

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

  describe("#onAction: SYSTEM_TICK", () => {
    it("should not refresh if DiscoveryStream has not been loaded", async () => {
      sandbox.stub(feed, "refreshAll").resolves();
      setPref(CONFIG_PREF_NAME, { enabled: true });

      await feed.onAction({ type: at.SYSTEM_TICK });
      assert.notCalled(feed.refreshAll);
    });

    it("should not refresh if no caches are expired", async () => {
      sandbox.stub(feed.cache, "set").resolves();
      setPref(CONFIG_PREF_NAME, { enabled: true });

      await feed.onAction({ type: at.INIT });

      sandbox.stub(feed, "checkIfAnyCacheExpired").resolves(false);
      sandbox.stub(feed, "refreshAll").resolves();

      await feed.onAction({ type: at.SYSTEM_TICK });
      assert.notCalled(feed.refreshAll);
    });

    it("should refresh if DiscoveryStream has been loaded at least once and a cache has expired", async () => {
      sandbox.stub(feed.cache, "set").resolves();
      setPref(CONFIG_PREF_NAME, { enabled: true });

      await feed.onAction({ type: at.INIT });

      sandbox.stub(feed, "checkIfAnyCacheExpired").resolves(true);
      sandbox.stub(feed, "refreshAll").resolves();

      await feed.onAction({ type: at.SYSTEM_TICK });
      assert.calledOnce(feed.refreshAll);
    });

    it("should refresh and not update open tabs if DiscoveryStream has been loaded at least once", async () => {
      sandbox.stub(feed.cache, "set").resolves();
      setPref(CONFIG_PREF_NAME, { enabled: true });

      await feed.onAction({ type: at.INIT });

      sandbox.stub(feed, "checkIfAnyCacheExpired").resolves(true);
      sandbox.stub(feed, "refreshAll").resolves();

      await feed.onAction({ type: at.SYSTEM_TICK });
      assert.calledWith(feed.refreshAll, { updateOpenTabs: false });
    });
  });

  describe("#onCollectionsChanged", () => {
    it("should call loadLayout when Pocket config changes", async () => {
      sandbox.stub(feed, "loadLayout").callsFake(dispatch => dispatch("foo"));
      sandbox.stub(feed.store, "dispatch");
      await feed.onCollectionsChanged();
      assert.calledOnce(feed.loadLayout);
      assert.calledWith(feed.store.dispatch, ac.AlsoToPreloaded("foo"));
    });
  });

  describe("#onPrefChange", () => {
    it("should call loadLayout when Pocket config changes", async () => {
      sandbox.stub(feed, "loadLayout");
      feed._prefCache.config = {
        enabled: true,
      };
      await feed.onPrefChange();
      assert.calledOnce(feed.loadLayout);
    });
  });

  describe("#onAction: PREF_SHOW_SPONSORED", () => {
    it("should call loadSpocs when preference changes", async () => {
      sandbox.stub(feed, "loadSpocs").resolves();
      sandbox.stub(feed.store, "dispatch");

      await feed.onAction({
        type: at.PREF_CHANGED,
        data: { name: "showSponsored" },
      });

      assert.calledOnce(feed.loadSpocs);
      const [dispatchFn] = feed.loadSpocs.firstCall.args;
      dispatchFn({});
      assert.calledWith(feed.store.dispatch, ac.BroadcastToContent({}));
    });
  });

  describe("#onAction: DISCOVERY_STREAM_DEV_IDLE_DAILY", () => {
    it("should trigger idle-daily observer", async () => {
      sandbox.stub(global.Services.obs, "notifyObservers").returns();
      await feed.onAction({
        type: at.DISCOVERY_STREAM_DEV_IDLE_DAILY,
      });
      assert.calledWith(
        global.Services.obs.notifyObservers,
        null,
        "idle-daily"
      );
    });
  });

  describe("#onAction: DISCOVERY_STREAM_DEV_SYNC_RS", () => {
    it("should fire remote settings pollChanges", async () => {
      sandbox.stub(global.RemoteSettings, "pollChanges").returns();
      await feed.onAction({
        type: at.DISCOVERY_STREAM_DEV_SYNC_RS,
      });
      assert.calledOnce(global.RemoteSettings.pollChanges);
    });
  });

  describe("#onAction: DISCOVERY_STREAM_DEV_SYSTEM_TICK", () => {
    it("should refresh if DiscoveryStream has been loaded at least once and a cache has expired", async () => {
      sandbox.stub(feed.cache, "set").resolves();
      setPref(CONFIG_PREF_NAME, { enabled: true });

      await feed.onAction({ type: at.INIT });

      sandbox.stub(feed, "checkIfAnyCacheExpired").resolves(true);
      sandbox.stub(feed, "refreshAll").resolves();

      await feed.onAction({ type: at.DISCOVERY_STREAM_DEV_SYSTEM_TICK });
      assert.calledOnce(feed.refreshAll);
    });
  });

  describe("#onAction: DISCOVERY_STREAM_DEV_EXPIRE_CACHE", () => {
    it("should fire resetCache", async () => {
      sandbox.stub(feed, "resetContentCache").returns();
      await feed.onAction({
        type: at.DISCOVERY_STREAM_DEV_EXPIRE_CACHE,
      });
      assert.calledOnce(feed.resetContentCache);
    });
  });

  describe("#spocsCacheUpdateTime", () => {
    it("should call setupSpocsCacheUpdateTime", () => {
      const defaultCacheTime = 30 * 60 * 1000;
      sandbox.spy(feed, "setupSpocsCacheUpdateTime");
      const cacheTime = feed.spocsCacheUpdateTime;
      assert.equal(feed._spocsCacheUpdateTime, defaultCacheTime);
      assert.equal(cacheTime, defaultCacheTime);
      assert.calledOnce(feed.setupSpocsCacheUpdateTime);
    });
    it("should return _spocsCacheUpdateTime", () => {
      sandbox.spy(feed, "setupSpocsCacheUpdateTime");
      const testCacheTime = 123;
      feed._spocsCacheUpdateTime = testCacheTime;
      const cacheTime = feed.spocsCacheUpdateTime;
      // Ensure _spocsCacheUpdateTime was not changed.
      assert.equal(feed._spocsCacheUpdateTime, testCacheTime);
      assert.equal(cacheTime, testCacheTime);
      assert.notCalled(feed.setupSpocsCacheUpdateTime);
    });
  });

  describe("#setupSpocsCacheUpdateTime", () => {
    it("should set _spocsCacheUpdateTime with default value", () => {
      const defaultCacheTime = 30 * 60 * 1000;
      feed.setupSpocsCacheUpdateTime();
      assert.equal(feed._spocsCacheUpdateTime, defaultCacheTime);
    });
    it("should set _spocsCacheUpdateTime with min", () => {
      const defaultCacheTime = 30 * 60 * 1000;
      feed.store.getState = () => ({
        Prefs: {
          values: {
            pocketConfig: {
              spocsCacheTimeout: 1,
            },
          },
        },
      });
      feed.setupSpocsCacheUpdateTime();
      assert.equal(feed._spocsCacheUpdateTime, defaultCacheTime);
    });
    it("should set _spocsCacheUpdateTime with max", () => {
      const defaultCacheTime = 30 * 60 * 1000;
      feed.store.getState = () => ({
        Prefs: {
          values: {
            pocketConfig: {
              spocsCacheTimeout: 31,
            },
          },
        },
      });
      feed.setupSpocsCacheUpdateTime();
      assert.equal(feed._spocsCacheUpdateTime, defaultCacheTime);
    });
    it("should set _spocsCacheUpdateTime with spocsCacheTimeout", () => {
      feed.store.getState = () => ({
        Prefs: {
          values: {
            pocketConfig: {
              spocsCacheTimeout: 20,
            },
          },
        },
      });
      feed.setupSpocsCacheUpdateTime();
      assert.equal(feed._spocsCacheUpdateTime, 20 * 60 * 1000);
    });
  });

  describe("#isExpired", () => {
    it("should throw if the key is not valid", () => {
      assert.throws(() => {
        feed.isExpired({}, "foo");
      });
    });
    it("should return false for layout on startup for content under 1 week", () => {
      const layout = { lastUpdated: Date.now() };
      const result = feed.isExpired({
        cachedData: { layout },
        key: "layout",
        isStartup: true,
      });

      assert.isFalse(result);
    });
    it("should return true for layout for isStartup=false after 30 mins", () => {
      const layout = { lastUpdated: Date.now() };
      clock.tick(THIRTY_MINUTES + 1);
      const result = feed.isExpired({ cachedData: { layout }, key: "layout" });

      assert.isTrue(result);
    });
    it("should return true for layout on startup for content over 1 week", () => {
      const layout = { lastUpdated: Date.now() };
      clock.tick(ONE_WEEK + 1);
      const result = feed.isExpired({
        cachedData: { layout },
        key: "layout",
        isStartup: true,
      });

      assert.isTrue(result);
    });
    it("should return false for hardcoded layout on startup for content over 1 week", () => {
      feed._prefCache.config = {
        hardcoded_layout: true,
      };
      const layout = { lastUpdated: Date.now() };
      clock.tick(ONE_WEEK + 1);
      const result = feed.isExpired({
        cachedData: { layout },
        key: "layout",
        isStartup: true,
      });

      assert.isFalse(result);
    });
  });

  describe("#checkIfAnyCacheExpired", () => {
    let cache;
    beforeEach(() => {
      cache = {
        layout: { lastUpdated: Date.now() },
        feeds: { "foo.com": { lastUpdated: Date.now() } },
        spocs: { lastUpdated: Date.now() },
      };
      Object.defineProperty(feed, "showSpocs", { get: () => true });
      sandbox.stub(feed.cache, "get").resolves(cache);
    });

    it("should return false if nothing in the cache is expired", async () => {
      const result = await feed.checkIfAnyCacheExpired();
      assert.isFalse(result);
    });

    it("should return true if .layout is missing", async () => {
      delete cache.layout;
      assert.isTrue(await feed.checkIfAnyCacheExpired());
    });
    it("should return true if .layout is expired", async () => {
      clock.tick(THIRTY_MINUTES + 1);
      // Update other caches we aren't testing
      cache.feeds["foo.com"].lastUpdate = Date.now();
      cache.spocs.lastUpdate = Date.now();

      assert.isTrue(await feed.checkIfAnyCacheExpired());
    });

    it("should return true if .spocs is missing", async () => {
      delete cache.spocs;
      assert.isTrue(await feed.checkIfAnyCacheExpired());
    });
    it("should return true if .spocs is expired", async () => {
      clock.tick(THIRTY_MINUTES + 1);
      // Update other caches we aren't testing
      cache.layout.lastUpdated = Date.now();
      cache.feeds["foo.com"].lastUpdate = Date.now();

      assert.isTrue(await feed.checkIfAnyCacheExpired());
    });

    it("should return true if .feeds is missing", async () => {
      delete cache.feeds;
      assert.isTrue(await feed.checkIfAnyCacheExpired());
    });
    it("should return true if data for .feeds[url] is missing", async () => {
      cache.feeds["foo.com"] = null;
      assert.isTrue(await feed.checkIfAnyCacheExpired());
    });
    it("should return true if data for .feeds[url] is expired", async () => {
      clock.tick(THIRTY_MINUTES + 1);
      // Update other caches we aren't testing
      cache.layout.lastUpdated = Date.now();
      cache.spocs.lastUpdate = Date.now();
      assert.isTrue(await feed.checkIfAnyCacheExpired());
    });
  });

  describe("#refreshAll", () => {
    beforeEach(() => {
      sandbox.stub(feed, "loadLayout").resolves();
      sandbox.stub(feed, "loadComponentFeeds").resolves();
      sandbox.stub(feed, "loadSpocs").resolves();
      sandbox.spy(feed.store, "dispatch");
      Object.defineProperty(feed, "showSpocs", { get: () => true });
    });

    it("should call layout, component, spocs update and telemetry reporting functions", async () => {
      await feed.refreshAll();

      assert.calledOnce(feed.loadLayout);
      assert.calledOnce(feed.loadComponentFeeds);
      assert.calledOnce(feed.loadSpocs);
    });
    it("should pass in dispatch wrapped with broadcast if options.updateOpenTabs is true", async () => {
      await feed.refreshAll({ updateOpenTabs: true });
      [feed.loadLayout, feed.loadComponentFeeds, feed.loadSpocs].forEach(fn => {
        assert.calledOnce(fn);
        const result = fn.firstCall.args[0]({ type: "FOO" });
        assert.isTrue(au.isBroadcastToContent(result));
      });
    });
    it("should pass in dispatch with regular actions if options.updateOpenTabs is false", async () => {
      await feed.refreshAll({ updateOpenTabs: false });
      [feed.loadLayout, feed.loadComponentFeeds, feed.loadSpocs].forEach(fn => {
        assert.calledOnce(fn);
        const result = fn.firstCall.args[0]({ type: "FOO" });
        assert.deepEqual(result, { type: "FOO" });
      });
    });
    it("should set loaded to true if loadSpocs and loadComponentFeeds fails", async () => {
      feed.loadComponentFeeds.rejects("loadComponentFeeds error");
      feed.loadSpocs.rejects("loadSpocs error");

      await feed.enable();

      assert.isTrue(feed.loaded);
    });
    it("should call loadComponentFeeds and loadSpocs in Promise.all", async () => {
      sandbox.stub(global.Promise, "all").resolves();

      await feed.refreshAll();

      assert.calledOnce(global.Promise.all);
      const { args } = global.Promise.all.firstCall;
      assert.equal(args[0].length, 2);
    });
    describe("test startup cache behaviour", () => {
      beforeEach(() => {
        feed._maybeUpdateCachedData.restore();
        sandbox.stub(feed.cache, "set").resolves();
      });
      it("should refresh layout on startup if it was served from cache", async () => {
        feed.loadLayout.restore();
        sandbox
          .stub(feed.cache, "get")
          .resolves({ layout: { lastUpdated: Date.now(), layout: {} } });
        sandbox.stub(feed, "fetchFromEndpoint").resolves({ layout: {} });
        clock.tick(THIRTY_MINUTES + 1);

        await feed.refreshAll({ isStartup: true });

        assert.calledOnce(feed.fetchFromEndpoint);
        // Once from cache, once to update the store
        assert.calledTwice(feed.store.dispatch);
        assert.equal(
          feed.store.dispatch.firstCall.args[0].type,
          at.DISCOVERY_STREAM_LAYOUT_UPDATE
        );
      });
      it("should not refresh layout on startup if it is under THIRTY_MINUTES", async () => {
        feed.loadLayout.restore();
        sandbox
          .stub(feed.cache, "get")
          .resolves({ layout: { lastUpdated: Date.now(), layout: {} } });
        sandbox.stub(feed, "fetchFromEndpoint").resolves({ layout: {} });

        await feed.refreshAll({ isStartup: true });

        assert.notCalled(feed.fetchFromEndpoint);
      });
      it("should refresh spocs on startup if it was served from cache", async () => {
        feed.loadSpocs.restore();
        sandbox.stub(feed, "getPlacements").returns([{ name: "spocs" }]);
        sandbox
          .stub(feed.cache, "get")
          .resolves({ spocs: { lastUpdated: Date.now() } });
        sandbox.stub(feed, "fetchFromEndpoint").resolves("data");
        clock.tick(THIRTY_MINUTES + 1);

        await feed.refreshAll({ isStartup: true });

        assert.calledOnce(feed.fetchFromEndpoint);
        // Once from cache, once to update the store
        assert.calledTwice(feed.store.dispatch);
        assert.equal(
          feed.store.dispatch.firstCall.args[0].type,
          at.DISCOVERY_STREAM_SPOCS_UPDATE
        );
      });
      it("should not refresh spocs on startup if it is under THIRTY_MINUTES", async () => {
        feed.loadSpocs.restore();
        sandbox
          .stub(feed.cache, "get")
          .resolves({ spocs: { lastUpdated: Date.now() } });
        sandbox.stub(feed, "fetchFromEndpoint").resolves("data");

        await feed.refreshAll({ isStartup: true });

        assert.notCalled(feed.fetchFromEndpoint);
      });
      it("should refresh feeds on startup if it was served from cache", async () => {
        feed.loadComponentFeeds.restore();

        const fakeComponents = { components: [{ feed: { url: "foo.com" } }] };
        const fakeLayout = [fakeComponents];
        const fakeDiscoveryStream = {
          DiscoveryStream: {
            layout: fakeLayout,
          },
          Prefs: {
            values: {
              "feeds.section.topstories": true,
              "feeds.system.topstories": true,
            },
          },
        };
        sandbox.stub(feed.store, "getState").returns(fakeDiscoveryStream);
        sandbox.stub(feed, "rotate").callsFake(val => val);
        sandbox
          .stub(feed, "scoreItems")
          .callsFake(val => ({ data: val, filtered: [] }));
        sandbox.stub(feed, "cleanUpTopRecImpressionPref").callsFake(val => val);

        const fakeCache = {
          feeds: { "foo.com": { lastUpdated: Date.now(), data: "data" } },
        };
        sandbox.stub(feed.cache, "get").resolves(fakeCache);
        clock.tick(THIRTY_MINUTES + 1);
        sandbox.stub(feed, "fetchFromEndpoint").resolves({
          recommendations: "data",
          settings: {
            recsExpireTime: 1,
          },
        });

        await feed.refreshAll({ isStartup: true });

        assert.calledOnce(feed.fetchFromEndpoint);
        // Once from cache, once to update the feed, once to update that all feeds are done.
        assert.calledThrice(feed.store.dispatch);
        assert.equal(
          feed.store.dispatch.secondCall.args[0].type,
          at.DISCOVERY_STREAM_FEEDS_UPDATE
        );
      });
    });
  });

  describe("#scoreFeeds", () => {
    it("should score feeds and set cache, and dispatch", async () => {
      sandbox.stub(feed.cache, "set").resolves();
      sandbox.spy(feed.store, "dispatch");
      const recsExpireTime = 5600;
      const fakeImpressions = {
        first: Date.now() - recsExpireTime * 1000,
        third: Date.now(),
      };
      sandbox.stub(feed, "readDataPref").returns(fakeImpressions);
      const fakeFeeds = {
        data: {
          "https://foo.com": {
            data: {
              recommendations: [
                {
                  id: "first",
                  item_score: 0.7,
                },
                {
                  id: "second",
                  item_score: 0.6,
                },
              ],
              settings: {
                recsExpireTime,
              },
            },
          },
          "https://bar.com": {
            data: {
              recommendations: [
                {
                  id: "third",
                  item_score: 0.4,
                },
                {
                  id: "fourth",
                  item_score: 0.6,
                },
                {
                  id: "fifth",
                  item_score: 0.8,
                },
              ],
              settings: {
                recsExpireTime,
              },
            },
          },
        },
      };
      const feedsTestResult = {
        "https://foo.com": {
          data: {
            recommendations: [
              {
                id: "second",
                item_score: 0.6,
                score: 0.6,
              },
              {
                id: "first",
                item_score: 0.7,
                score: 0.7,
              },
            ],
            settings: {
              recsExpireTime,
            },
          },
        },
        "https://bar.com": {
          data: {
            recommendations: [
              {
                id: "fifth",
                item_score: 0.8,
                score: 0.8,
              },
              {
                id: "fourth",
                item_score: 0.6,
                score: 0.6,
              },
              {
                id: "third",
                item_score: 0.4,
                score: 0.4,
              },
            ],
            settings: {
              recsExpireTime,
            },
          },
        },
      };

      await feed.scoreFeeds(fakeFeeds);

      assert.calledWith(feed.cache.set, "feeds", feedsTestResult);
      assert.equal(
        feed.store.dispatch.firstCall.args[0].type,
        at.DISCOVERY_STREAM_FEED_UPDATE
      );
      assert.deepEqual(feed.store.dispatch.firstCall.args[0].data, {
        url: "https://foo.com",
        feed: feedsTestResult["https://foo.com"],
      });
      assert.equal(
        feed.store.dispatch.secondCall.args[0].type,
        at.DISCOVERY_STREAM_FEED_UPDATE
      );
      assert.deepEqual(feed.store.dispatch.secondCall.args[0].data, {
        url: "https://bar.com",
        feed: feedsTestResult["https://bar.com"],
      });
    });
  });

  describe("#scoreSpocs", () => {
    it("should score spocs and set cache, dispatch", async () => {
      sandbox.stub(feed.cache, "set").resolves();
      sandbox.spy(feed.store, "dispatch");
      const fakeDiscoveryStream = {
        Prefs: {
          values: {
            "discoverystream.spocs.personalized": true,
            "discoverystream.recs.personalized": false,
          },
        },
        DiscoveryStream: {
          spocs: {
            placements: [
              { name: "placement1" },
              { name: "placement2" },
              { name: "placement3" },
            ],
          },
        },
      };
      sandbox.stub(feed.store, "getState").returns(fakeDiscoveryStream);
      const fakeSpocs = {
        lastUpdated: 1234,
        data: {
          placement1: {
            items: [
              {
                item_score: 0.6,
              },
              {
                item_score: 0.4,
              },
              {
                item_score: 0.8,
              },
            ],
          },
          placement2: {
            items: [
              {
                item_score: 0.6,
              },
              {
                item_score: 0.8,
              },
            ],
          },
          placement3: { items: [] },
        },
      };

      await feed.scoreSpocs(fakeSpocs);

      const spocsTestResult = {
        lastUpdated: 1234,
        spocs: {
          placement1: {
            items: [
              {
                score: 0.8,
                item_score: 0.8,
              },
              {
                score: 0.6,
                item_score: 0.6,
              },
              {
                score: 0.4,
                item_score: 0.4,
              },
            ],
          },
          placement2: {
            items: [
              {
                score: 0.8,
                item_score: 0.8,
              },
              {
                score: 0.6,
                item_score: 0.6,
              },
            ],
          },
          placement3: { items: [] },
        },
      };
      assert.calledWith(feed.cache.set, "spocs", spocsTestResult);
      assert.equal(
        feed.store.dispatch.firstCall.args[0].type,
        at.DISCOVERY_STREAM_SPOCS_UPDATE
      );
      assert.deepEqual(
        feed.store.dispatch.firstCall.args[0].data,
        spocsTestResult
      );
    });
  });

  describe("#scoreContent", () => {
    it("should call scoreFeeds and scoreSpocs if loaded", async () => {
      const fakeDiscoveryStream = {
        Prefs: {
          values: {
            pocketConfig: {
              recsPersonalized: true,
              spocsPersonalized: true,
            },
            "discoverystream.personalization.enabled": true,
            "feeds.section.topstories": true,
            "feeds.system.topstories": true,
          },
        },
        DiscoveryStream: {
          feeds: { loaded: false },
          spocs: { loaded: false },
        },
      };

      sandbox.stub(feed, "scoreFeeds").resolves();
      sandbox.stub(feed, "scoreSpocs").resolves();
      sandbox.stub(feed, "refreshContent").resolves();
      sandbox.stub(feed, "loadPersonalizationScoresCache").resolves();
      sandbox.stub(feed.store, "getState").returns(fakeDiscoveryStream);
      sandbox.stub(feed, "_checkExpirationPerComponent").resolves({
        feeds: true,
        spocs: true,
      });

      await feed.refreshAll();

      assert.notCalled(feed.scoreFeeds);
      assert.notCalled(feed.scoreSpocs);

      fakeDiscoveryStream.DiscoveryStream.feeds.loaded = true;
      fakeDiscoveryStream.DiscoveryStream.spocs.loaded = true;

      await feed.refreshAll();

      assert.calledOnce(feed.scoreFeeds);
      assert.calledOnce(feed.scoreSpocs);
    });
  });

  describe("#loadPersonalizationScoresCache", () => {
    it("should create a personalization provider from cached scores", async () => {
      feed.store.getState = () => ({
        Prefs: {
          values: {
            pocketConfig: {
              recsPersonalized: true,
              spocsPersonalized: true,
            },
            "discoverystream.personalization.enabled": true,
            "feeds.section.topstories": true,
            "feeds.system.topstories": true,
          },
        },
      });
      const fakeCache = {
        personalization: {
          scores: 123,
          _timestamp: 456,
        },
      };
      sandbox.stub(feed.cache, "get").returns(Promise.resolve(fakeCache));

      await feed.loadPersonalizationScoresCache();

      assert.equal(feed.personalizationLastUpdated, 456);
    });
  });

  describe("#observe", () => {
    it("should call updatePersonalizationScores on idle daily", async () => {
      sandbox.stub(feed, "updatePersonalizationScores").returns();
      feed.observe(null, "idle-daily");
      assert.calledOnce(feed.updatePersonalizationScores);
    });
    it("should call configReset on Pocket button pref change", async () => {
      sandbox.stub(feed, "configReset").returns();
      feed.observe(null, "nsPref:changed", "extensions.pocket.enabled");
      assert.calledOnce(feed.configReset);
    });
  });

  describe("#updatePersonalizationScores", () => {
    it("should update recommendationProvider on updatePersonalizationScores", async () => {
      feed.store.getState = () => ({
        Prefs: {
          values: {
            pocketConfig: {
              recsPersonalized: true,
              spocsPersonalized: true,
            },
            "discoverystream.personalization.enabled": true,
            "feeds.section.topstories": true,
            "feeds.system.topstories": true,
          },
        },
      });
      sandbox.stub(feed.recommendationProvider, "init").returns();

      await feed.updatePersonalizationScores();

      assert.deepEqual(feed.recommendationProvider.provider.getScores(), {
        interestConfig: undefined,
        interestVector: undefined,
      });
    });
    it("should not update recommendationProvider on updatePersonalizationScores", async () => {
      feed.store.getState = () => ({
        Prefs: {
          values: {
            "discoverystream.spocs.personalized": true,
            "discoverystream.recs.personalized": true,
            "discoverystream.personalization.enabled": false,
          },
        },
      });
      await feed.updatePersonalizationScores();

      assert.isTrue(!feed.recommendationProvider.provider);
    });
  });
  describe("#scoreItem", () => {
    it("should call calculateItemRelevanceScore with recommendationProvider with initial score", async () => {
      const item = {
        item_score: 0.6,
      };
      feed.store.getState = () => ({
        Prefs: {
          values: {
            pocketConfig: {
              recsPersonalized: true,
              spocsPersonalized: true,
            },
            "discoverystream.personalization.enabled": true,
            "feeds.section.topstories": true,
            "feeds.system.topstories": true,
          },
        },
      });
      feed.recommendationProvider.calculateItemRelevanceScore = sandbox
        .stub()
        .returns();
      const result = await feed.scoreItem(item, true);
      assert.calledOnce(
        feed.recommendationProvider.calculateItemRelevanceScore
      );
      assert.equal(result.score, 0.6);
    });
    it("should fallback to score 1 without an initial score", async () => {
      const item = {};
      feed.store.getState = () => ({
        Prefs: {
          values: {
            "discoverystream.spocs.personalized": true,
            "discoverystream.recs.personalized": true,
            "discoverystream.personalization.enabled": true,
          },
        },
      });
      feed.recommendationProvider.calculateItemRelevanceScore = sandbox
        .stub()
        .returns();
      const result = await feed.scoreItem(item, true);
      assert.equal(result.score, 1);
    });
  });
  describe("new proxy feed", () => {
    beforeEach(() => {
      feed.store = createStore(combineReducers(reducers), {
        Prefs: {
          values: {
            pocketConfig: { regionBffConfig: "DE" },
          },
        },
      });
      sandbox.stub(global.Region, "home").get(() => "DE");
      globals.set("NimbusFeatures", {
        saveToPocket: {
          getVariable: sandbox.stub(),
        },
      });
      global.NimbusFeatures.saveToPocket.getVariable
        .withArgs("bffApi")
        .returns("bffApi");
      global.NimbusFeatures.saveToPocket.getVariable
        .withArgs("oAuthConsumerKeyBff")
        .returns("oAuthConsumerKeyBff");
    });
    it("should return true with isBff", async () => {
      assert.isUndefined(feed._isBff);
      assert.isTrue(feed.isBff);
      assert.isTrue(feed._isBff);
    });
    it("should update to new feed url", async () => {
      await feed.loadLayout(feed.store.dispatch);
      const { layout } = feed.store.getState().DiscoveryStream;
      assert.equal(
        layout[0].components[2].feed.url,
        "https://bffApi/desktop/v1/recommendations?locale=$locale&region=$region&count=30"
      );
    });
    it("should fetch proper data from getComponentFeed", async () => {
      const fakeCache = {};
      sandbox.stub(feed.cache, "get").returns(Promise.resolve(fakeCache));
      sandbox.stub(feed, "rotate").callsFake(val => val);
      sandbox
        .stub(feed, "scoreItems")
        .callsFake(val => ({ data: val, filtered: [] }));
      sandbox.stub(feed, "fetchFromEndpoint").resolves({
        data: [
          {
            tileId: 1234,
            url: "url",
            title: "title",
            excerpt: "excerpt",
            publisher: "publisher",
            imageUrl: "imageUrl",
          },
        ],
      });

      const feedData = await feed.getComponentFeed("url");
      assert.deepEqual(feedData, {
        lastUpdated: 0,
        data: {
          settings: {},
          recommendations: [
            {
              id: 1234,
              url: "url",
              title: "title",
              excerpt: "excerpt",
              publisher: "publisher",
              raw_image_src: "imageUrl",
            },
          ],
          status: "success",
        },
      });
      assert.equal(feed.fetchFromEndpoint.firstCall.args[0], "url");
      assert.equal(feed.fetchFromEndpoint.firstCall.args[1].method, "GET");
      assert.equal(
        feed.fetchFromEndpoint.firstCall.args[1].headers.get("consumer_key"),
        "oAuthConsumerKeyBff"
      );
    });
  });
});
