"use strict";

const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
const ReferrerInfo = Components.Constructor(
  "@mozilla.org/referrer-info;1",
  "nsIReferrerInfo",
  "init"
);

var running_single_process = false;

var predictor = null;

function is_child_process() {
  return Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT;
}

function extract_origin(uri) {
  var o = uri.scheme + "://" + uri.asciiHost;
  if (uri.port !== -1) {
    o = o + ":" + uri.port;
  }
  return o;
}

var origin_attributes = {};

var ValidityChecker = function (verifier, httpStatus) {
  this.verifier = verifier;
  this.httpStatus = httpStatus;
};

ValidityChecker.prototype = {
  verifier: null,
  httpStatus: 0,

  QueryInterface: ChromeUtils.generateQI(["nsICacheEntryOpenCallback"]),

  onCacheEntryCheck(entry) {
    return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED;
  },

  onCacheEntryAvailable(entry, isnew, status) {
    // Check if forced valid
    Assert.equal(entry.isForcedValid, this.httpStatus === 200);
    this.verifier.maybe_run_next_test();
  },
};

var Verifier = function _verifier(
  testing,
  expected_prefetches,
  expected_preconnects,
  expected_preresolves
) {
  this.verifying = testing;
  this.expected_prefetches = expected_prefetches;
  this.expected_preconnects = expected_preconnects;
  this.expected_preresolves = expected_preresolves;
};

Verifier.prototype = {
  complete: false,
  verifying: null,
  expected_prefetches: null,
  expected_preconnects: null,
  expected_preresolves: null,

  getInterface: function verifier_getInterface(iid) {
    return this.QueryInterface(iid);
  },

  QueryInterface: ChromeUtils.generateQI(["nsINetworkPredictorVerifier"]),

  maybe_run_next_test: function verifier_maybe_run_next_test() {
    if (
      this.expected_prefetches.length === 0 &&
      this.expected_preconnects.length === 0 &&
      this.expected_preresolves.length === 0 &&
      !this.complete
    ) {
      this.complete = true;
      Assert.ok(true, "Well this is unexpected...");
      // This kicks off the ability to run the next test
      reset_predictor();
    }
  },

  onPredictPrefetch: function verifier_onPredictPrefetch(uri, status) {
    var index = this.expected_prefetches.indexOf(uri.asciiSpec);
    if (index == -1 && !this.complete) {
      Assert.ok(false, "Got prefetch for unexpected uri " + uri.asciiSpec);
    } else {
      this.expected_prefetches.splice(index, 1);
    }

    dump("checking validity of entry for " + uri.spec + "\n");
    var checker = new ValidityChecker(this, status);
    asyncOpenCacheEntry(
      uri.spec,
      "disk",
      Ci.nsICacheStorage.OPEN_NORMALLY,
      Services.loadContextInfo.default,
      checker
    );
  },

  onPredictPreconnect: function verifier_onPredictPreconnect(uri) {
    var origin = extract_origin(uri);
    var index = this.expected_preconnects.indexOf(origin);
    if (index == -1 && !this.complete) {
      Assert.ok(false, "Got preconnect for unexpected uri " + origin);
    } else {
      this.expected_preconnects.splice(index, 1);
    }
    this.maybe_run_next_test();
  },

  onPredictDNS: function verifier_onPredictDNS(uri) {
    var origin = extract_origin(uri);
    var index = this.expected_preresolves.indexOf(origin);
    if (index == -1 && !this.complete) {
      Assert.ok(false, "Got preresolve for unexpected uri " + origin);
    } else {
      this.expected_preresolves.splice(index, 1);
    }
    this.maybe_run_next_test();
  },
};

function reset_predictor() {
  if (running_single_process || is_child_process()) {
    predictor.reset();
  } else {
    sendCommand("predictor.reset();");
  }
}

function newURI(s) {
  return Services.io.newURI(s);
}

