/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, you can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

let gAccount, gMessages;
let gDefaultTabmail, gDefaultAbout3Pane, gDefaultMessagePane;

add_setup(async () => {
  gAccount = createAccount();
  const rootFolder = gAccount.incomingServer.rootFolder;
  const folder = await createSubfolder(rootFolder, "messageDisplayScripts");
  await createMessages(folder, 11);
  gMessages = [...folder.messages];

  gDefaultTabmail = document.getElementById("tabmail");
  gDefaultAbout3Pane =
    gDefaultTabmail.currentTabInfo.chromeBrowser.contentWindow;
  gDefaultAbout3Pane.displayFolder(folder.URI);
  gDefaultMessagePane =
    gDefaultAbout3Pane.messageBrowser.contentDocument.getElementById(
      "messagepane"
    );
});

async function checkMessageBody(expected, message, browser) {
  if (message && "textContent" in expected) {
    let body = await new Promise(resolve => {
      window.MsgHdrToMimeMessage(message, null, (msgHdr, mimeMessage) => {
        resolve(mimeMessage.parts[0].body);
      });
    });
    // Ignore Windows line-endings, they're not important here.
    body = body.replace(/\r/g, "");
    expected.textContent = body + expected.textContent;
  }
  if (!browser) {
    browser = gDefaultMessagePane;
  }

  await checkContent(browser, expected);
}

/** Tests browser.scripting.insertCSS and browser.scripting.removeCSS. */
add_task(async function testInsertRemoveCSSViaScriptingAPI() {
  const extension = ExtensionTestUtils.loadExtension({
    files: {
      "background.js": async () => {
        const [tab] = await browser.tabs.query({ type: ["mail"] });
        await window.sendMessage();

        await browser.scripting.insertCSS({
          target: { tabId: tab.id },
          css: "body { background-color: lime; }",
        });
        await window.sendMessage();

        await browser.scripting.removeCSS({
          target: { tabId: tab.id },
          css: "body { background-color: lime; }",
        });
        await window.sendMessage();

        await browser.scripting.insertCSS({
          target: { tabId: tab.id },
          files: ["test.css"],
        });
        await window.sendMessage();

        await browser.scripting.removeCSS({
          target: { tabId: tab.id },
          files: ["test.css"],
        });

        browser.test.notifyPass("finished");
      },
      "test.css": "body { background-color: green; }",
      "utils.js": await getUtilsJS(),
    },
    manifest: {
      manifest_version: 3,
      background: { scripts: ["utils.js", "background.js"] },
      permissions: ["messagesRead", "scripting"],
    },
  });

  gDefaultAbout3Pane.threadTree.selectedIndex = 2;
  await awaitBrowserLoaded(gDefaultMessagePane);

  await extension.startup();

  await extension.awaitMessage();
  await checkMessageBody(
    { backgroundColor: "rgba(0, 0, 0, 0)" },
    gMessages.at(-3)
  );
  extension.sendMessage();

  await extension.awaitMessage();
  await checkMessageBody(
    { backgroundColor: "rgb(0, 255, 0)" },
    gMessages.at(-3)
  );
  extension.sendMessage();

  await extension.awaitMessage();
  await checkMessageBody(
    { backgroundColor: "rgba(0, 0, 0, 0)" },
    gMessages.at(-3)
  );
  extension.sendMessage();

  await extension.awaitMessage();
  await checkMessageBody(
    { backgroundColor: "rgb(0, 128, 0)" },
    gMessages.at(-3)
  );
  extension.sendMessage();

  await extension.awaitFinish("finished");
  await checkMessageBody(
    { backgroundColor: "rgba(0, 0, 0, 0)" },
    gMessages.at(-3)
  );

  await extension.unload();
});

