/* 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 { AddonTestUtils } = ChromeUtils.importESModule(
  "resource://testing-common/AddonTestUtils.sys.mjs"
);

const API_OHTTP_CONFIG = "http://example.com/ohttp-config";
const API_OHTTP_RELAY = "http://example.com/relay/";

AddonTestUtils.maybeInit(this);
const server = AddonTestUtils.createHttpServer({ hosts: ["example.com"] });

const ohttp = Cc["@mozilla.org/network/oblivious-http;1"].getService(
  Ci.nsIObliviousHttp
);
const ohttpServer = ohttp.server();

const serverContext = {
  statusCode: 0,
  configBody: ohttpServer.encodedConfig,
  failure: false,
};

server.registerPathHandler(
  new URL(API_OHTTP_CONFIG).pathname,
  (request, response) => {
    const bstream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(
      Ci.nsIBinaryOutputStream
    );
    bstream.setOutputStream(response.bodyOutputStream);
    bstream.writeByteArray(serverContext.configBody);
  }
);

let requestPromise, resolveRequest;
let responsePromise, resolveResponse;

server.registerPathHandler(
  new URL(API_OHTTP_RELAY).pathname,
  async (request, response) => {
    const inputStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
      Ci.nsIBinaryInputStream
    );
    inputStream.setInputStream(request.bodyInputStream);
    const requestBody = inputStream.readByteArray(inputStream.available());
    const ohttpRequest = ohttpServer.decapsulate(requestBody);
    const bhttp = Cc["@mozilla.org/network/binary-http;1"].getService(
      Ci.nsIBinaryHttp
    );
    const decodedRequest = bhttp.decodeRequest(ohttpRequest.request);

    response.processAsync();
    if (serverContext.failure) {
      response.setStatusLine(request.httpVersion, 500, "Internal Server Error");
      response.finish();
      resolveRequest(decodedRequest);
      return;
    }

    const BinaryHttpResponse = {
      status: serverContext.statusCode,
      headerNames: [],
      headerValues: [],
      content: new TextEncoder().encode(decodedRequest.content),
      QueryInterface: ChromeUtils.generateQI(["nsIBinaryHttpResponse"]),
    };

    const encResponse = ohttpRequest.encapsulate(
      bhttp.encodeResponse(BinaryHttpResponse)
    );
    response.setStatusLine(request.httpVersion, 200, "OK");
    response.setHeader("Content-Type", "message/ohttp-res", false);

    const bstream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(
      Ci.nsIBinaryOutputStream
    );
    bstream.setOutputStream(response.bodyOutputStream);
    bstream.writeByteArray(encResponse);
    response.finish();

    resolveRequest(decodedRequest);
  }
);

function resetPromises() {
  const tmp1 = Promise.withResolvers();
  requestPromise = tmp1.promise;
  resolveRequest = tmp1.resolve;

  const tmp2 = Promise.withResolvers();
  responsePromise = tmp2.promise;
  resolveResponse = tmp2.resolve;
}

async function test_success() {
  resetPromises();

  const request = {
    method: "POST",
    scheme: "https",
    authority: "example.com",
    path: "/my-path",
    headerNames: ["User-Agent"],
    headerValues: ["Mozilla/5.0"],
    content: "Hello, world!",
  };

  const expectedResponse = {
    url: `${request.scheme}://${request.authority}${request.path}`,
    statusCode: 200,
    error: "",
  };

  serverContext.statusCode = expectedResponse.statusCode;

  const ohttpClientTester = Cc[
    "@mozilla.org/ohttp-client-test;1"
  ].createInstance(Ci.nsIOhttpClientTest);

  ohttpClientTester.fetch(
    `${request.scheme}://${request.authority}${request.path}`,
    request.method,
    request.content,
    request.headerNames,
    request.headerValues,
    (url, statusCode, headerKeys, headerValues, error) => {
      resolveResponse({
        url,
        statusCode,
        headerKeys,
        headerValues,
        error,
      });
    }
  );

  const [ohttpRequest, ohttpResponse] = await Promise.all([
    requestPromise,
    responsePromise,
  ]);

  // Verify request
  Assert.equal(ohttpRequest.method, request.method);
  Assert.equal(ohttpRequest.scheme, request.scheme);
  Assert.equal(ohttpRequest.authority, request.authority);
  Assert.equal(ohttpRequest.path, request.path);
  Assert.deepEqual(ohttpRequest.headerNames, request.headerNames);
  Assert.deepEqual(ohttpRequest.headerValues, request.headerValues);
  Assert.deepEqual(
    ohttpRequest.content,
    request.content.split("").map(s => s.charCodeAt(0))
  );

  // Verify response
  Assert.equal(ohttpResponse.url, expectedResponse.url);
  Assert.equal(ohttpResponse.statusCode, expectedResponse.statusCode);
  Assert.equal(ohttpResponse.error, expectedResponse.error);
}

async function test_invalid_config() {
  resetPromises();

  const request = {
    method: "POST",
    scheme: "https",
    authority: "example.com",
    path: "/my-path",
    headerNames: ["User-Agent"],
    headerValues: ["Mozilla/5.0"],
    content: "Hello, world!",
  };

  const expectedResponse = {
    url: "",
    statusCode: 0,
    error: "Request failed, error=0x11, category=0x1",
  };

  // Provide invalid config
  // We still set status code to verify that we don't
  // get back 42 as the status.
  serverContext.configBody = [0, 0, 0, 0];
  serverContext.statusCode = 42;

  const ohttpClientTester = Cc[
    "@mozilla.org/ohttp-client-test;1"
  ].createInstance(Ci.nsIOhttpClientTest);

  ohttpClientTester.fetch(
    `${request.scheme}://${request.authority}${request.path}`,
    request.method,
    request.content,
    request.headerNames,
    request.headerValues,
    (url, statusCode, headerKeys, headerValues, error) => {
      resolveResponse({
        url,
        statusCode,
        headerKeys,
        headerValues,
        error,
      });
    }
  );

  // Request promise never resolves as getting config fails
  const ohttpResponse = await responsePromise;

  // Verify response
  Assert.equal(ohttpResponse.url, expectedResponse.url);
  Assert.equal(ohttpResponse.statusCode, expectedResponse.statusCode);
  Assert.equal(ohttpResponse.error, expectedResponse.error);
}

async function test_ohttp_failure() {
  resetPromises();

  const request = {
    method: "POST",
    scheme: "https",
    authority: "example.com",
    path: "/my-path",
    headerNames: ["User-Agent"],
    headerValues: ["Mozilla/5.0"],
    content: "Hello, world!",
  };

  const expectedResponse = {
    url: "",
    statusCode: 0,
    error: "Request failed, error=0x11, category=0x1",
  };

  // Provide a valid config, but make the server fail.
  // We still set status code to verify that we don't
  // get back 42 as the status.
  serverContext.configBody = ohttpServer.encodedConfig;
  serverContext.statusCode = 42;
  serverContext.failure = true;

  const ohttpClientTester = Cc[
    "@mozilla.org/ohttp-client-test;1"
  ].createInstance(Ci.nsIOhttpClientTest);

  ohttpClientTester.fetch(
    `${request.scheme}://${request.authority}${request.path}`,
    request.method,
    request.content,
    request.headerNames,
    request.headerValues,
    (url, statusCode, headerKeys, headerValues, error) => {
      resolveResponse({
        url,
        statusCode,
        headerKeys,
        headerValues,
        error,
      });
    }
  );

  const [ohttpRequest, ohttpResponse] = await Promise.all([
    requestPromise,
    responsePromise,
  ]);

  // Verify request
  Assert.equal(ohttpRequest.method, request.method);
  Assert.equal(ohttpRequest.scheme, request.scheme);
  Assert.equal(ohttpRequest.authority, request.authority);
  Assert.equal(ohttpRequest.path, request.path);
  Assert.deepEqual(ohttpRequest.headerNames, request.headerNames);
  Assert.deepEqual(ohttpRequest.headerValues, request.headerValues);
  Assert.deepEqual(
    ohttpRequest.content,
    request.content.split("").map(s => s.charCodeAt(0))
  );

  // Verify response
  Assert.equal(ohttpResponse.url, expectedResponse.url);
  Assert.equal(ohttpResponse.statusCode, expectedResponse.statusCode);
  Assert.equal(ohttpResponse.error, expectedResponse.error);
}

add_task(async function run_tests() {
  await test_success();
  await test_invalid_config();

  await test_ohttp_failure();
});