var prepListener = {
  numEntriesToOpen: 0,
  numEntriesOpened: 0,
  continueCallback: null,

  QueryInterface: ChromeUtils.generateQI(["nsICacheEntryOpenCallback"]),

  init(entriesToOpen, cb) {
    this.numEntriesOpened = 0;
    this.numEntriesToOpen = entriesToOpen;
    this.continueCallback = cb;
  },

  onCacheEntryCheck(entry) {
    return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED;
  },

  onCacheEntryAvailable(entry, isNew, result) {
    Assert.equal(result, Cr.NS_OK);
    entry.setMetaDataElement("predictor_test", "1");
    entry.metaDataReady();
    this.numEntriesOpened++;
    if (this.numEntriesToOpen == this.numEntriesOpened) {
      this.continueCallback();
    }
  },
};

function open_and_continue(uris, continueCallback) {
  var ds = Services.cache2.diskCacheStorage(Services.loadContextInfo.default);

  prepListener.init(uris.length, continueCallback);
  for (var i = 0; i < uris.length; ++i) {
    ds.asyncOpenURI(
      uris[i],
      "",
      Ci.nsICacheStorage.OPEN_NORMALLY,
      prepListener
    );
  }
}

function test_link_hover() {
  if (!running_single_process && !is_child_process()) {
    // This one we can just proxy to the child and be done with, no extra setup
    // is necessary.
    sendCommand("test_link_hover();");
    return;
  }

  var uri = newURI("http://localhost:4444/foo/bar");
  var referrer = newURI("http://localhost:4444/foo");
  var preconns = ["http://localhost:4444"];

  var verifier = new Verifier("hover", [], preconns, []);
  predictor.predict(
    uri,
    referrer,
    predictor.PREDICT_LINK,
    origin_attributes,
    verifier
  );
}

const pageload_toplevel = newURI("http://localhost:4444/index.html");

function continue_test_pageload() {
  var subresources = [
    "http://localhost:4444/style.css",
    "http://localhost:4443/jquery.js",
    "http://localhost:4444/image.png",
  ];

  // This is necessary to learn the origin stuff
  predictor.learn(
    pageload_toplevel,
    null,
    predictor.LEARN_LOAD_TOPLEVEL,
    origin_attributes
  );
  do_timeout(0, () => {
    // allow the learn() to run on the main thread
    var preconns = [];

    var sruri = newURI(subresources[0]);
    predictor.learn(
      sruri,
      pageload_toplevel,
      predictor.LEARN_LOAD_SUBRESOURCE,
      origin_attributes
    );
    do_timeout(0, () => {
      preconns.push(extract_origin(sruri));

      sruri = newURI(subresources[1]);
      predictor.learn(
        sruri,
        pageload_toplevel,
        predictor.LEARN_LOAD_SUBRESOURCE,
        origin_attributes
      );
      do_timeout(0, () => {
        preconns.push(extract_origin(sruri));

        sruri = newURI(subresources[2]);
        predictor.learn(
          sruri,
          pageload_toplevel,
          predictor.LEARN_LOAD_SUBRESOURCE,
          origin_attributes
        );
        do_timeout(0, () => {
          preconns.push(extract_origin(sruri));

          var verifier = new Verifier("pageload", [], preconns, []);
          predictor.predict(
            pageload_toplevel,
            null,
            predictor.PREDICT_LOAD,
            origin_attributes,
            verifier
          );
        });
      });
    });
  });
}

function test_pageload() {
  open_and_continue([pageload_toplevel], function () {
    if (running_single_process) {
      continue_test_pageload();
    } else {
      sendCommand("continue_test_pageload();");
    }
  });
}

const redirect_inituri = newURI("http://localhost:4443/redirect");
const redirect_targeturi = newURI("http://localhost:4444/index.html");

