"use strict";

const { HttpServer } = ChromeUtils.importESModule(
  "resource://testing-common/httpd.sys.mjs"
);

// Generate a post with known pre-calculated md5 sum.
function generateContent(size) {
  let content = "";
  for (let i = 0; i < size; i++) {
    content += "0";
  }
  return content;
}

let post = generateContent(10);

// Max concurent stream number in neqo is 100.
// Openning 120 streams will test queuing of streams.
let number_of_parallel_requests = 120;
let h1Server = null;
let h3Route;
let httpsOrigin;
let httpOrigin;
let h3AltSvc;
let h3Port;

let prefs;

let tests = [
  // This test must be the first because it setsup alt-svc connection, that
  // other tests use.
  test_https_alt_svc,
  test_multiple_requests,
  test_request_cancelled_by_server,
  test_stream_cancelled_by_necko,
  test_multiple_request_one_is_cancelled,
  test_multiple_request_one_is_cancelled_by_necko,
  test_post,
  test_patch,
  test_http_alt_svc,
  test_slow_receiver,
  // This test should be at the end, because it will close http3
  // connection and the transaction will switch to already existing http2
  // connection.
  // TODO: Bug 1582667 should try to fix issue with connection being closed.
  test_version_fallback,
  testsDone,
];

let current_test = 0;

function run_next_test() {
  if (current_test < tests.length) {
    dump("starting test number " + current_test + "\n");
    tests[current_test]();
    current_test++;
  }
}

function run_test() {
  let h2Port = Services.env.get("MOZHTTP2_PORT");
  Assert.notEqual(h2Port, null);
  Assert.notEqual(h2Port, "");
  h3Port = Services.env.get("MOZHTTP3_PORT");
  Assert.notEqual(h3Port, null);
  Assert.notEqual(h3Port, "");
  h3AltSvc = ":" + h3Port;

  h3Route = "foo.example.com:" + h3Port;
  do_get_profile();
  prefs = Services.prefs;

  prefs.setBoolPref("network.http.http3.enable", true);
  prefs.setCharPref("network.dns.localDomains", "foo.example.com");
  // We always resolve elements of localDomains as it's hardcoded without the
  // following pref:
  prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true);
  prefs.setBoolPref("network.http.altsvc.oe", true);

  // The certificate for the http3server server is for foo.example.com and
  // is signed by http2-ca.pem so add that cert to the trust list as a
  // signing cert.
  let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
    Ci.nsIX509CertDB
  );
  addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
  httpsOrigin = "https://foo.example.com:" + h2Port + "/";

  h1Server = new HttpServer();
  h1Server.registerPathHandler("/http3-test", h1Response);
  h1Server.registerPathHandler("/.well-known/http-opportunistic", h1ServerWK);
  h1Server.registerPathHandler("/VersionFallback", h1Response);
  h1Server.start(-1);
  h1Server.identity.setPrimary(
    "http",
    "foo.example.com",
    h1Server.identity.primaryPort
  );
  httpOrigin = "http://foo.example.com:" + h1Server.identity.primaryPort + "/";

  run_next_test();
}

function h1Response(metadata, response) {
  response.setStatusLine(metadata.httpVersion, 200, "OK");
  response.setHeader("Content-Type", "text/plain", false);
  response.setHeader("Connection", "close", false);
  response.setHeader("Cache-Control", "no-cache", false);
  response.setHeader("Access-Control-Allow-Origin", "*", false);
  response.setHeader("Access-Control-Allow-Method", "GET", false);
  response.setHeader("Access-Control-Allow-Headers", "x-altsvc", false);

  try {
    let hval = "h3-29=" + metadata.getHeader("x-altsvc");
    response.setHeader("Alt-Svc", hval, false);
  } catch (e) {}

  let body = "Q: What did 0 say to 8? A: Nice Belt!\n";
  response.bodyOutputStream.write(body, body.length);
}

function h1ServerWK(metadata, response) {
  response.setStatusLine(metadata.httpVersion, 200, "OK");
  response.setHeader("Content-Type", "application/json", false);
  response.setHeader("Connection", "close", false);
  response.setHeader("Cache-Control", "no-cache", false);
  response.setHeader("Access-Control-Allow-Origin", "*", false);
  response.setHeader("Access-Control-Allow-Method", "GET", false);
  response.setHeader("Access-Control-Allow-Headers", "x-altsvc", false);

  let body = '["http://foo.example.com:' + h1Server.identity.primaryPort + '"]';
  response.bodyOutputStream.write(body, body.length);
}

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

let Http3CheckListener = function () {};

