// -*- 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";

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

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

var deleted = false;

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

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

async function testHelper(connectURL, expectedURL) {
  let win = await BrowserTestUtils.openNewBrowserWindow();

  await SpecialPowers.pushPrefEnv({
    set: [["security.default_personal_cert", "Ask Every Time"]],
  });

  BrowserTestUtils.startLoadingURIString(
    win.gBrowser.selectedBrowser,
    connectURL
  );

  await BrowserTestUtils.browserLoaded(
    win.gBrowser.selectedBrowser,
    false,
    expectedURL,
    true
  );
  let loadedURL = win.gBrowser.selectedBrowser.documentURI.spec;
  Assert.ok(
    loadedURL.startsWith(expectedURL),
    `Expected and actual URLs should match (got '${loadedURL}', expected '${expectedURL}')`
  );

  await win.close();

  // This clears the TLS session cache so we don't use a previously-established
  // ticket to connect and bypass selecting a client auth certificate in
  // subsequent tests.
  sdr.logout();
}

async function openRequireClientCert() {
  gClientAuthDialogService.chooseCertificateCalled = false;
  await testHelper(
    "https://requireclientcert.example.com:443",
    "https://requireclientcert.example.com/"
  );
}

async function openRequireClientCert2() {
  gClientAuthDialogService.chooseCertificateCalled = false;
  await testHelper(
    "https://requireclientcert-2.example.com:443",
    "https://requireclientcert-2.example.com/"
  );
}

// Mock implementation of nsIClientAuthRememberService
const gClientAuthRememberService = {
  forgetRememberedDecision(key) {
    deleted = true;
    Assert.equal(
      key,
      "exampleKey2",
      "Expected to get the same key that was passed in getDecisions()"
    );
  },

  getDecisions() {
    return [
      {
        asciiHost: "example.com",
        dbKey: cert.dbKey,
        entryKey: "exampleKey1",
      },
      {
        asciiHost: "example.org",
        dbKey: cert2.dbKey,
        entryKey: "exampleKey2",
      },
      {
        asciiHost: "example.test",
        dbKey: cert3.dbKey,
        entryKey: "exampleKey3",
      },
      {
        asciiHost: "unavailable.example.com",
        // This dbKey should not correspond to any real certificate. The first
        // 8 bytes have to be 0, followed by the lengths of the serial number
        // and issuer distinguished name, respectively, and then followed by
        // the bytes of the serial number and finally the encoded issuer
        // distinguished name. In this case, the serial number is a single 0
        // byte and the issuer distinguished name is a DER SEQUENCE of length 0
        // (the bytes 0x30 and 0).
        // See also the documentation in nsNSSCertificateDB::FindCertByDBKey.
        dbKey: "AAAAAAAAAAAAAAABAAAAAgAeAA==",
        entryKey: "exampleKey4",
      },
    ];
  },

  QueryInterface: ChromeUtils.generateQI(["nsIClientAuthRememberService"]),
};

const gClientAuthDialogService = {
  _chooseCertificateCalled: false,

  get chooseCertificateCalled() {
    return this._chooseCertificateCalled;
  },

  set chooseCertificateCalled(value) {
    this._chooseCertificateCalled = value;
  },

  chooseCertificate(hostname, certArray, loadContext, callback) {
    this.chooseCertificateCalled = true;
    callback.certificateChosen(certArray[0], true);
  },

  QueryInterface: ChromeUtils.generateQI([Ci.nsIClientAuthDialogService]),
};