function continue_test_redirect() {
  var subresources = [
    "http://localhost:4444/style.css",
    "http://localhost:4443/jquery.js",
    "http://localhost:4444/image.png",
  ];

  predictor.learn(
    redirect_inituri,
    null,
    predictor.LEARN_LOAD_TOPLEVEL,
    origin_attributes
  );
  do_timeout(0, () => {
    predictor.learn(
      redirect_targeturi,
      null,
      predictor.LEARN_LOAD_TOPLEVEL,
      origin_attributes
    );
    do_timeout(0, () => {
      predictor.learn(
        redirect_targeturi,
        redirect_inituri,
        predictor.LEARN_LOAD_REDIRECT,
        origin_attributes
      );
      do_timeout(0, () => {
        var preconns = [];
        preconns.push(extract_origin(redirect_targeturi));

        var sruri = newURI(subresources[0]);
        predictor.learn(
          sruri,
          redirect_targeturi,
          predictor.LEARN_LOAD_SUBRESOURCE,
          origin_attributes
        );
        do_timeout(0, () => {
          preconns.push(extract_origin(sruri));

          sruri = newURI(subresources[1]);
          predictor.learn(
            sruri[1],
            redirect_targeturi,
            predictor.LEARN_LOAD_SUBRESOURCE,
            origin_attributes
          );
          do_timeout(0, () => {
            preconns.push(extract_origin(sruri));

            sruri = newURI(subresources[2]);
            predictor.learn(
              sruri[2],
              redirect_targeturi,
              predictor.LEARN_LOAD_SUBRESOURCE,
              origin_attributes
            );
            do_timeout(0, () => {
              preconns.push(extract_origin(sruri));

              var verifier = new Verifier("redirect", [], preconns, []);
              predictor.predict(
                redirect_inituri,
                null,
                predictor.PREDICT_LOAD,
                origin_attributes,
                verifier
              );
            });
          });
        });
      });
    });
  });
}

// Test is currently disabled.
// eslint-disable-next-line no-unused-vars
function test_redirect() {
  open_and_continue([redirect_inituri, redirect_targeturi], function () {
    if (running_single_process) {
      continue_test_redirect();
    } else {
      sendCommand("continue_test_redirect();");
    }
  });
}

// Test is currently disabled.
// eslint-disable-next-line no-unused-vars
function test_startup() {
  if (!running_single_process && !is_child_process()) {
    // This one we can just proxy to the child and be done with, no extra setup
    // is necessary.
    sendCommand("test_startup();");
    return;
  }

  var uris = ["http://localhost:4444/startup", "http://localhost:4443/startup"];
  var preconns = [];
  var uri = newURI(uris[0]);
  predictor.learn(uri, null, predictor.LEARN_STARTUP, origin_attributes);
  do_timeout(0, () => {
    preconns.push(extract_origin(uri));

    uri = newURI(uris[1]);
    predictor.learn(uri, null, predictor.LEARN_STARTUP, origin_attributes);
    do_timeout(0, () => {
      preconns.push(extract_origin(uri));

      var verifier = new Verifier("startup", [], preconns, []);
      predictor.predict(
        null,
        null,
        predictor.PREDICT_STARTUP,
        origin_attributes,
        verifier
      );
    });
  });
}

const dns_toplevel = newURI("http://localhost:4444/index.html");

function continue_test_dns() {
  var subresource = "http://localhost:4443/jquery.js";

  predictor.learn(
    dns_toplevel,
    null,
    predictor.LEARN_LOAD_TOPLEVEL,
    origin_attributes
  );
  do_timeout(0, () => {
    var sruri = newURI(subresource);
    predictor.learn(
      sruri,
      dns_toplevel,
      predictor.LEARN_LOAD_SUBRESOURCE,
      origin_attributes
    );
    do_timeout(0, () => {
      var preresolves = [extract_origin(sruri)];
      var verifier = new Verifier("dns", [], [], preresolves);
      predictor.predict(
        dns_toplevel,
        null,
        predictor.PREDICT_LOAD,
        origin_attributes,
        verifier
      );
    });
  });
}

