/* 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";

/* import-globals-from head_cache.js */
/* import-globals-from head_cookies.js */
/* import-globals-from head_channels.js */

// We don't normally allow localhost channels to be proxied, but this
// is easier than updating all the certs and/or domains.
Services.prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true);
registerCleanupFunction(() => {
  Services.prefs.clearUserPref("network.proxy.allow_hijacking_localhost");
});

const { HttpServer } = ChromeUtils.importESModule(
  "resource://testing-common/httpd.sys.mjs"
);
const {
  NodeHTTPServer,
  NodeHTTPSServer,
  NodeHTTP2Server,
  NodeHTTPProxyServer,
  NodeHTTPSProxyServer,
  NodeHTTP2ProxyServer,
  with_node_servers,
} = ChromeUtils.importESModule("resource://testing-common/NodeServer.sys.mjs");

function makeChan(uri) {
  let chan = NetUtil.newChannel({
    uri,
    loadUsingSystemPrincipal: true,
  }).QueryInterface(Ci.nsIHttpChannel);
  chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
  return chan;
}

function channelOpenPromise(chan, flags) {
  return new Promise(resolve => {
    function finish(req, buffer) {
      resolve([req, buffer]);
    }
    chan.asyncOpen(new ChannelListener(finish, null, flags));
  });
}

function registerSimplePathHandler(server, path) {
  return server.registerPathHandler(path, (req, resp) => {
    resp.writeHead(200);
    resp.end("done");
  });
}

function regiisterServerNamePathHandler(server, path) {
  return server.registerPathHandler(path, (req, resp) => {
    resp.writeHead(200);
    resp.end(global.server_name);
  });
}

function verifyGleanValues(aDescription, aExpected) {
  info(aDescription);

  let metric = Glean.networking.connectionAddressType;
  Assert.equal(metric[aExpected.metricKey].testGetValue(), aExpected.value);
}

add_setup(async function setup() {
  Services.fog.initializeFOG();
});

add_task(async function test_dual_stack() {
  Services.fog.testResetFOG();
  let httpserv = new HttpServer();
  let content = "ok";
  httpserv.registerPathHandler("/", function handler(metadata, response) {
    response.setHeader("Content-Length", `${content.length}`);
    response.bodyOutputStream.write(content, content.length);
  });
  httpserv.start_dualStack(-1);

  let chan = makeChan(`http://127.0.0.1:${httpserv.identity.primaryPort}/`);
  let [, response] = await channelOpenPromise(chan);
  Assert.equal(response, content);

  chan = makeChan(`http://[::1]:${httpserv.identity.primaryPort}/`);
  [, response] = await channelOpenPromise(chan);
  Assert.equal(response, content);

  verifyGleanValues("Check HTTP/1 IPv4", {
    metricKey: "http_1_ipv4",
    value: 1,
  });
  verifyGleanValues("Check HTTP/1 IPv6", {
    metricKey: "http_1_ipv6",
    value: 1,
  });
  await new Promise(resolve => httpserv.stop(resolve));
});

add_task(async function test_http() {
  let server = new NodeHTTPServer();
  await server.start();
  registerCleanupFunction(async () => {
    await server.stop();
  });
  let chan = makeChan(`http://localhost:${server.port()}/test`);
  let req = await new Promise(resolve => {
    chan.asyncOpen(new ChannelListener(resolve, null, CL_ALLOW_UNKNOWN_CL));
  });
  equal(req.status, Cr.NS_OK);
  equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 404);
  await registerSimplePathHandler(server, "/test");
  chan = makeChan(`http://localhost:${server.port()}/test`);
  req = await new Promise(resolve => {
    chan.asyncOpen(new ChannelListener(resolve, null, CL_ALLOW_UNKNOWN_CL));
  });
  equal(req.status, Cr.NS_OK);
  equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200);
  equal(req.QueryInterface(Ci.nsIHttpChannel).protocolVersion, "http/1.1");
  equal(req.QueryInterface(Ci.nsIHttpChannelInternal).isProxyUsed, false);

  await server.stop();
});

add_task(async function test_https() {
  let server = new NodeHTTPSServer();
  await server.start();
  registerCleanupFunction(async () => {
    await server.stop();
  });
  let chan = makeChan(`https://localhost:${server.port()}/test`);
  let req = await new Promise(resolve => {
    chan.asyncOpen(new ChannelListener(resolve, null, CL_ALLOW_UNKNOWN_CL));
  });
  equal(req.status, Cr.NS_OK);
  equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 404);
  await registerSimplePathHandler(server, "/test");
  chan = makeChan(`https://localhost:${server.port()}/test`);
  req = await new Promise(resolve => {
    chan.asyncOpen(new ChannelListener(resolve, null, CL_ALLOW_UNKNOWN_CL));
  });
  equal(req.status, Cr.NS_OK);
  equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200);
  equal(req.QueryInterface(Ci.nsIHttpChannel).protocolVersion, "http/1.1");

  await server.stop();
});