/** Tests browser.scripting.insertCSS fails without the "messagesRead" permission. */
add_task(async function testInsertRemoveCSSNoHostPermissions() {
  const extension = ExtensionTestUtils.loadExtension({
    files: {
      "background.js": async () => {
        const [tab] = await browser.tabs.query({ type: ["mail"] });

        await browser.test.assertRejects(
          browser.scripting.insertCSS({
            target: { tabId: tab.id },
            css: "body { background-color: darkred; }",
          }),
          /Missing host permission for the tab/,
          "insertCSS without permission should throw"
        );

        await browser.test.assertRejects(
          browser.scripting.insertCSS({
            target: { tabId: tab.id },
            files: ["test.css"],
          }),
          /Missing host permission for the tab/,
          "insertCSS without permission should throw"
        );

        await browser.test.assertRejects(
          browser.scripting.insertCSS({
            target: { tabId: tab.id },
            files: ["test.css"],
          }),
          /Missing host permission for the tab/,
          "insertCSS without permission should throw"
        );

        browser.test.notifyPass("finished");
      },
      "test.css": "body { background-color: red; }",
      "utils.js": await getUtilsJS(),
    },
    manifest: {
      manifest_version: 3,
      background: { scripts: ["utils.js", "background.js"] },
      permissions: ["scripting"],
    },
  });

  gDefaultAbout3Pane.threadTree.selectedIndex = 1;
  await awaitBrowserLoaded(gDefaultMessagePane);

  await extension.startup();

  await extension.awaitFinish("finished");
  await checkMessageBody(
    {
      backgroundColor: "rgba(0, 0, 0, 0)",
      textContent: "",
    },
    gMessages.at(-2)
  );

  await extension.unload();
});

/** Tests browser.scripting.executeScript. */
add_task(async function testExecuteScript() {
  const extension = ExtensionTestUtils.loadExtension({
    files: {
      "background.js": async () => {
        const [tab] = await browser.tabs.query({ type: ["mail"] });
        await window.sendMessage();

        await browser.scripting.executeScript({
          target: { tabId: tab.id },
          func: () => {
            document.body.setAttribute("foo", "bar");
          },
        });
        await window.sendMessage();

        await browser.scripting.executeScript({
          target: { tabId: tab.id },
          files: ["test.js"],
        });

        browser.test.notifyPass("finished");
      },
      "test.js": () => {
        document.body.querySelector(".moz-text-flowed").textContent +=
          "Hey look, the script ran!";
      },
      "utils.js": await getUtilsJS(),
    },
    manifest: {
      manifest_version: 3,
      background: { scripts: ["utils.js", "background.js"] },
      permissions: ["messagesRead", "scripting"],
    },
  });

  gDefaultAbout3Pane.threadTree.selectedIndex = 2;
  await awaitBrowserLoaded(gDefaultMessagePane);

  await extension.startup();

  await extension.awaitMessage();
  await checkMessageBody({ textContent: "" }, gMessages.at(-3));
  extension.sendMessage();

  await extension.awaitMessage();
  await checkMessageBody({ foo: "bar" }, gMessages.at(-3));
  extension.sendMessage();

  await extension.awaitFinish("finished");
  await checkMessageBody(
    {
      foo: "bar",
      textContent: "Hey look, the script ran!",
    },
    gMessages.at(-3)
  );

  await extension.unload();
});

