/* 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/. */

const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
  AddonSettings: "resource://gre/modules/addons/AddonSettings.sys.mjs",
  CertUtils: "resource://gre/modules/CertUtils.sys.mjs",
  ServiceRequest: "resource://gre/modules/ServiceRequest.sys.mjs",
});

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

function waitForNotificationPromise(notification) {
  return new Promise(resolve => {
    function observer(aSubject, _aTopic, _aData) {
      info(aSubject);
      Services.obs.removeObserver(observer, notification);
      resolve(aSubject);
    }
    Services.obs.addObserver(observer, notification);
  });
}

function openChannelPromise(url, options = { loadUsingSystemPrincipal: true }) {
  let uri = Services.io.newURI(url);
  options.uri = uri;
  let chan = NetUtil.newChannel(options);
  let flags = CL_ALLOW_UNKNOWN_CL;
  if (options.expectFailure) {
    flags |= CL_EXPECT_FAILURE;
  }
  return new Promise(resolve => {
    chan.asyncOpen(
      new ChannelListener((req, buf) => resolve({ req, buf }), null, flags)
    );
  });
}

let backupServer;
const override = Cc["@mozilla.org/network/native-dns-override;1"].getService(
  Ci.nsINativeDNSResolverOverride
);

add_setup(async function setup() {
  let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
    Ci.nsIX509CertDB
  );
  addCertFromFile(certdb, "../unit/http2-ca.pem", "CTu,u,u");
  Services.prefs.setBoolPref("network.essential_domains_fallback", true);

  backupServer = new NodeHTTPSServer();
  await backupServer.start();
  registerCleanupFunction(async () => {
    await backupServer.stop();
  });
  await backupServer.registerPathHandler("/stuff", (req, res) => {
    res.end("Good stuff");
  });

  // Just so we can simulate mapping the default HTTPS port and domain.
  Services.prefs.setStringPref(
    "network.socket.forcePort",
    `443=${backupServer.port()}`
  );

  Services.io.addEssentialDomainMapping("aus5.mozilla.org", "foo.example.com");

  let ncs = Cc[
    "@mozilla.org/network/network-connectivity-service;1"
  ].getService(Ci.nsINetworkConnectivityService);
  ncs.IPv4 = Ci.nsINetworkConnectivityService.OK;
});

add_task(async function test_fallback_on_dns_failure() {
  Services.fog.testResetFOG();
  Services.obs.notifyObservers(null, "net:cancel-all-connections");
  Services.dns.clearCache(true);
  override.addIPOverride("aus5.mozilla.org", "N/A");
  override.addIPOverride("foo.example.com", "127.0.0.1");

  let { buf } = await openChannelPromise("https://aus5.mozilla.org/stuff");
  equal(buf, "Good stuff");

  equal(
    await Glean.network.systemChannelUpdateStatus.dns.testGetValue(),
    1,
    "Expecting one failed request due to DNS"
  );
  equal(
    await Glean.network.retriedSystemChannelUpdateStatus.ok.testGetValue(),
    1,
    "Expecting retried update request to succeed"
  );
});

add_task(async function test_dns_xhr() {
  Services.dns.clearCache(true);
  override.clearOverrides();
  override.addIPOverride("aus5.mozilla.org", "N/A");
  override.addIPOverride("foo.example.com", "127.0.0.1");

  let text = await new Promise((resolve, reject) => {
    let request = new XMLHttpRequest({ mozAnon: true, mozSystem: true });
    request.open("GET", `https://aus5.mozilla.org/stuff`, true);
    request.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
    // Prevent the request from writing to cache.
    request.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
    request.overrideMimeType("text/plain");
    request.timeout = 5000;
    request.addEventListener("load", () => resolve(request.responseText));
    request.addEventListener("error", reject);
    request.addEventListener("timeout", reject);
    request.send(null);
  });
  equal(text, "Good stuff");
});

add_task(async function test_dns_service_request() {
  Services.dns.clearCache(true);
  override.clearOverrides();
  override.addIPOverride("aus5.mozilla.org", "N/A");
  override.addIPOverride("foo.example.com", "127.0.0.1");

  let text = await new Promise((resolve, reject) => {
    let request = new lazy.ServiceRequest({ mozAnon: true });
    request.open("GET", `https://aus5.mozilla.org/stuff`, true);
    request.channel.notificationCallbacks = new lazy.CertUtils.BadCertHandler(
      !lazy.AddonSettings.UPDATE_REQUIREBUILTINCERTS
    );
    request.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
    // Prevent the request from writing to cache.
    request.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
    request.overrideMimeType("text/plain");
    request.timeout = 5000;
    request.addEventListener("load", () => resolve(request.responseText));
    request.addEventListener("error", reject);
    request.addEventListener("timeout", reject);
    request.send(null);
  });
  equal(text, "Good stuff");
});

add_task(async function test_fallback_on_tls_failure() {
  Services.obs.notifyObservers(null, "net:cancel-all-connections");
  Services.dns.clearCache(true);

  override.clearOverrides();
  override.addIPOverride("aus5.mozilla.org", "127.0.0.1");
  override.addIPOverride("foo.example.com", "127.0.0.1");
  let { buf } = await openChannelPromise("https://aus5.mozilla.org/stuff");
  equal(buf, "Good stuff");
});

add_task(async function test_no_fallback_with_content_principal() {
  Services.obs.notifyObservers(null, "net:cancel-all-connections");
  Services.dns.clearCache(true);

  let { req } = await openChannelPromise("https://aus5.mozilla.org/stuff", {
    loadingPrincipal: Services.scriptSecurityManager.createContentPrincipal(
      Services.io.newURI("https://aus5.mozilla.org"),
      {}
    ),
    securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT,
    contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER,
    expectFailure: true,
  });
  // "NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_SECURITY, SSL_ERROR_BAD_CERT_DOMAIN)"
  equal(req.status, 0x805a2ff4);
});

// A top level navigation started from the browser UI or from a privileged page
// (the address bar, a link in about:addons) has a system triggering principal,
// but it is still an ordinary content load and must not be redirected to the
// fallback domain.
add_task(async function test_no_fallback_for_document_load() {
  Services.obs.notifyObservers(null, "net:cancel-all-connections");
  Services.dns.clearCache(true);

  override.clearOverrides();
  override.addIPOverride("aus5.mozilla.org", "N/A");
  override.addIPOverride("foo.example.com", "127.0.0.1");

  let { req } = await openChannelPromise("https://aus5.mozilla.org/stuff", {
    loadUsingSystemPrincipal: true,
    contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
    expectFailure: true,
  });
  equal(req.status, Cr.NS_ERROR_UNKNOWN_HOST);
});

add_task(async function test_fallback_on_connection_failure() {
  Services.obs.notifyObservers(null, "net:cancel-all-connections");
  Services.dns.clearCache(true);

  Services.prefs.setIntPref("network.http.connection-timeout", 1);
  override.clearOverrides();
  override.addIPOverride("aus5.mozilla.org", "10.99.99.99"); // Local IP that doesn't exist (hopefully).
  override.addIPOverride("foo.example.com", "127.0.0.1");

  await new Promise(resolve => do_timeout(100, resolve));
  let chanNotif = waitForNotificationPromise("httpchannel-fallback");
  let chanPromise = openChannelPromise("https://aus5.mozilla.org/stuff");
  let chan = await chanNotif;
  equal(chan.status, Cr.NS_ERROR_NET_TIMEOUT);
  let { buf } = await chanPromise;
  equal(buf, "Good stuff");
});
