/* Any copyright is dedicated to the Public Domain.
 * http://creativecommons.org/publicdomain/zero/1.0/
 */

"use strict";

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

var httpserver = new HttpServer();
httpserver.start(-1);
const PORT = httpserver.identity.primaryPort;

function make_channel(url) {
  return NetUtil.newChannel({
    uri: url,
    loadUsingSystemPrincipal: true,
  }).QueryInterface(Ci.nsIHttpChannel);
}

let gResponseBody = "blahblah";
let g200Counter = 0;
let g304Counter = 0;
function test_handler(metadata, response) {
  response.setHeader("Content-Type", "text/plain");
  response.setHeader("Cache-Control", "no-cache");
  response.setHeader("ETag", "test-etag1");

  let etag;
  try {
    etag = metadata.getHeader("If-None-Match");
  } catch (ex) {
    etag = "";
  }

  if (etag == "test-etag1") {
    response.setStatusLine(metadata.httpVersion, 304, "Not Modified");
    g304Counter++;
  } else {
    response.setStatusLine(metadata.httpVersion, 200, "OK");
    response.bodyOutputStream.write(gResponseBody, gResponseBody.length);
    g200Counter++;
  }
}

function cached_handler(metadata, response) {
  response.setHeader("Content-Type", "text/plain");
  response.setHeader("Cache-Control", "max-age=3600");
  response.setHeader("ETag", "test-etag1");

  response.setStatusLine(metadata.httpVersion, 200, "OK");
  response.bodyOutputStream.write(gResponseBody, gResponseBody.length);

  g200Counter++;
}

let gResponseCounter = 0;
let gIsFromCache = 0;
function checkContent(request, buffer, context, isFromCache) {
  Assert.equal(buffer, gResponseBody);
  info(
    "isRacing: " +
      request.QueryInterface(Ci.nsICacheInfoChannel).isRacing() +
      "\n"
  );
  gResponseCounter++;
  if (isFromCache) {
    gIsFromCache++;
  }
  executeSoon(() => {
    testGenerator.next();
  });
}

function run_test() {
  do_get_profile();
  // In this test, we manually use |TriggerNetwork| to prove we could send
  // net and cache reqeust simultaneously. Therefore we should disable
  // racing in the HttpChannel first.
  Services.prefs.setBoolPref("network.http.rcwn.enabled", false);
  httpserver.registerPathHandler("/rcwn", test_handler);
  httpserver.registerPathHandler("/rcwn_cached", cached_handler);
  testGenerator.next();
  do_test_pending();
}