/** Tests browser.scripting.executeScript fails without the "messagesRead" permission. */
add_task(async function testExecuteScriptNoHostPermissions() {
  const extension = ExtensionTestUtils.loadExtension({
    files: {
      "background.js": async () => {
        const [tab] = await browser.tabs.query({ type: ["mail"] });

        await browser.test.assertRejects(
          browser.scripting.executeScript({
            target: { tabId: tab.id },
            func: () => {
              document.body.setAttribute("foo", "bar");
            },
          }),
          /Missing host permission for the tab/,
          "executeScript without permission should throw"
        );

        await browser.test.assertRejects(
          browser.scripting.executeScript({
            target: { tabId: tab.id },
            files: ["test.js"],
          }),
          /Missing host permission for the tab/,
          "executeScript without permission should throw"
        );

        await browser.test.assertRejects(
          browser.scripting.executeScript({
            target: { tabId: tab.id },
            files: ["test.js"],
          }),
          /Missing host permission for the tab/,
          "executeScript without permission should throw"
        );

        browser.test.notifyPass("finished");
      },
      "test.js": () => {
        document.body.querySelector(".moz-text-flowed").textContent +=
          "Hey look, the script ran!";
      },
      "utils.js": await getUtilsJS(),
    },
    manifest: {
      manifest_version: 3,
      background: { scripts: ["utils.js", "background.js"] },
      permissions: ["scripting"],
    },
  });

  gDefaultAbout3Pane.threadTree.selectedIndex = 3;
  await awaitBrowserLoaded(gDefaultMessagePane);

  await extension.startup();

  await extension.awaitFinish("finished");
  await checkMessageBody({ foo: null, textContent: "" }, gMessages.at(-4));

  await extension.unload();
});

/** Tests the messenger alias is available. */
add_task(async function testExecuteScriptAlias() {
  const extension = ExtensionTestUtils.loadExtension({
    files: {
      "background.js": async () => {
        const [tab] = await browser.tabs.query({ type: ["mail"] });
        await window.sendMessage();

        await browser.scripting.executeScript({
          target: { tabId: tab.id },
          func: () => {
            // eslint-disable-next-line no-undef
            const id = messenger.runtime.getManifest().applications.gecko.id;
            document.body.querySelector(".moz-text-flowed").textContent += id;
          },
        });

        browser.test.notifyPass("finished");
      },
      "utils.js": await getUtilsJS(),
    },
    manifest: {
      manifest_version: 3,
      browser_specific_settings: {
        gecko: { id: "message_display_scripts@mochitest" },
      },
      background: { scripts: ["utils.js", "background.js"] },
      permissions: ["messagesRead", "scripting"],
    },
  });

  gDefaultAbout3Pane.threadTree.selectedIndex = 3;
  await awaitBrowserLoaded(gDefaultMessagePane);

  await extension.startup();

  await extension.awaitMessage();
  await checkMessageBody({ textContent: "" }, gMessages.at(-4));
  extension.sendMessage();

  await extension.awaitFinish("finished");
  await checkMessageBody(
    { textContent: "message_display_scripts@mochitest" },
    gMessages.at(-4)
  );

  await extension.unload();
});

/**
 * Tests `browser.scripting.messageDisplay.registerScripts()` correctly adds CSS
 * and JavaScript to message display windows. Also tests calling
 * `browser.scripting.messageDisplay.registerScripts()`.
 */
