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

"use strict";

// We explicitly need HTTP URLs in this test
/* eslint-disable @microsoft/sdl/no-insecure-url */

requestLongerTimeout(3);

ChromeUtils.defineLazyGetter(this, "UrlbarTestUtils", () => {
  const { UrlbarTestUtils: module } = ChromeUtils.importESModule(
    "resource://testing-common/UrlbarTestUtils.sys.mjs"
  );
  module.init(this);
  return module;
});

const TEST_PATH_HTTP = getRootDirectory(gTestPath).replace(
  "chrome://mochitests/content",
  "http://example.com"
);
const TEST_PATH_HTTPS = getRootDirectory(gTestPath).replace(
  "chrome://mochitests/content",
  "https://example.com"
);
const TEST_PATH_SCHEMELESS = getRootDirectory(gTestPath).replace(
  "chrome://mochitests/content",
  "example.com"
);

const HSTS_SITE = TEST_PATH_HTTPS + "file_https_telemetry_hsts.sjs";

const NO_HTTPS_SUPPORT_SITE = TEST_PATH_HTTP + "file_no_https_support.sjs";
const NO_HTTPS_SUPPORT_SITE_SCHEMELESS =
  TEST_PATH_SCHEMELESS + "file_no_https_support.sjs";

async function setPrefsAndResetFog(
  aHTTPSOnlyPref,
  aHTTPSFirstPref,
  aSchemeLessPref
) {
  Services.fog.testResetFOG();

  await SpecialPowers.pushPrefEnv({
    set: [
      ["dom.security.https_only_mode", aHTTPSOnlyPref],
      ["dom.security.https_first", aHTTPSFirstPref],
      ["dom.security.https_first_schemeless", aSchemeLessPref],
    ],
  });
}

function verifyGleanValues(aDescription, aExpected) {
  info(aDescription);

  let notInitialized = aExpected.notInitialized || null;
  let noUpgrade = aExpected.noUpgrade || null;
  let alreadyHTTPS = aExpected.alreadyHTTPS || null;
  let hsts = aExpected.hsts || null;
  let httpsOnlyUpgrade = aExpected.httpsOnlyUpgrade || null;
  let httpsOnlyUpgradeDowngrade = aExpected.httpsOnlyUpgradeDowngrade || null;
  let httpsFirstUpgrade = aExpected.httpsFirstUpgrade || null;
  let httpsFirstUpgradeDowngrade = aExpected.httpsFirstUpgradeDowngrade || null;
  let httpsFirstSchemelessUpgrade =
    aExpected.httpsFirstSchemelessUpgrade || null;
  let httpsFirstSchemelessUpgradeDowngrade =
    aExpected.httpsFirstSchemelessUpgradeDowngrade || null;
  let cspUpgradeInsecureRequests = aExpected.cspUpgradeInsecureRequests || null;
  let httpsRR = aExpected.httpsRR || null;
  let webExtensionUpgrade = aExpected.webExtensionUpgrade || null;
  let upgradeException = aExpected.upgradeException || null;

  let glean = Glean.networking.httpToHttpsUpgradeReason;
  is(
    glean.not_initialized.testGetValue(),
    notInitialized,
    "verify not_initialized"
  );
  is(glean.no_upgrade.testGetValue(), noUpgrade, "verify no_upgrade");
  is(glean.already_https.testGetValue(), alreadyHTTPS, "verify already_https");
  is(glean.hsts.testGetValue(), hsts, "verify hsts");
  is(
    glean.https_only_upgrade.testGetValue(),
    httpsOnlyUpgrade,
    "verify https_only_upgrade"
  );
  is(
    glean.https_only_upgrade_downgrade.testGetValue(),
    httpsOnlyUpgradeDowngrade,
    "verify https_only_upgrade_downgrade"
  );
  is(
    glean.https_first_upgrade.testGetValue(),
    httpsFirstUpgrade,
    "verify https_first_upgrade"
  );
  is(
    glean.https_first_upgrade_downgrade.testGetValue(),
    httpsFirstUpgradeDowngrade,
    "verify https_first_upgrade_downgrade"
  );
  is(
    glean.https_first_schemeless_upgrade.testGetValue(),
    httpsFirstSchemelessUpgrade,
    "verify https_first_schemeless_upgrade"
  );
  is(
    glean.https_first_schemeless_upgrade_downgrade.testGetValue(),
    httpsFirstSchemelessUpgradeDowngrade,
    "verify https_first_schemeless_upgrade_downgrade"
  );
  is(
    glean.csp_uir.testGetValue(),
    cspUpgradeInsecureRequests,
    "verify csp_uir"
  );
  is(glean.https_rr.testGetValue(), httpsRR, "verify https_rr");
  is(
    glean.web_extension_upgrade.testGetValue(),
    webExtensionUpgrade,
    "verify web_extension_upgrade"
  );
  is(
    glean.upgrade_exception.testGetValue(),
    upgradeException,
    "verify upgrade_exception"
  );
}

