import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { ParsedUrl, UrlMatcher, UrlClientCertificates, ClientCertificateStore, clientCertificateStoreSingleton, ClientCertificates, loadClientCertificateConfig } from '../../lib/client-certificates'
// import { clientCertificateStore } from '../../lib/agent'
import urllib from 'url'
import fs from 'fs-extra'
import os from 'os'
import path from 'path'
import { execFileSync } from 'child_process'
import { randomUUID } from 'crypto'

type CertAlgo = 'rsa' | 'ec'

function opensslNewkeyArgs (algo: CertAlgo): string[] {
  return algo === 'ec'
    ? ['-newkey', 'ec', '-pkeyopt', 'ec_paramgen_curve:P-256']
    : ['-newkey', 'rsa:2048']
}

const testSubject = '/CN=example.org/C=US/ST=California/L=San Fran/O=Test/OU=Test'

function urlShouldMatch (url: string, matcher: string) {
  let rule = UrlMatcher.buildMatcherRule(matcher)
  let parsedUrl = new ParsedUrl(url)

  expect(UrlMatcher.matchUrl(parsedUrl.host, parsedUrl.path, parsedUrl.port, rule), `'${url}' should match '${matcher}' (rule: ${JSON.stringify(rule)})`).toBe(true)
}

function urlShouldNotMatch (url: string, matcher: string) {
  let rule = UrlMatcher.buildMatcherRule(matcher)
  let parsedUrl = new ParsedUrl(url)

  expect(UrlMatcher.matchUrl(parsedUrl.host, parsedUrl.path, parsedUrl.port, rule), `'${url}' should not match '${matcher}' (rule: ${JSON.stringify(rule)})`).toBe(false)
}

function checkParsed (parsed: ParsedUrl, host: string, path: string | undefined, port: number | undefined) {
  expect(parsed.host, `'host ${parsed.host}' should be '${host}'`).toEqual(host)
  expect(parsed.path, `'path ${parsed.path}' should be '${path}'`).toEqual(path)
  expect(parsed.port, `'port ${parsed.port}' should be '${port}'`).toEqual(port)
}