add_task(async function testRegister() {
  const extension = ExtensionTestUtils.loadExtension({
    files: {
      "background.js": () => {
        // Keep track of registered scrips being executed and ready.
        browser.runtime.onMessage.addListener((message, sender) => {
          if (message == "LOADED") {
            browser.test.sendMessage("ScriptLoaded", sender.tab.id);
          }
        });

        const EXPECTED_DETAILS = [
          {
            id: "test-1",
            runAt: "document_idle",
            css: ["test.css"],
            js: ["test.js"],
          },
        ];

        // Register the message display script only during install, and not when
        // the background script wakes up again.
        browser.runtime.onInstalled.addListener(async () => {
          await browser.scripting.messageDisplay.registerScripts([
            {
              id: "test-1",
              css: ["test.css"],
              js: ["test.js"],
            },
          ]);

          // Test getRegisteredScripts(filter).
          const testsForGetRegisteredScripts = [
            { filter: {}, expected: EXPECTED_DETAILS },
            { filter: { ids: [] }, expected: [] },
            { filter: { ids: ["test-1"] }, expected: EXPECTED_DETAILS },
            {
              filter: { ids: ["test-1", "test-2"] },
              expected: EXPECTED_DETAILS,
            },
            { filter: { ids: ["test-2"] }, expected: [] },
          ];
          for (const test of testsForGetRegisteredScripts) {
            window.assertDeepEqual(
              test.expected,
              await browser.scripting.messageDisplay.getRegisteredScripts(
                test.filter
              ),
              `Return value of getRegisteredScripts(${JSON.stringify(
                test.filter
              )}) should be correct`,
              { strict: true }
            );
          }
          browser.test.sendMessage("Installed");
        });

        browser.test.onMessage.addListener(async (message, data) => {
          switch (message) {
            case "Unregister":
              {
                const testsForUnregisterScripts = [
                  { filter: {}, expected: [] },
                  { filter: { ids: [] }, expected: EXPECTED_DETAILS },
                  {
                    filter: { ids: ["test-2"] },
                    expected: EXPECTED_DETAILS,
                    expectedError: `The messageDisplayScript with id "test-2" does not exist.`,
                  },
                  { filter: { ids: ["test-1"] }, expected: [] },
                  {
                    filter: { ids: ["test-1", "test-2"] },
                    // The entire call rejects, not just the request to unregister
                    // the test-2 script.
                    expected: EXPECTED_DETAILS,
                    expectedError: `The messageDisplayScript with id "test-2" does not exist.`,
                  },
                ];
                for (const test of testsForUnregisterScripts) {
                  let error = false;
                  try {
                    await browser.scripting.messageDisplay.unregisterScripts(
                      test.filter
                    );
                  } catch (ex) {
                    browser.test.assertEq(
                      test.expectedError,
                      ex.message,
                      "Error message of unregisterScripts() should be correct"
                    );
                    error = true;
                  }
                  browser.test.assertEq(
                    !!test.expectedError,
                    error,
                    "unregisterScripts() should throw as expected"
                  );
                  window.assertDeepEqual(
                    test.expected,
                    await browser.scripting.messageDisplay.getRegisteredScripts(),
                    `Return value of getRegisteredScripts() should be correct`,
                    { strict: true }
                  );
                  // Re-Register.
                  try {
                    await browser.scripting.messageDisplay.registerScripts([
                      {
                        id: "test-1",
                        css: ["test.css"],
                        js: ["test.js"],
                      },
                    ]);
                  } catch (ex) {
                    // Yep, this may throw, if we re-register a script which
                    // exists already.
                  }
                  // Re-Check.
                  window.assertDeepEqual(
                    EXPECTED_DETAILS,
                    await browser.scripting.messageDisplay.getRegisteredScripts(),
                    `Return value of getRegisteredScripts() should be correct`,
                    { strict: true }
                  );
                }

                // Test unregisterScripts(). Should unregister all scripts.
                await browser.scripting.messageDisplay.unregisterScripts();
                window.assertDeepEqual(
                  [],
                  await browser.scripting.messageDisplay.getRegisteredScripts(),
                  `Return value of getRegisteredScripts() should be correct`,
                  { strict: true }
                );

                browser.test.notifyPass("finished");
              }
              break;

            case "RuntimeMessageTest":
              try {
                browser.test.assertEq(
                  `Received: ${data.tabId}`,
                  await browser.tabs.sendMessage(data.tabId, data.tabId)
                );
              } catch (ex) {
                browser.test.fail(
                  `Failed to send message to messageDisplayScript: ${ex}`
                );
              }
              browser.test.sendMessage("RuntimeMessageTestDone");
              break;
          }
        });

        browser.test.sendMessage("Ready");
      },
      "test.css": "body { color: white; background-color: green; }",
      "test.js": () => {
        document.body.setAttribute("foo", "bar");
        document.body.querySelector(".moz-text-flowed").textContent +=
          "Hey look, the script ran!";
        browser.runtime.onMessage.addListener(async message => {
          return `Received: ${message}`;
        });
        browser.runtime.sendMessage("LOADED");
      },
      "utils.js": await getUtilsJS(),
    },
    manifest: {
      manifest_version: 3,
      background: { scripts: ["utils.js", "background.js"] },
      permissions: ["messagesRead", "scripting"],
      host_permissions: ["<all_urls>"],
    },
  });

  gDefaultAbout3Pane.threadTree.selectedIndex = 5;
  await awaitBrowserLoaded(gDefaultMessagePane);

  extension.startup();
  // During startup we get the "Installed" message triggered by the onInstalled
  // event handler (which registers the message display script), and the "Ready"
  // message which is send at the end of the background script.
  await Promise.all([
    extension.awaitMessage("Installed"),
    extension.awaitMessage("Ready"),
  ]);

  // Check a message that was already loaded. This tab has not loaded the
  // registered scripts.
  await checkMessageBody(
    {
      backgroundColor: "rgba(0, 0, 0, 0)",
      textContent: "",
    },
    gMessages.at(-6)
  );

  // Terminate the background page. The message display script should stay registered.
  await extension.terminateBackground({
    disableResetIdleForTest: true,
  });

  // Load a new message and check it is modified. Since the message display scripts
  // sends a runtime message, the background will wake up and send a "Ready" message
  // alongside the "ScriptLoaded" message.
  let loadPromise = Promise.all([
    extension.awaitMessage("ScriptLoaded"),
    extension.awaitMessage("Ready"),
  ]);
  gDefaultAbout3Pane.threadTree.selectedIndex = 6;
  const [tabId] = await loadPromise;

  await checkMessageBody(
    {
      backgroundColor: "rgb(0, 128, 0)",
      color: "rgb(255, 255, 255)",
      foo: "bar",
      textContent: "Hey look, the script ran!",
    },
    gMessages.at(-7)
  );

  // Check runtime messaging.
  let testDonePromise = extension.awaitMessage("RuntimeMessageTestDone");
  extension.sendMessage("RuntimeMessageTest", { tabId });
  await testDonePromise;

  // Open the message in a new tab.
  loadPromise = extension.awaitMessage("ScriptLoaded");
  const messageTab = await openMessageInTab(gMessages.at(-7));
  const messageTabId = await loadPromise;
  Assert.equal(gDefaultTabmail.tabInfo.length, 2);

  await checkMessageBody(
    {
      backgroundColor: "rgb(0, 128, 0)",
      color: "rgb(255, 255, 255)",
      foo: "bar",
      textContent: "Hey look, the script ran!",
    },
    gMessages.at(-7),
    messageTab.browser
  );
  // Check runtime messaging.
  testDonePromise = extension.awaitMessage("RuntimeMessageTestDone");
  extension.sendMessage("RuntimeMessageTest", { tabId: messageTabId });
  await testDonePromise;

  // Open a content tab. The CSS and script shouldn't apply.
  const contentTab = window.openContentTab("http://mochi.test:8888/");
  // Let's wait a while and see if anything happens:
  // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
  await new Promise(resolve => setTimeout(resolve, 1000));
  await checkMessageBody(
    {
      backgroundColor: "rgba(0, 0, 0, 0)",
      color: "rgb(0, 0, 0)",
      foo: null,
    },
    undefined,
    contentTab.browser
  );

  // Closing this tab should bring us back to the message in a tab.
  gDefaultTabmail.closeTab(contentTab);
  Assert.equal(gDefaultTabmail.currentTabInfo, messageTab);
  await checkMessageBody(
    {
      backgroundColor: "rgb(0, 128, 0)",
      color: "rgb(255, 255, 255)",
      foo: "bar",
      textContent: "Hey look, the script ran!",
    },
    gMessages.at(-7),
    messageTab.browser
  );
  // Check runtime messaging.
  testDonePromise = extension.awaitMessage("RuntimeMessageTestDone");
  extension.sendMessage("RuntimeMessageTest", { tabId: messageTabId });
  await testDonePromise;

  // Open the message in a new window.
  loadPromise = extension.awaitMessage("ScriptLoaded");
  const newWindow = await openMessageInWindow(gMessages.at(-8));
  const newWindowMessagePane = newWindow.getBrowser();
  const windowTabId = await loadPromise;

  await checkMessageBody(
    {
      backgroundColor: "rgb(0, 128, 0)",
      color: "rgb(255, 255, 255)",
      foo: "bar",
      textContent: "Hey look, the script ran!",
    },
    gMessages.at(-8),
    newWindowMessagePane
  );
  // Check runtime messaging.
  testDonePromise = extension.awaitMessage("RuntimeMessageTestDone");
  extension.sendMessage("RuntimeMessageTest", { tabId: windowTabId });
  await testDonePromise;

  // Unregister.
  extension.sendMessage("Unregister");
  await extension.awaitFinish("finished");
  await extension.unload();

  // Check the CSS is unloaded from the message in a tab.
  await checkMessageBody(
    {
      backgroundColor: "rgba(0, 0, 0, 0)",
      color: "rgb(0, 0, 0)",
      foo: "bar",
      textContent: "Hey look, the script ran!",
    },
    gMessages.at(-7),
    messageTab.browser
  );

  // Close the new tab.
  gDefaultTabmail.closeTab(messageTab);

  await checkMessageBody(
    {
      backgroundColor: "rgba(0, 0, 0, 0)",
      color: "rgb(0, 0, 0)",
      foo: "bar",
      textContent: "Hey look, the script ran!",
    },
    gMessages.at(-7)
  );

  // Check the CSS is unloaded from the message in a window.
  await checkMessageBody(
    {
      backgroundColor: "rgba(0, 0, 0, 0)",
      color: "rgb(0, 0, 0)",
      foo: "bar",
      textContent: "Hey look, the script ran!",
    },
    gMessages.at(-8),
    newWindowMessagePane
  );

  await BrowserTestUtils.closeWindow(newWindow);
});