Http3CheckListener.prototype = {
  onDataAvailableFired: false,
  expectedStatus: Cr.NS_OK,
  expectedRoute: "",

  onStartRequest: function testOnStartRequest(request) {
    Assert.ok(request instanceof Ci.nsIHttpChannel);

    Assert.equal(request.status, this.expectedStatus);
    if (Components.isSuccessCode(this.expectedStatus)) {
      Assert.equal(request.responseStatus, 200);
    }
  },

  onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) {
    this.onDataAvailableFired = true;
    read_stream(stream, cnt);
  },

  onStopRequest: function testOnStopRequest(request, status) {
    Assert.equal(status, this.expectedStatus);
    let routed = "NA";
    try {
      routed = request.getRequestHeader("Alt-Used");
    } catch (e) {}
    dump("routed is " + routed + "\n");

    Assert.equal(routed, this.expectedRoute);

    if (Components.isSuccessCode(this.expectedStatus)) {
      let httpVersion = "";
      try {
        httpVersion = request.protocolVersion;
      } catch (e) {}
      Assert.equal(httpVersion, "h3-29");
      Assert.equal(this.onDataAvailableFired, true);
      Assert.equal(request.getResponseHeader("X-Firefox-Http3"), "h3-29");
    }
    run_next_test();
    do_test_finished();
  },
};

let WaitForHttp3Listener = function () {};

WaitForHttp3Listener.prototype = new Http3CheckListener();

WaitForHttp3Listener.prototype.uri = "";
WaitForHttp3Listener.prototype.h3AltSvc = "";

WaitForHttp3Listener.prototype.onStopRequest = function testOnStopRequest(
  request,
  status
) {
  Assert.equal(status, this.expectedStatus);

  let routed = "NA";
  try {
    routed = request.getRequestHeader("Alt-Used");
  } catch (e) {}
  dump("routed is " + routed + "\n");

  let httpVersion = "";
  try {
    httpVersion = request.protocolVersion;
  } catch (e) {}

  if (routed == this.expectedRoute) {
    Assert.equal(routed, this.expectedRoute); // always true, but a useful log
    Assert.equal(httpVersion, "h3-29");
    run_next_test();
  } else {
    dump("poll later for alt svc mapping\n");
    if (httpVersion == "h2") {
      request.QueryInterface(Ci.nsIHttpChannelInternal);
      Assert.ok(request.supportsHTTP3);
    }
    do_test_pending();
    do_timeout(500, () => {
      doTest(this.uri, this.expectedRoute, this.h3AltSvc);
    });
  }

  do_test_finished();
};

function doTest(uri, expectedRoute, altSvc) {
  let chan = makeChan(uri);
  let listener = new WaitForHttp3Listener();
  listener.uri = uri;
  listener.expectedRoute = expectedRoute;
  listener.h3AltSvc = altSvc;
  chan.setRequestHeader("x-altsvc", altSvc, false);
  chan.asyncOpen(listener);
}

// Test Alt-Svc for http3.
// H2 server returns alt-svc=h3-29=:h3port
function test_https_alt_svc() {
  dump("test_https_alt_svc()\n");
  do_test_pending();
  if (mozinfo.os == "android") {
    // Set necessary prefs to make Firefox connect to the http3Server on the
    // host machine.
    prefs.setCharPref("network.dns.localDomains", "");
    const overrideService = Cc[
      "@mozilla.org/network/native-dns-override;1"
    ].getService(Ci.nsINativeDNSResolverOverride);
    overrideService.addIPOverride("foo.example.com", "10.0.2.2");
    prefs.setCharPref(
      "network.http.http3.alt-svc-mapping-for-testing",
      `foo.example.com;h3-29=:${h3Port}`
    );
  }
  doTest(httpsOrigin + "http3-test", h3Route, h3AltSvc);
}

// Listener for a number of parallel requests. if with_error is set, one of
// the channels will be cancelled (by the server or in onStartRequest).
let MultipleListener = function () {};

