/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";

const server = createHttpServer();
server.registerDirectory("/data/", do_get_file("data"));
const BASE_URL = `http://localhost:${server.identity.primaryPort}/data`;

add_task(async function runtimeSendMessageReply() {
  function background() {
    browser.runtime.onMessage.addListener((msg, sender, respond) => {
      if (msg == "respond-now") {
        respond(msg);
      } else if (msg == "respond-soon") {
        setTimeout(() => {
          respond(msg);
        }, 0);
        return true;
      } else if (msg == "respond-promise") {
        return Promise.resolve(msg);
      } else if (msg == "respond-promise-false") {
        return Promise.resolve(false);
      } else if (msg == "respond-false") {
        // return false means that respond() is not expected to be called.
        setTimeout(() => respond("should be ignored"));
        return false;
      } else if (msg == "respond-never") {
        return undefined;
      } else if (msg == "respond-error") {
        return Promise.reject(new Error(msg));
      } else if (msg == "throw-error") {
        throw new Error(msg);
      } else if (msg === "respond-uncloneable") {
        return Promise.resolve(window);
      } else if (msg === "reject-uncloneable") {
        return Promise.reject(window);
      } else if (msg == "reject-undefined") {
        return Promise.reject();
      } else if (msg == "throw-undefined") {
        throw undefined; // eslint-disable-line no-throw-literal
      }
    });

    browser.runtime.onMessage.addListener((msg, sender, respond) => {
      if (msg == "respond-now") {
        respond("hello");
      } else if (msg == "respond-now-2") {
        respond(msg);
      }
    });

    browser.runtime.onMessage.addListener(msg => {
      if (msg == "respond-now") {
        // If a response from another listener is received first, this
        // exception should be ignored.  Test fails if it is not.

        // All this is of course stupid, but some extensions depend on it.
        msg.blah.this.throws();
      }
    });

    let childFrame = document.createElement("iframe");
    childFrame.src = "extensionpage.html";
    document.body.appendChild(childFrame);
  }

  function senderScript() {
    Promise.all([
      browser.runtime.sendMessage("respond-now"),
      browser.runtime.sendMessage("respond-now-2"),
      new Promise(resolve =>
        browser.runtime.sendMessage("respond-soon", resolve)
      ),
      browser.runtime.sendMessage("respond-promise"),
      browser.runtime.sendMessage("respond-promise-false"),
      browser.runtime.sendMessage("respond-false"),
      browser.runtime.sendMessage("respond-never"),
      new Promise(resolve => {
        browser.runtime.sendMessage("respond-never", response => {
          resolve(response);
        });
      }),

      browser.runtime
        .sendMessage("respond-error")
        .catch(error => Promise.resolve({ error })),
      browser.runtime
        .sendMessage("throw-error")
        .catch(error => Promise.resolve({ error })),

      browser.runtime
        .sendMessage("respond-uncloneable")
        .catch(error => Promise.resolve({ error })),
      browser.runtime
        .sendMessage("reject-uncloneable")
        .catch(error => Promise.resolve({ error })),
      browser.runtime
        .sendMessage("reject-undefined")
        .catch(error => Promise.resolve({ error })),
      browser.runtime
        .sendMessage("throw-undefined")
        .catch(error => Promise.resolve({ error })),
    ])
      .then(
        ([
          respondNow,
          respondNow2,
          respondSoon,
          respondPromise,
          respondPromiseFalse,
          respondFalse,
          respondNever,
          respondNever2,
          respondError,
          throwError,
          respondUncloneable,
          rejectUncloneable,
          rejectUndefined,
          throwUndefined,
        ]) => {
          browser.test.assertEq(
            "respond-now",
            respondNow,
            "Got the expected immediate response"
          );
          browser.test.assertEq(
            "respond-now-2",
            respondNow2,
            "Got the expected immediate response from the second listener"
          );
          browser.test.assertEq(
            "respond-soon",
            respondSoon,
            "Got the expected delayed response"
          );
          browser.test.assertEq(
            "respond-promise",
            respondPromise,
            "Got the expected promise response"
          );
          browser.test.assertEq(
            false,
            respondPromiseFalse,
            "Got the expected false value as a promise result"
          );
          browser.test.assertEq(
            undefined,
            respondFalse,
            "Got the expected no-response when onMessage returns false"
          );
          browser.test.assertEq(
            undefined,
            respondNever,
            "Got the expected no-response resolution"
          );
          browser.test.assertEq(
            undefined,
            respondNever2,
            "Got the expected no-response resolution"
          );

          browser.test.assertEq(
            "respond-error",
            respondError.error.message,
            "Got the expected error response"
          );
          browser.test.assertEq(
            "throw-error",
            throwError.error.message,
            "Got the expected thrown error response"
          );

          browser.test.assertEq(
            "Could not establish connection. Receiving end does not exist.",
            respondUncloneable.error.message,
            "An uncloneable response should be ignored"
          );
          browser.test.assertEq(
            "An unexpected error occurred",
            rejectUncloneable.error.message,
            "Got the expected error for a rejection with an uncloneable value"
          );
          browser.test.assertEq(
            "An unexpected error occurred",
            rejectUndefined.error.message,
            "Got the expected error for a void rejection"
          );
          browser.test.assertEq(
            "An unexpected error occurred",
            throwUndefined.error.message,
            "Got the expected error for a void throw"
          );

          browser.test.notifyPass("sendMessage");
        }
      )
      .catch(e => {
        browser.test.fail(`Error: ${e} :: ${e.stack}`);
        browser.test.notifyFail("sendMessage");
      });
  }

  let extension = ExtensionTestUtils.loadExtension({
    background,
    files: {
      "senderScript.js": senderScript,
      "extensionpage.html": `<!DOCTYPE html><meta charset="utf-8"><script src="senderScript.js"></script>`,
    },
  });

  await extension.startup();
  await extension.awaitFinish("sendMessage");
  await extension.unload();
});