add_task(async function test_http2() {
  Services.fog.testResetFOG();

  let server = new NodeHTTP2Server();
  await server.start();
  registerCleanupFunction(async () => {
    await server.stop();
  });
  let chan = makeChan(`https://localhost:${server.port()}/test`);
  let req = await new Promise(resolve => {
    chan.asyncOpen(new ChannelListener(resolve, null, CL_ALLOW_UNKNOWN_CL));
  });
  equal(req.status, Cr.NS_OK);
  equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 404);
  await registerSimplePathHandler(server, "/test");
  chan = makeChan(`https://localhost:${server.port()}/test`);
  req = await new Promise(resolve => {
    chan.asyncOpen(new ChannelListener(resolve, null, CL_ALLOW_UNKNOWN_CL));
  });
  equal(req.status, Cr.NS_OK);
  equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200);
  equal(req.QueryInterface(Ci.nsIHttpChannel).protocolVersion, "h2");

  let heEnabled = Services.prefs.getBoolPref(
    "network.http.happy_eyeballs_enabled",
    false
  );
  verifyGleanValues(heEnabled ? "Check HTTP/2 IPv6" : "Check HTTP/2 IPv4", {
    metricKey: heEnabled ? "http_2_ipv6" : "http_2_ipv4",
    value: 1,
  });

  await server.stop();
});

add_task(async function test_http2_custom_domain() {
  const customDomain = "custom_domain.example";
  Services.prefs.setCharPref("network.dns.localDomains", customDomain);
  registerCleanupFunction(() => {
    Services.prefs.clearUserPref("network.dns.localDomains");
  });

  let server = new NodeHTTP2Server();
  await server.start(0, [customDomain]);
  registerCleanupFunction(async () => {
    await server.stop();
  });

  equal(server.domain(), customDomain);
  equal(server.origin(), `https://${customDomain}:${server.port()}`);

  await registerSimplePathHandler(server, "/test");
  let chan = makeChan(`${server.origin()}/test`);
  let req = await new Promise(resolve => {
    chan.asyncOpen(new ChannelListener(resolve, null, CL_ALLOW_UNKNOWN_CL));
  });
  equal(req.status, Cr.NS_OK);
  equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200);
  equal(req.QueryInterface(Ci.nsIHttpChannel).protocolVersion, "h2");

  await server.stop();
});

add_task(async function test_http1_proxy() {
  let proxy = new NodeHTTPProxyServer();
  await proxy.start();
  registerCleanupFunction(async () => {
    await proxy.stop();
  });

  let chan = makeChan(`http://localhost:${proxy.port()}/test`);
  let req = await new Promise(resolve => {
    chan.asyncOpen(new ChannelListener(resolve, null, CL_ALLOW_UNKNOWN_CL));
  });
  equal(req.status, Cr.NS_OK);
  equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 405);

  await with_node_servers(
    [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server],
    async server => {
      await server.execute(
        `global.server_name = "${server.constructor.name}";`
      );
      await regiisterServerNamePathHandler(server, "/test");
      let [req1, buff] = await channelOpenPromise(
        makeChan(`${server.origin()}/test`),
        CL_ALLOW_UNKNOWN_CL
      );
      equal(req1.status, Cr.NS_OK);
      equal(req1.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200);
      equal(buff, server.constructor.name);
      //Bug 1792187: Check if proxy is set to true when a proxy is used.
      equal(req1.QueryInterface(Ci.nsIHttpChannelInternal).isProxyUsed, true);
      equal(
        req1.QueryInterface(Ci.nsIHttpChannel).protocolVersion,
        server.constructor.name == "NodeHTTP2Server" ? "h2" : "http/1.1"
      );
    }
  );

  await proxy.stop();
});