MultipleListener.prototype = {
  number_of_parallel_requests: 0,
  with_error: Cr.NS_OK,
  count_of_done_requests: 0,
  error_found_onstart: false,
  error_found_onstop: false,
  need_cancel_found: false,

  onStartRequest: function testOnStartRequest(request) {
    Assert.ok(request instanceof Ci.nsIHttpChannel);

    let need_cancel = "";
    try {
      need_cancel = request.getRequestHeader("CancelMe");
    } catch (e) {}
    if (need_cancel != "") {
      this.need_cancel_found = true;
      request.cancel(Cr.NS_ERROR_ABORT);
    } else if (Components.isSuccessCode(request.status)) {
      Assert.equal(request.responseStatus, 200);
    } else if (this.error_found_onstart) {
      do_throw("We should have only one request failing.");
    } else {
      Assert.equal(request.status, this.with_error);
      this.error_found_onstart = true;
    }
  },

  onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) {
    read_stream(stream, cnt);
  },

  onStopRequest: function testOnStopRequest(request) {
    let routed = "";
    try {
      routed = request.getRequestHeader("Alt-Used");
    } catch (e) {}
    Assert.equal(routed, this.expectedRoute);

    if (Components.isSuccessCode(request.status)) {
      let httpVersion = "";
      try {
        httpVersion = request.protocolVersion;
      } catch (e) {}
      Assert.equal(httpVersion, "h3-29");
    }

    if (!Components.isSuccessCode(request.status)) {
      if (this.error_found_onstop) {
        do_throw("We should have only one request failing.");
      } else {
        Assert.equal(request.status, this.with_error);
        this.error_found_onstop = true;
      }
    }
    this.count_of_done_requests++;
    if (this.count_of_done_requests == this.number_of_parallel_requests) {
      if (Components.isSuccessCode(this.with_error)) {
        Assert.equal(this.error_found_onstart, false);
        Assert.equal(this.error_found_onstop, false);
      } else {
        Assert.ok(this.error_found_onstart || this.need_cancel_found);
        Assert.equal(this.error_found_onstop, true);
      }
      run_next_test();
    }
    do_test_finished();
  },
};

// Multiple requests
function test_multiple_requests() {
  dump("test_multiple_requests()\n");

  let listener = new MultipleListener();
  listener.number_of_parallel_requests = number_of_parallel_requests;
  listener.expectedRoute = h3Route;

  for (let i = 0; i < number_of_parallel_requests; i++) {
    let chan = makeChan(httpsOrigin + "20000");
    chan.asyncOpen(listener);
    do_test_pending();
  }
}

// A request cancelled by a server.
function test_request_cancelled_by_server() {
  dump("test_request_cancelled_by_server()\n");

  let listener = new Http3CheckListener();
  listener.expectedStatus = Cr.NS_ERROR_NET_INTERRUPT;
  listener.expectedRoute = h3Route;
  let chan = makeChan(httpsOrigin + "RequestCancelled");
  chan.asyncOpen(listener);
  do_test_pending();
}

let CancelRequestListener = function () {};

CancelRequestListener.prototype = new Http3CheckListener();

CancelRequestListener.prototype.expectedStatus = Cr.NS_ERROR_ABORT;

CancelRequestListener.prototype.onStartRequest = function testOnStartRequest(
  request
) {
  Assert.ok(request instanceof Ci.nsIHttpChannel);

  Assert.equal(Components.isSuccessCode(request.status), true);
  request.cancel(Cr.NS_ERROR_ABORT);
};

// Cancel stream after OnStartRequest.
function test_stream_cancelled_by_necko() {
  dump("test_stream_cancelled_by_necko()\n");

  let listener = new CancelRequestListener();
  listener.expectedRoute = h3Route;
  let chan = makeChan(httpsOrigin + "20000");
  chan.asyncOpen(listener);
  do_test_pending();
}

// Multiple requests, one gets cancelled by the server, the other should finish normally.
function test_multiple_request_one_is_cancelled() {
  dump("test_multiple_request_one_is_cancelled()\n");

  let listener = new MultipleListener();
  listener.number_of_parallel_requests = number_of_parallel_requests;
  listener.with_error = Cr.NS_ERROR_NET_INTERRUPT;
  listener.expectedRoute = h3Route;

  for (let i = 0; i < number_of_parallel_requests; i++) {
    let uri = httpsOrigin + "20000";
    if (i == 4) {
      // Add a request that will be cancelled by the server.
      uri = httpsOrigin + "RequestCancelled";
    }
    let chan = makeChan(uri);
    chan.asyncOpen(listener);
    do_test_pending();
  }
}

// Multiple requests, one gets cancelled by us, the other should finish normally.
function test_multiple_request_one_is_cancelled_by_necko() {
  dump("test_multiple_request_one_is_cancelled_by_necko()\n");

  let listener = new MultipleListener();
  listener.number_of_parallel_requests = number_of_parallel_requests;
  listener.with_error = Cr.NS_ERROR_ABORT;
  listener.expectedRoute = h3Route;
  for (let i = 0; i < number_of_parallel_requests; i++) {
    let chan = makeChan(httpsOrigin + "20000");
    if (i == 4) {
      // MultipleListener will cancel request with this header.
      chan.setRequestHeader("CancelMe", "true", false);
    }
    chan.asyncOpen(listener);
    do_test_pending();
  }
}

let PostListener = function () {};

PostListener.prototype = new Http3CheckListener();