add_task(async function runtimeSendMessageBlob() {
  function background() {
    browser.runtime.onMessage.addListener(msg => {
      // eslint-disable-next-line mozilla/use-isInstance -- this function runs in an extension
      browser.test.assertTrue(msg.blob instanceof Blob, "Message is a blob");
      return Promise.resolve(msg);
    });

    let childFrame = document.createElement("iframe");
    childFrame.src = "extensionpage.html";
    document.body.appendChild(childFrame);
  }

  function senderScript() {
    browser.runtime
      .sendMessage({ blob: new Blob(["hello"]) })
      .then(response => {
        browser.test.assertTrue(
          // eslint-disable-next-line mozilla/use-isInstance -- this function runs in an extension
          response.blob instanceof Blob,
          "Response is a blob"
        );
        browser.test.notifyPass("sendBlob");
      });
  }

  let extension = ExtensionTestUtils.loadExtension({
    background,
    files: {
      "senderScript.js": senderScript,
      "extensionpage.html": `<!DOCTYPE html><meta charset="utf-8"><script src="senderScript.js"></script>`,
    },
  });

  await extension.startup();
  await extension.awaitFinish("sendBlob");
  await extension.unload();
});

add_task(async function sendMessageResponseGC() {
  function background() {
    let savedResolve, savedRespond;

    browser.runtime.onMessage.addListener((msg, _, respond) => {
      browser.test.log(`Got request: ${msg}`);
      switch (msg) {
        case "ping":
          respond("pong");
          return;

        case "promise-save":
          return new Promise(resolve => {
            savedResolve = resolve;
          });
        case "promise-resolve":
          savedResolve("saved-resolve");
          return;
        case "promise-never":
          return new Promise(() => {});

        case "callback-save":
          savedRespond = respond;
          return true;
        case "callback-call":
          savedRespond("saved-respond");
          return;
        case "callback-never":
          return true;
      }
    });

    const frame = document.createElement("iframe");
    frame.src = "page.html";
    document.body.appendChild(frame);
  }

  function page() {
    browser.test.onMessage.addListener(msg => {
      browser.runtime.sendMessage(msg).then(
        response => {
          if (response) {
            browser.test.log(`Got response: ${response}`);
            browser.test.sendMessage(response);
          }
        },
        error => {
          browser.test.assertEq(
            "Promised response from onMessage listener went out of scope",
            error.message,
            `Promise rejected with the correct error message`
          );

          browser.test.assertTrue(
            /^moz-extension:\/\/[\w-]+\/%7B[\w-]+%7D\.js/.test(error.fileName),
            `Promise rejected with the correct error filename: ${error.fileName}`
          );

          browser.test.assertEq(
            4,
            error.lineNumber,
            `Promise rejected with the correct error line number`
          );

          browser.test.assertTrue(
            /moz-extension:\/\/[\w-]+\/%7B[\w-]+%7D\.js:4/.test(error.stack),
            `Promise rejected with the correct error stack: ${error.stack}`
          );
          browser.test.sendMessage("rejected");
        }
      );
    });
    browser.test.sendMessage("ready");
  }

  let extension = ExtensionTestUtils.loadExtension({
    background,
    files: {
      "page.html":
        "<!DOCTYPE html><meta charset=utf-8><script src=page.js></script>",
      "page.js": page,
    },
  });

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

  // Setup long-running tasks before GC.
  extension.sendMessage("promise-save");
  extension.sendMessage("callback-save");

  // Test returning a Promise that can never resolve.
  extension.sendMessage("promise-never");

  extension.sendMessage("ping");
  await extension.awaitMessage("pong");

  Services.prefs.setBoolPref(
    "security.allow_parent_unrestricted_js_loads",
    true
  );
  Services.ppmm.loadProcessScript("data:,Components.utils.forceGC()", false);
  await extension.awaitMessage("rejected");

  // Test returning `true` without holding the response handle.
  extension.sendMessage("callback-never");

  extension.sendMessage("ping");
  await extension.awaitMessage("pong");

  Services.ppmm.loadProcessScript("data:,Components.utils.forceGC()", false);
  Services.prefs.setBoolPref(
    "security.allow_parent_unrestricted_js_loads",
    false
  );
  await extension.awaitMessage("rejected");

  // Test that promises from long-running tasks didn't get GCd.
  extension.sendMessage("promise-resolve");
  await extension.awaitMessage("saved-resolve");

  extension.sendMessage("callback-call");
  await extension.awaitMessage("saved-respond");

  ok("Long running tasks responded");
  await extension.unload();
});