describe('lib/client-certificates', () => {
  describe('ParsedUrl', () => {
    it('parses clean URLs', () => {
      let parsed = new ParsedUrl('https://a.host.com')

      checkParsed(parsed, 'a.host.com', undefined, undefined)

      parsed = new ParsedUrl('https://a.host.com:1234')
      expect(parsed.host).toEqual('a.host.com')
      expect(parsed.port).toEqual(1234)

      parsed = new ParsedUrl('https://a.host.com/a/path/')
      expect(parsed.host).toEqual('a.host.com')
      expect(parsed.path).toEqual('/a/path/')
    })

    it('parses wildcard URLs', () => {
      let parsed = new ParsedUrl('https://a.host.*')

      expect(parsed.host).toEqual('a.host.*')

      parsed = new ParsedUrl('https://*.host.com')
      expect(parsed.host).toEqual('*.host.com')

      parsed = new ParsedUrl('https://a.host.com/a/path/*')
      expect(parsed.host).toEqual('a.host.com')
      expect(parsed.path).toEqual('/a/path/*')

      parsed = new ParsedUrl('https://a.host.com/*/path/')
      expect(parsed.host).toEqual('a.host.com')
      expect(parsed.path).toEqual('/*/path/')

      parsed = new ParsedUrl('*')
      expect(parsed.host).toEqual('*')
      expect(parsed.path).toEqual(undefined)
    })
  })

  describe('ClientCertificateUrlMatcher', () => {
    it('matches basic hostnames', () => {
      let matcher = 'https://a.host.com'

      urlShouldMatch('https://a.host.com', matcher)
      urlShouldMatch('https://a.host.com/a/path', matcher)
      urlShouldNotMatch('https://a.host.co.uk', matcher)
    })

    it('matches wildcard hostnames', () => {
      let matcher1 = 'https://a.host.*'

      urlShouldMatch('https://a.host.com', matcher1)
      urlShouldMatch('https://a.host.com/a/path', matcher1)
      urlShouldMatch('https://a.host.co.uk', matcher1)
      urlShouldNotMatch('https://a.b.host.co.uk', matcher1)

      matcher1 = 'https://a.*.host.*'
      urlShouldNotMatch('https://a.host.com', matcher1)
      urlShouldNotMatch('https://z.a.host.com', matcher1)
      urlShouldMatch('https://a.b.host.com', matcher1)
      urlShouldMatch('https://a.b.c.host.com', matcher1)
      urlShouldMatch('https://a.b.c.host.co.uk', matcher1)

      matcher1 = '*'
      urlShouldMatch('https://a.host.com', matcher1)
      urlShouldMatch('https://a.b.c.d.e.f.host.co.uk', matcher1)
    })

    it('matches basic paths', () => {
      let matcher = 'https://a.path.com/a'

      urlShouldMatch('https://a.path.com/a', matcher)
      urlShouldNotMatch('https://a.path.com', matcher)
      urlShouldNotMatch('https://a.path.com/a/b', matcher)
    })

    it('matches wildcard paths', () => {
      let matcher = 'https://a.path2.com/**'

      urlShouldMatch('https://a.path2.com/a', matcher)
      urlShouldMatch('https://a.path2.com/a/b', matcher)
    })
  })

  describe('UrlClientCertificates', () => {
    it('constructs, populates default properties', () => {
      let url = 'http://a.host.com/home'
      let certs = new UrlClientCertificates(url)

      expect(certs.url).toEqual(url)
      expect(certs.pathnameLength).toEqual(5)
      expect(certs)
    })
  })

  describe('ClientCertificateStore', () => {
    it('adds and retrieves certs for urls', () => {
      const url1 = urllib.parse('https://host.com')
      const url2 = urllib.parse('https://company.com')
      const store = new ClientCertificateStore()

      expect(store.getCertCount()).toEqual(0)

      let options = store.getClientCertificateAgentOptionsForUrl(url1)

      expect(options).toEqual(null)

      const certs1 = new UrlClientCertificates(url1.href)

      certs1.clientCertificates = new ClientCertificates()
      certs1.clientCertificates.ca.push(Buffer.from([1, 2, 3, 4]))

      const certs2 = new UrlClientCertificates(url2.href)

      certs2.clientCertificates = new ClientCertificates()
      certs2.clientCertificates.ca.push(Buffer.from([4, 3, 2, 1]))

      store.addClientCertificatesForUrl(certs1)
      expect(store.getCertCount()).toEqual(1)

      store.addClientCertificatesForUrl(certs2)
      expect(store.getCertCount()).toEqual(2)

      expect(() => {
        store.addClientCertificatesForUrl(certs2)
      }).toThrow('ClientCertificateStore::addClientCertificatesForUrl: Url https://company.com/ already in store')

      const options1 = store.getClientCertificateAgentOptionsForUrl(url1)
      const options2 = store.getClientCertificateAgentOptionsForUrl(url2)

      expect(options1.ca).toEqual(certs1.clientCertificates.ca)
      expect(options2.ca).toEqual(certs2.clientCertificates.ca)
    })
  })
})

// The following testing covers the areas:
// PEM:
// Valid crt/key, no passphrase
// Valid crt/key, passphrase
// Valid crt/key, relative pathing
// Valid crt/key, invalid (and not there) passphrase
// Multiple crt/key
// Invalid crt
// Invalid key
// Invalid ca
// Missing crt
// Missing key
// Missing ca
// PFX:
// Valid pfx and passphrase
// Valid pfx, passphrase
// Valid pfx, INVALID passphrase (invalid and not there)
// Invalid pfx
// Missing pfx
// Missing passphrase
//
// Neither PEM nor PFX supplied

function createPemFiles (
  certFilepath: string,
  keyFilepath: string,
  passphraseFilepath: string | undefined,
  passphrase: string | undefined,
  algo: CertAlgo = 'rsa',
) {
  const args = [
    'req', '-x509',
    ...opensslNewkeyArgs(algo),
    '-keyout', keyFilepath,
    '-out', certFilepath,
    '-days', '1',
    '-subj', testSubject,
  ]

  if (passphrase) {
    args.push('-passout', `pass:${passphrase}`)
  } else {
    args.push('-nodes')
  }

  execFileSync('openssl', args, { stdio: 'ignore' })

  if (passphraseFilepath && passphrase) {
    fs.writeFileSync(passphraseFilepath, passphrase)
  }
}

