import { _ToolbarPanelHub } from "lib/ToolbarPanelHub.jsm";
import { GlobalOverrider } from "test/unit/utils";
import { OnboardingMessageProvider } from "lib/OnboardingMessageProvider.jsm";
import { PanelTestProvider } from "lib/PanelTestProvider.sys.mjs";

describe("ToolbarPanelHub", () => {
  let globals;
  let sandbox;
  let instance;
  let everyWindowStub;
  let preferencesStub;
  let fakeDocument;
  let fakeWindow;
  let fakeElementById;
  let fakeElementByTagName;
  let createdCustomElements = [];
  let eventListeners = {};
  let addObserverStub;
  let removeObserverStub;
  let getBoolPrefStub;
  let setBoolPrefStub;
  let waitForInitializedStub;
  let isBrowserPrivateStub;
  let fakeSendTelemetry;
  let getEarliestRecordedDateStub;
  let getEventsByDateRangeStub;
  let defaultSearchStub;
  let scriptloaderStub;
  let fakeRemoteL10n;
  let getViewNodeStub;

  beforeEach(async () => {
    sandbox = sinon.createSandbox();
    globals = new GlobalOverrider();
    instance = new _ToolbarPanelHub();
    waitForInitializedStub = sandbox.stub().resolves();
    fakeElementById = {
      setAttribute: sandbox.stub(),
      removeAttribute: sandbox.stub(),
      querySelector: sandbox.stub().returns(null),
      querySelectorAll: sandbox.stub().returns([]),
      appendChild: sandbox.stub(),
      addEventListener: sandbox.stub(),
      hasAttribute: sandbox.stub(),
      toggleAttribute: sandbox.stub(),
      remove: sandbox.stub(),
      removeChild: sandbox.stub(),
    };
    fakeElementByTagName = {
      setAttribute: sandbox.stub(),
      removeAttribute: sandbox.stub(),
      querySelector: sandbox.stub().returns(null),
      querySelectorAll: sandbox.stub().returns([]),
      appendChild: sandbox.stub(),
      addEventListener: sandbox.stub(),
      hasAttribute: sandbox.stub(),
      toggleAttribute: sandbox.stub(),
      remove: sandbox.stub(),
      removeChild: sandbox.stub(),
    };
    fakeDocument = {
      getElementById: sandbox.stub().returns(fakeElementById),
      getElementsByTagName: sandbox.stub().returns(fakeElementByTagName),
      querySelector: sandbox.stub().returns({}),
      createElement: tagName => {
        const element = {
          tagName,
          classList: {},
          addEventListener: (ev, fn) => {
            eventListeners[ev] = fn;
          },
          appendChild: sandbox.stub(),
          setAttribute: sandbox.stub(),
          textContent: "",
        };
        element.classList.add = sandbox.stub();
        element.classList.includes = className =>
          element.classList.add.firstCall.args[0] === className;
        createdCustomElements.push(element);
        return element;
      },
      l10n: {
        translateElements: sandbox.stub(),
        translateFragment: sandbox.stub(),
        formatMessages: sandbox.stub().resolves([{}]),
        setAttributes: sandbox.stub(),
      },
    };
    fakeWindow = {
      // eslint-disable-next-line object-shorthand
      DocumentFragment: function () {
        return fakeElementById;
      },
      document: fakeDocument,
      browser: {
        ownerDocument: fakeDocument,
      },
      MozXULElement: { insertFTLIfNeeded: sandbox.stub() },
      ownerGlobal: {
        openLinkIn: sandbox.stub(),
        gBrowser: "gBrowser",
      },
      PanelUI: {
        panel: fakeElementById,
        whatsNewPanel: fakeElementById,
      },
      customElements: { get: sandbox.stub() },
    };
    everyWindowStub = {
      registerCallback: sandbox.stub(),
      unregisterCallback: sandbox.stub(),
    };
    preferencesStub = {
      get: sandbox.stub(),
      set: sandbox.stub(),
    };
    scriptloaderStub = { loadSubScript: sandbox.stub() };
    addObserverStub = sandbox.stub();
    removeObserverStub = sandbox.stub();
    getBoolPrefStub = sandbox.stub();
    setBoolPrefStub = sandbox.stub();
    fakeSendTelemetry = sandbox.stub();
    isBrowserPrivateStub = sandbox.stub();
    getEarliestRecordedDateStub = sandbox.stub().returns(
      // A random date that's not the current timestamp
      new Date() - 500
    );
    getEventsByDateRangeStub = sandbox.stub().returns([]);
    getViewNodeStub = sandbox.stub().returns(fakeElementById);
    defaultSearchStub = { defaultEngine: { name: "DDG" } };
    fakeRemoteL10n = {
      l10n: {},
      reloadL10n: sandbox.stub(),
      createElement: sandbox
        .stub()
        .callsFake((doc, el) => fakeDocument.createElement(el)),
    };
    globals.set({
      EveryWindow: everyWindowStub,
      Services: {
        ...Services,
        prefs: {
          addObserver: addObserverStub,
          removeObserver: removeObserverStub,
          getBoolPref: getBoolPrefStub,
          setBoolPref: setBoolPrefStub,
        },
        search: defaultSearchStub,
        scriptloader: scriptloaderStub,
      },
      PrivateBrowsingUtils: {
        isBrowserPrivate: isBrowserPrivateStub,
      },
      Preferences: preferencesStub,
      TrackingDBService: {
        getEarliestRecordedDate: getEarliestRecordedDateStub,
        getEventsByDateRange: getEventsByDateRangeStub,
      },
      SpecialMessageActions: {
        handleAction: sandbox.stub(),
      },
      RemoteL10n: fakeRemoteL10n,
      PanelMultiView: {
        getViewNode: getViewNodeStub,
      },
    });
  });
  afterEach(() => {
    instance.uninit();
    sandbox.restore();
    globals.restore();
    eventListeners = {};
    createdCustomElements = [];
  });
  it("should create an instance", () => {
    assert.ok(instance);
  });
  it("should enableAppmenuButton() on init() just once", async () => {
    instance.enableAppmenuButton = sandbox.stub();

    await instance.init(waitForInitializedStub, { getMessages: () => {} });
    await instance.init(waitForInitializedStub, { getMessages: () => {} });

    assert.calledOnce(instance.enableAppmenuButton);

    instance.uninit();

    await instance.init(waitForInitializedStub, { getMessages: () => {} });

    assert.calledTwice(instance.enableAppmenuButton);
  });
  it("should unregisterCallback on uninit()", () => {
    instance.uninit();
    assert.calledTwice(everyWindowStub.unregisterCallback);
  });
  describe("#maybeLoadCustomElement", () => {
    it("should not load customElements a second time", () => {
      instance.maybeLoadCustomElement({ customElements: new Map() });
      instance.maybeLoadCustomElement({
        customElements: new Map([["remote-text", true]]),
      });

      assert.calledOnce(scriptloaderStub.loadSubScript);
    });
  });
  describe("#toggleWhatsNewPref", () => {
    it("should call Preferences.set() with the opposite value", () => {
      let checkbox = {};
      let event = { target: checkbox };
      // checkbox starts false
      checkbox.checked = false;

      // toggling the checkbox to set the value to true;
      // Preferences.set() gets called before the checkbox changes,
      // so we have to call it with the opposite value.
      instance.toggleWhatsNewPref(event);

      assert.calledOnce(preferencesStub.set);
      assert.calledWith(
        preferencesStub.set,
        "browser.messaging-system.whatsNewPanel.enabled",
        !checkbox.checked
      );
    });
    it("should report telemetry with the opposite value", () => {
      let sendUserEventTelemetryStub = sandbox.stub(
        instance,
        "sendUserEventTelemetry"
      );
      let event = {
        target: { checked: true, ownerGlobal: fakeWindow },
      };

      instance.toggleWhatsNewPref(event);

      assert.calledOnce(sendUserEventTelemetryStub);
      const { args } = sendUserEventTelemetryStub.firstCall;
      assert.equal(args[1], "WNP_PREF_TOGGLE");
      assert.propertyVal(args[3].value, "prefValue", false);
    });
  });
  describe("#enableAppmenuButton", () => {
    it("should registerCallback on enableAppmenuButton() if there are messages", async () => {
      await instance.init(waitForInitializedStub, {
        getMessages: sandbox.stub().resolves([{}, {}]),
      });
      // init calls `enableAppmenuButton`
      everyWindowStub.registerCallback.resetHistory();

      await instance.enableAppmenuButton();

      assert.calledOnce(everyWindowStub.registerCallback);
      assert.calledWithExactly(
        everyWindowStub.registerCallback,
        "appMenu-whatsnew-button",
        sinon.match.func,
        sinon.match.func
      );
    });
    it("should not registerCallback on enableAppmenuButton() if there are no messages", async () => {
      instance.init(waitForInitializedStub, {
        getMessages: sandbox.stub().resolves([]),
      });
      // init calls `enableAppmenuButton`
      everyWindowStub.registerCallback.resetHistory();

      await instance.enableAppmenuButton();

      assert.notCalled(everyWindowStub.registerCallback);
    });
  });
  describe("#disableAppmenuButton", () => {
    it("should call the unregisterCallback", () => {
      assert.notCalled(everyWindowStub.unregisterCallback);

      instance.disableAppmenuButton();

      assert.calledOnce(everyWindowStub.unregisterCallback);
      assert.calledWithExactly(
        everyWindowStub.unregisterCallback,
        "appMenu-whatsnew-button"
      );
    });
  });
  describe("#enableToolbarButton", () => {
    it("should registerCallback on enableToolbarButton if messages.length", async () => {
      await instance.init(waitForInitializedStub, {
        getMessages: sandbox.stub().resolves([{}, {}]),
      });
      // init calls `enableAppmenuButton`
      everyWindowStub.registerCallback.resetHistory();

      await instance.enableToolbarButton();

      assert.calledOnce(everyWindowStub.registerCallback);
      assert.calledWithExactly(
        everyWindowStub.registerCallback,
        "whats-new-menu-button",
        sinon.match.func,
        sinon.match.func
      );
    });
    it("should not registerCallback on enableToolbarButton if no messages", async () => {
      await instance.init(waitForInitializedStub, {
        getMessages: sandbox.stub().resolves([]),
      });

      await instance.enableToolbarButton();

      assert.notCalled(everyWindowStub.registerCallback);
    });
  });
  describe("Show/Hide functions", () => {
    it("should unhide appmenu button on _showAppmenuButton()", async () => {
      await instance._showAppmenuButton(fakeWindow);

      assert.equal(fakeElementById.hidden, false);
    });
    it("should hide appmenu button on _hideAppmenuButton()", () => {
      instance._hideAppmenuButton(fakeWindow);
      assert.equal(fakeElementById.hidden, true);
    });
    it("should not do anything if the window is closed", () => {
      instance._hideAppmenuButton(fakeWindow, true);
      assert.notCalled(global.PanelMultiView.getViewNode);
    });
    it("should not throw if the element does not exist", () => {
      let fn = instance._hideAppmenuButton.bind(null, {
        browser: { ownerDocument: {} },
      });
      getViewNodeStub.returns(undefined);
      assert.doesNotThrow(fn);
    });
    it("should unhide toolbar button on _showToolbarButton()", async () => {
      await instance._showToolbarButton(fakeWindow);

      assert.equal(fakeElementById.hidden, false);
    });
    it("should hide toolbar button on _hideToolbarButton()", () => {
      instance._hideToolbarButton(fakeWindow);
      assert.equal(fakeElementById.hidden, true);
    });
  });
  describe("#renderMessages", () => {
    let getMessagesStub;
    beforeEach(() => {
      getMessagesStub = sandbox.stub();
      instance.init(waitForInitializedStub, {
        getMessages: getMessagesStub,
        sendTelemetry: fakeSendTelemetry,
      });
    });
    it("should have correct state", async () => {
      const messages = (await PanelTestProvider.getMessages()).filter(
        m => m.template === "whatsnew_panel_message"
      );

      getMessagesStub.returns(messages);
      const ev1 = sandbox.stub();
      ev1.withArgs("type").returns(1); // tracker
      ev1.withArgs("count").returns(4);
      const ev2 = sandbox.stub();
      ev2.withArgs("type").returns(4); // fingerprinter
      ev2.withArgs("count").returns(3);
      getEventsByDateRangeStub.returns([
        { getResultByName: ev1 },
        { getResultByName: ev2 },
      ]);

      await instance.renderMessages(fakeWindow, fakeDocument, "container-id");

      assert.propertyVal(instance.state.contentArguments, "trackerCount", 4);
      assert.propertyVal(
        instance.state.contentArguments,
        "fingerprinterCount",
        3
      );
    });
    it("should render messages to the panel on renderMessages()", async () => {
      const messages = (await PanelTestProvider.getMessages()).filter(
        m => m.template === "whatsnew_panel_message"
      );
      messages[0].content.link_text = { string_id: "link_text_id" };

      getMessagesStub.returns(messages);
      const ev1 = sandbox.stub();
      ev1.withArgs("type").returns(1); // tracker
      ev1.withArgs("count").returns(4);
      const ev2 = sandbox.stub();
      ev2.withArgs("type").returns(4); // fingerprinter
      ev2.withArgs("count").returns(3);
      getEventsByDateRangeStub.returns([
        { getResultByName: ev1 },
        { getResultByName: ev2 },
      ]);

      await instance.renderMessages(fakeWindow, fakeDocument, "container-id");

      for (let message of messages) {
        assert.ok(
          fakeRemoteL10n.createElement.args.find(
            ([doc, el, args]) =>
              args && args.classList === "whatsNew-message-title"
          )
        );
        if (message.content.layout === "tracking-protections") {
          assert.ok(
            fakeRemoteL10n.createElement.args.find(
              ([doc, el, args]) =>
                args && args.classList === "whatsNew-message-subtitle"
            )
          );
        }
        if (message.id === "WHATS_NEW_FINGERPRINTER_COUNTER_72") {
          assert.ok(
            fakeRemoteL10n.createElement.args.find(
              ([doc, el, args]) => el === "h2" && args.content === 3
            )
          );
        }
        assert.ok(
          fakeRemoteL10n.createElement.args.find(
            ([doc, el, args]) =>
              args && args.classList === "whatsNew-message-content"
          )
        );
      }
      // Call the click handler to make coverage happy.
      eventListeners.mouseup();
      assert.calledOnce(global.SpecialMessageActions.handleAction);
    });
    it("should clear previous messages on 2nd renderMessages()", async () => {
      const messages = (await PanelTestProvider.getMessages()).filter(
        m => m.template === "whatsnew_panel_message"
      );
      const removeStub = sandbox.stub();
      fakeElementById.querySelectorAll.onCall(0).returns([]);
      fakeElementById.querySelectorAll
        .onCall(1)
        .returns([{ remove: removeStub }, { remove: removeStub }]);

      getMessagesStub.returns(messages);

      await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
      await instance.renderMessages(fakeWindow, fakeDocument, "container-id");

      assert.calledTwice(removeStub);
    });
    it("should sort based on order field value", async () => {
      const messages = (await PanelTestProvider.getMessages()).filter(
        m =>
          m.template === "whatsnew_panel_message" &&
          m.content.published_date === 1560969794394
      );

      messages.forEach(m => (m.content.title = m.order));

      getMessagesStub.returns(messages);

      await instance.renderMessages(fakeWindow, fakeDocument, "container-id");

      // Select the title elements that are supposed to be set to the same
      // value as the `order` field of the message
      const titleEls = fakeRemoteL10n.createElement.args
        .filter(
          ([doc, el, args]) =>
            args && args.classList === "whatsNew-message-title"
        )
        .map(([doc, el, args]) => args.content);
      assert.deepEqual(titleEls, [1, 2, 3]);
    });
    it("should accept string for image attributes", async () => {
      const messages = (await PanelTestProvider.getMessages()).filter(
        m => m.id === "WHATS_NEW_70_1"
      );
      getMessagesStub.returns(messages);

      await instance.renderMessages(fakeWindow, fakeDocument, "container-id");

      const imageEl = createdCustomElements.find(el => el.tagName === "img");
      assert.calledOnce(imageEl.setAttribute);
      assert.calledWithExactly(
        imageEl.setAttribute,
        "alt",
        "Firefox Send Logo"
      );
    });
    it("should set state values as data-attribute", async () => {
      const message = (await PanelTestProvider.getMessages()).find(
        m => m.template === "whatsnew_panel_message"
      );
      getMessagesStub.returns([message]);
      instance.state.contentArguments = { foo: "foo", bar: "bar" };

      await instance.renderMessages(fakeWindow, fakeDocument, "container-id");

      const [, , args] = fakeRemoteL10n.createElement.args.find(
        ([doc, el, elArgs]) => elArgs && elArgs.attributes
      );
      assert.ok(args);
      // Currently this.state.contentArguments has 8 different entries
      assert.lengthOf(Object.keys(args.attributes), 8);
      assert.equal(
        args.attributes.searchEngineName,
        defaultSearchStub.defaultEngine.name
      );
    });
    it("should only render unique dates (no duplicates)", async () => {
      const messages = (await PanelTestProvider.getMessages()).filter(
        m => m.template === "whatsnew_panel_message"
      );
      const uniqueDates = [
        ...new Set(messages.map(m => m.content.published_date)),
      ];
      getMessagesStub.returns(messages);

      await instance.renderMessages(fakeWindow, fakeDocument, "container-id");

      const dateElements = fakeRemoteL10n.createElement.args.filter(
        ([doc, el, args]) =>
          el === "p" && args.classList === "whatsNew-message-date"
      );
      assert.lengthOf(dateElements, uniqueDates.length);
    });
    it("should listen for panelhidden and remove the toolbar button", async () => {
      getMessagesStub.returns([]);
      fakeDocument.getElementById
        .withArgs("customizationui-widget-panel")
        .returns(null);

      await instance.renderMessages(fakeWindow, fakeDocument, "container-id");

      assert.notCalled(fakeElementById.addEventListener);
    });
    it("should attach doCommand cbs that handle user actions", async () => {
      const messages = (await PanelTestProvider.getMessages()).filter(
        m => m.template === "whatsnew_panel_message"
      );
      getMessagesStub.returns(messages);

      await instance.renderMessages(fakeWindow, fakeDocument, "container-id");

      const messageEl = createdCustomElements.find(
        el =>
          el.tagName === "div" && el.classList.includes("whatsNew-message-body")
      );
      const anchorEl = createdCustomElements.find(el => el.tagName === "a");

      assert.notCalled(global.SpecialMessageActions.handleAction);

      messageEl.doCommand();
      anchorEl.doCommand();

      assert.calledTwice(global.SpecialMessageActions.handleAction);
    });
    it("should listen for panelhidden and remove the toolbar button", async () => {
      getMessagesStub.returns([]);

      await instance.renderMessages(fakeWindow, fakeDocument, "container-id");

      assert.calledOnce(fakeElementById.addEventListener);
      assert.calledWithExactly(
        fakeElementById.addEventListener,
        "popuphidden",
        sinon.match.func,
        {
          once: true,
        }
      );
      const [, cb] = fakeElementById.addEventListener.firstCall.args;

      assert.notCalled(everyWindowStub.unregisterCallback);

      cb();

      assert.calledOnce(everyWindowStub.unregisterCallback);
      assert.calledWithExactly(
        everyWindowStub.unregisterCallback,
        "whats-new-menu-button"
      );
    });
    describe("#IMPRESSION", () => {
      it("should dispatch a IMPRESSION for messages", async () => {
        // means panel is triggered from the toolbar button
        fakeElementById.hasAttribute.returns(true);
        const messages = (await PanelTestProvider.getMessages()).filter(
          m => m.template === "whatsnew_panel_message"
        );
        getMessagesStub.returns(messages);
        const spy = sandbox.spy(instance, "sendUserEventTelemetry");

        await instance.renderMessages(fakeWindow, fakeDocument, "container-id");

        assert.calledOnce(spy);
        assert.calledOnce(fakeSendTelemetry);
        assert.propertyVal(
          spy.firstCall.args[2],
          "id",
          messages
            .map(({ id }) => id)
            .sort()
            .join(",")
        );
      });
      it("should dispatch a CLICK for clicking a message", async () => {
        // means panel is triggered from the toolbar button
        fakeElementById.hasAttribute.returns(true);
        // Force to render the message
        fakeElementById.querySelector.returns(null);
        const messages = (await PanelTestProvider.getMessages()).filter(
          m => m.template === "whatsnew_panel_message"
        );
        getMessagesStub.returns([messages[0]]);
        const spy = sandbox.spy(instance, "sendUserEventTelemetry");

        await instance.renderMessages(fakeWindow, fakeDocument, "container-id");

        assert.calledOnce(spy);
        assert.calledOnce(fakeSendTelemetry);

        spy.resetHistory();

        // Message click event listener cb
        eventListeners.mouseup();

        assert.calledOnce(spy);
        assert.calledWithExactly(spy, fakeWindow, "CLICK", messages[0]);
      });
      it("should dispatch a IMPRESSION with toolbar_dropdown", async () => {
        // means panel is triggered from the toolbar button
        fakeElementById.hasAttribute.returns(true);
        const messages = (await PanelTestProvider.getMessages()).filter(
          m => m.template === "whatsnew_panel_message"
        );
        getMessagesStub.resolves(messages);
        const spy = sandbox.spy(instance, "sendUserEventTelemetry");
        const panelPingId = messages
          .map(({ id }) => id)
          .sort()
          .join(",");

        await instance.renderMessages(fakeWindow, fakeDocument, "container-id");

        assert.calledOnce(spy);
        assert.calledWithExactly(
          spy,
          fakeWindow,
          "IMPRESSION",
          {
            id: panelPingId,
          },
          {
            value: {
              view: "toolbar_dropdown",
            },
          }
        );
        assert.calledOnce(fakeSendTelemetry);
        const {
          args: [dispatchPayload],
        } = fakeSendTelemetry.lastCall;
        assert.propertyVal(dispatchPayload, "type", "TOOLBAR_PANEL_TELEMETRY");
        assert.propertyVal(dispatchPayload.data, "message_id", panelPingId);
        assert.deepEqual(dispatchPayload.data.event_context, {
          view: "toolbar_dropdown",
        });
      });
      it("should dispatch a IMPRESSION with application_menu", async () => {
        // means panel is triggered as a subview in the application menu
        fakeElementById.hasAttribute.returns(false);
        const messages = (await PanelTestProvider.getMessages()).filter(
          m => m.template === "whatsnew_panel_message"
        );
        getMessagesStub.resolves(messages);
        const spy = sandbox.spy(instance, "sendUserEventTelemetry");
        const panelPingId = messages
          .map(({ id }) => id)
          .sort()
          .join(",");

        await instance.renderMessages(fakeWindow, fakeDocument, "container-id");

        assert.calledOnce(spy);
        assert.calledWithExactly(
          spy,
          fakeWindow,
          "IMPRESSION",
          {
            id: panelPingId,
          },
          {
            value: {
              view: "application_menu",
            },
          }
        );
        assert.calledOnce(fakeSendTelemetry);
        const {
          args: [dispatchPayload],
        } = fakeSendTelemetry.lastCall;
        assert.propertyVal(dispatchPayload, "type", "TOOLBAR_PANEL_TELEMETRY");
        assert.propertyVal(dispatchPayload.data, "message_id", panelPingId);
        assert.deepEqual(dispatchPayload.data.event_context, {
          view: "application_menu",
        });
      });
    });
    describe("#forceShowMessage", () => {
      const panelSelector = "PanelUI-whatsNew-message-container";
      let removeMessagesSpy;
      let renderMessagesStub;
      let addEventListenerStub;
      let messages;
      let browser;
      beforeEach(async () => {
        messages = (await PanelTestProvider.getMessages()).find(
          m => m.id === "WHATS_NEW_70_1"
        );
        removeMessagesSpy = sandbox.spy(instance, "removeMessages");
        renderMessagesStub = sandbox.spy(instance, "renderMessages");
        addEventListenerStub = fakeElementById.addEventListener;
        browser = { ownerGlobal: fakeWindow, ownerDocument: fakeDocument };
        fakeElementById.querySelectorAll.returns([fakeElementById]);
      });
      it("should call removeMessages when forcing a message to show", () => {
        instance.forceShowMessage(browser, messages);

        assert.calledWithExactly(removeMessagesSpy, fakeWindow, panelSelector);
      });
      it("should call renderMessages when forcing a message to show", () => {
        instance.forceShowMessage(browser, messages);

        assert.calledOnce(renderMessagesStub);
        assert.calledWithExactly(
          renderMessagesStub,
          fakeWindow,
          fakeDocument,
          panelSelector,
          {
            force: true,
            messages: Array.isArray(messages) ? messages : [messages],
          }
        );
      });
      it("should cleanup after the panel is hidden when forcing a message to show", () => {
        instance.forceShowMessage(browser, messages);

        assert.calledOnce(addEventListenerStub);
        assert.calledWithExactly(
          addEventListenerStub,
          "popuphidden",
          sinon.match.func
        );

        const [, cb] = addEventListenerStub.firstCall.args;
        // Reset the call count from the first `forceShowMessage` call
        removeMessagesSpy.resetHistory();
        cb({ target: { ownerGlobal: fakeWindow } });

        assert.calledOnce(removeMessagesSpy);
        assert.calledWithExactly(removeMessagesSpy, fakeWindow, panelSelector);
      });
      it("should exit gracefully if called before a browser exists", () => {
        instance.forceShowMessage(null, messages);
        assert.neverCalledWith(removeMessagesSpy, fakeWindow, panelSelector);
      });
    });
  });
  describe("#insertProtectionPanelMessage", () => {
    const fakeInsert = () =>
      instance.insertProtectionPanelMessage({
        target: { ownerGlobal: fakeWindow, ownerDocument: fakeDocument },
      });
    let getMessagesStub;
    beforeEach(async () => {
      const onboardingMsgs =
        await OnboardingMessageProvider.getUntranslatedMessages();
      getMessagesStub = sandbox
        .stub()
        .resolves(
          onboardingMsgs.find(msg => msg.template === "protections_panel")
        );
      await instance.init(waitForInitializedStub, {
        sendTelemetry: fakeSendTelemetry,
        getMessages: getMessagesStub,
      });
    });
    it("should remember it showed", async () => {
      await fakeInsert();

      assert.calledWithExactly(
        setBoolPrefStub,
        "browser.protections_panel.infoMessage.seen",
        true
      );
    });
    it("should toggle/expand when default collapsed/disabled", async () => {
      fakeElementById.hasAttribute.returns(true);

      await fakeInsert();

      assert.calledThrice(fakeElementById.toggleAttribute);
    });
    it("should toggle again when popup hides", async () => {
      fakeElementById.addEventListener.callsArg(1);

      await fakeInsert();

      assert.callCount(fakeElementById.toggleAttribute, 6);
    });
    it("should open link on click (separate link element)", async () => {
      const sendTelemetryStub = sandbox.stub(
        instance,
        "sendUserEventTelemetry"
      );
      const onboardingMsgs =
        await OnboardingMessageProvider.getUntranslatedMessages();
      const msg = onboardingMsgs.find(m => m.template === "protections_panel");

      await fakeInsert();

      assert.calledOnce(sendTelemetryStub);
      assert.calledWithExactly(
        sendTelemetryStub,
        fakeWindow,
        "IMPRESSION",
        msg
      );

      eventListeners.mouseup();

      assert.calledOnce(global.SpecialMessageActions.handleAction);
      assert.calledWithExactly(
        global.SpecialMessageActions.handleAction,
        {
          type: "OPEN_URL",
          data: {
            args: sinon.match.string,
            where: "tabshifted",
          },
        },
        fakeWindow.browser
      );
    });
    it("should format the url", async () => {
      const stub = sandbox
        .stub(global.Services.urlFormatter, "formatURL")
        .returns("formattedURL");
      const onboardingMsgs =
        await OnboardingMessageProvider.getUntranslatedMessages();
      const msg = onboardingMsgs.find(m => m.template === "protections_panel");

      await fakeInsert();

      eventListeners.mouseup();

      assert.calledOnce(stub);
      assert.calledWithExactly(stub, msg.content.cta_url);
      assert.calledOnce(global.SpecialMessageActions.handleAction);
      assert.calledWithExactly(
        global.SpecialMessageActions.handleAction,
        {
          type: "OPEN_URL",
          data: {
            args: "formattedURL",
            where: "tabshifted",
          },
        },
        fakeWindow.browser
      );
    });
    it("should report format url errors", async () => {
      const stub = sandbox
        .stub(global.Services.urlFormatter, "formatURL")
        .throws();
      const onboardingMsgs =
        await OnboardingMessageProvider.getUntranslatedMessages();
      const msg = onboardingMsgs.find(m => m.template === "protections_panel");
      sandbox.spy(global.console, "error");

      await fakeInsert();

      eventListeners.mouseup();

      assert.calledOnce(stub);
      assert.calledOnce(global.console.error);
      assert.calledOnce(global.SpecialMessageActions.handleAction);
      assert.calledWithExactly(
        global.SpecialMessageActions.handleAction,
        {
          type: "OPEN_URL",
          data: {
            args: msg.content.cta_url,
            where: "tabshifted",
          },
        },
        fakeWindow.browser
      );
    });
    it("should open link on click (directly attached to the message)", async () => {
      const onboardingMsgs =
        await OnboardingMessageProvider.getUntranslatedMessages();
      const msg = onboardingMsgs.find(m => m.template === "protections_panel");
      getMessagesStub.resolves({
        ...msg,
        content: { ...msg.content, link_text: null },
      });
      await fakeInsert();

      eventListeners.mouseup();

      assert.calledOnce(global.SpecialMessageActions.handleAction);
      assert.calledWithExactly(
        global.SpecialMessageActions.handleAction,
        {
          type: "OPEN_URL",
          data: {
            args: sinon.match.string,
            where: "tabshifted",
          },
        },
        fakeWindow.browser
      );
    });
    it("should handle user actions from mouseup and keyup", async () => {
      await fakeInsert();

      eventListeners.mouseup();
      eventListeners.keyup({ key: "Enter" });
      eventListeners.keyup({ key: " " });
      assert.calledThrice(global.SpecialMessageActions.handleAction);
    });
  });
});