add_task(async function sendMessage_async_response_multiple_contexts() {
  let extension = ExtensionTestUtils.loadExtension({
    background() {
      browser.runtime.onMessage.addListener((msg, _, respond) => {
        browser.test.log(`Background got request: ${msg}`);

        switch (msg) {
          case "ask-bg-fast":
            respond("bg-respond");
            return true;

          case "ask-bg-slow":
            return new Promise(r => setTimeout(() => r("bg-promise")), 1000);
        }
      });
      browser.test.sendMessage("bg-ready");
    },

    manifest: {
      content_scripts: [
        {
          matches: ["http://localhost/*/file_sample.html"],
          js: ["cs.js"],
        },
      ],
    },

    files: {
      "page.html":
        "<!DOCTYPE html><meta charset=utf-8><script src=page.js></script>",
      "page.js"() {
        browser.runtime.onMessage.addListener((msg, _, respond) => {
          browser.test.log(`Page got request: ${msg}`);

          switch (msg) {
            case "ask-page-fast":
              respond("page-respond");
              return true;

            case "ask-page-slow":
              return new Promise(r => setTimeout(() => r("page-promise")), 500);
          }
        });
        browser.test.sendMessage("page-ready");
      },

      "cs.js"() {
        Promise.all([
          browser.runtime.sendMessage("ask-bg-fast"),
          browser.runtime.sendMessage("ask-bg-slow"),
          browser.runtime.sendMessage("ask-page-fast"),
          browser.runtime.sendMessage("ask-page-slow"),
        ]).then(responses => {
          browser.test.assertEq(
            responses.join(),
            ["bg-respond", "bg-promise", "page-respond", "page-promise"].join(),
            "Got all expected responses from correct contexts"
          );
          browser.test.notifyPass("cs-done");
        });
      },
    },
  });

  await extension.startup();
  await extension.awaitMessage("bg-ready");

  let url = `moz-extension://${extension.uuid}/page.html`;
  let page = await ExtensionTestUtils.loadContentPage(url, { extension });
  await extension.awaitMessage("page-ready");

  let content = await ExtensionTestUtils.loadContentPage(
    BASE_URL + "/file_sample.html"
  );
  await extension.awaitFinish("cs-done");
  await content.close();

  await page.close();
  await extension.unload();
});
