// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
// Any copyright is dedicated to the Public Domain.
// http://creativecommons.org/publicdomain/zero/1.0/
"use strict";

// Tests that the client authentication certificate chooser correctly displays
// provided information and correctly returns user input.

const TEST_HOSTNAME = "Test Hostname";
const TEST_ORG = "Test Org";
const TEST_ISSUER_ORG = "Test Issuer Org";
const TEST_PORT = 123;

var certDB = Cc["@mozilla.org/security/x509certdb;1"].getService(
  Ci.nsIX509CertDB
);

ChromeUtils.defineESModuleGetters(this, {
  PromptUtils: "resource://gre/modules/PromptUtils.sys.mjs",
});

/**
 * Test certificate (i.e. build/pgo/certs/mochitest.client).
 *
 * @type {nsIX509Cert}
 */
var cert;

/**
 * Opens the client auth cert chooser dialog.
 *
 * @param {nsIX509Cert} cert The cert to pass to the dialog for display.
 * @returns {Promise}
 *          A promise that resolves when the dialog has finished loading, with
 *          an array consisting of:
 *            1. The window of the opened dialog.
 *            2. The return value nsIWritablePropertyBag2 passed to the dialog.
 */
function openClientAuthDialog(cert) {
  let certArray = [cert];
  let retVals = { cert: undefined, rememberDuration: undefined };
  let args = PromptUtils.objectToPropBag({
    hostname: TEST_HOSTNAME,
    certArray,
    retVals,
  });
  let win = window.openDialog(
    "chrome://pippki/content/clientauthask.xhtml",
    "",
    "",
    args
  );
  return TestUtils.topicObserved("cert-dialog-loaded").then(() => {
    return { win, retVals };
  });
}

/**
 * Checks that the contents of the given cert chooser dialog match the details
 * of build/pgo/certs/mochitest.client.
 *
 * @param {window} win The cert chooser window.
 * @param {string} notBefore
 *        The formatted notBefore date of mochitest.client.
 * @param {string} notAfter
 *        The formatted notAfter date of mochitest.client.
 */
async function checkDialogContents(win, notBefore, notAfter) {
  await TestUtils.waitForCondition(() => {
    return win.document
      .getElementById("clientAuthSiteIdentification")
      .textContent.includes(`${TEST_HOSTNAME}`);
  });
  let nicknames = win.document.getElementById("nicknames");
  await TestUtils.waitForCondition(() => {
    return nicknames.label == "Mochitest client [03]";
  });
  await TestUtils.waitForCondition(() => {
    return nicknames.itemCount == 1;
  });
  let subject = win.document.getElementById("clientAuthCertDetailsIssuedTo");
  await TestUtils.waitForCondition(() => {
    return subject.textContent == "Issued to: CN=Mochitest client";
  });
  let serialNum = win.document.getElementById(
    "clientAuthCertDetailsSerialNumber"
  );
  await TestUtils.waitForCondition(() => {
    return serialNum.textContent == "Serial number: 03";
  });
  let validity = win.document.getElementById(
    "clientAuthCertDetailsValidityPeriod"
  );
  await TestUtils.waitForCondition(() => {
    return validity.textContent == `Valid from ${notBefore} to ${notAfter}`;
  });
  let issuer = win.document.getElementById("clientAuthCertDetailsIssuedBy");
  await TestUtils.waitForCondition(() => {
    return (
      issuer.textContent ==
      "Issued by: OU=Profile Guided Optimization,O=Mozilla Testing,CN=Temporary Certificate Authority"
    );
  });
  let tokenName = win.document.getElementById("clientAuthCertDetailsStoredOn");
  await TestUtils.waitForCondition(() => {
    return tokenName.textContent == "Stored on: Software Security Device";
  });
}

function findCertByCommonName(commonName) {
  for (let cert of certDB.getCerts()) {
    if (cert.commonName == commonName) {
      return cert;
    }
  }
  return null;
}

add_setup(async function () {
  cert = findCertByCommonName("Mochitest client");
  isnot(cert, null, "Should be able to find the test client cert");
});

// Test that the contents of the dialog correspond to the details of the
// provided cert.
add_task(async function testContents() {
  const formatter = new Intl.DateTimeFormat(undefined, {
    dateStyle: "medium",
    timeStyle: "long",
  });
  let { win } = await openClientAuthDialog(cert);
  await checkDialogContents(
    win,
    formatter.format(new Date(cert.validity.notBefore / 1000)),
    formatter.format(new Date(cert.validity.notAfter / 1000))
  );
  await BrowserTestUtils.closeWindow(win);
});

// Test that the right values are returned when the dialog is accepted.
add_task(async function testAcceptDialogReturnValues() {
  let { win, retVals } = await openClientAuthDialog(cert);
  win.document.getElementById("rememberSetting").value = 1;
  info("Accepting dialog");
  win.document.getElementById("certAuthAsk").acceptDialog();
  await BrowserTestUtils.windowClosed(win);

  is(retVals.cert, cert, "cert should be returned as chosen cert");
  is(
    retVals.rememberDuration,
    1,
    "Return value should signal 'Remember persistently' option was selected"
  );
});

// Test that the right values are returned when the dialog is canceled.
add_task(async function testCancelDialogReturnValues() {
  let { win, retVals } = await openClientAuthDialog(cert);
  win.document.getElementById("rememberSetting").value = 0;
  info("Canceling dialog");
  win.document.getElementById("certAuthAsk").cancelDialog();
  await BrowserTestUtils.windowClosed(win);

  ok(
    !retVals.cert,
    "Return value should signal user did not choose a certificate"
  );
  is(
    retVals.rememberDuration,
    0,
    "Return value should signal 'Remember once' was selected"
  );
});

async function testDefaultRememberSettingFromPreference(
  preferenceValue,
  expectedSetting
) {
  Services.prefs.setIntPref(
    "security.client_auth_certificate_default_remember_setting",
    preferenceValue
  );
  let { win } = await openClientAuthDialog(cert);
  is(
    win.document.getElementById("rememberSetting").value,
    expectedSetting,
    "remember setting should default to value specified by preference"
  );
  info("Canceling dialog");
  win.document.getElementById("certAuthAsk").cancelDialog();
  await BrowserTestUtils.windowClosed(win);
}

// Test that the preference to set the default remember duration works.
add_task(async function testDefaultRememberSettingFromAllPreferenceValues() {
  await testDefaultRememberSettingFromPreference(0, "0");
  await testDefaultRememberSettingFromPreference(1, "1");
  await testDefaultRememberSettingFromPreference(2, "2");
  // Setting the preference to an invalid value will result in the default of "2".
  await testDefaultRememberSettingFromPreference(3, "2");
});