/** Tests content_scripts in the manifest do not affect message display. */
async function subtestContentScriptManifest(message, permissions) {
  const extension = ExtensionTestUtils.loadExtension({
    files: {
      "test.css": "body { background-color: red; }",
      "test.js": () => {
        document.body.textContent += "Hey look, the script ran!";
      },
    },
    manifest: {
      manifest_version: 3,
      permissions,
      content_scripts: [
        {
          matches: ["<all_urls>"],
          css: ["test.css"],
          js: ["test.js"],
          match_about_blank: true,
          match_origin_as_fallback: true,
        },
      ],
    },
  });

  // match_origin_as_fallback is not implemented yet. Bug 1475831.
  ExtensionTestUtils.failOnSchemaWarnings(false);
  await extension.startup();
  ExtensionTestUtils.failOnSchemaWarnings(true);

  await checkMessageBody(
    {
      backgroundColor: "rgba(0, 0, 0, 0)",
      textContent: "",
    },
    message
  );

  await extension.unload();
}

add_task(async function testContentScriptManifestNoPermission() {
  gDefaultAbout3Pane.threadTree.selectedIndex = 7;
  await awaitBrowserLoaded(gDefaultMessagePane);
  await subtestContentScriptManifest(gMessages.at(-8));
});
add_task(async function testContentScriptManifest() {
  gDefaultAbout3Pane.threadTree.selectedIndex = 8;
  await awaitBrowserLoaded(gDefaultMessagePane);
  await subtestContentScriptManifest(gMessages.at(-9), ["messagesRead"]);
});

