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

"use strict";

const SSL3_PAGE = "https://ssl3.example.com/";
const TLS10_PAGE = "https://tls1.example.com/";
const TLS12_PAGE = "https://tls12.example.com/";
const TRIPLEDES_PAGE = "https://3des.example.com/";

const lazy = {};

XPCOMUtils.defineLazyServiceGetter(
  lazy,
  "gDNSOverride",
  "@mozilla.org/network/native-dns-override;1",
  Ci.nsINativeDNSResolverOverride
);

const DES_PREF = "security.ssl3.deprecated.rsa_des_ede3_sha";

// This includes all the cipher suite prefs we have.
function resetPrefs() {
  Services.prefs.clearUserPref("security.tls.version.min");
  Services.prefs.clearUserPref("security.tls.version.max");
  Services.prefs.clearUserPref("security.tls.version.enable-deprecated");
  Services.prefs.clearUserPref("browser.fixup.alternate.enabled");
  Services.prefs.clearUserPref(DES_PREF);
}

const SSL_ERROR_BASE = -0x3000;
const SSL_ERROR_NO_CYPHER_OVERLAP = SSL_ERROR_BASE + 2;
const SSL_ERROR_PROTOCOL_VERSION_ALERT = SSL_ERROR_BASE + 98;

function nssErrorToNSErrorAsString(nssError) {
  let nssErrorsService = Cc["@mozilla.org/nss_errors_service;1"].getService(
    Ci.nsINSSErrorsService
  );
  return nssErrorsService.getXPCOMFromNSSError(nssError).toString();
}

async function resetTelemetry() {
  Services.telemetry.clearEvents();
  await TestUtils.waitForCondition(() => {
    let events = Services.telemetry.snapshotEvents(
      Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
      true
    ).content;
    return !events || !events.length;
  });
}

async function checkTelemetry(errorString, nssError) {
  let loadEvent = await TestUtils.waitForCondition(() => {
    let events = Services.telemetry.snapshotEvents(
      Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
      true
    ).content;
    return events?.find(e => e[1] == "security.ui.tlserror" && e[2] == "load");
  }, "recorded telemetry for the load");
  loadEvent.shift();
  Assert.deepEqual(loadEvent, [
    "security.ui.tlserror",
    "load",
    "abouttlserror",
    errorString,
    {
      is_frame: "false",
      channel_status: nssErrorToNSErrorAsString(nssError),
    },
  ]);
}

add_task(async function resetToDefaultConfig() {
  info(
    "Change TLS config to cause page load to fail, check that reset button is shown and that it works"
  );

  // Set ourselves up for a TLS error.
  Services.prefs.setIntPref("security.tls.version.min", 1); // TLS 1.0
  Services.prefs.setIntPref("security.tls.version.max", 1);

  await resetTelemetry();

  let browser;
  let pageLoaded;
  await BrowserTestUtils.openNewForegroundTab(
    gBrowser,
    () => {
      gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, TLS12_PAGE);
      browser = gBrowser.selectedBrowser;
      pageLoaded = BrowserTestUtils.waitForErrorPage(browser);
    },
    false
  );

  info("Loading and waiting for the net error");
  await pageLoaded;

  await checkTelemetry(
    "SSL_ERROR_PROTOCOL_VERSION_ALERT",
    SSL_ERROR_PROTOCOL_VERSION_ALERT
  );

  // Setup an observer for the target page.
  const finalLoadComplete = BrowserTestUtils.browserLoaded(
    browser,
    false,
    TLS12_PAGE
  );

  await SpecialPowers.spawn(browser, [], async function () {
    const doc = content.document;
    ok(
      doc.documentURI.startsWith("about:neterror"),
      "Should be showing error page"
    );

    const netErrorCard = await ContentTaskUtils.waitForCondition(
      () => content.document.querySelector("net-error-card")?.wrappedJSObject
    );
    netErrorCard.advancedButton.scrollIntoView(true);
    EventUtils.synthesizeMouseAtCenter(
      netErrorCard.advancedButton,
      {},
      content
    );
    await ContentTaskUtils.waitForCondition(
      () => ContentTaskUtils.isVisible(netErrorCard.prefResetButton),
      "prefResetButton is visible"
    );

    if (!Services.focus.focusedElement == netErrorCard.prefResetButton) {
      await ContentTaskUtils.waitForEvent(
        netErrorCard.prefResetButton,
        "focus"
      );
    }

    Assert.ok(true, "prefResetButton has focus");

    EventUtils.synthesizeMouseAtCenter(
      netErrorCard.prefResetButton,
      {},
      content
    );
  });

  info("Waiting for the page to load after the click");
  await finalLoadComplete;

  resetPrefs();
  BrowserTestUtils.removeTab(gBrowser.selectedTab);
});