add_task(async function test_https_proxy() {
  let proxy = new NodeHTTPSProxyServer();
  await proxy.start();
  registerCleanupFunction(async () => {
    await proxy.stop();
  });

  let chan = makeChan(`https://localhost:${proxy.port()}/test`);
  let req = await new Promise(resolve => {
    chan.asyncOpen(new ChannelListener(resolve, null, CL_ALLOW_UNKNOWN_CL));
  });
  equal(req.status, Cr.NS_OK);
  equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 405);

  await with_node_servers(
    [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server],
    async server => {
      await server.execute(
        `global.server_name = "${server.constructor.name}";`
      );
      await regiisterServerNamePathHandler(server, "/test");

      let [req1, buff] = await channelOpenPromise(
        makeChan(`${server.origin()}/test`),
        CL_ALLOW_UNKNOWN_CL
      );
      equal(req1.status, Cr.NS_OK);
      equal(req1.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200);
      equal(buff, server.constructor.name);
    }
  );

  await proxy.stop();
});

add_task(async function test_http2_proxy() {
  let proxy = new NodeHTTP2ProxyServer();
  await proxy.start();
  registerCleanupFunction(async () => {
    await proxy.stop();
  });

  let chan = makeChan(`https://localhost:${proxy.port()}/test`);
  let req = await new Promise(resolve => {
    chan.asyncOpen(new ChannelListener(resolve, null, CL_ALLOW_UNKNOWN_CL));
  });
  equal(req.status, Cr.NS_OK);
  equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 405);

  await with_node_servers(
    [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server],
    async server => {
      await server.execute(
        `global.server_name = "${server.constructor.name}";`
      );
      await regiisterServerNamePathHandler(server, "/test");
      let [req1, buff] = await channelOpenPromise(
        makeChan(`${server.origin()}/test`),
        CL_ALLOW_UNKNOWN_CL
      );
      equal(req1.status, Cr.NS_OK);
      equal(req1.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200);
      equal(buff, server.constructor.name);
    }
  );

  await proxy.stop();
});

add_task(async function test_proxy_with_redirects() {
  let proxies = [
    NodeHTTPProxyServer,
    NodeHTTPSProxyServer,
    NodeHTTP2ProxyServer,
  ];
  for (let p of proxies) {
    let proxy = new p();
    await proxy.start();
    registerCleanupFunction(async () => {
      await proxy.stop();
    });

    await with_node_servers(
      [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server],
      async server => {
        info(`Testing ${p.name} with ${server.constructor.name}`);
        await server.execute(
          `global.server_name = "${server.constructor.name}";`
        );
        await server.registerPathHandler("/redirect", (req, resp) => {
          resp.writeHead(302, {
            Location: "/test",
          });
          resp.end(global.server_name);
        });
        await server.registerPathHandler("/test", (req, resp) => {
          resp.writeHead(200);
          resp.end(global.server_name);
        });

        let chan = makeChan(`${server.origin()}/redirect`);
        let [req, buff] = await channelOpenPromise(chan, CL_ALLOW_UNKNOWN_CL);
        equal(req.status, Cr.NS_OK);
        equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200);
        equal(buff, server.constructor.name);
        req.QueryInterface(Ci.nsIProxiedChannel);
        ok(!!req.proxyInfo);
        notEqual(req.proxyInfo.type, "direct");
      }
    );
    await proxy.stop();
  }
});

add_task(async function test_async_event() {
  let server = new NodeHTTP2Server();
  await server.start();
  registerCleanupFunction(async () => {
    await server.stop();
  });

  await server.execute(`new Promise(r => setTimeout(r, 500))`);

  await server.stop();
});

add_task(async function test_async_state_management() {
  let server = new NodeHTTP2Server();
  await server.start();
  registerCleanupFunction(async () => {
    await server.stop();
  });

  await server.execute(`global.asyncResults = [];`);

  await server.execute(`
    global.asyncCounter = 0;
    global.performAsyncOperation = function(delay, value) {
      return new Promise(resolve => {
        setTimeout(() => {
          global.asyncCounter++;
          global.asyncResults.push({ counter: global.asyncCounter, value });
          resolve({ counter: global.asyncCounter, value });
        }, delay);
      });
    };
  `);

  let op1 = server.execute(`performAsyncOperation(100, "first")`);
  let op2 = server.execute(`performAsyncOperation(50, "second")`);

  let result1 = await op1;
  let result2 = await op2;
  // This ran after 100 ms, so it comes in second
  equal(result1.counter, 2);
  equal(result1.value, "first");

  // this rand after 50 ms, so it comes in first.
  equal(result2.counter, 1);
  equal(result2.value, "second");

  let results = await server.execute(`global.asyncResults`);
  equal(results.length, 2);
  equal(results[0].value, "second");
  equal(results[1].value, "first");

  let counter = await server.execute(`global.asyncCounter`);
  equal(counter, 2);

  await server.stop();
});