function createPfxFiles (
  pfxFilepath: string,
  passphraseFilepath: string | undefined,
  passphrase: string,
  algo: CertAlgo = 'rsa',
) {
  const certTmp = `${pfxFilepath}.cert.tmp`
  const keyTmp = `${pfxFilepath}.key.tmp`

  execFileSync('openssl', [
    'req', '-x509',
    ...opensslNewkeyArgs(algo),
    '-nodes',
    '-keyout', keyTmp,
    '-out', certTmp,
    '-days', '1',
    '-subj', testSubject,
  ], { stdio: 'ignore' })

  execFileSync('openssl', [
    'pkcs12', '-export',
    '-in', certTmp,
    '-inkey', keyTmp,
    '-out', pfxFilepath,
    '-password', `pass:${passphrase}`,
    '-keypbe', 'AES-256-CBC',
    '-certpbe', 'AES-256-CBC',
    '-macalg', 'sha256',
  ], { stdio: 'ignore' })

  fs.removeSync(certTmp)
  fs.removeSync(keyTmp)

  if (passphraseFilepath) {
    fs.writeFileSync(passphraseFilepath, passphrase)
  }
}

function createCaFile (filepath: string, algo: CertAlgo = 'rsa') {
  const keyTmp = `${filepath}.key.tmp`

  execFileSync('openssl', [
    'req', '-x509',
    ...opensslNewkeyArgs(algo),
    '-nodes',
    '-keyout', keyTmp,
    '-out', filepath,
    '-days', '1',
    '-subj', testSubject,
  ], { stdio: 'ignore' })

  fs.removeSync(keyTmp)
}

function createUniqueUrl (): string {
  return `http://${randomUUID()}`
}

function createSinglePemConfig (
  url,
  caFilepath,
  pemFilepath,
  pemKeyFilepath,
  pemPassphraseFilepath,
) {
  return {
    projectRoot: __dirname,
    clientCertificates: [
      {
        url,
        ca: [caFilepath],
        certs: [
          {
            cert: pemFilepath,
            key: pemKeyFilepath,
            passphrase: pemPassphraseFilepath,
          },
        ],
      },
    ],
  }
}

function createSinglePfxConfig (
  url,
  caFilepath,
  pfxFilepath,
  pfxPassphraseFilepath,
) {
  return {
    projectRoot: __dirname,
    clientCertificates: [
      {
        url,
        ca: [caFilepath],
        certs: [
          {
            pfx: pfxFilepath,
            passphrase: pfxPassphraseFilepath,
          },
        ],
      },
    ],
  }
}

const tempDirName = 'server-pki-tests'
const caFilename = 'testca.crt'
const pemFilename = 'testpem.crt'
const pemKeyFilename = 'testpem.key'
const pemPassphraseFilename = 'testpem.pass'
const pfxFilename = 'testpfx.p12'
const pfxPassphraseFilename = 'testpfx.pass'

const tempDirPath = path.join(os.tmpdir(), tempDirName)
const caFilepath = path.join(tempDirPath, caFilename)
const pemFilepath = path.join(tempDirPath, pemFilename)
const pemKeyFilepath = path.join(tempDirPath, pemKeyFilename)
const pemPassphraseFilepath = path.join(tempDirPath, pemPassphraseFilename)
const pfxFilepath = path.join(tempDirPath, pfxFilename)
const pfxPassphraseFilepath = path.join(tempDirPath, pfxPassphraseFilename)