async function runUpgradeTest(aURI, aDesc, aAssertURLStartsWith) {
  await BrowserTestUtils.withNewTab("about:blank", async function (browser) {
    const loaded = BrowserTestUtils.browserLoaded(browser, false, null, true);
    BrowserTestUtils.startLoadingURIString(browser, aURI);
    await loaded;

    await SpecialPowers.spawn(
      browser,
      [aDesc, aAssertURLStartsWith],
      async function (aDesc, aAssertURLStartsWith) {
        ok(
          content.document.location.href.startsWith(aAssertURLStartsWith),
          aDesc
        );
      }
    );
    await SpecialPowers.removePermission("https-only-load-insecure", aURI);
  });
}

async function runSchemelessTest(aURI, aDesc, aAssertURLStartsWith) {
  await BrowserTestUtils.withNewTab("about:blank", async function (browser) {
    const loaded = BrowserTestUtils.browserLoaded(browser, false, null, true);
    await UrlbarTestUtils.promiseAutocompleteResultPopup({
      window,
      value: aURI,
    });
    EventUtils.synthesizeKey("KEY_Enter", { ctrlKey: true });
    await loaded;
    await SpecialPowers.spawn(
      browser,
      [aDesc, aAssertURLStartsWith],
      async function (aDesc, aAssertURLStartsWith) {
        ok(
          content.document.location.href.startsWith(aAssertURLStartsWith),
          aDesc
        );
      }
    );
    // we can't pass a schemeless uri to removePermission
    let uri = "https://" + aURI;
    await SpecialPowers.removePermission("https-only-load-insecure", uri);
  });
}

add_task(async function () {
  info("(0) exempt loopback addresses");

  await setPrefsAndResetFog(
    false /* aHTTPSOnlyPref */,
    false /* aHTTPSFirstPref */,
    false /* aSchemeLessPref */
  );

  // the request to localhost will actually fail in our test infra,
  // though the telemetry would be recorded.
  await runUpgradeTest(
    "https://localhost",
    "(0) exempt loopback addresses",
    "https://"
  );
  verifyGleanValues("(0) exempt loopback addresses", {});
});

add_task(async function () {
  info("(1) no upgrade test");

  await setPrefsAndResetFog(
    false /* aHTTPSOnlyPref */,
    false /* aHTTPSFirstPref */,
    false /* aSchemeLessPref */
  );

  await runUpgradeTest(
    "http://example.com?test1",
    "(1) no upgrade test",
    "http://"
  );
  verifyGleanValues("(1) no upgrade test", { noUpgrade: 1 });
});

add_task(async function () {
  info("(2) already https test");

  await setPrefsAndResetFog(
    false /* aHTTPSOnlyPref */,
    false /* aHTTPSFirstPref */,
    false /* aSchemeLessPref */
  );

  await runUpgradeTest(
    "https://example.com?test2",
    "(2) already https test",
    "https://"
  );

  verifyGleanValues("(2) already https test", { alreadyHTTPS: 1 });
});

add_task(async function () {
  info("(2b) already https test all prefs true");

  await setPrefsAndResetFog(
    true /* aHTTPSOnlyPref */,
    true /* aHTTPSFirstPref */,
    true /* aSchemeLessPref */
  );

  await runUpgradeTest(
    "https://example.com?test2b",
    "(2b) already https test all prefs true",
    "https://"
  );

  verifyGleanValues("(2b) already https test all prefs true", {
    alreadyHTTPS: 1,
  });
});