PostListener.prototype.onDataAvailable = function (request, stream, off, cnt) {
  this.onDataAvailableFired = true;
  read_stream(stream, cnt);
};

// Support for doing a POST
function do_post(content, chan, listener, method) {
  let stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
    Ci.nsIStringInputStream
  );
  stream.data = content;

  let uchan = chan.QueryInterface(Ci.nsIUploadChannel);
  uchan.setUploadStream(stream, "text/plain", stream.available());

  chan.requestMethod = method;

  chan.asyncOpen(listener);
}

// Test a simple POST
function test_post() {
  dump("test_post()");
  let chan = makeChan(httpsOrigin + "post");
  let listener = new PostListener();
  listener.expectedRoute = h3Route;
  do_post(post, chan, listener, "POST");
  do_test_pending();
}

// Test a simple PATCH
function test_patch() {
  dump("test_patch()");
  let chan = makeChan(httpsOrigin + "patch");
  let listener = new PostListener();
  listener.expectedRoute = h3Route;
  do_post(post, chan, listener, "PATCH");
  do_test_pending();
}

// Test alt-svc for http (without s)
function test_http_alt_svc() {
  dump("test_http_alt_svc()\n");
  // Skip this test on Android because the httpOrigin (http://foo.example.com)
  // is on 127.0.0.1, while the http3Server (https://foo.example.com) is
  // on 10.0.2.2. Currently, we can't change the IP mapping dynamically.
  if (mozinfo.os == "android") {
    current_test++;
    run_next_test();
    return;
  }
  do_test_pending();
  doTest(httpOrigin + "http3-test", h3Route, h3AltSvc);
}

let SlowReceiverListener = function () {};

SlowReceiverListener.prototype = new Http3CheckListener();
SlowReceiverListener.prototype.count = 0;

SlowReceiverListener.prototype.onDataAvailable = function (
  request,
  stream,
  off,
  cnt
) {
  this.onDataAvailableFired = true;
  this.count += cnt;
  read_stream(stream, cnt);
};

SlowReceiverListener.prototype.onStopRequest = function (request, status) {
  Assert.equal(status, this.expectedStatus);
  Assert.equal(this.count, 10000000);
  let routed = "NA";
  try {
    routed = request.getRequestHeader("Alt-Used");
  } catch (e) {}
  dump("routed is " + routed + "\n");

  Assert.equal(routed, this.expectedRoute);

  if (Components.isSuccessCode(this.expectedStatus)) {
    let httpVersion = "";
    try {
      httpVersion = request.protocolVersion;
    } catch (e) {}
    Assert.equal(httpVersion, "h3-29");
    Assert.equal(this.onDataAvailableFired, true);
  }
  run_next_test();
  do_test_finished();
};

function test_slow_receiver() {
  dump("test_slow_receiver()\n");
  let chan = makeChan(httpsOrigin + "10000000");
  let listener = new SlowReceiverListener();
  listener.expectedRoute = h3Route;
  chan.asyncOpen(listener);
  do_test_pending();
  chan.suspend();
  do_timeout(1000, chan.resume);
}

let CheckFallbackListener = function () {};

CheckFallbackListener.prototype = {
  onStartRequest: function testOnStartRequest(request) {
    Assert.ok(request instanceof Ci.nsIHttpChannel);

    Assert.equal(request.status, Cr.NS_OK);
    Assert.equal(request.responseStatus, 200);
  },

  onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) {
    read_stream(stream, cnt);
  },

  onStopRequest: function testOnStopRequest(request, status) {
    Assert.equal(status, Cr.NS_OK);
    let routed = "NA";
    try {
      routed = request.getRequestHeader("Alt-Used");
    } catch (e) {}
    dump("routed is " + routed + "\n");

    Assert.equal(routed, "0");

    let httpVersion = "";
    try {
      httpVersion = request.protocolVersion;
    } catch (e) {}
    Assert.equal(httpVersion, "http/1.1");
    run_next_test();
    do_test_finished();
  },
};

// Server cancels request with VersionFallback.
function test_version_fallback() {
  dump("test_version_fallback()\n");

  let chan = makeChan(httpsOrigin + "VersionFallback");
  let listener = new CheckFallbackListener();
  chan.asyncOpen(listener);
  do_test_pending();
}

function testsDone() {
  prefs.clearUserPref("network.http.http3.enable");
  prefs.clearUserPref("network.dns.localDomains");
  prefs.clearUserPref("network.proxy.allow_hijacking_localhost");
  prefs.clearUserPref("network.http.altsvc.oe");
  dump("testDone\n");
  do_test_pending();
  h1Server.stop(do_test_finished);
}
