const BinaryOutputStream = Components.Constructor(
  "@mozilla.org/binaryoutputstream;1",
  "nsIBinaryOutputStream",
  "setOutputStream"
);

const server = new HttpServer();
server.start(-1);
registerCleanupFunction(() => server.stop(() => {}));
const SERVER_BASE_URL = `http://localhost:${server.identity.primaryPort}`;

const proxyServer = new HttpServer();
proxyServer.identity.add("http", "localhost", server.identity.primaryPort);
proxyServer.start(-1);
registerCleanupFunction(() => proxyServer.stop(() => {}));
const PROXY_PORT = proxyServer.identity.primaryPort;

// A sequence of bytes that would become garbage if it were to be read as UTF-8:
// - 0xEF 0xBB 0xBF is a byte order mark.
// - 0xC0 on its own is invalid (it's the first byte of a 2-byte encoding).
const INVALID_UTF_8_BYTES = [0xef, 0xbb, 0xbf, 0xc0];

server.registerPathHandler("/binary.dat", (request, response) => {
  response.setStatusLine(null, 201, "StatusLineHere");
  response.setHeader("headerName", "HeaderValue: HeaderValueEnd");
  let binaryOut = new BinaryOutputStream(response.bodyOutputStream);
  binaryOut.writeByteArray([0xef, 0xbb, 0xbf, 0xc0]);
});

// HTTPS requests are proxied with CONNECT, but our test server is HTTP,
// which means that the proxy will receive GET http://localhost:port.
var proxiedCount = 0;
proxyServer.registerPrefixHandler("/", (request, response) => {
  ++proxiedCount;
  Assert.equal(request.path, "/binary.dat", `Proxy request ${proxiedCount}`);
  // Close connection without sending any response.
  response.seizePower();
  response.finish();
});

add_task(async function test_utils_fetch_binary() {
  let res = await Utils.fetch(`${SERVER_BASE_URL}/binary.dat`);

  Assert.equal(res.status, 201, "res.status");
  Assert.equal(res.statusText, "StatusLineHere", "res.statusText");
  Assert.equal(
    res.headers.get("headerName"),
    "HeaderValue: HeaderValueEnd",
    "Utils.fetch should return the header"
  );

  Assert.deepEqual(
    Array.from(new Uint8Array(await res.arrayBuffer())),
    INVALID_UTF_8_BYTES,
    "Binary response body should be returned as is"
  );
});

add_task(async function test_utils_fetch_binary_as_text() {
  let res = await Utils.fetch(`${SERVER_BASE_URL}/binary.dat`);
  Assert.deepEqual(
    Array.from(await res.text(), c => c.charCodeAt(0)),
    [65533],
    "Interpreted as UTF-8, the response becomes garbage"
  );
});

add_task(async function test_utils_fetch_binary_as_json() {
  let res = await Utils.fetch(`${SERVER_BASE_URL}/binary.dat`);
  await Assert.rejects(
    res.json(),
    /SyntaxError: JSON.parse: unexpected character/,
    "Binary data is invalid JSON"
  );
});

add_task(async function test_utils_fetch_has_conservative() {
  let channelPromise = TestUtils.topicObserved("http-on-modify-request");
  await Utils.fetch(`${SERVER_BASE_URL}/binary.dat`);

  let channel = (await channelPromise)[0].QueryInterface(Ci.nsIHttpChannel);

  Assert.equal(channel.URI.spec, `${SERVER_BASE_URL}/binary.dat`, "URL OK");

  let internalChannel = channel.QueryInterface(Ci.nsIHttpChannelInternal);
  Assert.ok(internalChannel.beConservative, "beConservative flag is set");
});

add_task(async function test_utils_fetch_has_conservative() {
  let channelPromise = TestUtils.topicObserved("http-on-modify-request");
  await Utils.fetch(`${SERVER_BASE_URL}/binary.dat`);

  let channel = (await channelPromise)[0].QueryInterface(Ci.nsIHttpChannel);

  Assert.equal(channel.URI.spec, `${SERVER_BASE_URL}/binary.dat`, "URL OK");

  let internalChannel = channel.QueryInterface(Ci.nsIHttpChannelInternal);
  Assert.ok(internalChannel.beConservative, "beConservative flag is set");
});

add_task(async function test_utils_fetch_with_bad_proxy() {
  Services.prefs.setIntPref("network.proxy.type", 1);
  Services.prefs.setStringPref("network.proxy.http", "127.0.0.1");
  Services.prefs.setIntPref("network.proxy.http_port", PROXY_PORT);
  Services.prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true);

  // The URL that we're going to request.
  const DESTINATION_URL = `${SERVER_BASE_URL}/binary.dat`;

  Assert.equal(proxiedCount, 0, "Proxy not used yet");
  {
    info("Bad proxy, default prefs");
    let res = await Utils.fetch(DESTINATION_URL);
    Assert.equal(res.status, 201, "Bypassed bad proxy");
    // 10 instead of 1 because of reconnect attempts after a dropped request.
    Assert.equal(proxiedCount, 10, "Proxy was used by HttpChannel");
  }

  // Disables the failover logic from HttpChannel.
  Services.prefs.setBoolPref("network.proxy.failover_direct", false);
  proxiedCount = 0;
  {
    info("Bad proxy, disabled network.proxy.failover_direct");
    let res = await Utils.fetch(DESTINATION_URL);
    Assert.equal(res.status, 201, "Bypassed bad proxy");
    // 10 instead of 1 because of reconnect attempts after a dropped request.
    Assert.equal(proxiedCount, 10, "Proxy was used by ServiceRequest");
  }

  proxiedCount = 0;
  {
    info("Using internal option of Utils.fetch: bypassProxy=true");
    let res = await Utils.fetch(DESTINATION_URL, { bypassProxy: true });
    Assert.equal(res.status, 201, "Bypassed bad proxy");
    Assert.equal(proxiedCount, 0, "Not using proxy when bypassProxy=true");
  }

  // Disables the failover logic from ServiceRequest/Utils.fetch
  Services.prefs.setBoolPref("network.proxy.allow_bypass", false);
  proxiedCount = 0;

  info("Bad proxy, disabled network.proxy.allow_bypass");
  await Assert.rejects(
    Utils.fetch(DESTINATION_URL),
    /NetworkError/,
    "Bad proxy request should fail without failover"
  );
  // 10 instead of 1 because of reconnect attempts after a dropped request.
  Assert.equal(proxiedCount, 10, "Attempted to use proxy again");

  Services.prefs.clearUserPref("network.proxy.type");
  Services.prefs.clearUserPref("network.proxy.http");
  Services.prefs.clearUserPref("network.proxy.http_port");
  Services.prefs.clearUserPref("network.proxy.allow_hijacking_localhost");
  Services.prefs.clearUserPref("network.proxy.failover_direct");
  Services.prefs.clearUserPref("network.proxy.allow_bypass");
});