let testGenerator = testSteps();
function* testSteps() {
  /*
   * In this test, we have a relatively low timeout of 200ms and an assertion that
   * the timer works properly by checking that the time was greater than 200ms.
   * With a timer precision of 100ms (for example) we will clamp downwards to 200
   * and cause the assertion to fail. To resolve this, we hardcode a precision of
   * 20ms.
   */
  Services.prefs.setBoolPref("privacy.reduceTimerPrecision", true);
  Services.prefs.setIntPref(
    "privacy.resistFingerprinting.reduceTimerPrecision.microseconds",
    20000
  );

  registerCleanupFunction(function () {
    Services.prefs.clearUserPref("privacy.reduceTimerPrecision");
    Services.prefs.clearUserPref(
      "privacy.resistFingerprinting.reduceTimerPrecision.microseconds"
    );
  });

  // Initial request. Stores the response in the cache.
  let channel = make_channel("http://localhost:" + PORT + "/rcwn");
  channel.asyncOpen(new ChannelListener(checkContent, null));
  yield undefined;
  equal(gResponseCounter, 1);
  equal(g200Counter, 1, "check number of 200 responses");
  equal(g304Counter, 0, "check number of 304 responses");

  // Checks that response is returned from the cache, after a 304 response.
  channel = make_channel("http://localhost:" + PORT + "/rcwn");
  channel.asyncOpen(new ChannelListener(checkContent, null));
  yield undefined;
  equal(gResponseCounter, 2);
  equal(g200Counter, 1, "check number of 200 responses");
  equal(g304Counter, 1, "check number of 304 responses");

  // Checks that delaying the response from the cache works.
  channel = make_channel("http://localhost:" + PORT + "/rcwn");
  channel
    .QueryInterface(Ci.nsIRaceCacheWithNetwork)
    .test_delayCacheEntryOpeningBy(200);
  let startTime = Date.now();
  channel.asyncOpen(new ChannelListener(checkContent, null));
  yield undefined;
  greaterOrEqual(
    Date.now() - startTime,
    200,
    "Check that timer works properly"
  );
  equal(gResponseCounter, 3);
  equal(g200Counter, 1, "check number of 200 responses");
  equal(g304Counter, 2, "check number of 304 responses");

  // Checks that we can trigger the cache open immediately, even if the cache delay is set very high.
  channel = make_channel("http://localhost:" + PORT + "/rcwn");
  channel
    .QueryInterface(Ci.nsIRaceCacheWithNetwork)
    .test_delayCacheEntryOpeningBy(100000);
  channel.asyncOpen(new ChannelListener(checkContent, null));
  do_timeout(50, function () {
    channel
      .QueryInterface(Ci.nsIRaceCacheWithNetwork)
      .test_triggerDelayedOpenCacheEntry();
  });
  yield undefined;
  equal(gResponseCounter, 4);
  equal(g200Counter, 1, "check number of 200 responses");
  equal(g304Counter, 3, "check number of 304 responses");

  // Sets a high delay for the cache fetch, and triggers the network activity.
  channel = make_channel("http://localhost:" + PORT + "/rcwn");
  channel
    .QueryInterface(Ci.nsIRaceCacheWithNetwork)
    .test_delayCacheEntryOpeningBy(100000);
  channel.QueryInterface(Ci.nsIRaceCacheWithNetwork).test_triggerNetwork(50);
  channel.asyncOpen(new ChannelListener(checkContent, null));
  // Trigger network after 50 ms.
  yield undefined;
  equal(gResponseCounter, 5);
  equal(g200Counter, 2, "check number of 200 responses");
  equal(g304Counter, 3, "check number of 304 responses");

  // Sets a high delay for the cache fetch, and triggers the network activity.
  // While the network response is produced, we trigger the cache fetch.
  // Because the network response was the first, a non-conditional request is sent.
  channel = make_channel("http://localhost:" + PORT + "/rcwn");
  channel
    .QueryInterface(Ci.nsIRaceCacheWithNetwork)
    .test_delayCacheEntryOpeningBy(100000);
  channel.QueryInterface(Ci.nsIRaceCacheWithNetwork).test_triggerNetwork(50);
  channel.asyncOpen(new ChannelListener(checkContent, null));
  yield undefined;
  equal(gResponseCounter, 6);
  equal(g200Counter, 3, "check number of 200 responses");
  equal(g304Counter, 3, "check number of 304 responses");

  // Triggers cache open before triggering network.
  channel = make_channel("http://localhost:" + PORT + "/rcwn");
  channel
    .QueryInterface(Ci.nsIRaceCacheWithNetwork)
    .test_delayCacheEntryOpeningBy(100000);
  channel.QueryInterface(Ci.nsIRaceCacheWithNetwork).test_triggerNetwork(5000);
  channel.asyncOpen(new ChannelListener(checkContent, null));
  channel
    .QueryInterface(Ci.nsIRaceCacheWithNetwork)
    .test_triggerDelayedOpenCacheEntry();
  yield undefined;
  equal(gResponseCounter, 7);
  equal(
    g200Counter,
    3,
    `check number of 200 responses | 200: ${g200Counter}, 304: ${g304Counter}`
  );
  equal(
    g304Counter,
    4,
    `check number of 304 responses | 200: ${g200Counter}, 304: ${g304Counter}`
  );

  // Load the cached handler so we don't need to revalidate
  channel = make_channel("http://localhost:" + PORT + "/rcwn_cached");
  channel.asyncOpen(new ChannelListener(checkContent, null));
  yield undefined;
  equal(gResponseCounter, 8);
  equal(g200Counter, 4, "check number of 200 responses");
  equal(g304Counter, 4, "check number of 304 responses");

  // Make sure response is loaded from the cache, not the network
  channel = make_channel("http://localhost:" + PORT + "/rcwn_cached");
  channel.asyncOpen(new ChannelListener(checkContent, null));
  yield undefined;
  equal(gResponseCounter, 9);
  equal(g200Counter, 4, "check number of 200 responses");
  equal(g304Counter, 4, "check number of 304 responses");

  // Cache times out, so we trigger the network
  gIsFromCache = 0;
  channel = make_channel("http://localhost:" + PORT + "/rcwn_cached");
  channel
    .QueryInterface(Ci.nsIRaceCacheWithNetwork)
    .test_delayCacheEntryOpeningBy(100000);
  // trigger network after 50 ms
  channel.QueryInterface(Ci.nsIRaceCacheWithNetwork).test_triggerNetwork(50);
  channel.asyncOpen(new ChannelListener(checkContent, null));
  yield undefined;
  equal(gResponseCounter, 10);
  equal(gIsFromCache, 0, "should be from the network");
  equal(g200Counter, 5, "check number of 200 responses");
  equal(g304Counter, 4, "check number of 304 responses");

  // Cache callback comes back right after network is triggered.
  channel = make_channel("http://localhost:" + PORT + "/rcwn_cached");
  channel
    .QueryInterface(Ci.nsIRaceCacheWithNetwork)
    .test_delayCacheEntryOpeningBy(55);
  channel.QueryInterface(Ci.nsIRaceCacheWithNetwork).test_triggerNetwork(50);
  channel.asyncOpen(new ChannelListener(checkContent, null));
  yield undefined;
  equal(gResponseCounter, 11);
  info("IsFromCache: " + gIsFromCache + "\n");
  info("Number of 200 responses: " + g200Counter + "\n");
  equal(g304Counter, 4, "check number of 304 responses");

  // Set an increasingly high timeout to trigger opening the cache entry
  // This way we ensure that some of the entries we will get from the network,
  // and some we will get from the cache.
  gIsFromCache = 0;
  for (var i = 0; i < 50; i++) {
    channel = make_channel("http://localhost:" + PORT + "/rcwn_cached");
    channel
      .QueryInterface(Ci.nsIRaceCacheWithNetwork)
      .test_delayCacheEntryOpeningBy(i * 100);
    channel.QueryInterface(Ci.nsIRaceCacheWithNetwork).test_triggerNetwork(10);
    channel.asyncOpen(new ChannelListener(checkContent, null));
    // This may be racy. The delay was chosen because the distribution of net-cache
    // results was around 25-25 on my machine.
    yield undefined;
  }

  greater(gIsFromCache, 0, "Some of the responses should be from the cache");
  less(gIsFromCache, 50, "Some of the responses should be from the net");

  httpserver.stop(do_test_finished);
}