/** Tests registered content scripts do not affect message display. */
async function subtestContentScriptRegister(message, permissions) {
  const extension = ExtensionTestUtils.loadExtension({
    files: {
      "background.js": async () => {
        await browser.scripting.registerContentScripts([
          {
            id: "test",
            matches: ["<all_urls>"],
            css: ["test.css"],
            js: ["test.js"],
          },
        ]);

        browser.test.notifyPass("finished");
      },
      "test.css": "body { background-color: red; }",
      "test.js": () => {
        document.body.querySelector(".moz-text-flowed").textContent +=
          "Hey look, the script ran!";
      },
      "utils.js": await getUtilsJS(),
    },
    manifest: {
      manifest_version: 3,
      background: { scripts: ["utils.js", "background.js"] },
      permissions,
      host_permissions: ["<all_urls>"],
    },
  });

  await extension.startup();

  await extension.awaitFinish("finished");
  await checkMessageBody(
    {
      backgroundColor: "rgba(0, 0, 0, 0)",
      textContent: "",
    },
    message
  );

  await extension.unload();
}

add_task(async function testContentScriptRegisterNoPermission() {
  gDefaultAbout3Pane.threadTree.selectedIndex = 9;
  await awaitBrowserLoaded(gDefaultMessagePane);
  await subtestContentScriptRegister(gMessages.at(-10), ["scripting"]);
});
add_task(async function testContentScriptRegister() {
  gDefaultAbout3Pane.threadTree.selectedIndex = 10;
  await awaitBrowserLoaded(gDefaultMessagePane);
  await subtestContentScriptRegister(gMessages.at(-11), [
    "scripting",
    "messagesRead",
  ]);
});