add_task(async function checkLearnMoreLink() {
  info("Load an unsupported TLS page and check for a learn more link");

  // Set ourselves up for TLS error
  Services.prefs.setIntPref("security.tls.version.min", 3);
  Services.prefs.setIntPref("security.tls.version.max", 4);

  await resetTelemetry();

  let browser;
  let pageLoaded;
  await BrowserTestUtils.openNewForegroundTab(
    gBrowser,
    () => {
      gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, TLS10_PAGE);
      browser = gBrowser.selectedBrowser;
      pageLoaded = BrowserTestUtils.waitForErrorPage(browser);
    },
    false
  );

  info("Loading and waiting for the net error");
  await pageLoaded;

  await checkTelemetry(
    "SSL_ERROR_PROTOCOL_VERSION_ALERT",
    SSL_ERROR_PROTOCOL_VERSION_ALERT
  );

  const baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL");

  await SpecialPowers.spawn(browser, [baseURL], async function (_baseURL) {
    const doc = content.document;
    ok(
      doc.documentURI.startsWith("about:neterror"),
      "Should be showing error page"
    );

    const netErrorCard = await ContentTaskUtils.waitForCondition(
      () => content.document.querySelector("net-error-card")?.wrappedJSObject
    );
    netErrorCard.advancedButton.scrollIntoView(true);
    EventUtils.synthesizeMouseAtCenter(
      netErrorCard.advancedButton,
      {},
      content
    );
    const tlsVersionNotice = await ContentTaskUtils.waitForCondition(
      () => netErrorCard.tlsNotice
    );
    ok(
      ContentTaskUtils.isVisible(tlsVersionNotice),
      "TLS version notice is visible"
    );

    const learnMoreLink = netErrorCard.learnMoreLink;
    ok(ContentTaskUtils.isVisible(learnMoreLink), "Learn More link is visible");
    is(learnMoreLink.getAttribute("href"), _baseURL + "connection-not-secure");

    const titleEl = netErrorCard.errorTitle;
    const actualDataL10nID = titleEl.getAttribute("data-l10n-id");
    is(
      actualDataL10nID,
      "nssFailure2-title",
      "Correct error page title is set"
    );

    const errorCodeEl = netErrorCard.errorIntro.children[0];
    is(
      errorCodeEl.getAttribute("data-l10n-id"),
      "cert-error-ssl-connection-error",
      "Correct error code is set"
    );
  });

  resetPrefs();
  BrowserTestUtils.removeTab(gBrowser.selectedTab);
});

