"use strict";

AddonTestUtils.init(this);
AddonTestUtils.overrideCertDB();
AddonTestUtils.createAppInfo(
  "xpcshell@tests.mozilla.org",
  "XPCShell",
  "1",
  "43"
);

// Necessary for the pac script to proxy localhost requests
Services.prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true);

// Pref is not builtin if direct failover is disabled in compile config.
ChromeUtils.defineLazyGetter(this, "directFailoverDisabled", () => {
  return (
    Services.prefs.getPrefType("network.proxy.failover_direct") ==
    Ci.nsIPrefBranch.PREF_INVALID
  );
});

const { ServiceRequest } = ChromeUtils.importESModule(
  "resource://gre/modules/ServiceRequest.sys.mjs"
);

// Prevent the request from reaching out to the network.
const { HttpServer } = ChromeUtils.importESModule(
  "resource://testing-common/httpd.sys.mjs"
);

// No hosts defined to avoid the default proxy filter setup.
const nonProxiedServer = createHttpServer();
nonProxiedServer.registerPathHandler("/", (request, response) => {
  response.setStatusLine(request.httpVersion, 200, "OK");
  response.write("ok!");
});
const { primaryHost, primaryPort } = nonProxiedServer.identity;

function getProxyData(channel) {
  if (!(channel instanceof Ci.nsIProxiedChannel) || !channel.proxyInfo) {
    return;
  }
  let { type, host, port, sourceId } = channel.proxyInfo;
  return { type, host, port, sourceId };
}

// Get a free port with no listener to use in the proxyinfo.
function getBadProxyPort() {
  let server = new HttpServer();
  server.start(-1);
  const badPort = server.identity.primaryPort;
  server.stop();
  return badPort;
}

function xhr(url, options = { beConservative: true, bypassProxy: false }) {
  return new Promise((resolve, reject) => {
    let req = new XMLHttpRequest();
    req.open("GET", `${url}?t=${Math.random()}`);
    req.channel.QueryInterface(Ci.nsIHttpChannelInternal).beConservative =
      options.beConservative;
    req.channel.QueryInterface(Ci.nsIHttpChannelInternal).bypassProxy =
      options.bypassProxy;
    req.onload = () => {
      resolve({ text: req.responseText, proxy: getProxyData(req.channel) });
    };
    req.onerror = () => {
      reject({ status: req.status, proxy: getProxyData(req.channel) });
    };
    req.send();
  });
}

// Same as the above xhr call, but ServiceRequest is always beConservative.
// This is here to specifically test bypassProxy with ServiceRequest.
function serviceRequest(url, options = { bypassProxy: false }) {
  return new Promise((resolve, reject) => {
    let req = new ServiceRequest();
    req.open("GET", `${url}?t=${Math.random()}`, options);
    req.onload = () => {
      resolve({ text: req.responseText, proxy: getProxyData(req.channel) });
    };
    req.onerror = () => {
      reject({ status: req.status, proxy: getProxyData(req.channel) });
    };
    req.send();
  });
}

add_task(async function setup() {
  await AddonTestUtils.promiseStartupManager();
});

async function getProxyExtension(proxyDetails) {
  async function background(proxyDetails) {
    browser.proxy.onRequest.addListener(
      () => {
        return proxyDetails;
      },
      { urls: ["<all_urls>"] }
    );

    browser.test.sendMessage("proxied");
  }
  let extensionData = {
    manifest: {
      permissions: ["proxy", "<all_urls>"],
    },
    background: `(${background})(${JSON.stringify(proxyDetails)})`,
    incognitoOverride: "spanning",
    useAddonManager: "temporary",
  };

  let extension = ExtensionTestUtils.loadExtension(extensionData);
  await extension.startup();
  await extension.awaitMessage("proxied");
  return extension;
}

add_task(async function test_failover_content_direct() {
  // load a content page for fetch and non-system principal, expect
  // failover to direct will work.
  const proxyDetails = [
    { type: "http", host: "127.0.0.1", port: getBadProxyPort() },
    { type: "direct" },
  ];

  // We need to load the content page before loading the proxy extension
  // to ensure that we have a valid content page to run fetch from.
  let contentUrl = `http://${primaryHost}:${primaryPort}/`;
  let page = await ExtensionTestUtils.loadContentPage(contentUrl);

  let extension = await getProxyExtension(proxyDetails);

  await ExtensionTestUtils.fetch(contentUrl, `${contentUrl}?t=${Math.random()}`)
    .then(text => {
      equal(text, "ok!", "fetch completed");
    })
    .catch(() => {
      ok(false, "fetch failed");
    });

  await extension.unload();
  await page.close();
});