function test_dns() {
  open_and_continue([dns_toplevel], function () {
    // Ensure that this will do preresolves
    Services.prefs.setIntPref(
      "network.predictor.preconnect-min-confidence",
      101
    );
    if (running_single_process) {
      continue_test_dns();
    } else {
      sendCommand("continue_test_dns();");
    }
  });
}

const origin_toplevel = newURI("http://localhost:4444/index.html");

function continue_test_origin() {
  var subresources = [
    "http://localhost:4444/style.css",
    "http://localhost:4443/jquery.js",
    "http://localhost:4444/image.png",
  ];
  predictor.learn(
    origin_toplevel,
    null,
    predictor.LEARN_LOAD_TOPLEVEL,
    origin_attributes
  );
  do_timeout(0, () => {
    var preconns = [];

    var sruri = newURI(subresources[0]);
    predictor.learn(
      sruri,
      origin_toplevel,
      predictor.LEARN_LOAD_SUBRESOURCE,
      origin_attributes
    );
    do_timeout(0, () => {
      var origin = extract_origin(sruri);
      if (!preconns.includes(origin)) {
        preconns.push(origin);
      }

      sruri = newURI(subresources[1]);
      predictor.learn(
        sruri,
        origin_toplevel,
        predictor.LEARN_LOAD_SUBRESOURCE,
        origin_attributes
      );
      do_timeout(0, () => {
        var origin = extract_origin(sruri);
        if (!preconns.includes(origin)) {
          preconns.push(origin);
        }

        sruri = newURI(subresources[2]);
        predictor.learn(
          sruri,
          origin_toplevel,
          predictor.LEARN_LOAD_SUBRESOURCE,
          origin_attributes
        );
        do_timeout(0, () => {
          var origin = extract_origin(sruri);
          if (!preconns.includes(origin)) {
            preconns.push(origin);
          }

          var loaduri = newURI("http://localhost:4444/anotherpage.html");
          var verifier = new Verifier("origin", [], preconns, []);
          predictor.predict(
            loaduri,
            null,
            predictor.PREDICT_LOAD,
            origin_attributes,
            verifier
          );
        });
      });
    });
  });
}

function test_origin() {
  open_and_continue([origin_toplevel], function () {
    if (running_single_process) {
      continue_test_origin();
    } else {
      sendCommand("continue_test_origin();");
    }
  });
}

var httpserv = null;
var prefetch_tluri;
var prefetch_sruri;

function prefetchHandler(metadata, response) {
  response.setStatusLine(metadata.httpVersion, 200, "OK");
  var body = "Success (meow meow meow).";

  response.bodyOutputStream.write(body, body.length);
}

var prefetchListener = {
  onStartRequest(request) {
    Assert.equal(request.status, Cr.NS_OK);
  },

  onDataAvailable(request, stream, offset, cnt) {
    read_stream(stream, cnt);
  },

  onStopRequest(request, status) {
    run_next_test();
  },
};

function test_prefetch_setup() {
  // Disable preconnects and preresolves
  Services.prefs.setIntPref("network.predictor.preconnect-min-confidence", 101);
  Services.prefs.setIntPref("network.predictor.preresolve-min-confidence", 101);

  Services.prefs.setBoolPref("network.predictor.enable-prefetch", true);

  // Makes it so we only have to call test_prefetch_prime twice to make prefetch
  // do its thing.
  Services.prefs.setIntPref("network.predictor.prefetch-rolling-load-count", 2);

  // This test does not run in e10s-mode, so we'll just go ahead and skip it.
  // We've left the e10s test code in below, just in case someone wants to try
  // to make it work at some point in the future.
  if (!running_single_process) {
    dump("skipping test_prefetch_setup due to e10s\n");
    run_next_test();
    return;
  }

  httpserv = new HttpServer();
  httpserv.registerPathHandler("/cat.jpg", prefetchHandler);
  httpserv.start(-1);

  var tluri =
    "http://127.0.0.1:" + httpserv.identity.primaryPort + "/index.html";
  var sruri = "http://127.0.0.1:" + httpserv.identity.primaryPort + "/cat.jpg";
  prefetch_tluri = newURI(tluri);
  prefetch_sruri = newURI(sruri);
  if (!running_single_process && !is_child_process()) {
    // Give the child process access to these values
    sendCommand('prefetch_tluri = newURI("' + tluri + '");');
    sendCommand('prefetch_sruri = newURI("' + sruri + '");');
  }

  run_next_test();
}