// When a user tries going to a host without a suffix
// and the term doesn't match a host and we are able to suggest a
// valid correction, the page should show the correction.
// e.g. http://example/example2 -> https://www.example.com/example2
add_task(async function checkDomainCorrectionReplacesLearnMoreLink() {
  await SpecialPowers.pushPrefEnv({
    set: [["browser.fixup.alternate.enabled", false]],
  });
  lazy.gDNSOverride.addIPOverride("www.example.com", "::1");

  info("Try loading a URI that should result in an error page");
  BrowserTestUtils.openNewForegroundTab(
    gBrowser,
    // eslint-disable-next-line @microsoft/sdl/no-insecure-url
    "http://example/example2/",
    false
  );

  info("Loading and waiting for the net error");
  let browser = gBrowser.selectedBrowser;
  let pageLoaded = BrowserTestUtils.waitForErrorPage(browser);
  await pageLoaded;

  const baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL");

  await SpecialPowers.spawn(browser, [baseURL], async function (_baseURL) {
    const doc = content.document;
    ok(
      doc.documentURI.startsWith("about:neterror"),
      "Should be showing error page"
    );

    const netErrorCard = await ContentTaskUtils.waitForCondition(
      () => doc.querySelector("net-error-card")?.wrappedJSObject
    );
    const errorNotice = netErrorCard.errorIntro;
    ok(ContentTaskUtils.isVisible(errorNotice), "Error text is visible");

    // Wait for the domain suggestion to be resolved and for the link href to be updated
    let link;
    await ContentTaskUtils.waitForCondition(() => {
      link = netErrorCard.learnMoreLink;
      return (
        link &&
        link.textContent != "" &&
        link.getAttribute("href") === "https://www.example.com/example2/"
      );
    }, "Helper link has been set to corrected domain");

    is(
      link.getAttribute("href"),
      "https://www.example.com/example2/",
      "Link points to corrected domain instead of SUMO page"
    );
  });

  lazy.gDNSOverride.clearHostOverride("www.example.com");
  resetPrefs();
  BrowserTestUtils.removeTab(gBrowser.selectedTab);
});

// When a user tries to access a non-existent domain and no domain
// suggestion is available, the learn more link should point to the
// SUMO support page for DNS troubleshooting.
add_task(async function checkDnsNotFoundLearnMoreLink() {
  info("Load a non-existent domain and check the learn more link");

  BrowserTestUtils.openNewForegroundTab(
    gBrowser,
    // eslint-disable-next-line @microsoft/sdl/no-insecure-url
    "http://thisdomaindoesnotexist123456.test/",
    false
  );
  let browser = gBrowser.selectedBrowser;
  let pageLoaded = BrowserTestUtils.waitForErrorPage(browser);
  await pageLoaded;

  const baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL");

  await SpecialPowers.spawn(browser, [baseURL], async function (_baseURL) {
    const doc = content.document;

    const netErrorCard = await ContentTaskUtils.waitForCondition(
      () => doc.querySelector("net-error-card")?.wrappedJSObject
    );

    let learnMoreLink;
    await ContentTaskUtils.waitForCondition(() => {
      learnMoreLink = netErrorCard.learnMoreLink;
      return learnMoreLink && learnMoreLink.textContent != "";
    }, "Learn more link has been set");

    ok(ContentTaskUtils.isVisible(learnMoreLink), "Learn More link is visible");

    is(
      learnMoreLink.getAttribute("href"),
      _baseURL + "server-not-found-connection-problem",
      "Link points to SUMO DNS troubleshooting page"
    );
  });

  BrowserTestUtils.removeTab(gBrowser.selectedTab);
});