/**
 * Tests if scripts are correctly injected according to their runAt option.
 */
add_task(async function testRunAt() {
  const extension = ExtensionTestUtils.loadExtension({
    files: {
      "background.js": async () => {
        // Report script results.
        browser.runtime.onMessage.addListener((message, sender) => {
          if (message?.runAt) {
            browser.test.sendMessage(`ScriptLoaded:${message.runAt}`, {
              senderTabId: sender.tab.id,
              ...message,
            });
          }
        });

        await browser.scripting.messageDisplay.registerScripts([
          {
            id: "test-start",
            runAt: "document_start",
            js: ["start.js"],
          },
          {
            id: "test-end",
            runAt: "document_end",
            js: ["end.js"],
          },
          {
            id: "test-idle",
            runAt: "document_idle",
            js: ["idle.js"],
          },
        ]);

        browser.test.onMessage.addListener(async message => {
          switch (message) {
            case "Unregister":
              await browser.scripting.messageDisplay.unregisterScripts();
              browser.test.notifyPass("finished");
              break;
          }
        });

        browser.test.sendMessage("Ready");
      },
      "start.js": () => {
        browser.runtime.sendMessage({
          runAt: "document_start",
          readyState: document?.readyState,
          document: !!document,
          body: !!document?.body,
          textContent:
            document.querySelector(".moz-text-flowed")?.textContent ?? "",
        });
      },
      "end.js": () => {
        browser.runtime.sendMessage({
          runAt: "document_end",
          readyState: document?.readyState,
          document: !!document,
          body: !!document?.body,
          textContent:
            document.querySelector(".moz-text-flowed")?.textContent ?? "",
        });
      },
      "idle.js": () => {
        browser.runtime.sendMessage({
          runAt: "document_idle",
          readyState: document?.readyState,
          document: !!document,
          body: !!document?.body,
          textContent:
            document.querySelector(".moz-text-flowed")?.textContent ?? "",
        });
      },
      "utils.js": await getUtilsJS(),
    },
    manifest: {
      manifest_version: 3,
      background: { scripts: ["utils.js", "background.js"] },
      permissions: ["messagesRead", "scripting"],
      host_permissions: ["<all_urls>"],
    },
  });

  gDefaultAbout3Pane.threadTree.selectedIndex = 2;
  await awaitBrowserLoaded(gDefaultMessagePane);

  extension.startup();
  await extension.awaitMessage("Ready");

  function verifyResult(result, expected_individual) {
    const expected_standard = [
      {
        runAt: "document_start",
        readyState: "loading",
        document: true,
        body: false,
      },
      {
        runAt: "document_end",
        readyState: "interactive",
        document: true,
        body: true,
      },
      {
        runAt: "document_idle",
        readyState: "complete",
        document: true,
        body: true,
      },
    ];
    for (let i = 0; i < result.length; i++) {
      Assert.equal(
        expected_standard[i].runAt,
        result[i].runAt,
        `The 'runAt' value for state #${i} should be correct`
      );
      Assert.equal(
        expected_standard[i].readyState,
        result[i].readyState,
        `The 'readyState' value at state #${i} should be correct`
      );
      Assert.equal(
        expected_standard[i].document,
        result[i].document,
        `The document element at state #${i} ${
          expected_standard[i].document ? "should" : "should not"
        } exist`
      );
      Assert.equal(
        expected_standard[i].body,
        result[i].body,
        `The body element at state #${i} ${
          expected_standard[i].body ? "should" : "should not"
        } exist`
      );
      Assert.equal(
        expected_individual[i].textContent.trim(),
        result[i].textContent.trim(),
        `The content at state #${i} should be correct`
      );
    }
  }

  // Select a new message.
  const firstLoadPromise = Promise.all([
    extension.awaitMessage("ScriptLoaded:document_start"),
    extension.awaitMessage("ScriptLoaded:document_end"),
    extension.awaitMessage("ScriptLoaded:document_idle"),
  ]);
  gDefaultAbout3Pane.threadTree.selectedIndex = 3;
  verifyResult(await firstLoadPromise, [
    { textContent: "" },
    { textContent: "Hello Pete Price!" },
    { textContent: "Hello Pete Price!" },
  ]);

  // Select a different message.
  const secondLoadPromise = Promise.all([
    extension.awaitMessage("ScriptLoaded:document_start"),
    extension.awaitMessage("ScriptLoaded:document_end"),
    extension.awaitMessage("ScriptLoaded:document_idle"),
  ]);
  gDefaultAbout3Pane.threadTree.selectedIndex = 4;
  verifyResult(await secondLoadPromise, [
    { textContent: "" },
    { textContent: "Hello Neil Nagel!" },
    { textContent: "Hello Neil Nagel!" },
  ]);

  // Open the message in a new tab.
  const thirdLoadPromise = Promise.all([
    extension.awaitMessage("ScriptLoaded:document_start"),
    extension.awaitMessage("ScriptLoaded:document_end"),
    extension.awaitMessage("ScriptLoaded:document_idle"),
  ]);
  const messageTab = await openMessageInTab(gMessages.at(-6));
  verifyResult(await thirdLoadPromise, [
    { textContent: "" },
    { textContent: "Hello Lilia Lowe!" },
    { textContent: "Hello Lilia Lowe!" },
  ]);
  Assert.equal(gDefaultTabmail.tabInfo.length, 2);

  // Open a content tab. The message display scripts should not be injected.
  // If they DO get injected, we will end up with 3 additional messages from the
  // extension and the test will fail.
  const contentTab = window.openContentTab("http://mochi.test:8888/");
  Assert.equal(gDefaultTabmail.tabInfo.length, 3);
  // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
  await new Promise(resolve => setTimeout(resolve, 1000));

  // Closing this tab should bring us back to the message in a tab.
  gDefaultTabmail.closeTab(contentTab);
  Assert.equal(gDefaultTabmail.tabInfo.length, 2);
  Assert.equal(gDefaultTabmail.currentTabInfo, messageTab);

  // Open the message in a new window.
  const fourthLoadPromise = Promise.all([
    extension.awaitMessage("ScriptLoaded:document_start"),
    extension.awaitMessage("ScriptLoaded:document_end"),
    extension.awaitMessage("ScriptLoaded:document_idle"),
  ]);
  const newWindow = await openMessageInWindow(gMessages.at(-7));
  verifyResult(await fourthLoadPromise, [
    { textContent: "" },
    { textContent: "Hello Johnny Jones!" },
    { textContent: "Hello Johnny Jones!" },
  ]);

  // Unregister.
  extension.sendMessage("Unregister");
  await extension.awaitFinish("finished");
  await extension.unload();

  // Close the new tab.
  gDefaultTabmail.closeTab(messageTab);
  await BrowserTestUtils.closeWindow(newWindow);
});
