// Testing lib
import * as chai from "chai";
import * as chaiAsPromised from "chai-as-promised";

// Helpers
import { coerceToBase64Url, jsObjectToB64, str2ab, tools } from "../lib/main.js";

import * as h from "./helpers/fido2-helpers.js";

// Test subject
import { MdsCollection, MdsEntry } from "../lib/main.js";
import { mdsV3jwt } from "./fixtures/mdsV3.jwt.js";
chai.use(chaiAsPromised.default);
const { assert } = chai;

describe("MdsCollection", function() {
	it("is a function", function() {
		assert.isFunction(MdsCollection);
	});

	it("is a class", function() {
		const mdsCollection = new MdsCollection("test");
		assert.isObject(mdsCollection);
		assert.isFunction(mdsCollection.addToc);
		assert.isFunction(mdsCollection.addEntry);
		assert.isFunction(mdsCollection.validate);
		assert.isFunction(mdsCollection.findEntry);
		assert.strictEqual(mdsCollection.name, "test");
	});

	it("throws if no name specified in constructor", function() {
		assert.throws(function() {
			new MdsCollection();
		}, Error, "expected 'collectionName' to be non-empty string, got: undefined");
	});

	it("throws if name is empty string", function() {
		assert.throws(function() {
			new MdsCollection("");
		}, Error, "expected 'collectionName' to be non-empty string, got: ");
	});

	describe("addToc", function() {
		let mc;
		beforeEach(function() {
			mc = new MdsCollection("test");
		});

		it("returns a promise", async function() {
			const p = mc.addToc();
			assert.instanceOf(p, Promise);
			try {
				await p;
			} catch (_e) {
				// empty
			}
		});

		it("rejects if no jwk provided", function() {
			return assert.isRejected(mc.addToc(undefined), Error, "expected MDS TOC to be non-empty string");
		});

		it("rejects if TOC is empty string", function() {
			// bad toc
			const toc = "";
			return assert.isRejected(mc.addToc(toc), Error, "expected MDS TOC to be non-empty string");
		});

		it("rejects if TOC is junk string", function() {
			// bad toc
			const toc = "sL39APyTmisrjh11vghaqNfuruLQmCfR0c1ryKtaQ81jkEhNa5u9xLTnkibvXC9YpzBLFwWEZ3k9CR_sxzm_pWYbBOtKxeZu9z2GT8b6QW4iQvRlyumCT3oENx_8401r";
			return assert.isRejected(mc.addToc(toc), Error, "could not parse and validate MDS TOC: Invalid Token or Protected Header formatting");
		});

		it("rejects if TOC header is missing alg", function() {
			let jwtHeader = {
				// alg: "foo",
				typ: "JWT",
				x5c: [
					"sL39APyTmisrjh11vghaqNfuruLQmCfR0c1ryKtaQ81jkEhNa5u9xLTnkibvXC9YpzBLFwWEZ3k9CR_sxzm_pWYbBOtKxeZu9z2GT8b6QW4iQvRlyumCT3oENx_8401r",
					"sL39APyTmisrjh11vghaqNfuruLQmCfR0c1ryKtaQ81jkEhNa5u9xLTnkibvXC9YpzBLFwWEZ3k9CR_sxzm_pWYbBOtKxeZu9z2GT8b6QW4iQvRlyumCT3oENx_8401r",
				],
			};
			jwtHeader = coerceToBase64Url(str2ab(JSON.stringify(jwtHeader)), "JWT header");
			const jwtBody = "sL39APyTmisrjh11vghaqNfuruLQmCfR0c1ryKtaQ81jkEhNa5u9xLTnkibvXC9YpzBLFwWEZ3k9CR_sxzm_pWYbBOtKxeZu9z2GT8b6QW4iQvRlyumCT3oENx_8401r";
			const jwtSig = "sL39APyTmisrjh11vghaqNfuruLQmCfR0c1ryKtaQ81jkEhNa5u9xLTnkibvXC9YpzBLFwWEZ3k9CR_sxzm_pWYbBOtKxeZu9z2GT8b6QW4iQvRlyumCT3oENx_8401r";
			const toc = jwtHeader + "." + jwtBody + "." + jwtSig;
			return assert.isRejected(mc.addToc(toc), Error, "could not parse and validate MDS TOC: error parsing ASN.1");
		});

		it("rejects if TOC header is missing typ", function() {
			let jwtHeader = {
				alg: "foo",
				// typ: "JWT",
				x5c: [
					"sL39APyTmisrjh11vghaqNfuruLQmCfR0c1ryKtaQ81jkEhNa5u9xLTnkibvXC9YpzBLFwWEZ3k9CR_sxzm_pWYbBOtKxeZu9z2GT8b6QW4iQvRlyumCT3oENx_8401r",
					"sL39APyTmisrjh11vghaqNfuruLQmCfR0c1ryKtaQ81jkEhNa5u9xLTnkibvXC9YpzBLFwWEZ3k9CR_sxzm_pWYbBOtKxeZu9z2GT8b6QW4iQvRlyumCT3oENx_8401r",
				],
			};
			jwtHeader = coerceToBase64Url(str2ab(JSON.stringify(jwtHeader)), "JWT header");
			const jwtBody = "sL39APyTmisrjh11vghaqNfuruLQmCfR0c1ryKtaQ81jkEhNa5u9xLTnkibvXC9YpzBLFwWEZ3k9CR_sxzm_pWYbBOtKxeZu9z2GT8b6QW4iQvRlyumCT3oENx_8401r";
			const jwtSig = "sL39APyTmisrjh11vghaqNfuruLQmCfR0c1ryKtaQ81jkEhNa5u9xLTnkibvXC9YpzBLFwWEZ3k9CR_sxzm_pWYbBOtKxeZu9z2GT8b6QW4iQvRlyumCT3oENx_8401r";
			const toc = jwtHeader + "." + jwtBody + "." + jwtSig;
			return assert.isRejected(mc.addToc(toc), Error, "ould not parse and validate MDS TOC: error parsing ASN.1");
		});

		it("rejects if TOC header x5c only has one entry", function() {
			let jwtHeader = {
				alg: "foo",
				typ: "JWT",
				x5c: [
					"sL39APyTmisrjh11vghaqNfuruLQmCfR0c1ryKtaQ81jkEhNa5u9xLTnkibvXC9YpzBLFwWEZ3k9CR_sxzm_pWYbBOtKxeZu9z2GT8b6QW4iQvRlyumCT3oENx_8401r",
					// "sL39APyTmisrjh11vghaqNfuruLQmCfR0c1ryKtaQ81jkEhNa5u9xLTnkibvXC9YpzBLFwWEZ3k9CR_sxzm_pWYbBOtKxeZu9z2GT8b6QW4iQvRlyumCT3oENx_8401r"
				],
			};
			jwtHeader = coerceToBase64Url(str2ab(JSON.stringify(jwtHeader)), "JWT header");
			const jwtBody = "sL39APyTmisrjh11vghaqNfuruLQmCfR0c1ryKtaQ81jkEhNa5u9xLTnkibvXC9YpzBLFwWEZ3k9CR_sxzm_pWYbBOtKxeZu9z2GT8b6QW4iQvRlyumCT3oENx_8401r";
			const jwtSig = "sL39APyTmisrjh11vghaqNfuruLQmCfR0c1ryKtaQ81jkEhNa5u9xLTnkibvXC9YpzBLFwWEZ3k9CR_sxzm_pWYbBOtKxeZu9z2GT8b6QW4iQvRlyumCT3oENx_8401r";
			const toc = jwtHeader + "." + jwtBody + "." + jwtSig;
			return assert.isRejected(mc.addToc(toc), Error, "could not parse and validate MDS TOC: error parsing ASN.1");
		});

		it("rejects if TOC header x5c is missing", function() {
			let jwtHeader = {
				alg: "foo",
				typ: "JWT",
				// x5c: [
				//     "sL39APyTmisrjh11vghaqNfuruLQmCfR0c1ryKtaQ81jkEhNa5u9xLTnkibvXC9YpzBLFwWEZ3k9CR_sxzm_pWYbBOtKxeZu9z2GT8b6QW4iQvRlyumCT3oENx_8401r",
				//     "sL39APyTmisrjh11vghaqNfuruLQmCfR0c1ryKtaQ81jkEhNa5u9xLTnkibvXC9YpzBLFwWEZ3k9CR_sxzm_pWYbBOtKxeZu9z2GT8b6QW4iQvRlyumCT3oENx_8401r"
				// ]
			};
			jwtHeader = coerceToBase64Url(str2ab(JSON.stringify(jwtHeader)), "JWT header");
			const jwtBody = "sL39APyTmisrjh11vghaqNfuruLQmCfR0c1ryKtaQ81jkEhNa5u9xLTnkibvXC9YpzBLFwWEZ3k9CR_sxzm_pWYbBOtKxeZu9z2GT8b6QW4iQvRlyumCT3oENx_8401r";
			const jwtSig = "sL39APyTmisrjh11vghaqNfuruLQmCfR0c1ryKtaQ81jkEhNa5u9xLTnkibvXC9YpzBLFwWEZ3k9CR_sxzm_pWYbBOtKxeZu9z2GT8b6QW4iQvRlyumCT3oENx_8401r";
			const toc = jwtHeader + "." + jwtBody + "." + jwtSig;
			return assert.isRejected(mc.addToc(toc), Error, "could not parse and validate MDS TOC: getEmbeddedJwk: JWK not found in JWS.");
		});

		it("parses MDS1 TOC", async function() {
			return assert.isRejected(mc.addToc(
				h.mds.mds1TocJwt,
				h.mds.mdsRootCert,
				[
					h.mds.mdsRootCrl,
					h.mds.ca1Crl,
				],
			), Error, "No revocation values found for one of certificates: No valid CRLs found");
			/*assert.isObject(toc);
			assert.isString(toc.nextUpdate);
			assert.strictEqual(toc.nextUpdate, "2018-06-18");
			assert.isNumber(toc.no);
			assert.strictEqual(toc.no, 62);
			assert.isArray(toc.entries);
			assert.strictEqual(toc.entries.length, 66);
			assert.isString(toc.raw);
			assert.strictEqual(toc.raw, h.mds.mds1TocJwt);*/
		});

		it("parses MDS2 TOC", async function() {
			return assert.isRejected(mc.addToc(
				h.mds.mds2TocJwt,
				h.mds.mdsRootCert,
				[
					h.mds.mdsRootCrl,
					h.mds.ca1Crl,
				],
			),
			Error, "No revocation values found for one of certificates: No valid CRLs found");
			/*
			assert.isObject(toc);
			assert.isString(toc.nextUpdate);
			assert.strictEqual(toc.nextUpdate, "2018-06-18");
			assert.isString(toc.legalHeader);
			assert.isNumber(toc.no);
			assert.strictEqual(toc.no, 2);
			assert.isArray(toc.entries);
			assert.strictEqual(toc.entries.length, 7);
			assert.isString(toc.raw);*/
		});

		it("works with array of root certs", async function() {
			// MDS v2 is deprecated, certificate chain is no longer valid
			return assert.isRejected(
				mc.addToc(
					h.mds.mds1TocJwt,
					[
						h.mds.mdsRootCert,
					],
					[
						h.mds.mdsRootCrl,
						h.mds.ca1Crl,
					],
				)
				,Error, "No valid CRLs found");
			return; 
		});

		it("MDS1 works with default root cert", async function() {
			return await mc.addToc(h.mds.mds1TocJwt);
		});

		it("MDS2 works with default root cert", async function() {
			return await mc.addToc(h.mds.mds2TocJwt);
		});

		it("works with no CRLs", async function() {
			return await mc.addToc(
				h.mds.mds1TocJwt,
				h.mds.mdsRootCert,
			);
		});

		it("parses MDS2 TOC", async function() {
			return await mc.addToc(h.mds.mds2TocJwt);
		});

		it("throws on bad signature", function() {
			const tocParts = h.mds.mds2TocJwt.split(".");
			tocParts[2] = tocParts[2].toUpperCase(); // mess up the signature
			const toc = tocParts.join(".");
			return assert.isRejected(mc.addToc(toc), Error, "could not parse and validate MDS TOC: signature verification failed");
		});

		it("throws on bad cert chain", function() {
			return assert.isRejected(mc.addToc(h.mds.mds2TocJwt, [h.certs.yubicoRoot]), Error, "No valid certificate paths found");
		});
	});

	describe("getToc", function() {
		let mc;
		beforeEach(function() {
			mc = new MdsCollection("test");
		});

		it("starts as null", function() {
			const toc = mc.getToc();
			assert.isNull(mc.toc);
			assert.isNull(toc);
		});

		it("returns correct object for MDS1", async function() {
			assert.isNull(mc.toc);
			await mc.addToc(h.mds.mds1TocJwt);
			const toc = mc.getToc();
			assert.isObject(toc);
			assert.isString(toc.nextUpdate);
			assert.strictEqual(toc.nextUpdate, "2018-06-18");
			assert.isNumber(toc.no);
			assert.strictEqual(toc.no, 62);
			assert.isArray(toc.entries);
			assert.strictEqual(toc.entries.length, 66);
			assert.isString(toc.raw);
			assert.strictEqual(toc.raw, h.mds.mds1TocJwt);
		});

		it("returns correct object for MDS2", async function() {
			assert.isNull(mc.toc);
			await mc.addToc(h.mds.mds2TocJwt);
			const toc = mc.getToc();
			assert.isObject(toc);
			assert.isString(toc.nextUpdate);
			assert.strictEqual(toc.nextUpdate, "2018-06-18");
			assert.isString(toc.legalHeader);
			assert.isNumber(toc.no);
			assert.strictEqual(toc.no, 2);
			assert.isArray(toc.entries);
			assert.strictEqual(toc.entries.length, 7);
			assert.isString(toc.raw);
			assert.strictEqual(toc.raw, h.mds.mds2TocJwt);
		});
	});

	describe("addEntry", function() {
		let mc;
		beforeEach(function() {
			mc = new MdsCollection("test");
		});

		it("throws on invalid jwk", function() {
			assert.throws(function() {
				mc.addEntry();
			}, Error, "expected MDS entry to be non-empty string");
		});

		it("creates new MDS1 entry");

		it("creates new MDS1 UAF entry", function() {
			assert.strictEqual(mc.unvalidatedEntryList.size, 0);
			mc.addEntry(h.mds.mds1UafEntry);
			assert.strictEqual(mc.unvalidatedEntryList.size, 1);
		});

		it("creates new MDS1 U2F entry", function() {
			assert.strictEqual(mc.unvalidatedEntryList.size, 0);
			mc.addEntry(h.mds.mds1U2fEntry);
			assert.strictEqual(mc.unvalidatedEntryList.size, 1);
			assert.isTrue(mc.unvalidatedEntryList.has("923881fe2f214ee465484371aeb72e97f5a58e0a"));
		});

		it("creates new MDS2 UAF entry", function() {
			assert.strictEqual(mc.unvalidatedEntryList.size, 0);
			mc.addEntry(h.mds.mds2UafEntry);
			assert.strictEqual(mc.unvalidatedEntryList.size, 1);
			assert.isTrue(mc.unvalidatedEntryList.has("4e4e#4005"));
		});

		it("has raw");
		it("has correct ID");
	});

	describe("validate", function() {
		let mc;
		beforeEach(function() {
			mc = new MdsCollection("test");
		});

		it("throws if no TOC", function() {
			mc.addEntry(h.mds.mds2UafEntry);
			return assert.isRejected(
				mc.validate(),
				Error, 
				"add MDS TOC before attempting to validate MDS collection"
			);
		});

		it("throws if entry hash doesn't match TOC hash");

		it("throws if no entries", async function() {
			await mc.addToc(h.mds.mds2TocJwt);
			return assert.isRejected(
				mc.validate(),
				Error,
				"add MDS entries before attempting to validate MDS collection",
			);
		});

		it("adds MDS1 U2F entry", async function() {
			await mc.addToc(h.mds.mds1TocJwt);
			mc.addEntry(h.mds.mds1U2fEntry);
			await mc.validate();
			assert.strictEqual(mc.entryList.size, 1);
			assert.isTrue(mc.entryList.has("923881fe2f214ee465484371aeb72e97f5a58e0a"), "added entry 4e4e#4005");
			const entry = mc.entryList.get("923881fe2f214ee465484371aeb72e97f5a58e0a");
			assert.strictEqual(entry.protocolFamily, "u2f");
			assert.strictEqual(entry.authenticationAlgorithm, "ALG_SIGN_SECP256R1_ECDSA_SHA256_RAW");
			assert.strictEqual(entry.publicKeyAlgAndEncoding, "ALG_KEY_ECC_X962_RAW");
			assert.deepEqual(entry.attestationTypes, ["basic-full"]);
			assert.deepEqual(entry.userVerificationDetails, [[{ userVerification: ["fingerprint"] }]]);
			assert.strictEqual(entry.isSecondFactorOnly, true);
			assert.deepEqual(entry.statusReports, [
				{
					status: "FIDO_CERTIFIED",
					url: "",
					certificate: "",
					effectiveDate: "2017-11-28",
				},
			]);
		});

		it("adds MDS1 UAF entry", async function() {
			await mc.addToc(h.mds.mds1TocJwt);
			mc.addEntry(h.mds.mds1UafEntry);
			await mc.validate();
			assert.strictEqual(mc.entryList.size, 1);
			assert.isTrue(mc.entryList.has("0013#0001"), "added entry 4e4e#4005");
			const entry = mc.entryList.get("0013#0001");
			assert.strictEqual(entry.protocolFamily, "uaf");
			assert.strictEqual(entry.hash, "06LZxJ5mNuNZj48IZLV816bfp3A7GVtO2O-EeQ1pkTY=");
			assert.strictEqual(entry.aaid, "0013#0001");
			assert.strictEqual(entry.timeOfLastStatusChange, "2015-05-20");
		});

		it("adds MDS1 FIDO2 entry");

		it("adds MDS2 U2F entry");
		it("adds MDS2 FIDO2 entry");
		it("adds MDS2 UAF entry", async function() {
			await mc.addToc(h.mds.mds2TocJwt);
			mc.addEntry(h.mds.mds2UafEntry);
			await mc.validate();
			assert.strictEqual(mc.entryList.size, 1);
			assert.isTrue(mc.entryList.has("4e4e#4005"), "added entry 4e4e#4005");
			const entry = mc.entryList.get("4e4e#4005");

			// check that TOC data was copied to new entry:
			// url
			assert.strictEqual(entry.url, "https://mds2.fidoalliance.org/metadata/4e4e%234005");
			// timeOfLastStatusChange
			assert.strictEqual(entry.timeOfLastStatusChange, "2018-05-19");
			// hash
			assert.strictEqual(entry.hash, "iuRviMMnBrXnVrjI0TiacTzKqdG8VXTA6PUy4r7Sxhk=");
			// id
			assert.strictEqual(entry.aaid, "4e4e#4005");
			assert.isUndefined(entry.aaguid);
			assert.isUndefined(entry.attestationCertificateKeyIdentifiers);
			// statusReports
			assert.isArray(entry.statusReports);
			assert.strictEqual(entry.statusReports.length, 1);

			// check the entry data was copied to new entry:
			// assertionScheme
			assert.strictEqual(entry.assertionScheme, "UAFV1TLV");
			// attachmentHint
			assert.isArray(entry.attachmentHint);
			assert.strictEqual(entry.attachmentHint.length, 1);
			assert.isTrue(entry.attachmentHint.includes("internal"));
			// attestationRootCertificates
			assert.isArray(entry.attestationRootCertificates);
			assert.strictEqual(entry.attestationRootCertificates.length, 0);
			// attestationTypes
			assert.isArray(entry.attestationTypes);
			assert.strictEqual(entry.attestationTypes.length, 1);
			assert.isTrue(entry.attestationTypes.includes("basic-surrogate"));
			// authenticationAlgorithm
			assert.strictEqual(entry.authenticationAlgorithm, "ALG_SIGN_RSA_EMSA_PKCS1_SHA256_RAW");
			// authenticatorVersion
			assert.strictEqual(entry.authenticatorVersion, 256);
			// description
			assert.strictEqual(entry.description, "Touch ID, Face ID, or Passcode");
			// icon
			assert.isString(entry.icon);
			// isSecondFactorOnly
			assert.isFalse(entry.isSecondFactorOnly);
			// keyProtection
			assert.isArray(entry.keyProtection);
			assert.strictEqual(entry.keyProtection.length, 2);
			assert.isTrue(entry.keyProtection.includes("hardware"));
			assert.isTrue(entry.keyProtection.includes("tee"));
			// legalHeader
			assert.isString(entry.legalHeader);
			// matcherProtection
			assert.isArray(entry.matcherProtection);
			assert.strictEqual(entry.matcherProtection.length, 1);
			assert.isTrue(entry.matcherProtection.includes("hardware"));
			// protocolFamily
			assert.strictEqual(entry.protocolFamily, "uaf");
			// publicKeyAlgAndEncoding
			assert.strictEqual(entry.publicKeyAlgAndEncoding, "ALG_KEY_RSA_2048_RAW");
			// tcDisplay
			assert.isArray(entry.tcDisplay);
			assert.strictEqual(entry.tcDisplay.length, 1);
			assert.isTrue(entry.tcDisplay.includes("any"));
			assert.strictEqual(entry.tcDisplayContentType, "text/plain");
			assert.deepEqual(entry.upv, [
				{
					major: 1,
					minor: 1,
				}, {
					major: 1,
					minor: 0,
				},
			]);
			assert.isArray(entry.userVerificationDetails);
			assert.strictEqual(entry.userVerificationDetails.length, 2);
			assert.isArray(entry.userVerificationDetails[0]);
			assert.deepEqual(
				entry.userVerificationDetails[0],
				[
					{
						type: "code",
						userVerification: ["passcode"],
						base: 10,
						blockSlowdown: 60,
						maxRetries: 5,
						minLength: 4,
					},
				]
			);
			assert.isArray(entry.userVerificationDetails[1]);
			assert.deepEqual(
				entry.userVerificationDetails[1],
				[
					{
						type: "biometric",
						userVerification: ["fingerprint"],
						blockSlowdown: 0,
						maxReferenceDataSets: 5,
						maxRetries: 5,
					},
				]
			);
			// raw
			assert.isString(entry.raw);
			// collection
			assert.instanceOf(entry.collection, MdsCollection);
		});

		it("adds MDS3 UAF entry", async function() {
			await mc.addToc(mdsV3jwt);
			mc.addEntry(" ");
			await mc.validate();
			assert.isTrue(mc.entryList.has("4e4e#4005"), "added entry 4e4e#4005");
			const entry = mc.entryList.get("4e4e#4005");

			// check that TOC data was copied to new entry:
			// schema
			assert.strictEqual(entry.schema, 3);
			// timeOfLastStatusChange
			assert.strictEqual(entry.timeOfLastStatusChange, "2018-05-19");
			// hash
			assert.isUndefined(entry.hash);

			const tocHashBuf = await tools.hashDigest(jsObjectToB64(entry.raw));
			const tocHash = tools.base64.fromArrayBuffer(tocHashBuf);

			assert.strictEqual(tocHash, "KuTk5OTMPHkbY7bREv6ocsu8J739llB9u6ya1JPnqW4=");
			// id
			assert.strictEqual(entry.aaid, "4e4e#4005");
			assert.isUndefined(entry.aaguid);
			assert.isUndefined(entry.attestationCertificateKeyIdentifiers);
			// statusReports
			assert.isArray(entry.statusReports);
			assert.strictEqual(entry.statusReports.length, 1);

			// check the entry data was copied to new entry:
			// description
			assert.strictEqual(entry.description, "Touch ID, Face ID, or Passcode");
			// authenticatorVersion
			assert.strictEqual(entry.authenticatorVersion, 256);
			// protocolFamily
			assert.strictEqual(entry.protocolFamily, "uaf");
			assert.deepEqual(entry.upv, [
				{
					major: 1,
					minor: 0,
				},
				{
					major: 1,
					minor: 1,
				},
			]);
			// authenticationAlgorithms
			assert.deepEqual(entry.authenticationAlgorithms, [ "rsa_emsa_pkcs1_sha256_raw" ]);
			// publicKeyAlgAndEncoding
			assert.deepEqual(entry.publicKeyAlgAndEncodings, [ "rsa_2048_raw" ]);
			// attestationTypes
			assert.deepEqual(entry.attestationTypes, [ "basic_surrogate" ]);
			// userVerificationDetails
			assert.strictEqual(entry.userVerificationDetails.length, 2);
			assert.deepEqual(
				entry.userVerificationDetails[0],
				[
					{
						base: 10,
						blockSlowdown: 60,
						maxRetries: 5,
						minLength: 4,
						type: "code",
						userVerification: "passcode",
					},
				]
			);
			assert.deepEqual(
				entry.userVerificationDetails[1],
				[
					{
						blockSlowdown: 0,
						maxRetries: 5,
						maxTemplates: 0,
						selfAttestedFAR: 0,
						selfAttestedFRR: 0,
						type: "biometric",
						userVerification: "fingerprint",
					},
				]
			);
			// keyProtection
			assert.deepEqual(entry.keyProtection, [ "hardware", "tee" ]);
			// matcherProtection
			assert.deepEqual(entry.matcherProtection, [ "tee" ]);
			// attachmentHint
			assert.deepEqual(entry.attachmentHint, [ "internal" ]);
			// tcDisplay
			assert.deepEqual(entry.tcDisplay, [ "any" ]);
			// tcDisplayContentType
			assert.strictEqual(entry.tcDisplayContentType, "text/plain");
			// attestationRootCertificates
			assert.deepEqual(entry.attestationRootCertificates, []);
			// icon
			assert.isString(entry.icon);
			// raw
			assert.isString(entry.raw);
			// collection
			assert.instanceOf(entry.collection, MdsCollection);
		});

		it("adds MDS3 FIDO2 entry", async function() {
			await mc.addToc(mdsV3jwt);
			mc.addEntry(" ");
			await mc.validate();

			const entry = mc.findEntry("c5ef55ff-ad9a-4b9f-b580-adebafe026d0");
			assert.isDefined(entry, "added entry c5ef55ff-ad9a-4b9f-b580-adebafe026d0");

			// check that TOC data was copied to new entry:
			// schema
			assert.strictEqual(entry.schema, 3);
			// timeOfLastStatusChange
			assert.strictEqual(entry.timeOfLastStatusChange, "2020-05-12");
			// hash
			assert.isUndefined(entry.hash);

			const tocHashBuf = await tools.hashDigest(jsObjectToB64(entry.raw));
			const tocHash = tools.base64.fromArrayBuffer(tocHashBuf);

			assert.strictEqual(tocHash, "cYyOrPCsXMBUvX03jMqmRZ9PRrW07WPwnVi1Dke9ze8=");
			// id
			assert.strictEqual(entry.aaguid, "c5ef55ff-ad9a-4b9f-b580-adebafe026d0");
			assert.isUndefined(entry.aaid);
			assert.isUndefined(entry.attestationCertificateKeyIdentifiers);
			// statusReports
			assert.isArray(entry.statusReports);
			assert.strictEqual(entry.statusReports.length, 1);

			// check the entry data was copied to new entry:
			// description
			assert.strictEqual(entry.description, "YubiKey 5Ci");
			// authenticatorVersion
			assert.strictEqual(entry.authenticatorVersion, 50200);
			// protocolFamily
			assert.strictEqual(entry.protocolFamily, "fido2");
			assert.deepEqual(entry.upv, [
				{
					major: 1,
					minor: 0,
				},
			]);
			// authenticationAlgorithms
			assert.deepEqual(entry.authenticationAlgorithms, [ "ed25519_eddsa_sha512_raw", "secp256r1_ecdsa_sha256_raw" ]);
			// publicKeyAlgAndEncoding
			assert.deepEqual(entry.publicKeyAlgAndEncodings, [ "cose" ]);
			// attestationTypes
			assert.deepEqual(entry.attestationTypes, [ "basic_full" ]);
			// userVerificationDetails
			assert.strictEqual(entry.userVerificationDetails.length, 1);
			assert.deepEqual(
				entry.userVerificationDetails[0],
				[
					{
						userVerification: "presence",
					},
					{
						userVerification: "passcode",
						base: 64,
						blockSlowdown: 0,
						maxRetries: 8,
						minLength: 4,
						type: "code",
					},
					{
						userVerification: "none",
					},
				]
			);
			// keyProtection
			assert.deepEqual(entry.keyProtection, [ "hardware", "secure_element" ]);
			// matcherProtection
			assert.deepEqual(entry.matcherProtection, [ "on_chip" ]);
			// cryptoStrength
			assert.strictEqual(entry.cryptoStrength, 128);
			// attachmentHint
			assert.deepEqual(entry.attachmentHint, [ "external", "wired" ]);
			// tcDisplay
			assert.deepEqual(entry.tcDisplay, [ ]);
			// tcDisplayContentType
			assert.isUndefined(entry.tcDisplayContentType);
			// attestationRootCertificates
			assert.deepEqual(entry.attestationRootCertificates, [ "MIIDHjCCAgagAwIBAgIEG0BT9zANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC/jwYuhBVlqaiYWEMsrWFisgJ+PtM91eSrpI4TK7U53mwCIawSDHy8vUmk5N2KAj9abvT9NP5SMS1hQi3usxoYGonXQgfO6ZXyUA9a+KAkqdFnBnlyugSeCOep8EdZFfsaRFtMjkwz5Gcz2Py4vIYvCdMHPtwaz0bVuzneueIEz6TnQjE63Rdt2zbwnebwTG5ZybeWSwbzy+BJ34ZHcUhPAY89yJQXuE0IzMZFcEBbPNRbWECRKgjq//qT9nmDOFVlSRCt2wiqPSzluwn+v+suQEBsUjTGMEd25tKXXTkNW21wIWbxeSyUoTXwLvGS6xlwQSgNpk2qXYwf8iXg7VWZAgMBAAGjQjBAMB0GA1UdDgQWBBQgIvz0bNGJhjgpToksyKpP9xv9oDAPBgNVHRMECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAjvjuOMDSa+JXFCLyBKsycXtBVZsJ4Ue3LbaEsPY4MYN/hIQ5ZM5p7EjfcnMG4CtYkNsfNHc0AhBLdq45rnT87q/6O3vUEtNMafbhU6kthX7Y+9XFN9NpmYxr+ekVY5xOxi8h9JDIgoMP4VB1uS0aunL1IGqrNooL9mmFnL2kLVVee6/VR6C5+KSTCMCWppMuJIZII2v9o4dkoZ8Y7QRjQlLfYzd3qGtKbw7xaF1UsG/5xUb/Btwb2X2g4InpiB/yt/3CpQXpiWX/K4mBvUKiGn05ZsqeY1gx4g0xLBqcU9psmyPzK+Vsgw2jeRQ5JlKDyqE0hebfC1tvFu0CCrJFcw==" ]);
			// icon
			assert.isString(entry.icon);
			// authenticatorGetInfo
			const { authenticatorGetInfo } = entry;
			assert.isDefined(authenticatorGetInfo);

			// versions
			assert.deepEqual(authenticatorGetInfo.versions, [ "U2F_V2", "FIDO_2_0", "FIDO_2_1_PRE" ]);
			// extensions
			assert.deepEqual(authenticatorGetInfo.extensions, [ "credProtect", "hmac-secret" ]);
			// aaguid
			assert.strictEqual(authenticatorGetInfo.aaguid, "c5ef55ffad9a4b9fb580adebafe026d0");
			// options
			assert.deepEqual(authenticatorGetInfo.options, {
				plat: false,
				rk: true,
				clientPin: true,
				up: true,
				credentialMgmtPreview: true,
			});
			// maxMsgSize
			assert.strictEqual(authenticatorGetInfo.maxMsgSize, 1200);
			// pinUvAuthProtocols
			assert.deepEqual(authenticatorGetInfo.pinUvAuthProtocols, [ 2, 1 ]);
			// maxCredentialCountInList
			assert.strictEqual(authenticatorGetInfo.maxCredentialCountInList, 8);
			// maxCredentialIdLength
			assert.strictEqual(authenticatorGetInfo.maxCredentialIdLength, 128);
			// transports
			assert.deepEqual(authenticatorGetInfo.transports, [ "usb", "lightning" ]);
			// algorithms
			assert.deepEqual(authenticatorGetInfo.algorithms, [ 
				{
					type: "public-key",
					alg: -7,
				},
				{
					type: "public-key",
					alg: -8,
				}, 
			]);
			// minPINLength
			assert.strictEqual(authenticatorGetInfo.minPINLength, 4);
			// raw
			assert.isString(entry.raw);
			// collection
			assert.instanceOf(entry.collection, MdsCollection);
		});

		it("adds MDS3 U2F entry", async function() {
			await mc.addToc(mdsV3jwt);
			mc.addEntry(" ");
			await mc.validate();

			const entry = mc.findEntry("bf7bcaa0d0c6187a8c6abbdd16a15640e7c7bde2");
			assert.isDefined(entry, "added entry bf7bcaa0d0c6187a8c6abbdd16a15640e7c7bde2");

			// check that TOC data was copied to new entry:
			// schema
			assert.strictEqual(entry.schema, 3);
			// timeOfLastStatusChange
			assert.strictEqual(entry.timeOfLastStatusChange, "2020-05-12");
			// hash
			assert.isUndefined(entry.hash);

			const tocHashBuf = await tools.hashDigest(jsObjectToB64(entry.raw));
			const tocHash = tools.base64.fromArrayBuffer(tocHashBuf);

			assert.strictEqual(tocHash, "QaFyj/U7FwUCdztfF5Evz7VU2e+paGqlVyVNw1DpVws=");
			// id
			assert.isUndefined(entry.aaguid);
			assert.isUndefined(entry.aaid);
			// statusReports
			assert.isArray(entry.statusReports);
			assert.strictEqual(entry.statusReports.length, 1);
			// attestationCertificateKeyIdentifiers
			assert.isArray(entry.attestationCertificateKeyIdentifiers);
			assert.isTrue(entry.attestationCertificateKeyIdentifiers.includes("bf7bcaa0d0c6187a8c6abbdd16a15640e7c7bde2"), "Identifier bf7bcaa0d0c6187a8c6abbdd16a15640e7c7bde2 found");

			// check the entry data was copied to new entry:
			// description
			assert.strictEqual(entry.description, "YubiKey 5Ci");
			// authenticatorVersion
			assert.strictEqual(entry.authenticatorVersion, 2);
			// protocolFamily
			assert.strictEqual(entry.protocolFamily, "u2f");
			assert.deepEqual(entry.upv, [
				{
					major: 1,
					minor: 1,
				},
			]);
			// authenticationAlgorithms
			assert.deepEqual(entry.authenticationAlgorithms, [ "secp256r1_ecdsa_sha256_raw" ]);
			// publicKeyAlgAndEncoding
			assert.deepEqual(entry.publicKeyAlgAndEncodings, [ "ecc_x962_raw" ]);
			// attestationTypes
			assert.deepEqual(entry.attestationTypes, [ "basic_full" ]);
			// userVerificationDetails
			assert.strictEqual(entry.userVerificationDetails.length, 1);
			assert.deepEqual(
				entry.userVerificationDetails[0],
				[
					{
						userVerification: "presence",
					},
				]
			);
			// keyProtection
			assert.deepEqual(entry.keyProtection, [ "hardware", "secure_element", "remote_handle" ]);
			// matcherProtection
			assert.deepEqual(entry.matcherProtection, [ "on_chip" ]);
			// cryptoStrength
			assert.strictEqual(entry.cryptoStrength, 128);
			// attachmentHint
			assert.deepEqual(entry.attachmentHint, [ "external", "wired" ]);
			// tcDisplay
			assert.deepEqual(entry.tcDisplay, [ ]);
			// tcDisplayContentType
			assert.isUndefined(entry.tcDisplayContentType);
			// attestationRootCertificates
			assert.deepEqual(entry.attestationRootCertificates, [ "MIIDHjCCAgagAwIBAgIEG0BT9zANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC/jwYuhBVlqaiYWEMsrWFisgJ+PtM91eSrpI4TK7U53mwCIawSDHy8vUmk5N2KAj9abvT9NP5SMS1hQi3usxoYGonXQgfO6ZXyUA9a+KAkqdFnBnlyugSeCOep8EdZFfsaRFtMjkwz5Gcz2Py4vIYvCdMHPtwaz0bVuzneueIEz6TnQjE63Rdt2zbwnebwTG5ZybeWSwbzy+BJ34ZHcUhPAY89yJQXuE0IzMZFcEBbPNRbWECRKgjq//qT9nmDOFVlSRCt2wiqPSzluwn+v+suQEBsUjTGMEd25tKXXTkNW21wIWbxeSyUoTXwLvGS6xlwQSgNpk2qXYwf8iXg7VWZAgMBAAGjQjBAMB0GA1UdDgQWBBQgIvz0bNGJhjgpToksyKpP9xv9oDAPBgNVHRMECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAjvjuOMDSa+JXFCLyBKsycXtBVZsJ4Ue3LbaEsPY4MYN/hIQ5ZM5p7EjfcnMG4CtYkNsfNHc0AhBLdq45rnT87q/6O3vUEtNMafbhU6kthX7Y+9XFN9NpmYxr+ekVY5xOxi8h9JDIgoMP4VB1uS0aunL1IGqrNooL9mmFnL2kLVVee6/VR6C5+KSTCMCWppMuJIZII2v9o4dkoZ8Y7QRjQlLfYzd3qGtKbw7xaF1UsG/5xUb/Btwb2X2g4InpiB/yt/3CpQXpiWX/K4mBvUKiGn05ZsqeY1gx4g0xLBqcU9psmyPzK+Vsgw2jeRQ5JlKDyqE0hebfC1tvFu0CCrJFcw==" ]);
			// icon
			assert.isString(entry.icon);
			// raw
			assert.isString(entry.raw);
			// collection
			assert.instanceOf(entry.collection, MdsCollection);
		});
	});

	describe("findEntry", function() {
		let mc;
		beforeEach(async function() {
			mc = new MdsCollection("test");
			await mc.addToc(h.mds.mds2TocJwt);
			mc.addEntry(h.mds.mds2UafEntry);
			await mc.validate();
		});

		it("throws if id is bad type", function() {
			assert.throws(function() {
				mc.findEntry();
			}, Error, "expected 'id' to be String, got: undefined");
		});

		it("returns MdsEntry", function() {
			const entry = mc.findEntry("4e4e#4005");
			assert.instanceOf(entry, MdsEntry);
			assert.strictEqual(entry.aaid, "4e4e#4005");
		});

		it("returns null on entry not found", function() {
			const entry = mc.findEntry("ffff#ffff");
			assert.isNull(entry);
		});
	});
});