add_task(async function () {
  info("(3) hsts");

  await setPrefsAndResetFog(
    false /* aHTTPSOnlyPref */,
    false /* aHTTPSFirstPref */,
    false /* aSchemeLessPref */
  );

  // we need to setup hsts first
  await BrowserTestUtils.withNewTab("about:blank", async function (browser) {
    const loaded = BrowserTestUtils.browserLoaded(browser, false, null, true);
    BrowserTestUtils.startLoadingURIString(browser, HSTS_SITE);
    await loaded;
  });

  // now we reset glean again
  Services.fog.testResetFOG();

  await runUpgradeTest("http://example.com?test3", "(3) hsts", "https://");

  verifyGleanValues("(3) hsts", { hsts: 1 });

  // finally we need to reset hsts
  await BrowserTestUtils.withNewTab("about:blank", async function (browser) {
    const loaded = BrowserTestUtils.browserLoaded(browser, false, null, true);
    BrowserTestUtils.startLoadingURIString(browser, HSTS_SITE + "?reset");
    await loaded;
  });

  info("(3b) hsts with all prefs true");

  await setPrefsAndResetFog(
    true /* aHTTPSOnlyPref */,
    true /* aHTTPSFirstPref */,
    true /* aSchemeLessPref */
  );

  // we need to setup hsts first
  await BrowserTestUtils.withNewTab("about:blank", async function (browser) {
    const loaded = BrowserTestUtils.browserLoaded(browser, false, null, true);
    BrowserTestUtils.startLoadingURIString(browser, HSTS_SITE);
    await loaded;
  });

  // now we reset glean again
  Services.fog.testResetFOG();

  await runUpgradeTest(
    "http://example.com?test3b",
    "(3b) hsts with all prefs true",
    "https://"
  );

  verifyGleanValues("(3b) hsts with all prefs true", { hsts: 1 });

  // finally we need to reset the hsts host
  await BrowserTestUtils.withNewTab("about:blank", async function (browser) {
    const loaded = BrowserTestUtils.browserLoaded(browser, false, null, true);
    BrowserTestUtils.startLoadingURIString(browser, HSTS_SITE + "?reset");
    await loaded;
  });
});

add_task(async function () {
  info("(4) https-only upgrade");

  await setPrefsAndResetFog(
    true /* aHTTPSOnlyPref */,
    false /* aHTTPSFirstPref */,
    false /* aSchemeLessPref */
  );

  await runUpgradeTest(
    "http://example.com?test4",
    "(4) https-only upgrade",
    "https://"
  );

  verifyGleanValues("(4) https-only upgrade", { httpsOnlyUpgrade: 1 });

  info("(4b) https-only upgrade downgrade");

  await setPrefsAndResetFog(
    true /* aHTTPSOnlyPref */,
    false /* aHTTPSFirstPref */,
    false /* aSchemeLessPref */
  );

  // We specifically want a insecure url here that will fail to upgrade
  let uri = "http://untrusted.example.com:80";
  let desc = "(4b) https-only upgrade downgrade";
  let assertErrorPageStartsWith = "https://";
  let assertDowngradedURLStartsWith = "http://";

  await BrowserTestUtils.withNewTab("about:blank", async function (browser) {
    const loaded = BrowserTestUtils.waitForErrorPage(browser);
    BrowserTestUtils.startLoadingURIString(browser, uri);
    await loaded;

    await SpecialPowers.spawn(
      browser,
      [desc, assertErrorPageStartsWith],
      async function (desc, assertErrorPageStartsWith) {
        ok(
          content.document.location.href.startsWith(assertErrorPageStartsWith),
          desc
        );
      }
    );

    const downGradeLoaded = BrowserTestUtils.browserLoaded(
      browser,
      false,
      null,
      true
    );

    // click the 'continue to insecure page' button
    await SpecialPowers.spawn(browser, [], async function () {
      let openInsecureButton = content.document.getElementById("openInsecure");
      Assert.notEqual(
        openInsecureButton,
        null,
        "openInsecureButton should exist."
      );
      info("Waiting for openInsecureButton to be enabled.");
      function callback() {
        if (!openInsecureButton.classList.contains("disabled")) {
          observer.disconnect();
          content.requestAnimationFrame(() => {
            content.requestAnimationFrame(() => {
              openInsecureButton.click();
            });
          });
        }
      }
      const observer = new content.MutationObserver(callback);
      observer.observe(openInsecureButton, { attributeFilter: ["class"] });
      callback();
    });

    await downGradeLoaded;

    await SpecialPowers.spawn(
      browser,
      [desc, assertDowngradedURLStartsWith],
      async function (desc, assertDowngradedURLStartsWith) {
        ok(
          content.document.location.href.startsWith(
            assertDowngradedURLStartsWith
          ),
          desc
        );
      }
    );
    await SpecialPowers.removePermission("https-only-load-insecure", uri);
  });

  verifyGleanValues("(4b) https-only upgrade downgrade", {
    httpsOnlyUpgrade: 1,
    httpsOnlyUpgradeDowngrade: 1,
  });
});