add_task(async function testRememberedDecisionsUI() {
  cert = findCertByCommonName("Mochitest client");
  cert2 = await readCertificate("pgo-ca-all-usages.pem", ",,");
  cert3 = await readCertificate("client-cert-via-intermediate.pem", ",,");
  isnot(cert, null, "Should be able to find the test client cert");
  isnot(cert2, null, "Should be able to find pgo-ca-all-usages.pem");
  isnot(cert3, null, "Should be able to find client-cert-via-intermediate.pem");

  let clientAuthRememberServiceCID = MockRegistrar.register(
    "@mozilla.org/security/clientAuthRememberService;1",
    gClientAuthRememberService
  );

  let win = await openCertManager();

  let listItems = win.document
    .getElementById("rememberedList")
    .querySelectorAll("richlistitem");

  Assert.equal(
    listItems.length,
    4,
    "rememberedList has expected number of items"
  );

  let labels = win.document
    .getElementById("rememberedList")
    .querySelectorAll("label");

  Assert.equal(
    labels.length,
    12,
    "rememberedList has expected number of labels"
  );

  await BrowserTestUtils.waitForCondition(
    () => !!labels[10].textContent.length,
    "Localized label is populated"
  );

  let expectedHosts = [
    "example.com",
    "example.org",
    "example.test",
    "unavailable.example.com",
  ];
  let hosts = [
    labels[0].value,
    labels[3].value,
    labels[6].value,
    labels[9].value,
  ];
  let expectedNames = [
    cert.commonName,
    cert2.commonName,
    cert3.commonName,
    "(Unavailable)",
  ];
  let names = [
    labels[1].value,
    labels[4].value,
    labels[7].value,
    labels[10].textContent,
  ];
  let expectedSerialNumbers = [
    cert.serialNumber,
    cert2.serialNumber,
    cert3.serialNumber,
    "(Unavailable)",
  ];
  let serialNumbers = [
    labels[2].value,
    labels[5].value,
    labels[8].value,
    labels[11].textContent,
  ];

  for (let i = 0; i < listItems.length; i++) {
    Assert.equal(hosts[i], expectedHosts[i], "got expected asciiHost");
    Assert.equal(names[i], expectedNames[i], "got expected commonName");
    Assert.equal(
      serialNumbers[i],
      expectedSerialNumbers[i],
      "got expected serialNumber"
    );
  }

  win.document.getElementById("rememberedList").selectedIndex = 1;
  win.document.getElementById("remembered_deleteButton").click();

  Assert.ok(deleted, "Expected forgetRememberedDecision() to get called");

  win.document.getElementById("certmanager").acceptDialog();
  await BrowserTestUtils.windowClosed(win);

  MockRegistrar.unregister(clientAuthRememberServiceCID);
});

add_task(async function testDeletingRememberedDecisions() {
  let clientAuthDialogServiceCID = MockRegistrar.register(
    "@mozilla.org/security/ClientAuthDialogService;1",
    gClientAuthDialogService
  );
  let cars = Cc["@mozilla.org/security/clientAuthRememberService;1"].getService(
    Ci.nsIClientAuthRememberService
  );

  await openRequireClientCert();
  Assert.ok(
    gClientAuthDialogService.chooseCertificateCalled,
    "chooseCertificate should have been called if visiting 'requireclientcert.example.com' for the first time"
  );

  await openRequireClientCert();
  Assert.ok(
    !gClientAuthDialogService.chooseCertificateCalled,
    "chooseCertificate should not have been called if visiting 'requireclientcert.example.com' for the second time"
  );

  await openRequireClientCert2();
  Assert.ok(
    gClientAuthDialogService.chooseCertificateCalled,
    "chooseCertificate should have been called if visiting 'requireclientcert-2.example.com' for the first time"
  );

  let originAttributes = { privateBrowsingId: 0 };
  cars.deleteDecisionsByHost("requireclientcert.example.com", originAttributes);

  await openRequireClientCert();
  Assert.ok(
    gClientAuthDialogService.chooseCertificateCalled,
    "chooseCertificate should have been called after removing all remembered decisions for 'requireclientcert.example.com'"
  );

  await openRequireClientCert2();
  Assert.ok(
    !gClientAuthDialogService.chooseCertificateCalled,
    "chooseCertificate should not have been called if visiting 'requireclientcert-2.example.com' for the second time"
  );

  MockRegistrar.unregister(clientAuthDialogServiceCID);
});