// Used to "prime the pump" for prefetch - it makes sure all our learns go
// through as expected so that prefetching will happen.
function test_prefetch_prime() {
  // This test does not run in e10s-mode, so we'll just go ahead and skip it.
  // We've left the e10s test code in below, just in case someone wants to try
  // to make it work at some point in the future.
  if (!running_single_process) {
    dump("skipping test_prefetch_prime due to e10s\n");
    run_next_test();
    return;
  }

  open_and_continue([prefetch_tluri], function () {
    if (running_single_process) {
      predictor.learn(
        prefetch_tluri,
        null,
        predictor.LEARN_LOAD_TOPLEVEL,
        origin_attributes
      );
      predictor.learn(
        prefetch_sruri,
        prefetch_tluri,
        predictor.LEARN_LOAD_SUBRESOURCE,
        origin_attributes
      );
    } else {
      sendCommand(
        "predictor.learn(prefetch_tluri, null, predictor.LEARN_LOAD_TOPLEVEL, origin_attributes);"
      );
      sendCommand(
        "predictor.learn(prefetch_sruri, prefetch_tluri, predictor.LEARN_LOAD_SUBRESOURCE, origin_attributes);"
      );
    }

    // This runs in the parent or only process
    var channel = NetUtil.newChannel({
      uri: prefetch_sruri.asciiSpec,
      loadUsingSystemPrincipal: true,
    }).QueryInterface(Ci.nsIHttpChannel);
    channel.requestMethod = "GET";
    channel.referrerInfo = new ReferrerInfo(
      Ci.nsIReferrerInfo.EMPTY,
      true,
      prefetch_tluri
    );
    channel.asyncOpen(prefetchListener);
  });
}

function test_prefetch() {
  // This test does not run in e10s-mode, so we'll just go ahead and skip it.
  // We've left the e10s test code in below, just in case someone wants to try
  // to make it work at some point in the future.
  if (!running_single_process) {
    dump("skipping test_prefetch due to e10s\n");
    run_next_test();
    return;
  }

  // Setup for this has all been taken care of by test_prefetch_prime, so we can
  // continue on without pausing here.
  if (running_single_process) {
    continue_test_prefetch();
  } else {
    sendCommand("continue_test_prefetch();");
  }
}

function continue_test_prefetch() {
  var prefetches = [prefetch_sruri.asciiSpec];
  var verifier = new Verifier("prefetch", prefetches, [], []);
  predictor.predict(
    prefetch_tluri,
    null,
    predictor.PREDICT_LOAD,
    origin_attributes,
    verifier
  );
}

