/* eslint max-nested-callbacks: ["error", 100] */

import { CFRPageActions, PageAction } from "lib/CFRPageActions.jsm";
import { FAKE_RECOMMENDATION } from "./constants";
import { GlobalOverrider } from "test/unit/utils";
import { CFRMessageProvider } from "lib/CFRMessageProvider.sys.mjs";

describe("CFRPageActions", () => {
  let sandbox;
  let clock;
  let fakeRecommendation;
  let fakeHost;
  let fakeBrowser;
  let dispatchStub;
  let globals;
  let containerElem;
  let elements;
  let announceStub;
  let fakeRemoteL10n;

  const elementIDs = [
    "urlbar",
    "urlbar-input",
    "contextual-feature-recommendation",
    "cfr-button",
    "cfr-label",
    "contextual-feature-recommendation-notification",
    "cfr-notification-header-label",
    "cfr-notification-header-link",
    "cfr-notification-header-image",
    "cfr-notification-author",
    "cfr-notification-footer",
    "cfr-notification-footer-text",
    "cfr-notification-footer-filled-stars",
    "cfr-notification-footer-empty-stars",
    "cfr-notification-footer-users",
    "cfr-notification-footer-spacer",
    "cfr-notification-footer-learn-more-link",
  ];
  const elementClassNames = ["popup-notification-body-container"];

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

    announceStub = sandbox.stub();
    const A11yUtils = { announce: announceStub };
    fakeRecommendation = { ...FAKE_RECOMMENDATION };
    fakeHost = "mozilla.org";
    fakeBrowser = {
      documentURI: {
        scheme: "https",
        host: fakeHost,
      },
      ownerGlobal: window,
    };
    dispatchStub = sandbox.stub();

    fakeRemoteL10n = {
      l10n: {},
      reloadL10n: sandbox.stub(),
      createElement: sandbox.stub().returns(document.createElement("div")),
    };

    const gURLBar = document.createElement("div");
    gURLBar.textbox = document.createElement("div");

    globals = new GlobalOverrider();
    globals.set({
      RemoteL10n: fakeRemoteL10n,
      promiseDocumentFlushed: sandbox
        .stub()
        .callsFake(fn => Promise.resolve(fn())),
      PopupNotifications: {
        show: sandbox.stub(),
        remove: sandbox.stub(),
      },
      PrivateBrowsingUtils: { isWindowPrivate: sandbox.stub().returns(false) },
      gBrowser: { selectedBrowser: fakeBrowser },
      A11yUtils,
      gURLBar,
    });
    document.createXULElement = document.createElement;

    elements = {};
    const [body] = document.getElementsByTagName("body");
    containerElem = document.createElement("div");
    body.appendChild(containerElem);
    for (const id of elementIDs) {
      const elem = document.createElement("div");
      elem.setAttribute("id", id);
      containerElem.appendChild(elem);
      elements[id] = elem;
    }
    for (const className of elementClassNames) {
      const elem = document.createElement("div");
      elem.setAttribute("class", className);
      containerElem.appendChild(elem);
      elements[className] = elem;
    }
  });

  afterEach(() => {
    CFRPageActions.clearRecommendations();
    containerElem.remove();
    sandbox.restore();
    globals.restore();
  });

  describe("PageAction", () => {
    let pageAction;

    beforeEach(() => {
      pageAction = new PageAction(window, dispatchStub);
    });

    describe("#addImpression", () => {
      it("should call _sendTelemetry with the impression payload", () => {
        const recommendation = {
          id: "foo",
          content: { bucket_id: "bar" },
        };
        sandbox.spy(pageAction, "_sendTelemetry");

        pageAction.addImpression(recommendation);

        assert.calledWith(pageAction._sendTelemetry, {
          message_id: "foo",
          bucket_id: "bar",
          event: "IMPRESSION",
        });
      });
    });

    describe("#showAddressBarNotifier", () => {
      it("should un-hideAddressBarNotifier the element and set the right label value", async () => {
        await pageAction.showAddressBarNotifier(fakeRecommendation);
        assert.isFalse(pageAction.container.hidden);
        assert.equal(
          pageAction.label.value,
          fakeRecommendation.content.notification_text
        );
      });
      it("should wait for the document layout to flush", async () => {
        sandbox.spy(pageAction.label, "getClientRects");
        await pageAction.showAddressBarNotifier(fakeRecommendation);
        assert.calledOnce(global.promiseDocumentFlushed);
        assert.callOrder(
          global.promiseDocumentFlushed,
          pageAction.label.getClientRects
        );
      });
      it("should set the CSS variable --cfr-label-width correctly", async () => {
        await pageAction.showAddressBarNotifier(fakeRecommendation);
        const expectedWidth = pageAction.label.getClientRects()[0].width;
        assert.equal(
          pageAction.urlbarinput.style.getPropertyValue("--cfr-label-width"),
          `${expectedWidth}px`
        );
      });
      it("should cause an expansion, and dispatch an impression if `expand` is true", async () => {
        sandbox.spy(pageAction, "_clearScheduledStateChanges");
        sandbox.spy(pageAction, "_expand");
        sandbox.spy(pageAction, "_dispatchImpression");

        await pageAction.showAddressBarNotifier(fakeRecommendation);
        assert.notCalled(pageAction._dispatchImpression);
        clock.tick(1001);
        assert.notEqual(
          pageAction.urlbarinput.getAttribute("cfr-recommendation-state"),
          "expanded"
        );

        await pageAction.showAddressBarNotifier(fakeRecommendation, true);
        assert.calledOnce(pageAction._clearScheduledStateChanges);
        clock.tick(1001);
        assert.equal(
          pageAction.urlbarinput.getAttribute("cfr-recommendation-state"),
          "expanded"
        );
        assert.calledOnce(pageAction._dispatchImpression);
        assert.calledWith(pageAction._dispatchImpression, fakeRecommendation);
      });
      it("should send telemetry if `expand` is true and the id and bucket_id are provided", async () => {
        await pageAction.showAddressBarNotifier(fakeRecommendation, true);
        assert.calledWith(dispatchStub, {
          type: "DOORHANGER_TELEMETRY",
          data: {
            action: "cfr_user_event",
            source: "CFR",
            message_id: fakeRecommendation.id,
            bucket_id: fakeRecommendation.content.bucket_id,
            event: "IMPRESSION",
          },
        });
      });
    });

    describe("#hideAddressBarNotifier", () => {
      it("should hideAddressBarNotifier the container, cancel any state changes, and remove the state attribute", () => {
        sandbox.spy(pageAction, "_clearScheduledStateChanges");
        pageAction.hideAddressBarNotifier();
        assert.isTrue(pageAction.container.hidden);
        assert.calledOnce(pageAction._clearScheduledStateChanges);
        assert.isNull(
          pageAction.urlbar.getAttribute("cfr-recommendation-state")
        );
      });
      it("should remove the `currentNotification`", () => {
        const notification = {};
        pageAction.currentNotification = notification;
        pageAction.hideAddressBarNotifier();
        assert.calledWith(global.PopupNotifications.remove, notification);
      });
    });

    describe("#_expand", () => {
      beforeEach(() => {
        pageAction._clearScheduledStateChanges();
        pageAction.urlbar.removeAttribute("cfr-recommendation-state");
      });
      it("without a delay, should clear other state changes and set the state to 'expanded'", () => {
        sandbox.spy(pageAction, "_clearScheduledStateChanges");
        pageAction._expand();
        assert.calledOnce(pageAction._clearScheduledStateChanges);
        assert.equal(
          pageAction.urlbarinput.getAttribute("cfr-recommendation-state"),
          "expanded"
        );
      });
      it("with a delay, should set the expanded state after the correct amount of time", () => {
        const delay = 1234;
        pageAction._expand(delay);
        // We expect that an expansion has been scheduled
        assert.lengthOf(pageAction.stateTransitionTimeoutIDs, 1);
        clock.tick(delay + 1);
        assert.equal(
          pageAction.urlbarinput.getAttribute("cfr-recommendation-state"),
          "expanded"
        );
      });
    });

    describe("#_collapse", () => {
      beforeEach(() => {
        pageAction._clearScheduledStateChanges();
        pageAction.urlbar.removeAttribute("cfr-recommendation-state");
      });
      it("without a delay, should clear other state changes and set the state to collapsed only if it's already expanded", () => {
        sandbox.spy(pageAction, "_clearScheduledStateChanges");
        pageAction._collapse();
        assert.calledOnce(pageAction._clearScheduledStateChanges);
        assert.isNull(
          pageAction.urlbarinput.getAttribute("cfr-recommendation-state")
        );
        pageAction.urlbarinput.setAttribute(
          "cfr-recommendation-state",
          "expanded"
        );
        pageAction._collapse();
        assert.equal(
          pageAction.urlbarinput.getAttribute("cfr-recommendation-state"),
          "collapsed"
        );
      });
      it("with a delay, should set the collapsed state after the correct amount of time", () => {
        const delay = 1234;
        pageAction._collapse(delay);
        clock.tick(delay + 1);
        // The state was _not_ "expanded" and so should not have been set to "collapsed"
        assert.isNull(
          pageAction.urlbar.getAttribute("cfr-recommendation-state")
        );

        pageAction._expand();
        pageAction._collapse(delay);
        // We expect that a collapse has been scheduled
        assert.lengthOf(pageAction.stateTransitionTimeoutIDs, 1);
        clock.tick(delay + 1);
        // This time it was "expanded" so should now (after the delay) be "collapsed"
        assert.equal(
          pageAction.urlbarinput.getAttribute("cfr-recommendation-state"),
          "collapsed"
        );
      });
    });

    describe("#_clearScheduledStateChanges", () => {
      it("should call .clearTimeout on all stored timeoutIDs", () => {
        pageAction.stateTransitionTimeoutIDs = [42, 73, 1997];
        sandbox.spy(pageAction.window, "clearTimeout");
        pageAction._clearScheduledStateChanges();
        assert.calledThrice(pageAction.window.clearTimeout);
        assert.calledWith(pageAction.window.clearTimeout, 42);
        assert.calledWith(pageAction.window.clearTimeout, 73);
        assert.calledWith(pageAction.window.clearTimeout, 1997);
      });
    });

    describe("#_popupStateChange", () => {
      it("should collapse the notification and send dismiss telemetry on 'dismissed'", () => {
        pageAction._expand();

        sandbox.spy(pageAction, "_sendTelemetry");

        pageAction._popupStateChange("dismissed");
        assert.equal(
          pageAction.urlbarinput.getAttribute("cfr-recommendation-state"),
          "collapsed"
        );

        assert.equal(
          pageAction._sendTelemetry.lastCall.args[0].event,
          "DISMISS"
        );
      });
      it("should remove the notification on 'removed'", () => {
        pageAction._expand();
        const fakeNotification = {};

        pageAction.currentNotification = fakeNotification;
        pageAction._popupStateChange("removed");
        assert.calledOnce(global.PopupNotifications.remove);
        assert.calledWith(global.PopupNotifications.remove, fakeNotification);
      });
      it("should do nothing for other states", () => {
        pageAction._popupStateChange("opened");
        assert.notCalled(global.PopupNotifications.remove);
      });
    });

    describe("#dispatchUserAction", () => {
      it("should call ._dispatchCFRAction with the right action", () => {
        const fakeAction = {};
        pageAction.dispatchUserAction(fakeAction);
        assert.calledOnce(dispatchStub);
        assert.calledWith(
          dispatchStub,
          { type: "USER_ACTION", data: fakeAction },
          fakeBrowser
        );
      });
    });

    describe("#_dispatchImpression", () => {
      it("should call ._dispatchCFRAction with the right action", () => {
        pageAction._dispatchImpression("fake impression");
        assert.calledWith(dispatchStub, {
          type: "IMPRESSION",
          data: "fake impression",
        });
      });
    });

    describe("#_sendTelemetry", () => {
      it("should call ._dispatchCFRAction with the right action", () => {
        const fakePing = { message_id: 42 };
        pageAction._sendTelemetry(fakePing);
        assert.calledWith(dispatchStub, {
          type: "DOORHANGER_TELEMETRY",
          data: {
            action: "cfr_user_event",
            source: "CFR",
            message_id: 42,
          },
        });
      });
    });

    describe("#_blockMessage", () => {
      it("should call ._dispatchCFRAction with the right action", () => {
        pageAction._blockMessage("fake id");
        assert.calledOnce(dispatchStub);
        assert.calledWith(dispatchStub, {
          type: "BLOCK_MESSAGE_BY_ID",
          data: { id: "fake id" },
        });
      });
    });

    describe("#getStrings", () => {
      let formatMessagesStub;
      const localeStrings = [
        {
          value: "你好世界",
          attributes: [
            { name: "first_attr", value: 42 },
            { name: "second_attr", value: "some string" },
            { name: "third_attr", value: [1, 2, 3] },
          ],
        },
      ];

      beforeEach(() => {
        formatMessagesStub = sandbox
          .stub()
          .withArgs({ id: "hello_world" })
          .resolves(localeStrings);
        global.RemoteL10n.l10n.formatMessages = formatMessagesStub;
      });

      it("should return the argument if a string_id is not defined", async () => {
        assert.deepEqual(await pageAction.getStrings({}), {});
        assert.equal(await pageAction.getStrings("some string"), "some string");
      });
      it("should get the right locale string", async () => {
        assert.equal(
          await pageAction.getStrings({ string_id: "hello_world" }),
          localeStrings[0].value
        );
      });
      it("should return the right sub-attribute if specified", async () => {
        assert.equal(
          await pageAction.getStrings(
            { string_id: "hello_world" },
            "second_attr"
          ),
          "some string"
        );
      });
      it("should attach attributes to string overrides", async () => {
        const fromJson = { value: "Add Now", attributes: { accesskey: "A" } };

        const result = await pageAction.getStrings(fromJson);

        assert.equal(result, fromJson.value);
        assert.propertyVal(result.attributes, "accesskey", "A");
      });
      it("should return subAttributes when doing string overrides", async () => {
        const fromJson = { value: "Add Now", attributes: { accesskey: "A" } };

        const result = await pageAction.getStrings(fromJson, "accesskey");

        assert.equal(result, "A");
      });
      it("should resolve ftl strings and attach subAttributes", async () => {
        const fromFtl = { string_id: "cfr-doorhanger-extension-ok-button" };
        formatMessagesStub.resolves([
          { value: "Add Now", attributes: [{ name: "accesskey", value: "A" }] },
        ]);

        const result = await pageAction.getStrings(fromFtl);

        assert.equal(result, "Add Now");
        assert.propertyVal(result.attributes, "accesskey", "A");
      });
      it("should return subAttributes from ftl ids", async () => {
        const fromFtl = { string_id: "cfr-doorhanger-extension-ok-button" };
        formatMessagesStub.resolves([
          { value: "Add Now", attributes: [{ name: "accesskey", value: "A" }] },
        ]);

        const result = await pageAction.getStrings(fromFtl, "accesskey");

        assert.equal(result, "A");
      });
      it("should report an error when no attributes are present but subAttribute is requested", async () => {
        const fromJson = { value: "Foo" };
        const stub = sandbox.stub(global.console, "error");

        await pageAction.getStrings(fromJson, "accesskey");

        assert.calledOnce(stub);
        stub.restore();
      });
    });

    describe("#_cfrUrlbarButtonClick", () => {
      let translateElementsStub;
      let setAttributesStub;
      let getStringsStub;
      beforeEach(async () => {
        CFRPageActions.PageActionMap.set(fakeBrowser.ownerGlobal, pageAction);
        await CFRPageActions.addRecommendation(
          fakeBrowser,
          fakeHost,
          fakeRecommendation,
          dispatchStub
        );
        getStringsStub = sandbox.stub(pageAction, "getStrings").resolves("");
        getStringsStub
          .callsFake(async a => a) // eslint-disable-line max-nested-callbacks
          .withArgs({ string_id: "primary_button_id" })
          .resolves({ value: "Primary Button", attributes: { accesskey: "p" } })
          .withArgs({ string_id: "secondary_button_id" })
          .resolves({
            value: "Secondary Button",
            attributes: { accesskey: "s" },
          })
          .withArgs({ string_id: "secondary_button_id_2" })
          .resolves({
            value: "Secondary Button 2",
            attributes: { accesskey: "a" },
          })
          .withArgs({ string_id: "secondary_button_id_3" })
          .resolves({
            value: "Secondary Button 3",
            attributes: { accesskey: "g" },
          })
          .withArgs(
            sinon.match({
              string_id: "cfr-doorhanger-extension-learn-more-link",
            })
          )
          .resolves("Learn more")
          .withArgs(
            sinon.match({ string_id: "cfr-doorhanger-extension-total-users" })
          )
          .callsFake(async ({ args }) => `${args.total} users`); // eslint-disable-line max-nested-callbacks

        translateElementsStub = sandbox.stub().resolves();
        setAttributesStub = sandbox.stub();
        global.RemoteL10n.l10n.setAttributes = setAttributesStub;
        global.RemoteL10n.l10n.translateElements = translateElementsStub;
      });

      it("should call `.hideAddressBarNotifier` and do nothing if there is no recommendation for the selected browser", async () => {
        sandbox.spy(pageAction, "hideAddressBarNotifier");
        CFRPageActions.RecommendationMap.delete(fakeBrowser);
        await pageAction._cfrUrlbarButtonClick({});
        assert.calledOnce(pageAction.hideAddressBarNotifier);
        assert.notCalled(global.PopupNotifications.show);
      });
      it("should cancel any planned state changes", async () => {
        sandbox.spy(pageAction, "_clearScheduledStateChanges");
        assert.notCalled(pageAction._clearScheduledStateChanges);
        await pageAction._cfrUrlbarButtonClick({});
        assert.calledOnce(pageAction._clearScheduledStateChanges);
      });
      it("should set the right text values", async () => {
        await pageAction._cfrUrlbarButtonClick({});
        const headerLabel = elements["cfr-notification-header-label"];
        const headerLink = elements["cfr-notification-header-link"];
        const headerImage = elements["cfr-notification-header-image"];
        const footerLink = elements["cfr-notification-footer-learn-more-link"];
        assert.equal(
          headerLabel.value,
          fakeRecommendation.content.heading_text
        );
        assert.isTrue(
          headerLink
            .getAttribute("href")
            .endsWith(fakeRecommendation.content.info_icon.sumo_path)
        );
        assert.equal(
          headerImage.getAttribute("tooltiptext"),
          fakeRecommendation.content.info_icon.label
        );
        const htmlFooterEl = fakeRemoteL10n.createElement.args.find(
          /* eslint-disable-next-line max-nested-callbacks */
          ([doc, el, args]) =>
            args && args.content === fakeRecommendation.content.text
        );
        assert.ok(htmlFooterEl);
        assert.equal(footerLink.value, "Learn more");
        assert.equal(
          footerLink.getAttribute("href"),
          fakeRecommendation.content.addon.amo_url
        );
      });
      it("should add the rating correctly", async () => {
        await pageAction._cfrUrlbarButtonClick();
        const footerFilledStars =
          elements["cfr-notification-footer-filled-stars"];
        const footerEmptyStars =
          elements["cfr-notification-footer-empty-stars"];
        // .toFixed to sort out some floating precision errors
        assert.equal(
          footerFilledStars.style.width,
          `${(4.2 * 16).toFixed(1)}px`
        );
        assert.equal(
          footerEmptyStars.style.width,
          `${(0.8 * 16).toFixed(1)}px`
        );
      });
      it("should add the number of users correctly", async () => {
        await pageAction._cfrUrlbarButtonClick();
        const footerUsers = elements["cfr-notification-footer-users"];
        assert.isNull(footerUsers.getAttribute("hidden"));
        assert.equal(
          footerUsers.getAttribute("value"),
          `${fakeRecommendation.content.addon.users}`
        );
      });
      it("should send the right telemetry", async () => {
        await pageAction._cfrUrlbarButtonClick();
        assert.calledWith(dispatchStub, {
          type: "DOORHANGER_TELEMETRY",
          data: {
            action: "cfr_user_event",
            source: "CFR",
            message_id: fakeRecommendation.id,
            bucket_id: fakeRecommendation.content.bucket_id,
            event: "CLICK_DOORHANGER",
          },
        });
      });
      it("should set the main action correctly", async () => {
        sinon
          .stub(CFRPageActions, "_fetchLatestAddonVersion")
          .resolves("latest-addon.xpi");
        await pageAction._cfrUrlbarButtonClick();
        const mainAction = global.PopupNotifications.show.firstCall.args[4]; // eslint-disable-line prefer-destructuring
        assert.deepEqual(mainAction.label, {
          value: "Primary Button",
          attributes: { accesskey: "p" },
        });
        sandbox.spy(pageAction, "hideAddressBarNotifier");
        await mainAction.callback();
        assert.calledOnce(pageAction.hideAddressBarNotifier);
        // Should block the message
        assert.calledWith(dispatchStub, {
          type: "BLOCK_MESSAGE_BY_ID",
          data: { id: fakeRecommendation.id },
        });
        // Should trigger the action
        assert.calledWith(
          dispatchStub,
          {
            type: "USER_ACTION",
            data: { id: "primary_action", data: { url: "latest-addon.xpi" } },
          },
          fakeBrowser
        );
        // Should send telemetry
        assert.calledWith(dispatchStub, {
          type: "DOORHANGER_TELEMETRY",
          data: {
            action: "cfr_user_event",
            source: "CFR",
            message_id: fakeRecommendation.id,
            bucket_id: fakeRecommendation.content.bucket_id,
            event: "INSTALL",
          },
        });
        // Should remove the recommendation
        assert.isFalse(CFRPageActions.RecommendationMap.has(fakeBrowser));
      });
      it("should set the secondary action correctly", async () => {
        await pageAction._cfrUrlbarButtonClick();
        // eslint-disable-next-line prefer-destructuring
        const [secondaryAction] =
          global.PopupNotifications.show.firstCall.args[5];

        assert.deepEqual(secondaryAction.label, {
          value: "Secondary Button",
          attributes: { accesskey: "s" },
        });
        sandbox.spy(pageAction, "hideAddressBarNotifier");
        CFRPageActions.RecommendationMap.set(fakeBrowser, {});
        secondaryAction.callback();
        // Should send telemetry
        assert.calledWith(dispatchStub, {
          type: "DOORHANGER_TELEMETRY",
          data: {
            action: "cfr_user_event",
            source: "CFR",
            message_id: fakeRecommendation.id,
            bucket_id: fakeRecommendation.content.bucket_id,
            event: "DISMISS",
          },
        });
        // Don't remove the recommendation on `DISMISS` action
        assert.isTrue(CFRPageActions.RecommendationMap.has(fakeBrowser));
        assert.notCalled(pageAction.hideAddressBarNotifier);
      });
      it("should send right telemetry for BLOCK secondary action", async () => {
        await pageAction._cfrUrlbarButtonClick();
        // eslint-disable-next-line prefer-destructuring
        const blockAction = global.PopupNotifications.show.firstCall.args[5][1];

        assert.deepEqual(blockAction.label, {
          value: "Secondary Button 2",
          attributes: { accesskey: "a" },
        });
        sandbox.spy(pageAction, "hideAddressBarNotifier");
        sandbox.spy(pageAction, "_blockMessage");
        CFRPageActions.RecommendationMap.set(fakeBrowser, {});
        blockAction.callback();
        assert.calledOnce(pageAction.hideAddressBarNotifier);
        assert.calledOnce(pageAction._blockMessage);
        // Should send telemetry
        assert.calledWith(dispatchStub, {
          type: "DOORHANGER_TELEMETRY",
          data: {
            action: "cfr_user_event",
            source: "CFR",
            message_id: fakeRecommendation.id,
            bucket_id: fakeRecommendation.content.bucket_id,
            event: "BLOCK",
          },
        });
        // Should remove the recommendation
        assert.isFalse(CFRPageActions.RecommendationMap.has(fakeBrowser));
      });
      it("should send right telemetry for MANAGE secondary action", async () => {
        await pageAction._cfrUrlbarButtonClick();
        // eslint-disable-next-line prefer-destructuring
        const manageAction =
          global.PopupNotifications.show.firstCall.args[5][2];

        assert.deepEqual(manageAction.label, {
          value: "Secondary Button 3",
          attributes: { accesskey: "g" },
        });
        sandbox.spy(pageAction, "hideAddressBarNotifier");
        CFRPageActions.RecommendationMap.set(fakeBrowser, {});
        manageAction.callback();
        // Should send telemetry
        assert.calledWith(dispatchStub, {
          type: "DOORHANGER_TELEMETRY",
          data: {
            action: "cfr_user_event",
            source: "CFR",
            message_id: fakeRecommendation.id,
            bucket_id: fakeRecommendation.content.bucket_id,
            event: "MANAGE",
          },
        });
        // Don't remove the recommendation on `MANAGE` action
        assert.isTrue(CFRPageActions.RecommendationMap.has(fakeBrowser));
        assert.notCalled(pageAction.hideAddressBarNotifier);
      });
      it("should call PopupNotifications.show with the right arguments", async () => {
        await pageAction._cfrUrlbarButtonClick();
        assert.calledWith(
          global.PopupNotifications.show,
          fakeBrowser,
          "contextual-feature-recommendation",
          fakeRecommendation.content.addon.title,
          "cfr",
          sinon.match.any, // Corresponds to the main action, tested above
          sinon.match.any, // Corresponds to the secondary action, tested above
          {
            popupIconURL: fakeRecommendation.content.addon.icon,
            hideClose: true,
            eventCallback: pageAction._popupStateChange,
            persistent: false,
            persistWhileVisible: false,
            popupIconClass: fakeRecommendation.content.icon_class,
            recordTelemetryInPrivateBrowsing:
              fakeRecommendation.content.show_in_private_browsing,
            name: {
              string_id: "cfr-doorhanger-extension-author",
              args: { name: fakeRecommendation.content.addon.author },
            },
          }
        );
      });
    });
    describe("#_cfrUrlbarButtonClick/cfr_urlbar_chiclet", () => {
      let heartbeatRecommendation;
      beforeEach(async () => {
        heartbeatRecommendation = (await CFRMessageProvider.getMessages()).find(
          m => m.template === "cfr_urlbar_chiclet"
        );
        CFRPageActions.PageActionMap.set(fakeBrowser.ownerGlobal, pageAction);
        await CFRPageActions.addRecommendation(
          fakeBrowser,
          fakeHost,
          heartbeatRecommendation,
          dispatchStub
        );
      });
      it("should dispatch a click event", async () => {
        await pageAction._cfrUrlbarButtonClick({});

        assert.calledWith(dispatchStub, {
          type: "DOORHANGER_TELEMETRY",
          data: {
            action: "cfr_user_event",
            source: "CFR",
            message_id: heartbeatRecommendation.id,
            bucket_id: heartbeatRecommendation.content.bucket_id,
            event: "CLICK_DOORHANGER",
          },
        });
      });
      it("should dispatch a USER_ACTION for chiclet_open_url layout", async () => {
        await pageAction._cfrUrlbarButtonClick({});

        assert.calledWith(dispatchStub, {
          type: "USER_ACTION",
          data: {
            data: {
              args: heartbeatRecommendation.content.action.url,
              where: heartbeatRecommendation.content.action.where,
            },
            type: "OPEN_URL",
          },
        });
      });
      it("should block the message after the click", async () => {
        await pageAction._cfrUrlbarButtonClick({});

        assert.calledWith(dispatchStub, {
          type: "BLOCK_MESSAGE_BY_ID",
          data: { id: heartbeatRecommendation.id },
        });
      });
      it("should remove the button and browser entry", async () => {
        sandbox.spy(pageAction, "hideAddressBarNotifier");

        await pageAction._cfrUrlbarButtonClick({});

        assert.calledOnce(pageAction.hideAddressBarNotifier);
        assert.isFalse(CFRPageActions.RecommendationMap.has(fakeBrowser));
      });
    });
  });

  describe("CFRPageActions", () => {
    beforeEach(() => {
      // Spy on the prototype methods to inspect calls for any PageAction instance
      sandbox.spy(PageAction.prototype, "showAddressBarNotifier");
      sandbox.spy(PageAction.prototype, "hideAddressBarNotifier");
    });

    describe("updatePageActions", () => {
      let savedRec;

      beforeEach(() => {
        const win = fakeBrowser.ownerGlobal;
        CFRPageActions.PageActionMap.set(
          win,
          new PageAction(win, dispatchStub)
        );
        const { id, content } = fakeRecommendation;
        savedRec = {
          id,
          host: fakeHost,
          content,
        };
        CFRPageActions.RecommendationMap.set(fakeBrowser, savedRec);
      });

      it("should do nothing if a pageAction doesn't exist for the window", () => {
        const win = fakeBrowser.ownerGlobal;
        CFRPageActions.PageActionMap.delete(win);
        CFRPageActions.updatePageActions(fakeBrowser);
        assert.notCalled(PageAction.prototype.showAddressBarNotifier);
        assert.notCalled(PageAction.prototype.hideAddressBarNotifier);
      });
      it("should do nothing if the browser is not the `selectedBrowser`", () => {
        const someOtherFakeBrowser = {};
        CFRPageActions.updatePageActions(someOtherFakeBrowser);
        assert.notCalled(PageAction.prototype.showAddressBarNotifier);
        assert.notCalled(PageAction.prototype.hideAddressBarNotifier);
      });
      it("should hideAddressBarNotifier the pageAction if a recommendation doesn't exist for the given browser", () => {
        CFRPageActions.RecommendationMap.delete(fakeBrowser);
        CFRPageActions.updatePageActions(fakeBrowser);
        assert.calledOnce(PageAction.prototype.hideAddressBarNotifier);
      });
      it("should show the pageAction if a recommendation exists and the host matches", () => {
        CFRPageActions.updatePageActions(fakeBrowser);
        assert.calledOnce(PageAction.prototype.showAddressBarNotifier);
        assert.calledWith(
          PageAction.prototype.showAddressBarNotifier,
          savedRec
        );
      });
      it("should show the pageAction if a recommendation exists and it doesn't have a host defined", () => {
        const recNoHost = { ...savedRec, host: undefined };
        CFRPageActions.RecommendationMap.set(fakeBrowser, recNoHost);
        CFRPageActions.updatePageActions(fakeBrowser);
        assert.calledOnce(PageAction.prototype.showAddressBarNotifier);
        assert.calledWith(
          PageAction.prototype.showAddressBarNotifier,
          recNoHost
        );
      });
      it("should hideAddressBarNotifier the pageAction and delete the recommendation if the recommendation exists but the host doesn't match", () => {
        const someOtherFakeHost = "subdomain.mozilla.com";
        fakeBrowser.documentURI.host = someOtherFakeHost;
        assert.isTrue(CFRPageActions.RecommendationMap.has(fakeBrowser));
        CFRPageActions.updatePageActions(fakeBrowser);
        assert.calledOnce(PageAction.prototype.hideAddressBarNotifier);
        assert.isFalse(CFRPageActions.RecommendationMap.has(fakeBrowser));
      });
      it("should not call `delete` if retain is true", () => {
        savedRec.retain = true;
        fakeBrowser.documentURI.host = "subdomain.mozilla.com";
        assert.isTrue(CFRPageActions.RecommendationMap.has(fakeBrowser));

        CFRPageActions.updatePageActions(fakeBrowser);
        assert.propertyVal(savedRec, "retain", false);
        assert.calledOnce(PageAction.prototype.hideAddressBarNotifier);
        assert.isTrue(CFRPageActions.RecommendationMap.has(fakeBrowser));
      });
      it("should call `delete` if retain is false", () => {
        savedRec.retain = false;
        fakeBrowser.documentURI.host = "subdomain.mozilla.com";
        assert.isTrue(CFRPageActions.RecommendationMap.has(fakeBrowser));

        CFRPageActions.updatePageActions(fakeBrowser);
        assert.propertyVal(savedRec, "retain", false);
        assert.calledOnce(PageAction.prototype.hideAddressBarNotifier);
        assert.isFalse(CFRPageActions.RecommendationMap.has(fakeBrowser));
      });
    });

    describe("forceRecommendation", () => {
      it("should succeed and add an element to the RecommendationMap", async () => {
        assert.isTrue(
          await CFRPageActions.forceRecommendation(
            fakeBrowser,
            fakeRecommendation,
            dispatchStub
          )
        );
        assert.deepInclude(CFRPageActions.RecommendationMap.get(fakeBrowser), {
          id: fakeRecommendation.id,
          content: fakeRecommendation.content,
        });
      });
      it("should create a PageAction if one doesn't exist for the window, save it in the PageActionMap, and call `show`", async () => {
        const win = fakeBrowser.ownerGlobal;
        assert.isFalse(CFRPageActions.PageActionMap.has(win));
        await CFRPageActions.forceRecommendation(
          fakeBrowser,
          fakeRecommendation,
          dispatchStub
        );
        const pageAction = CFRPageActions.PageActionMap.get(win);
        assert.equal(win, pageAction.window);
        assert.equal(dispatchStub, pageAction._dispatchCFRAction);
        assert.calledOnce(PageAction.prototype.showAddressBarNotifier);
      });
    });

    describe("showPopup", () => {
      let savedRec;
      let pageAction;
      let fakeAnchorId = "fake_anchor_id";
      let sandboxShowPopup = sinon.createSandbox();
      let fakePopUp = {
        id: "fake_id",
        template: "cfr_doorhanger",
        content: {
          skip_address_bar_notifier: true,
          heading_text: "Fake Heading Text",
          anchor_id: "fake_anchor_id",
        },
      };
      beforeEach(() => {
        const { id, content } = fakePopUp;
        savedRec = {
          id,
          host: fakeHost,
          content,
        };
        CFRPageActions.RecommendationMap.set(fakeBrowser, savedRec);
        pageAction = new PageAction(window, dispatchStub);

        sandboxShowPopup.stub(window.document, "getElementById");
        sandboxShowPopup.stub(pageAction, "_renderPopup");
        globals.set({
          CustomizableUI: {
            getWidget: sandboxShowPopup
              .stub()
              .withArgs(fakeAnchorId)
              .returns({ areaType: "menu-panel" }),
          },
        });
      });
      afterEach(() => {
        sandboxShowPopup.restore();
        globals.restore();
      });

      it("Should use default anchor_id if an alternate hasn't been provided", async () => {
        await pageAction.showPopup();

        assert.calledWith(window.document.getElementById, fakeAnchorId);
      });

      it("Should use alt_anchor_if if one has been provided AND the anchor_id has been removed", async () => {
        let fakeAltAnchorId = "fake_alt_anchor_id";

        fakePopUp.content.alt_anchor_id = fakeAltAnchorId;
        await pageAction.showPopup();
        assert.calledWith(window.document.getElementById, fakeAltAnchorId);
      });
    });

    describe("addRecommendation", () => {
      it("should fail and not add a recommendation if the browser is part of a private window", async () => {
        global.PrivateBrowsingUtils.isWindowPrivate.returns(true);
        assert.isFalse(
          await CFRPageActions.addRecommendation(
            fakeBrowser,
            fakeHost,
            fakeRecommendation,
            dispatchStub
          )
        );
        assert.isFalse(CFRPageActions.RecommendationMap.has(fakeBrowser));
      });
      it("should successfully add a private browsing recommendation and send correct telemetry", async () => {
        global.PrivateBrowsingUtils.isWindowPrivate.returns(true);
        fakeRecommendation.content.show_in_private_browsing = true;
        assert.isTrue(
          await CFRPageActions.addRecommendation(
            fakeBrowser,
            fakeHost,
            fakeRecommendation,
            dispatchStub
          )
        );
        assert.isTrue(CFRPageActions.RecommendationMap.has(fakeBrowser));

        const pageAction = CFRPageActions.PageActionMap.get(
          fakeBrowser.ownerGlobal
        );
        await pageAction.showAddressBarNotifier(fakeRecommendation, true);
        assert.calledWith(dispatchStub, {
          type: "DOORHANGER_TELEMETRY",
          data: {
            action: "cfr_user_event",
            source: "CFR",
            is_private: true,
            message_id: fakeRecommendation.id,
            bucket_id: fakeRecommendation.content.bucket_id,
            event: "IMPRESSION",
          },
        });
      });
      it("should fail and not add a recommendation if the browser is not the selected browser", async () => {
        global.gBrowser.selectedBrowser = {}; // Some other browser
        assert.isFalse(
          await CFRPageActions.addRecommendation(
            fakeBrowser,
            fakeHost,
            fakeRecommendation,
            dispatchStub
          )
        );
      });
      it("should fail and not add a recommendation if the browser does not exist", async () => {
        assert.isFalse(
          await CFRPageActions.addRecommendation(
            undefined,
            fakeHost,
            fakeRecommendation,
            dispatchStub
          )
        );
        assert.isFalse(CFRPageActions.RecommendationMap.has(fakeBrowser));
      });
      it("should fail and not add a recommendation if the host doesn't match", async () => {
        const someOtherFakeHost = "subdomain.mozilla.com";
        assert.isFalse(
          await CFRPageActions.addRecommendation(
            fakeBrowser,
            someOtherFakeHost,
            fakeRecommendation,
            dispatchStub
          )
        );
      });
      it("should otherwise succeed and add an element to the RecommendationMap", async () => {
        assert.isTrue(
          await CFRPageActions.addRecommendation(
            fakeBrowser,
            fakeHost,
            fakeRecommendation,
            dispatchStub
          )
        );
        assert.deepInclude(CFRPageActions.RecommendationMap.get(fakeBrowser), {
          id: fakeRecommendation.id,
          host: fakeHost,
          content: fakeRecommendation.content,
        });
      });
      it("should create a PageAction if one doesn't exist for the window, save it in the PageActionMap, and call `show`", async () => {
        const win = fakeBrowser.ownerGlobal;
        assert.isFalse(CFRPageActions.PageActionMap.has(win));
        await CFRPageActions.addRecommendation(
          fakeBrowser,
          fakeHost,
          fakeRecommendation,
          dispatchStub
        );
        const pageAction = CFRPageActions.PageActionMap.get(win);
        assert.equal(win, pageAction.window);
        assert.equal(dispatchStub, pageAction._dispatchCFRAction);
        assert.calledOnce(PageAction.prototype.showAddressBarNotifier);
      });
      it("should add the right url if we fetched and addon install URL", async () => {
        fakeRecommendation.template = "cfr_doorhanger";
        await CFRPageActions.addRecommendation(
          fakeBrowser,
          fakeHost,
          fakeRecommendation,
          dispatchStub
        );
        const recommendation =
          CFRPageActions.RecommendationMap.get(fakeBrowser);

        // sanity check - just go through some of the rest of the attributes to make sure they were untouched
        assert.equal(recommendation.id, fakeRecommendation.id);
        assert.equal(
          recommendation.content.heading_text,
          fakeRecommendation.content.heading_text
        );
        assert.equal(
          recommendation.content.addon,
          fakeRecommendation.content.addon
        );
        assert.equal(
          recommendation.content.text,
          fakeRecommendation.content.text
        );
        assert.equal(
          recommendation.content.buttons.secondary,
          fakeRecommendation.content.buttons.secondary
        );
        assert.equal(
          recommendation.content.buttons.primary.action.id,
          fakeRecommendation.content.buttons.primary.action.id
        );

        delete fakeRecommendation.template;
      });
      it("should prevent a second message if one is currently displayed", async () => {
        const secondMessage = { ...fakeRecommendation, id: "second_message" };
        let messageAdded = await CFRPageActions.addRecommendation(
          fakeBrowser,
          fakeHost,
          fakeRecommendation,
          dispatchStub
        );

        assert.isTrue(messageAdded);
        assert.deepInclude(CFRPageActions.RecommendationMap.get(fakeBrowser), {
          id: fakeRecommendation.id,
          host: fakeHost,
          content: fakeRecommendation.content,
        });

        messageAdded = await CFRPageActions.addRecommendation(
          fakeBrowser,
          fakeHost,
          secondMessage,
          dispatchStub
        );
        // Adding failed
        assert.isFalse(messageAdded);
        // First message is still there
        assert.deepInclude(CFRPageActions.RecommendationMap.get(fakeBrowser), {
          id: fakeRecommendation.id,
          host: fakeHost,
          content: fakeRecommendation.content,
        });
      });
      it("should send impressions just for the first message", async () => {
        const secondMessage = { ...fakeRecommendation, id: "second_message" };
        await CFRPageActions.addRecommendation(
          fakeBrowser,
          fakeHost,
          fakeRecommendation,
          dispatchStub
        );
        await CFRPageActions.addRecommendation(
          fakeBrowser,
          fakeHost,
          secondMessage,
          dispatchStub
        );

        // Doorhanger telemetry + Impression for just 1 message
        assert.calledTwice(dispatchStub);
        const [firstArgs] = dispatchStub.firstCall.args;
        const [secondArgs] = dispatchStub.secondCall.args;
        assert.equal(firstArgs.data.id, secondArgs.data.message_id);
      });
    });

    describe("clearRecommendations", () => {
      const createFakePageAction = () => ({
        hideAddressBarNotifier: sandbox.stub(),
      });
      const windows = [{}, {}, { closed: true }];
      const browsers = [{}, {}, {}, {}];

      beforeEach(() => {
        CFRPageActions.PageActionMap.set(windows[0], createFakePageAction());
        CFRPageActions.PageActionMap.set(windows[2], createFakePageAction());
        for (const browser of browsers) {
          CFRPageActions.RecommendationMap.set(browser, {});
        }
        globals.set({ Services: { wm: { getEnumerator: () => windows } } });
      });

      it("should hideAddressBarNotifier the PageActions of any existing, non-closed windows", () => {
        const pageActions = windows.map(win =>
          CFRPageActions.PageActionMap.get(win)
        );
        CFRPageActions.clearRecommendations();

        // Only the first window had a PageAction and wasn't closed
        assert.calledOnce(pageActions[0].hideAddressBarNotifier);
        assert.isUndefined(pageActions[1]);
        assert.notCalled(pageActions[2].hideAddressBarNotifier);
      });
      it("should clear the PageActionMap and the RecommendationMap", () => {
        CFRPageActions.clearRecommendations();

        // Both are WeakMaps and so are not iterable, cannot be cleared, and
        // cannot have their length queried directly, so we have to check
        // whether previous elements still exist
        assert.lengthOf(windows, 3);
        for (const win of windows) {
          assert.isFalse(CFRPageActions.PageActionMap.has(win));
        }
        assert.lengthOf(browsers, 4);
        for (const browser of browsers) {
          assert.isFalse(CFRPageActions.RecommendationMap.has(browser));
        }
      });
    });

    describe("reloadL10n", () => {
      const createFakePageAction = () => ({
        hideAddressBarNotifier() {},
        reloadL10n: sandbox.stub(),
      });
      const windows = [{}, {}, { closed: true }];

      beforeEach(() => {
        CFRPageActions.PageActionMap.set(windows[0], createFakePageAction());
        CFRPageActions.PageActionMap.set(windows[2], createFakePageAction());
        globals.set({ Services: { wm: { getEnumerator: () => windows } } });
      });

      it("should call reloadL10n for all the PageActions of any existing, non-closed windows", () => {
        const pageActions = windows.map(win =>
          CFRPageActions.PageActionMap.get(win)
        );
        CFRPageActions.reloadL10n();

        // Only the first window had a PageAction and wasn't closed
        assert.calledOnce(pageActions[0].reloadL10n);
        assert.isUndefined(pageActions[1]);
        assert.notCalled(pageActions[2].reloadL10n);
      });
    });
  });
});