// Test that ciphersuites that use 3DES (namely, TLS_RSA_WITH_3DES_EDE_CBC_SHA)
// can only be enabled when deprecated TLS is enabled.
add_task(async function onlyAllow3DESWithDeprecatedTLS() {
  await resetTelemetry();

  // By default, connecting to a server that only uses 3DES should fail.
  await BrowserTestUtils.withNewTab(
    { gBrowser, url: "about:blank" },
    async browser => {
      BrowserTestUtils.startLoadingURIString(browser, TRIPLEDES_PAGE);
      await BrowserTestUtils.waitForErrorPage(browser);
    }
  );

  await checkTelemetry(
    "SSL_ERROR_NO_CYPHER_OVERLAP",
    SSL_ERROR_NO_CYPHER_OVERLAP
  );

  // Enabling deprecated TLS should also enable 3DES.
  Services.prefs.setBoolPref("security.tls.version.enable-deprecated", true);
  await BrowserTestUtils.withNewTab(
    { gBrowser, url: "about:blank" },
    async browser => {
      BrowserTestUtils.startLoadingURIString(browser, TRIPLEDES_PAGE);
      await BrowserTestUtils.browserLoaded(browser, false, TRIPLEDES_PAGE);
    }
  );

  for (let feltPrivacy of [true, false]) {
    // 3DES can be disabled separately.
    Services.prefs.setBoolPref(DES_PREF, false);
    await SpecialPowers.pushPrefEnv({
      set: [["security.certerrors.felt-privacy-v1", feltPrivacy]],
    });

    await BrowserTestUtils.withNewTab(
      { gBrowser, url: "about:blank" },
      async browser => {
        BrowserTestUtils.startLoadingURIString(browser, TRIPLEDES_PAGE);
        await BrowserTestUtils.waitForErrorPage(browser);
        let prefWasReset = TestUtils.waitForPrefChange(DES_PREF);

        if (feltPrivacy) {
          await SpecialPowers.spawn(browser, [], async function () {
            const doc = content.document;
            const netErrorCard =
              doc.querySelector("net-error-card")?.wrappedJSObject;
            Assert.ok(netErrorCard, "netErrorCard is rendered.");

            netErrorCard.advancedButton.scrollIntoView();
            EventUtils.synthesizeMouseAtCenter(
              netErrorCard.advancedButton,
              {},
              content
            );
            await ContentTaskUtils.waitForCondition(
              () => ContentTaskUtils.isVisible(netErrorCard.advancedContainer),
              "Advanced container is visible"
            );

            const prefResetButton = netErrorCard.prefResetButton;
            Assert.ok(prefResetButton, "prefResetButton exists in the DOM.");
            netErrorCard.prefResetButton.scrollIntoView();
            await ContentTaskUtils.waitForCondition(
              () => ContentTaskUtils.isVisible(netErrorCard.prefResetButton),
              "Pref reset button is visible"
            );
            ok(
              ContentTaskUtils.isVisible(prefResetButton),
              "prefResetButton is visible"
            );

            prefResetButton.click();
          });
        } else {
          await SpecialPowers.spawn(browser, [], async function () {
            const doc = content.document;

            const prefResetButton = doc.getElementById("prefResetButton");
            Assert.ok(prefResetButton, "prefResetButton exists in the DOM.");

            await ContentTaskUtils.waitForCondition(
              () => ContentTaskUtils.isVisible(prefResetButton),
              "Pref reset button is visible"
            );
            ok(
              ContentTaskUtils.isVisible(prefResetButton),
              "prefResetButton is visible"
            );

            prefResetButton.click();
          });
        }

        await prefWasReset;
      }
    );
  }

  resetPrefs();
});

add_task(async function test_tryAgainButtonAutofocus() {
  Services.io.offline = true;
  registerCleanupFunction(() => {
    Services.io.offline = false;
  });

  let proxyPrefValue = SpecialPowers.getIntPref("network.proxy.type");
  await SpecialPowers.pushPrefEnv({
    set: [
      ["network.proxy.type", 0],
      ["browser.cache.disk.enable", false],
      ["browser.cache.memory.enable", false],
      ["security.certerrors.felt-privacy-v1", true],
    ],
  });

  await BrowserTestUtils.withNewTab("about:blank", async function (browser) {
    let netErrorLoaded = BrowserTestUtils.waitForErrorPage(browser);
    // eslint-disable-next-line @microsoft/sdl/no-insecure-url
    BrowserTestUtils.startLoadingURIString(browser, "http://example.com/");
    await netErrorLoaded;

    await SpecialPowers.pushPrefEnv({
      set: [["network.proxy.type", proxyPrefValue]],
    });

    await SpecialPowers.spawn(browser, [], async function () {
      const netErrorCard =
        content.document.querySelector("net-error-card").wrappedJSObject;
      await netErrorCard.getUpdateComplete();
      const tryAgainButton = netErrorCard.tryAgainButton;
      Assert.ok(tryAgainButton, "tryAgainButton exists");
      await tryAgainButton.updateComplete;
      Assert.equal(
        netErrorCard.renderRoot.activeElement,
        tryAgainButton,
        "tryAgainButton has focus"
      );
    });
  });

  await SpecialPowers.popPrefEnv();
});