function test_visitor_doom() {
  // See bug 1708673
  Services.prefs.setBoolPref("network.cache.bug1708673", true);
  registerCleanupFunction(() => {
    Services.prefs.clearUserPref("network.cache.bug1708673");
  });

  let p1 = new Promise(resolve => {
    let doomTasks = [];
    let visitor = {
      onCacheStorageInfo() {},
      async onCacheEntryInfo(
        aURI,
        aIdEnhance,
        aDataSize,
        aAltDataSize,
        aFetchCount,
        aLastModifiedTime,
        aExpirationTime,
        aPinned,
        aInfo
      ) {
        let storages = [
          Services.cache2.memoryCacheStorage(aInfo),
          Services.cache2.diskCacheStorage(aInfo, false),
        ];
        console.debug("asyncDoomURI", aURI.spec);
        let doomTask = Promise.all(
          storages.map(storage => {
            return new Promise(resolve => {
              storage.asyncDoomURI(aURI, aIdEnhance, {
                onCacheEntryDoomed: resolve,
              });
            });
          })
        );
        doomTasks.push(doomTask);
      },
      onCacheEntryVisitCompleted() {
        Promise.allSettled(doomTasks).then(resolve);
      },
      QueryInterface: ChromeUtils.generateQI(["nsICacheStorageVisitor"]),
    };
    Services.cache2.asyncVisitAllStorages(visitor, true);
  });

  let p2 = new Promise(resolve => {
    reset_predictor();
    resolve();
  });

  do_test_pending();
  Promise.allSettled([p1, p2]).then(() => {
    return new Promise(resolve => {
      let entryCount = 0;
      let visitor = {
        onCacheStorageInfo() {},
        async onCacheEntryInfo(
          aURI,
          aIdEnhance,
          aDataSize,
          aAltDataSize,
          aFetchCount,
          aLastModifiedTime,
          aExpirationTime,
          aPinned,
          aInfo
        ) {
          entryCount++;
        },
        onCacheEntryVisitCompleted() {
          Assert.equal(entryCount, 0);
          resolve();
        },
        QueryInterface: ChromeUtils.generateQI(["nsICacheStorageVisitor"]),
      };
      Services.cache2.asyncVisitAllStorages(visitor, true);
    }).then(run_next_test);
  });
}

function cleanup() {
  observer.cleaningUp = true;
  if (running_single_process) {
    // The http server is required (and started) by the prefetch test, which
    // only runs in single-process mode, so don't try to shut it down if we're
    // in e10s mode.
    do_test_pending();
    httpserv.stop(do_test_finished);
  }
  reset_predictor();
}

var tests = [
  // This must ALWAYS come first, to ensure a clean slate
  reset_predictor,
  test_link_hover,
  test_pageload,
  // TODO: These are disabled until the features are re-written
  //test_redirect,
  //test_startup,
  // END DISABLED TESTS
  test_origin,
  test_dns,
  test_prefetch_setup,
  test_prefetch_prime,
  test_prefetch_prime,
  test_prefetch,
  test_visitor_doom,
  // This must ALWAYS come last, to ensure we clean up after ourselves
  cleanup,
];

var observer = {
  cleaningUp: false,

  QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),

  observe(subject, topic, data) {
    if (topic != "predictor-reset-complete") {
      return;
    }

    if (this.cleaningUp) {
      unregisterObserver();
    }

    run_next_test();
  },
};

function registerObserver() {
  Services.obs.addObserver(observer, "predictor-reset-complete");
}

function unregisterObserver() {
  Services.obs.removeObserver(observer, "predictor-reset-complete");
}

function run_test_real() {
  tests.forEach(f => add_test(f));
  do_get_profile();

  Services.prefs.setBoolPref("network.predictor.enabled", true);
  Services.prefs.setBoolPref("network.predictor.doing-tests", true);

  predictor = Cc["@mozilla.org/network/predictor;1"].getService(
    Ci.nsINetworkPredictor
  );

  registerObserver();

  registerCleanupFunction(() => {
    Services.prefs.clearUserPref("network.predictor.preconnect-min-confidence");
    Services.prefs.clearUserPref("network.predictor.enabled");
    Services.prefs.clearUserPref("network.predictor.preresolve-min-confidence");
    Services.prefs.clearUserPref("network.predictor.enable-prefetch");
    Services.prefs.clearUserPref(
      "network.predictor.prefetch-rolling-load-count"
    );
    Services.prefs.clearUserPref("network.predictor.doing-tests");
  });

  run_next_test();
}

function run_test() {
  // This indirection is necessary to make e10s tests work as expected
  running_single_process = true;
  run_test_real();
}