describe('lib/client-certificates', () => {
  beforeAll(() => {
    if (!fs.existsSync(tempDirPath)) {
      fs.mkdirSync(tempDirPath)
    }
  })

  afterAll(() => {
    fs.rmdirSync(tempDirPath, { recursive: true })
  })

  describe('loads cert files', () => {
    it('loads valid single PEM (no passphrase) and CA via absolute pathing', () => {
      createPemFiles(pemFilepath, pemKeyFilepath, undefined, undefined)
      createCaFile(caFilepath)

      const url = createUniqueUrl()
      const config = createSinglePemConfig(
        url,
        caFilepath,
        pemFilepath,
        pemKeyFilepath,
        undefined,
      )
      const pemFileData = fs.readFileSync(pemFilepath)
      const keyFileData = fs.readFileSync(pemKeyFilepath)
      const caFileData = fs.readFileSync(caFilepath)

      loadClientCertificateConfig(config)
      const options = clientCertificateStoreSingleton.getClientCertificateAgentOptionsForUrl(
        urllib.parse(url),
      )

      expect(options).not.toBeNull()
      expect(options.ca.length).toEqual(1)
      expect(options.ca[0]).toEqual(caFileData)
      expect(options.pfx).toHaveLength(0)
      expect(options.cert.length).toEqual(1)
      expect(options.cert[0]).toEqual(pemFileData)
      expect(options.key.length).toEqual(1)
      expect(options.key[0].passphrase).toBeUndefined()
      expect(options.key[0].pem).toEqual(keyFileData)
    })

    it('loads valid multiple PEMs (no passphrase) and CAs', () => {
      const pemFilepath1 = path.join(tempDirPath, 'testpem1.crt')
      const keyFilepath1 = path.join(tempDirPath, 'testpem1.key')
      const caFilepath1 = path.join(tempDirPath, 'testca1.crt')
      const pemFilepath2 = path.join(tempDirPath, 'testpem2.crt')
      const keyFilepath2 = path.join(tempDirPath, 'testpem2.key')
      const caFilepath2 = path.join(tempDirPath, 'testca2.crt')
      const pemFilepath3 = path.join(tempDirPath, 'testpem3.crt')
      const keyFilepath3 = path.join(tempDirPath, 'testpem3.key')
      const caFilepath3 = path.join(tempDirPath, 'testca3.crt')

      createPemFiles(pemFilepath1, keyFilepath1, undefined, undefined)
      createPemFiles(pemFilepath2, keyFilepath2, undefined, undefined)
      createPemFiles(pemFilepath3, keyFilepath3, undefined, undefined)
      createCaFile(caFilepath1)
      createCaFile(caFilepath2)
      createCaFile(caFilepath3)

      const url = createUniqueUrl()
      const config = {
        projectRoot: __dirname,
        clientCertificates: [
          {
            url,
            ca: [caFilepath1, caFilepath2, caFilepath3],
            certs: [
              {
                cert: pemFilepath1,
                key: keyFilepath1,
              },
              {
                cert: pemFilepath2,
                key: keyFilepath2,
              },
              {
                cert: pemFilepath3,
                key: keyFilepath3,
              },
            ],
          },
        ],
      }

      const pemFileData1 = fs.readFileSync(pemFilepath1)
      const keyFileData1 = fs.readFileSync(keyFilepath1)
      const caFileData1 = fs.readFileSync(caFilepath1)
      const pemFileData2 = fs.readFileSync(pemFilepath2)
      const keyFileData2 = fs.readFileSync(keyFilepath2)
      const caFileData2 = fs.readFileSync(caFilepath2)
      const pemFileData3 = fs.readFileSync(pemFilepath3)
      const keyFileData3 = fs.readFileSync(keyFilepath3)
      const caFileData3 = fs.readFileSync(caFilepath3)

      loadClientCertificateConfig(config)
      const options = clientCertificateStoreSingleton.getClientCertificateAgentOptionsForUrl(
        urllib.parse(url),
      )

      expect(options).not.toBeNull()
      expect(options.ca.length).toEqual(3)
      expect(options.ca[0]).toEqual(caFileData1)
      expect(options.ca[1]).toEqual(caFileData2)
      expect(options.ca[2]).toEqual(caFileData3)
      expect(options.pfx).toHaveLength(0)
      expect(options.cert.length).toEqual(3)
      expect(options.cert[0]).toEqual(pemFileData1)
      expect(options.cert[1]).toEqual(pemFileData2)
      expect(options.cert[2]).toEqual(pemFileData3)
      expect(options.key.length).toEqual(3)
      expect(options.key[0].passphrase).toBeUndefined()
      expect(options.key[0].pem).toEqual(keyFileData1)
      expect(options.key[1].pem).toEqual(keyFileData2)
      expect(options.key[2].pem).toEqual(keyFileData3)
    })

    it('loads valid single PEM (with passphrase)', () => {
      const passphrase = 'a_phrase'

      createPemFiles(
        pemFilepath,
        pemKeyFilepath,
        pemPassphraseFilepath,
        passphrase,
      )

      const url = createUniqueUrl()
      const config = createSinglePemConfig(
        url,
        undefined,
        pemFilepath,
        pemKeyFilepath,
        pemPassphraseFilepath,
      )
      const pemFileData = fs.readFileSync(pemFilepath)
      const keyFileData = fs.readFileSync(pemKeyFilepath)

      loadClientCertificateConfig(config)
      const options = clientCertificateStoreSingleton.getClientCertificateAgentOptionsForUrl(
        urllib.parse(url),
      )

      expect(options).not.toBeNull()
      expect(options.ca.length).toEqual(0)
      expect(options.pfx).toHaveLength(0)
      expect(options.cert.length).toEqual(1)
      expect(options.cert[0]).toEqual(pemFileData)
      expect(options.key.length).toEqual(1)
      expect(options.key[0].passphrase).toEqual(passphrase)
      expect(options.key[0].pem).toEqual(keyFileData)
    })

    it('loads valid single PEM and CA via relative pathing', () => {
      createPemFiles(pemFilepath, pemKeyFilepath, undefined, undefined)
      createCaFile(caFilepath)

      const relativeCaFilepath = path.relative(__dirname, caFilepath)
      const relativePemFilepath = path.relative(__dirname, pemFilepath)
      const relativeKeyFilepath = path.relative(__dirname, pemKeyFilepath)

      const url = createUniqueUrl()
      const config = createSinglePemConfig(
        url,
        relativeCaFilepath,
        relativePemFilepath,
        relativeKeyFilepath,
        undefined,
      )

      loadClientCertificateConfig(config)
      const options = clientCertificateStoreSingleton.getClientCertificateAgentOptionsForUrl(
        urllib.parse(url),
      )

      expect(options).not.toBeNull()
      expect(options.ca.length).toEqual(1)
      expect(options.pfx).toHaveLength(0)
      expect(options.cert.length).toEqual(1)
      expect(options.key.length).toEqual(1)
      expect(options.key[0].passphrase).toBeUndefined()
    })

    it('loads valid single EC PEM (no passphrase)', () => {
      createPemFiles(pemFilepath, pemKeyFilepath, undefined, undefined, 'ec')
      createCaFile(caFilepath, 'ec')

      const url = createUniqueUrl()
      const config = createSinglePemConfig(
        url,
        caFilepath,
        pemFilepath,
        pemKeyFilepath,
        undefined,
      )
      const pemFileData = fs.readFileSync(pemFilepath)
      const keyFileData = fs.readFileSync(pemKeyFilepath)
      const caFileData = fs.readFileSync(caFilepath)

      loadClientCertificateConfig(config)
      const options = clientCertificateStoreSingleton.getClientCertificateAgentOptionsForUrl(
        urllib.parse(url),
      )

      expect(options).not.toBeNull()
      expect(options.ca.length).toEqual(1)
      expect(options.ca[0]).toEqual(caFileData)
      expect(options.pfx).toHaveLength(0)
      expect(options.cert.length).toEqual(1)
      expect(options.cert[0]).toEqual(pemFileData)
      expect(options.key.length).toEqual(1)
      expect(options.key[0].passphrase).toBeUndefined()
      expect(options.key[0].pem).toEqual(keyFileData)
    })

    it('loads valid single EC PEM (with passphrase)', () => {
      const passphrase = 'ec_phrase'

      createPemFiles(
        pemFilepath,
        pemKeyFilepath,
        pemPassphraseFilepath,
        passphrase,
        'ec',
      )

      const url = createUniqueUrl()
      const config = createSinglePemConfig(
        url,
        undefined,
        pemFilepath,
        pemKeyFilepath,
        pemPassphraseFilepath,
      )
      const pemFileData = fs.readFileSync(pemFilepath)
      const keyFileData = fs.readFileSync(pemKeyFilepath)

      loadClientCertificateConfig(config)
      const options = clientCertificateStoreSingleton.getClientCertificateAgentOptionsForUrl(
        urllib.parse(url),
      )

      expect(options).not.toBeNull()
      expect(options.ca.length).toEqual(0)
      expect(options.pfx).toHaveLength(0)
      expect(options.cert.length).toEqual(1)
      expect(options.cert[0]).toEqual(pemFileData)
      expect(options.key.length).toEqual(1)
      expect(options.key[0].passphrase).toEqual(passphrase)
      expect(options.key[0].pem).toEqual(keyFileData)
    })

    it('detects invalid EC PEM passphrase', () => {
      const passphrase = 'ec_phrase'

      createPemFiles(
        pemFilepath,
        pemKeyFilepath,
        pemPassphraseFilepath,
        passphrase,
        'ec',
      )

      fs.writeFileSync(pemPassphraseFilepath, 'not-the-passphrase')

      const url = createUniqueUrl()
      const config = createSinglePemConfig(
        url,
        undefined,
        pemFilepath,
        pemKeyFilepath,
        pemPassphraseFilepath,
      )

      expect(() => {
        loadClientCertificateConfig(config)
      }).toThrow(
        `Cannot decrypt PEM key with supplied passphrase (check the passphrase file content and that it doesn't have unexpected whitespace at the end)`,
      )
    })

    // TODO: fix this flaky test
    it.skip('detects invalid PEM key passphrase', () => {
      const passphrase = 'a_phrase'

      createPemFiles(
        pemFilepath,
        pemKeyFilepath,
        pemPassphraseFilepath,
        passphrase,
      )

      fs.writeFileSync(pemPassphraseFilepath, 'not-the-passphrase')

      const url = createUniqueUrl()
      const config = createSinglePemConfig(
        url,
        undefined,
        pemFilepath,
        pemKeyFilepath,
        pemPassphraseFilepath,
      )

      expect(() => {
        loadClientCertificateConfig(config)
      }).toThrow(
        `Cannot decrypt PEM key with supplied passphrase (check the passphrase file content and that it doesn't have unexpected whitespace at the end)`,
      )
    })

    it('detects invalid PEM key file (no passphrase)', () => {
      createPemFiles(pemFilepath, pemKeyFilepath, undefined, undefined)
      fs.writeFileSync(pemKeyFilepath, 'not-a-key')

      const url = createUniqueUrl()
      const config = createSinglePemConfig(
        url,
        undefined,
        pemFilepath,
        pemKeyFilepath,
        undefined,
      )

      expect(() => {
        loadClientCertificateConfig(config)
      }).toThrow('Cannot parse PEM key')
    })

    it('detects invalid PEM key file (with passphrase)', () => {
      const passphrase = 'a_phrase'

      createPemFiles(
        pemFilepath,
        pemKeyFilepath,
        pemPassphraseFilepath,
        passphrase,
      )

      fs.writeFileSync(pemKeyFilepath, 'not-a-key')

      const url = createUniqueUrl()
      const config = createSinglePemConfig(
        url,
        undefined,
        pemFilepath,
        pemKeyFilepath,
        pemPassphraseFilepath,
      )

      expect(() => {
        loadClientCertificateConfig(config)
      }).toThrow('Cannot parse PEM key')
    })

    it('detects invalid PEM cert file', () => {
      fs.writeFileSync(pemFilepath, 'not-a-cert')

      const url = createUniqueUrl()
      const config = createSinglePemConfig(
        url,
        undefined,
        pemFilepath,
        pemKeyFilepath,
        undefined,
      )

      expect(() => {
        loadClientCertificateConfig(config)
      }).toThrow('Cannot parse PEM cert')
    })

    it('detects invalid CA file', () => {
      fs.writeFileSync(caFilepath, 'not-a-cert')

      const url = createUniqueUrl()
      const config = createSinglePemConfig(
        url,
        caFilepath,
        pemFilepath,
        pemKeyFilepath,
        undefined,
      )

      expect(() => {
        loadClientCertificateConfig(config)
      }).toThrow('Cannot parse CA cert')
    })

    it('detects missing PEM cert file', () => {
      const url = createUniqueUrl()
      const passphrase = 'a_phrase'

      createPemFiles(
        pemFilepath,
        pemKeyFilepath,
        pemPassphraseFilepath,
        passphrase,
      )

      const config = createSinglePemConfig(
        url,
        undefined,
        'not-a-path',
        pemKeyFilepath,
        pemPassphraseFilepath,
      )

      expect(() => {
        loadClientCertificateConfig(config)
      }).toThrow('no such file or directory')
    })

    it('detects missing PEM key file', () => {
      const url = createUniqueUrl()
      const passphrase = 'a_phrase'

      createPemFiles(
        pemFilepath,
        pemKeyFilepath,
        pemPassphraseFilepath,
        passphrase,
      )

      const config = createSinglePemConfig(
        url,
        undefined,
        pemFilepath,
        'not-a-path',
        pemPassphraseFilepath,
      )

      expect(() => {
        loadClientCertificateConfig(config)
      }).toThrow('no such file or directory')
    })

    it('detects missing PEM passphrase file', () => {
      const url = createUniqueUrl()
      const passphrase = 'a_phrase'

      createPemFiles(
        pemFilepath,
        pemKeyFilepath,
        pemPassphraseFilepath,
        passphrase,
      )

      const config = createSinglePemConfig(
        url,
        undefined,
        pemFilepath,
        pemKeyFilepath,
        'not-a-path',
      )

      expect(() => {
        loadClientCertificateConfig(config)
      }).toThrow('no such file or directory')
    })

    it('detects missing CA file', () => {
      const url = createUniqueUrl()
      const passphrase = 'a_phrase'

      createPemFiles(
        pemFilepath,
        pemKeyFilepath,
        pemPassphraseFilepath,
        passphrase,
      )

      createCaFile(caFilepath)
      const config = createSinglePemConfig(
        url,
        'not-a-path',
        pemFilepath,
        pemKeyFilepath,
        pemPassphraseFilepath,
      )

      expect(() => {
        loadClientCertificateConfig(config)
      }).toThrow('no such file or directory')
    })

    it('loads valid single EC PFX', () => {
      const passphrase = 'ec_pfx_passphrase'

      createPfxFiles(pfxFilepath, pfxPassphraseFilepath, passphrase, 'ec')

      const url = createUniqueUrl()
      const config = createSinglePfxConfig(
        url,
        undefined,
        pfxFilepath,
        pfxPassphraseFilepath,
      )
      const pfxFileData = fs.readFileSync(pfxFilepath)

      loadClientCertificateConfig(config)

      const options = clientCertificateStoreSingleton.getClientCertificateAgentOptionsForUrl(
        urllib.parse(url),
      )

      expect(options).not.toBeNull()
      expect(options.cert).toHaveLength(0)
      expect(options.pfx.length).toEqual(1)
      expect(options.pfx[0].buf).toEqual(pfxFileData)
      expect(options.pfx[0].passphrase).toEqual(passphrase)
    })

    it('loads valid single PFX', () => {
      const passphrase = 'a_passphrase'

      createPfxFiles(pfxFilepath, pfxPassphraseFilepath, passphrase)

      const url = createUniqueUrl()
      const config = createSinglePfxConfig(
        url,
        undefined,
        pfxFilepath,
        pfxPassphraseFilepath,
      )
      const pfxFileData = fs.readFileSync(pfxFilepath)

      loadClientCertificateConfig(config)

      const options = clientCertificateStoreSingleton.getClientCertificateAgentOptionsForUrl(
        urllib.parse(url),
      )

      expect(options).not.toBeNull()
      expect(options.cert).toHaveLength(0)
      expect(options.pfx.length).toEqual(1)
      expect(options.pfx[0].buf).toEqual(pfxFileData)
      expect(options.pfx[0].passphrase).toEqual(passphrase)
    })

    it('detects invalid PFX passphrase', () => {
      const passphrase = 'a_passphrase'

      createPfxFiles(pfxFilepath, undefined, passphrase)
      fs.writeFileSync(pfxPassphraseFilepath, 'not-a-passphrase')

      const config = createSinglePfxConfig(
        createUniqueUrl(),
        undefined,
        pfxFilepath,
        pfxPassphraseFilepath,
      )

      expect(() => {
        loadClientCertificateConfig(config)
      }).toThrow('mac verify failure')
    })

    it('detects missing PFX passphrase file', () => {
      const passphrase = 'a_passphrase'

      createPfxFiles(pfxFilepath, undefined, passphrase)

      const config = createSinglePfxConfig(
        createUniqueUrl(),
        undefined,
        pfxFilepath,
        pfxPassphraseFilepath,
      )

      expect(() => {
        loadClientCertificateConfig(config)
      }).toThrow('mac verify failure')
    })

    it('detects invalid PFX file', () => {
      fs.writeFileSync(pfxFilepath, 'not-a-pfx')

      const config = createSinglePfxConfig(
        createUniqueUrl(),
        undefined,
        pfxFilepath,
        pfxPassphraseFilepath,
      )

      expect(() => {
        loadClientCertificateConfig(config)
      }).toThrow('Unable to load PFX file: not enough data')
    })

    it('detects missing PFX file', () => {
      const config = createSinglePfxConfig(
        createUniqueUrl(),
        undefined,
        pfxFilepath,
        pfxPassphraseFilepath,
      )

      expect(() => {
        loadClientCertificateConfig(config)
      }).toThrow('Unable to load PFX file: not enough data')
    })

    it('detects neither PEM nor PFX supplied', () => {
      const config = {
        projectRoot: __dirname,
        clientCertificates: [
          {
            url: createUniqueUrl(),
            ca: [],
            certs: [
            ],
          },
        ],
      }

      expect(() => {
        loadClientCertificateConfig(config)
      }).toThrow('Either PEM or PFX must be supplied')
    })
  })
})