add_task(
  { skip_if: () => directFailoverDisabled },
  async function test_failover_content() {
    // load a content page for fetch and non-system principal, expect
    // no failover
    const proxyDetails = [
      { type: "http", host: "127.0.0.1", port: getBadProxyPort() },
    ];

    // We need to load the content page before loading the proxy extension
    // to ensure that we have a valid content page to run fetch from.
    let contentUrl = `http://${primaryHost}:${primaryPort}/`;
    let page = await ExtensionTestUtils.loadContentPage(contentUrl);

    let extension = await getProxyExtension(proxyDetails);

    await ExtensionTestUtils.fetch(
      contentUrl,
      `${contentUrl}?t=${Math.random()}`
    )
      .then(() => {
        ok(false, "xhr unexpectedly completed");
      })
      .catch(e => {
        equal(
          e.message,
          "NetworkError when attempting to fetch resource.",
          "fetch failed"
        );
      });

    await extension.unload();
    await page.close();
  }
);

add_task(
  { skip_if: () => directFailoverDisabled },
  async function test_failover_system() {
    const proxyDetails = [
      { type: "http", host: "127.0.0.1", port: getBadProxyPort() },
      { type: "http", host: "127.0.0.1", port: getBadProxyPort() },
    ];

    let extension = await getProxyExtension(proxyDetails);

    await xhr(`http://${primaryHost}:${primaryPort}/`)
      .then(req => {
        equal(req.proxy.type, "direct", "proxy failover to direct");
        equal(req.text, "ok!", "xhr completed");
      })
      .catch(() => {
        ok(false, "xhr failed");
      });

    await extension.unload();
  }
);

add_task(
  {
    skip_if: () =>
      AppConstants.platform === "android" || directFailoverDisabled,
  },
  async function test_failover_pac() {
    const badPort = getBadProxyPort();

    async function background(badPort) {
      let pac = `function FindProxyForURL(url, host) { return "PROXY 127.0.0.1:${badPort}"; }`;
      let proxySettings = {
        proxyType: "autoConfig",
        autoConfigUrl: `data:application/x-ns-proxy-autoconfig;charset=utf-8,${encodeURIComponent(
          pac
        )}`,
      };

      await browser.proxy.settings.set({ value: proxySettings });
      browser.test.sendMessage("proxied");
    }
    let extensionData = {
      manifest: {
        permissions: ["proxy", "<all_urls>"],
      },
      background: `(${background})(${badPort})`,
      incognitoOverride: "spanning",
      useAddonManager: "temporary",
    };

    let extension = ExtensionTestUtils.loadExtension(extensionData);
    await extension.startup();
    await extension.awaitMessage("proxied");
    equal(
      Services.prefs.getIntPref("network.proxy.type"),
      2,
      "autoconfig type set"
    );
    ok(
      Services.prefs.getStringPref("network.proxy.autoconfig_url"),
      "autoconfig url set"
    );

    await xhr(`http://${primaryHost}:${primaryPort}/`)
      .then(req => {
        equal(req.proxy.type, "direct", "proxy failover to direct");
        equal(req.text, "ok!", "xhr completed");
      })
      .catch(() => {
        ok(false, "xhr failed");
      });

    await extension.unload();
  }
);

add_task(async function test_bypass_proxy() {
  const proxyDetails = [
    { type: "http", host: "127.0.0.1", port: getBadProxyPort() },
  ];

  let extension = await getProxyExtension(proxyDetails);

  await xhr(`http://${primaryHost}:${primaryPort}/`, { bypassProxy: true })
    .then(req => {
      equal(req.proxy, undefined, "no proxy used");
      ok(true, "xhr completed");
    })
    .catch(req => {
      equal(req.proxy, undefined, "no proxy used");
      ok(false, "xhr error");
    });

  await extension.unload();
});

add_task(async function test_bypass_proxy() {
  const proxyDetails = [
    { type: "http", host: "127.0.0.1", port: getBadProxyPort() },
  ];

  let extension = await getProxyExtension(proxyDetails);

  await serviceRequest(`http://${primaryHost}:${primaryPort}/`, {
    bypassProxy: true,
  })
    .then(req => {
      equal(req.proxy, undefined, "no proxy used");
      ok(true, "xhr completed");
    })
    .catch(req => {
      equal(req.proxy, undefined, "no proxy used");
      ok(false, "xhr error");
    });

  await extension.unload();
});

add_task(async function test_failover_system_off() {
  // Test test failover failures, uncomment and set pref to false
  Services.prefs.setBoolPref("network.proxy.failover_direct", false);

  const proxyDetails = [
    { type: "http", host: "127.0.0.1", port: getBadProxyPort() },
  ];

  let extension = await getProxyExtension(proxyDetails);

  await xhr(`http://${primaryHost}:${primaryPort}/`)
    .then(req => {
      equal(req.proxy.sourceId, extension.id, "extension matches");
      ok(false, "xhr completed");
    })
    .catch(req => {
      equal(req.proxy.sourceId, extension.id, "extension matches");
      equal(req.status, 0, "xhr failed");
    });

  await extension.unload();
});
