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

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

// 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");
});

// This test can be merged with test_progress.js once HTTP/3 tests are
// enabled on all plaforms.

var last = 0;
var max = 0;
var using_proxy = false;

const RESPONSE_LENGTH = 3000000;
const STATUS_RECEIVING_FROM = 0x4b0006;

const TYPE_ONSTATUS = 1;
const TYPE_ONPROGRESS = 2;
const TYPE_ONSTARTREQUEST = 3;
const TYPE_ONDATAAVAILABLE = 4;
const TYPE_ONSTOPREQUEST = 5;

var ProgressCallback = function () {};

ProgressCallback.prototype = {
  _listener: null,
  _got_onstartrequest: false,
  _got_onstatus_after_onstartrequest: false,
  _last_callback_handled: null,
  statusArg: "",
  finish: null,

  QueryInterface: ChromeUtils.generateQI([
    "nsIProgressEventSink",
    "nsIStreamListener",
    "nsIRequestObserver",
  ]),

  getInterface(iid) {
    if (
      iid.equals(Ci.nsIProgressEventSink) ||
      iid.equals(Ci.nsIStreamListener) ||
      iid.equals(Ci.nsIRequestObserver)
    ) {
      return this;
    }
    throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
  },

  onStartRequest(request) {
    Assert.equal(this._last_callback_handled, TYPE_ONSTATUS);
    this._got_onstartrequest = true;
    this._last_callback_handled = TYPE_ONSTARTREQUEST;

    this._listener = new ChannelListener(checkRequest, request);
    this._listener.onStartRequest(request);
  },

  onDataAvailable(request, data, offset, count) {
    Assert.equal(this._last_callback_handled, TYPE_ONPROGRESS);
    this._last_callback_handled = TYPE_ONDATAAVAILABLE;

    this._listener.onDataAvailable(request, data, offset, count);
  },

  onStopRequest(request, status) {
    Assert.equal(this._last_callback_handled, TYPE_ONDATAAVAILABLE);
    Assert.ok(this._got_onstatus_after_onstartrequest);
    this._last_callback_handled = TYPE_ONSTOPREQUEST;

    this._listener.onStopRequest(request, status);
    delete this._listener;

    check_http_info(request, this.expected_httpVersion, using_proxy);

    this.finish();
  },

  onProgress(request, progress, progressMax) {
    Assert.equal(this._last_callback_handled, TYPE_ONSTATUS);
    this._last_callback_handled = TYPE_ONPROGRESS;

    Assert.equal(this.mStatus, STATUS_RECEIVING_FROM);
    last = progress;
    max = progressMax;
  },

  onStatus(request, status, statusArg) {
    if (!this._got_onstartrequest) {
      // Ensure that all messages before onStartRequest are onStatus
      if (this._last_callback_handled) {
        Assert.equal(this._last_callback_handled, TYPE_ONSTATUS);
      }
    } else if (this._last_callback_handled == TYPE_ONSTARTREQUEST) {
      this._got_onstatus_after_onstartrequest = true;
    } else {
      Assert.equal(this._last_callback_handled, TYPE_ONDATAAVAILABLE);
    }
    this._last_callback_handled = TYPE_ONSTATUS;

    Assert.equal(statusArg, this.statusArg);
    this.mStatus = status;
  },

  mStatus: 0,
};

function chanPromise(uri, statusArg, version) {
  return new Promise(resolve => {
    var chan = makeHTTPChannel(uri, using_proxy);
    chan.requestMethod = "GET";
    let listener = new ProgressCallback();
    listener.statusArg = statusArg;
    chan.notificationCallbacks = listener;
    listener.expected_httpVersion = version;
    listener.finish = resolve;
    chan.asyncOpen(listener);
  });
}

function checkRequest() {
  Assert.equal(last, RESPONSE_LENGTH);
  Assert.equal(max, RESPONSE_LENGTH);
}

async function check_progress(server) {
  info(`Testing ${server.constructor.name}`);
  await server.registerPathHandler("/test", (req, resp) => {
    // Generate a post.
    function generateContent(size) {
      return "0".repeat(size);
    }

    resp.writeHead(200, {
      "content-type": "application/json",
      "content-length": "3000000",
    });
    resp.end(generateContent(3000000));
  });
  await chanPromise(
    `${server.origin()}/test`,
    `${server.domain()}`,
    server.version()
  );
}

add_task(async function setup() {
  let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
    Ci.nsIX509CertDB
  );
  addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
  addCertFromFile(certdb, "proxy-ca.pem", "CTu,u,u");
});

add_task(async function test_http_1_and_2() {
  await with_node_servers(
    [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server],
    check_progress
  );
});

add_task(async function test_http_proxy() {
  using_proxy = true;
  let proxy = new NodeHTTPProxyServer();
  await proxy.start();
  await with_node_servers(
    [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server],
    check_progress
  );
  await proxy.stop();
  using_proxy = false;
});

add_task(async function test_https_proxy() {
  using_proxy = true;
  let proxy = new NodeHTTPSProxyServer();
  await proxy.start();
  await with_node_servers(
    [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server],
    check_progress
  );
  await proxy.stop();
  using_proxy = false;
});

add_task(async function test_http2_proxy() {
  using_proxy = true;
  let proxy = new NodeHTTP2ProxyServer();
  await proxy.start();
  await with_node_servers(
    [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server],
    check_progress
  );
  await proxy.stop();
  using_proxy = false;
});

add_task(async function test_http3() {
  await http3_setup_tests("h3");
  await chanPromise(
    "https://foo.example.com/" + RESPONSE_LENGTH,
    "foo.example.com",
    "h3"
  );
  http3_clear_prefs();
});
