"use strict";

const HOSTS = new Set(["example.com"]);

const server = createHttpServer({ hosts: HOSTS });

const BASE_URL = "http://example.com";
const FETCH_ORIGIN = "http://example.com/dummy";

server.registerPathHandler("/return_headers.sjs", (request, response) => {
  response.setHeader("Content-Type", "text/plain", false);

  let headers = {};
  for (let { data: header } of request.headers) {
    headers[header.toLowerCase()] = request.getHeader(header);
  }

  response.write(JSON.stringify(headers));
});

server.registerPathHandler("/dummy", (request, response) => {
  response.setStatusLine(request.httpVersion, 200, "OK");
  response.write("ok");
});

add_task(async function test_suspend() {
  let extension = ExtensionTestUtils.loadExtension({
    manifest: {
      permissions: ["webRequest", "webRequestBlocking", `${BASE_URL}/`],
    },

    background() {
      browser.webRequest.onBeforeSendHeaders.addListener(
        () => {
          // Make sure that returning undefined or a promise that resolves to
          // undefined does not break later handlers.
        },
        { urls: ["<all_urls>"] },
        ["blocking", "requestHeaders"]
      );

      browser.webRequest.onBeforeSendHeaders.addListener(
        () => {
          return Promise.resolve();
        },
        { urls: ["<all_urls>"] },
        ["blocking", "requestHeaders"]
      );

      browser.webRequest.onBeforeSendHeaders.addListener(
        details => {
          let requestHeaders = details.requestHeaders.concat({
            name: "Foo",
            value: "Bar",
          });

          return new Promise(resolve => {
            // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
            setTimeout(resolve, 500);
          }).then(() => {
            return { requestHeaders };
          });
        },
        { urls: ["<all_urls>"] },
        ["blocking", "requestHeaders"]
      );
    },
  });

  await extension.startup();

  let headers = JSON.parse(
    await ExtensionTestUtils.fetch(
      FETCH_ORIGIN,
      `${BASE_URL}/return_headers.sjs`
    )
  );

  equal(
    headers.foo,
    "Bar",
    "Request header was correctly set on suspended request"
  );

  await extension.unload();
});

// Test that requests that were canceled while suspended for a blocking
// listener are correctly resumed.
add_task(async function test_error_resume() {
  let observer = channel => {
    if (
      channel instanceof Ci.nsIHttpChannel &&
      channel.URI.spec === "http://example.com/dummy"
    ) {
      Services.obs.removeObserver(observer, "http-on-before-connect");

      // Wait until the next tick to make sure this runs after WebRequest observers.
      Promise.resolve().then(() => {
        channel.cancel(Cr.NS_BINDING_ABORTED);
      });
    }
  };

  Services.obs.addObserver(observer, "http-on-before-connect");

  let extension = ExtensionTestUtils.loadExtension({
    manifest: {
      permissions: ["webRequest", "webRequestBlocking", `${BASE_URL}/`],
    },

    background() {
      browser.webRequest.onBeforeSendHeaders.addListener(
        details => {
          browser.test.log(`onBeforeSendHeaders({url: ${details.url}})`);

          if (details.url === "http://example.com/dummy") {
            browser.test.sendMessage("got-before-send-headers");
          }
        },
        { urls: ["<all_urls>"] },
        ["blocking"]
      );

      browser.webRequest.onErrorOccurred.addListener(
        details => {
          browser.test.log(`onErrorOccurred({url: ${details.url}})`);

          if (details.url === "http://example.com/dummy") {
            browser.test.sendMessage("got-error-occurred");
          }
        },
        { urls: ["<all_urls>"] }
      );
    },
  });

  await extension.startup();

  try {
    await ExtensionTestUtils.fetch(FETCH_ORIGIN, `${BASE_URL}/dummy`);
    ok(false, "Fetch should have failed.");
  } catch (e) {
    ok(true, "Got expected error.");
  }

  await extension.awaitMessage("got-before-send-headers");
  await extension.awaitMessage("got-error-occurred");

  // Wait for the next tick so the onErrorRecurred response can be
  // processed before shutting down the extension.
  await new Promise(resolve => executeSoon(resolve));

  await extension.unload();
});

// Test that response header modifications take effect before onStartRequest fires.
add_task(async function test_set_responseHeaders() {
  let extension = ExtensionTestUtils.loadExtension({
    manifest: {
      permissions: ["webRequest", "webRequestBlocking", "http://example.com/"],
    },

    background() {
      browser.webRequest.onHeadersReceived.addListener(
        details => {
          browser.test.log(`onHeadersReceived({url: ${details.url}})`);

          details.responseHeaders.push({ name: "foo", value: "bar" });

          return { responseHeaders: details.responseHeaders };
        },
        { urls: ["http://example.com/?modify_headers"] },
        ["blocking", "responseHeaders"]
      );
    },
  });

  await extension.startup();

  await new Promise(resolve => setTimeout(resolve, 0));

  let resolveHeaderPromise;
  let headerPromise = new Promise(resolve => {
    resolveHeaderPromise = resolve;
  });
  {
    let ssm = Services.scriptSecurityManager;

    let channel = NetUtil.newChannel({
      uri: "http://example.com/?modify_headers",
      loadingPrincipal:
        ssm.createContentPrincipalFromOrigin("http://example.com"),
      contentPolicyType: Ci.nsIContentPolicy.TYPE_XMLHTTPREQUEST,
      securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
    });

    channel.asyncOpen({
      QueryInterface: ChromeUtils.generateQI(["nsIStreamListener"]),

      onStartRequest(request) {
        request.QueryInterface(Ci.nsIHttpChannel);

        try {
          resolveHeaderPromise(request.getResponseHeader("foo"));
        } catch (e) {
          resolveHeaderPromise(null);
        }
        request.cancel(Cr.NS_BINDING_ABORTED);
      },

      onStopRequest() {},

      onDataAvailable() {
        throw new Components.Exception("", Cr.NS_ERROR_FAILURE);
      },
    });
  }

  let headerValue = await headerPromise;
  equal(headerValue, "bar", "Expected Foo header value");

  await extension.unload();
});

// Test that exceptions raised from a blocking webRequest listener that returns
// a promise are logged as expected.
add_task(async function test_logged_error_on_promise_result() {
  let extension = ExtensionTestUtils.loadExtension({
    manifest: {
      permissions: ["webRequest", "webRequestBlocking", `${BASE_URL}/`],
    },

    background() {
      async function onBeforeRequest() {
        throw new Error("Expected webRequest exception from a promise result");
      }

      let exceptionRaised = false;

      browser.webRequest.onBeforeRequest.addListener(
        () => {
          if (exceptionRaised) {
            return;
          }

          // We only need to raise the exception once.
          exceptionRaised = true;
          return onBeforeRequest();
        },
        {
          urls: ["http://example.com/*"],
          types: ["main_frame"],
        },
        ["blocking"]
      );

      browser.webRequest.onBeforeRequest.addListener(
        () => {
          browser.test.sendMessage("web-request-event-received");
        },
        {
          urls: ["http://example.com/*"],
          types: ["main_frame"],
        },
        ["blocking"]
      );
    },
  });

  let { messages } = await promiseConsoleOutput(async () => {
    await extension.startup();

    let contentPage = await ExtensionTestUtils.loadContentPage(
      `${BASE_URL}/dummy`
    );
    await extension.awaitMessage("web-request-event-received");
    await contentPage.close();
  });

  ok(
    messages.some(msg =>
      /Expected webRequest exception from a promise result/.test(msg.message)
    ),
    "Got expected console message"
  );

  await extension.unload();
});
