"use strict";

const server = createHttpServer();
server.registerDirectory("/data/", do_get_file("data"));

const BASE_URL = `http://localhost:${server.identity.primaryPort}/data`;
const TEST_URL_1 = `${BASE_URL}/file_sample.html`;
const TEST_URL_2 = `${BASE_URL}/file_content_script_errors.html`;

// ExtensionContent.jsm needs to know when it's running from xpcshell,
// to use the right timeout for content scripts executed at document_idle.
ExtensionTestUtils.mockAppInfo();

add_task(async function test_cached_contentscript_on_document_start() {
  let extension = ExtensionTestUtils.loadExtension({
    manifest: {
      content_scripts: [
        // Use distinct content scripts as some will throw and would prevent executing the next script
        {
          matches: ["http://*/*/file_content_script_errors.html"],
          js: ["script_does_not_exist.js"],
          run_at: "document_start",
        },
        {
          matches: ["http://*/*/file_content_script_errors.html"],
          js: ["script1.js"],
          run_at: "document_start",
        },
        {
          matches: ["http://*/*/file_content_script_errors.html"],
          js: ["script2.js"],
          run_at: "document_start",
        },
        {
          matches: ["http://*/*/file_content_script_errors.html"],
          js: ["script3.js"],
          run_at: "document_start",
        },
        {
          matches: ["http://*/*/file_content_script_errors.html"],
          js: ["script4.js"],
          run_at: "document_start",
        },
        {
          matches: ["http://*/*/file_content_script_errors.html"],
          js: ["script5.js"],
          run_at: "document_start",
        },
        {
          matches: ["http://*/*/file_content_script_errors.html"],
          js: ["script6.js"],
          run_at: "document_start",
        },
        {
          matches: ["http://*/*/file_content_script_errors.html"],
          js: ["script7.js"],
          run_at: "document_start",
        },
      ],
    },

    files: {
      "script1.js": `
        throw new Error("Object exception");
      `,
      "script2.js": `
        throw "String exception";
      `,
      "script3.js": `
        undefinedSymbol();
      `,
      "script4.js": `
        )
      `,
      "script5.js": `
        throw null;
      `,
      "script6.js": `
        throw Symbol("MySymbol");
      `,
      "script7.js": `
        Promise.reject("rejected promise");

        (async () => {
          /* make the async, really async */
          await new Promise(r => setTimeout(r, 0));
          throw new Error("async function exception");
        })();

        setTimeout(() => {
          asyncUndefinedSymbol();
        });

        /* Use a delay in order to resume test execution after these async errors */
        setTimeout(() => {
          browser.test.sendMessage("content-script-loaded");
        }, 500);
      `,
    },
  });

  await extension.startup();

  // Error messages, in roughly the order they appear above.
  let expectedMessages = [
    `Unable to load script: moz-extension://${extension.uuid}/script_does_not_exist.js`,
    "Error: Object exception",
    "uncaught exception: String exception",
    "ReferenceError: undefinedSymbol is not defined",
    "SyntaxError: expected expression, got ')'",
    "uncaught exception: null",
    "uncaught exception: Symbol(MySymbol)",
    "uncaught exception: rejected promise",
    "Error: async function exception",
    "ReferenceError: asyncUndefinedSymbol is not defined",
  ];

  // Load a first page in order to be able to register a console listener in the content process.
  // This has to be done in the same domain of the second page to stay in the same process.
  let contentPage = await ExtensionTestUtils.loadContentPage(TEST_URL_1);

  // Listen to the errors logged in the content process.
  // TODO: Switch to SpecialPowers.spawn
  // eslint-disable-next-line mozilla/reject-contenttask-spawn
  let errorsPromise = ContentTask.spawn(contentPage.browser, {}, async () => {
    return new Promise(resolve => {
      function listener(error0) {
        let error = error0.QueryInterface(Ci.nsIScriptError);

        // Ignore errors from ExtensionContent.jsm
        if (!error.innerWindowID) {
          return;
        }

        this.collectedErrors.push({
          innerWindowID: error.innerWindowID,
          message: error.errorMessage,
        });
        if (this.collectedErrors.length == 10) {
          Services.console.unregisterListener(this);
          resolve(this.collectedErrors);
        }
      }
      listener.collectedErrors = [];
      Services.console.registerListener(listener);
    });
  });

  // Reload the page and check that the cached content script is still able to
  // run on document_start.
  await contentPage.loadURL(TEST_URL_2);

  let errors = await errorsPromise;

  await extension.awaitMessage("content-script-loaded");

  equal(errors.length, 10);
  let messages = [];
  for (const { innerWindowID, message } of errors) {
    equal(
      innerWindowID,
      contentPage.browser.innerWindowID,
      `Message ${message} has the innerWindowID set`
    );

    messages.push(message);
  }

  messages.sort();
  expectedMessages.sort();
  Assert.deepEqual(messages, expectedMessages, "Got the expected errors");

  await extension.unload();

  await contentPage.close();
});