add_task(async function () {
  info("(5) https-first upgrade");

  await setPrefsAndResetFog(
    false /* aHTTPSOnlyPref */,
    true /* aHTTPSFirstPref */,
    false /* aSchemeLessPref */
  );

  await runUpgradeTest(
    "http://example.com?test5",
    "(5) https-first upgrade",
    "https://"
  );

  verifyGleanValues("(5) https-first upgrade", { httpsFirstUpgrade: 1 });

  info("(5b) https-first upgrade downgrade");

  await setPrefsAndResetFog(
    false /* aHTTPSOnlyPref */,
    true /* aHTTPSFirstPref */,
    false /* aSchemeLessPref */
  );

  await runUpgradeTest(
    NO_HTTPS_SUPPORT_SITE + "?test5b",
    "(5b) https-first upgrade downgrade",
    "http://"
  );

  verifyGleanValues("(5) https-first upgrade", {
    httpsFirstUpgrade: 1,
    httpsFirstUpgradeDowngrade: 1,
  });
});

add_task(async function () {
  info("(6) schemeless https-first upgrade");

  await setPrefsAndResetFog(
    false /* aHTTPSOnlyPref */,
    false /* aHTTPSFirstPref */,
    true /* aSchemeLessPref */
  );

  await runSchemelessTest(
    "example.com?test6",
    "(6) schemeless https-first upgrade",
    "https://"
  );

  verifyGleanValues("(6) schemeless https-first upgrade", {
    httpsFirstSchemelessUpgrade: 1,
  });

  info("(6b) schemeless https-first upgrade downgrade");

  await setPrefsAndResetFog(
    false /* aHTTPSOnlyPref */,
    false /* aHTTPSFirstPref */,
    true /* aSchemeLessPref */
  );

  await runSchemelessTest(
    NO_HTTPS_SUPPORT_SITE_SCHEMELESS + "?test6b",
    "(6) schemeless https-first upgrade downgrade",
    "http://"
  );

  verifyGleanValues("(6b) schemeless https-first upgrade downgrade", {
    httpsFirstSchemelessUpgrade: 1,
    httpsFirstSchemelessUpgradeDowngrade: 1,
  });
});

add_task(async function () {
  info("(7) https-rr upgrade");

  Services.fog.testResetFOG();

  await SpecialPowers.pushPrefEnv({
    set: [
      ["dom.security.https_only_mode", false],
      ["dom.security.https_first", false],
      ["dom.security.https_first_schemeless", false],
      ["network.dns.force_use_https_rr", true],
      ["network.dns.mock_HTTPS_RR_domain", "example.org"],
    ],
  });

  await runUpgradeTest(
    "http://example.org",
    "(7) https-rr upgrade",
    "https://"
  );

  verifyGleanValues("(7) https-rr upgrade", { httpsRR: 1 });
});

add_task(async function () {
  info("(8) upgrade/downgrade/reload");
  // This test performs an upgrade-downgrade and then reloads
  // the document which then triggers an upgrade_exception.

  await setPrefsAndResetFog(
    false /* aHTTPSOnlyPref */,
    true /* aHTTPSFirstPref */,
    false /* aSchemeLessPref */
  );

  await BrowserTestUtils.withNewTab("about:blank", async function (browser) {
    // First, perform the upgrade/downgrade
    const upgradeDowngradeLoaded = BrowserTestUtils.browserLoaded(
      browser,
      false,
      null,
      true
    );
    BrowserTestUtils.startLoadingURIString(
      browser,
      NO_HTTPS_SUPPORT_SITE + "?test8"
    );
    await upgradeDowngradeLoaded;

    await SpecialPowers.spawn(browser, [], async function () {
      ok(
        content.document.location.href.startsWith("http://"),
        "(8) upgrade/downgrade/reload"
      );
    });

    // Before reloading the doc we have to reset the fog
    Services.fog.testResetFOG();

    const reloadLoaded = BrowserTestUtils.browserLoaded(
      browser,
      false,
      null,
      true
    );

    await SpecialPowers.spawn(browser, [], async function () {
      content.location.reload();
    });

    await reloadLoaded;

    await SpecialPowers.spawn(browser, [], async function () {
      ok(
        content.document.location.href.startsWith("http://"),
        "(8) upgrade/downgrade/reload"
      );
    });
  });

  verifyGleanValues("(8) upgrade/downgrade/reload", { upgradeException: 1 });
});
