'use strict'

const FakeTimers = require('@sinonjs/fake-timers')
const { test, after } = require('node:test')
const net = require('node:net')
const { isIP } = net
const { lookup } = require('node:dns')
const { createServer } = require('node:http')
const { createServer: createSecureServer } = require('node:https')
const { once } = require('node:events')

const { tspl } = require('@matteo.collina/tspl')
const pem = require('@metcoder95/https-pem')

const { interceptors, Agent, Client, Pool, request } = require('../..')
const { dns, cache, deduplicate } = interceptors

// Helper to check if IPv6 is available for localhost
// This is called synchronously at test definition time
function hasIPv6LocalhostSync () {
  let ipv6Available = false
  try {
    const addresses = lookup.sync('localhost', { all: true, family: 6 })
    ipv6Available = addresses != null && addresses.length > 0 && addresses.some(addr => addr.family === 6)
  } catch (err) {
    // If lookup fails, assume IPv6 is not available
    ipv6Available = false
  }
  return ipv6Available
}

// Check IPv6 availability once at module load time
const ipv6Available = hasIPv6LocalhostSync()

test('Should validate options', t => {
  t = tspl(t, { plan: 11 })

  t.throws(() => dns({ dualStack: 'true' }), { code: 'UND_ERR_INVALID_ARG' })
  t.throws(() => dns({ dualStack: 0 }), { code: 'UND_ERR_INVALID_ARG' })
  t.throws(() => dns({ affinity: '4' }), { code: 'UND_ERR_INVALID_ARG' })
  t.throws(() => dns({ affinity: 7 }), { code: 'UND_ERR_INVALID_ARG' })
  t.throws(() => dns({ maxTTL: -1 }), { code: 'UND_ERR_INVALID_ARG' })
  t.throws(() => dns({ maxTTL: '0' }), { code: 'UND_ERR_INVALID_ARG' })
  t.throws(() => dns({ maxItems: '1' }), { code: 'UND_ERR_INVALID_ARG' })
  t.throws(() => dns({ maxItems: -1 }), { code: 'UND_ERR_INVALID_ARG' })
  t.throws(() => dns({ lookup: {} }), { code: 'UND_ERR_INVALID_ARG' })
  t.throws(() => dns({ pick: [] }), { code: 'UND_ERR_INVALID_ARG' })
  t.throws(() => dns({ storage: new Map() }), { code: 'UND_ERR_INVALID_ARG' })
})

test('Should automatically resolve IPs (dual stack)', async t => {
  t = tspl(t, { plan: 8 })

  const hostsnames = []
  const server = createServer({ joinDuplicateHeaders: true })
  const requestOptions = {
    method: 'GET',
    path: '/',
    headers: {
      'content-type': 'application/json'
    }
  }

  server.on('request', (req, res) => {
    res.writeHead(200, { 'content-type': 'text/plain' })
    res.end('hello world!')
  })

  server.listen(0)

  await once(server, 'listening')

  const client = new Agent().compose([
    dispatch => {
      return (opts, handler) => {
        const url = new URL(opts.origin)

        t.equal(hostsnames.includes(url.hostname), false)

        if (url.hostname[0] === '[') {
          // [::1] -> ::1
          t.equal(isIP(url.hostname.slice(1, 4)), 6)
        } else {
          t.equal(isIP(url.hostname), 4)
        }

        hostsnames.push(url.hostname)

        return dispatch(opts, handler)
      }
    },
    dns({
      lookup: (_origin, _opts, cb) => {
        cb(null, [
          {
            address: '::1',
            family: 6
          },
          {
            address: '127.0.0.1',
            family: 4
          }
        ])
      }
    })
  ])

  after(async () => {
    await client.close()
    server.close()

    await once(server, 'close')
  })

  const response = await client.request({
    ...requestOptions,
    origin: `http://localhost:${server.address().port}`
  })

  t.equal(response.statusCode, 200)
  t.equal(await response.body.text(), 'hello world!')

  const response2 = await client.request({
    ...requestOptions,
    origin: `http://localhost:${server.address().port}`
  })

  t.equal(response2.statusCode, 200)
  t.equal(await response2.body.text(), 'hello world!')
})

test('Should respect DNS origin hostname for SNI on TLS', async t => {
  t = tspl(t, { plan: 12 })

  const hostsnames = []
  const server = createSecureServer(pem)
  const requestOptions = {
    method: 'GET',
    path: '/',
    headers: {
      'content-type': 'application/json'
    }
  }

  server.on('request', (req, res) => {
    t.equal(req.headers.host, `localhost:${server.address().port}`)
    res.writeHead(200, { 'content-type': 'text/plain' })
    res.end('hello world!')
  })

  server.listen(0)

  await once(server, 'listening')

  const client = new Agent({
    connect: {
      rejectUnauthorized: false
    }
  }).compose([
    dispatch => {
      return (opts, handler) => {
        const url = new URL(opts.origin)

        t.equal(hostsnames.includes(url.hostname), false)
        t.equal(opts.servername, 'localhost')

        if (url.hostname[0] === '[') {
          // [::1] -> ::1
          t.equal(isIP(url.hostname.slice(1, 4)), 6)
        } else {
          t.equal(isIP(url.hostname), 4)
        }

        hostsnames.push(url.hostname)

        return dispatch(opts, handler)
      }
    },
    dns({
      lookup: (_origin, _opts, cb) => {
        cb(null, [
          {
            address: '::1',
            family: 6
          },
          {
            address: '127.0.0.1',
            family: 4
          }
        ])
      }
    })
  ])

  after(async () => {
    await client.close()
    server.close()

    await once(server, 'close')
  })

  const response = await client.request({
    ...requestOptions,
    origin: `https://localhost:${server.address().port}`
  })

  t.equal(response.statusCode, 200)
  t.equal(await response.body.text(), 'hello world!')

  const response2 = await client.request({
    ...requestOptions,
    origin: `https://localhost:${server.address().port}`
  })

  t.equal(response2.statusCode, 200)
  t.equal(await response2.body.text(), 'hello world!')
})

test('#5573 - Should preserve DNS origin hostname on HTTP sockets', async context => {
  const t = tspl(context, { plan: 5 })

  const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
    res.end('hello world!')
  })

  server.listen(0, '127.0.0.1')
  await once(server, 'listening')

  const client = new Agent().compose(dns({
    dualStack: false,
    lookup (_origin, _opts, cb) {
      cb(null, [{ address: '127.0.0.1', family: 4 }])
    }
  }))

  context.after(async () => {
    await client.close()
    await new Promise(resolve => server.close(resolve))
  })

  const originalConnect = net.connect
  let connectOptions

  net.connect = function (...args) {
    connectOptions = args[0]
    return originalConnect.apply(this, args)
  }
  context.after(() => {
    net.connect = originalConnect
  })

  const response = await client.request({
    method: 'GET',
    path: '/',
    origin: `http://localhost:${server.address().port}`
  })

  t.equal(response.statusCode, 200)
  t.equal(await response.body.text(), 'hello world!')
  t.equal(connectOptions.host, 'localhost')

  await new Promise((resolve, reject) => {
    connectOptions.lookup(connectOptions.host, {}, (err, address, family) => {
      if (err) {
        reject(err)
        return
      }

      t.equal(address, '127.0.0.1')
      t.equal(family, 4)
      resolve()
    })
  })
})

test('Should recover on network errors (dual stack - 4)', async t => {
  t = tspl(t, { plan: 7 })

  let counter = 0
  const server = createServer({ joinDuplicateHeaders: true })
  const requestOptions = {
    method: 'GET',
    path: '/',
    headers: {
      'content-type': 'application/json'
    }
  }

  server.on('request', (req, res) => {
    res.writeHead(200, { 'content-type': 'text/plain' })
    res.end('hello world!')
  })

  server.listen(0, '::1')

  await once(server, 'listening')

  const client = new Agent().compose([
    dispatch => {
      return (opts, handler) => {
        ++counter
        const url = new URL(opts.origin)

        switch (counter) {
          case 1:
            t.equal(isIP(url.hostname), 4)
            break

          case 2:
            // [::1] -> ::1
            t.equal(isIP(url.hostname.slice(1, 4)), 6)
            break

          case 3:
            // [::1] -> ::1
            t.equal(isIP(url.hostname.slice(1, 4)), 6)
            break
          default:
            t.fail('should not reach this point')
        }

        return dispatch(opts, handler)
      }
    },
    dns({
      lookup: (_origin, _opts, cb) => {
        cb(null, [
          {
            address: '::1',
            family: 6
          },
          {
            address: '127.0.0.1',
            family: 4
          }
        ])
      }
    })
  ])

  after(async () => {
    await client.close()
    server.close()

    await once(server, 'close')
  })

  const response = await client.request({
    ...requestOptions,
    origin: `http://localhost:${server.address().port}`
  })

  t.equal(response.statusCode, 200)
  t.equal(await response.body.text(), 'hello world!')

  const response2 = await client.request({
    ...requestOptions,
    origin: `http://localhost:${server.address().port}`
  })

  t.equal(response2.statusCode, 200)
  t.equal(await response2.body.text(), 'hello world!')
})

test('Should recover on network errors (dual stack - 6)', async t => {
  t = tspl(t, { plan: 7 })

  let counter = 0
  const server = createServer({ joinDuplicateHeaders: true })
  const requestOptions = {
    method: 'GET',
    path: '/',
    headers: {
      'content-type': 'application/json'
    }
  }

  server.on('request', (req, res) => {
    res.writeHead(200, { 'content-type': 'text/plain' })
    res.end('hello world!')
  })

  server.listen(0, '127.0.0.1')

  await once(server, 'listening')

  const client = new Agent().compose([
    dispatch => {
      return (opts, handler) => {
        ++counter
        const url = new URL(opts.origin)

        switch (counter) {
          case 1:
            t.equal(isIP(url.hostname), 4)
            break

          case 2:
            // [::1] -> ::1
            t.equal(isIP(url.hostname.slice(1, 4)), 6)
            break

          case 3:
            // [::1] -> ::1
            t.equal(isIP(url.hostname), 4)
            break
          default:
            t.fail('should not reach this point')
        }

        return dispatch(opts, handler)
      }
    },
    dns({
      lookup: (_origin, _opts, cb) => {
        cb(null, [
          {
            address: '::1',
            family: 6
          },
          {
            address: '127.0.0.1',
            family: 4
          }
        ])
      }
    })
  ])

  after(async () => {
    await client.close()
    server.close()

    await once(server, 'close')
  })

  const response = await client.request({
    ...requestOptions,
    origin: `http://localhost:${server.address().port}`
  })

  t.equal(response.statusCode, 200)
  t.equal(await response.body.text(), 'hello world!')

  const response2 = await client.request({
    ...requestOptions,
    origin: `http://localhost:${server.address().port}`
  })

  t.equal(response2.statusCode, 200)
  t.equal(await response2.body.text(), 'hello world!')
})

test('Should throw when on dual-stack disabled (4)', async t => {
  t = tspl(t, { plan: 2 })

  let counter = 0
  const requestOptions = {
    method: 'GET',
    path: '/',
    headers: {
      'content-type': 'application/json'
    }
  }

  const client = new Agent().compose([
    dispatch => {
      return (opts, handler) => {
        ++counter
        const url = new URL(opts.origin)

        switch (counter) {
          case 1:
            t.equal(isIP(url.hostname), 4)
            break

          default:
            t.fail('should not reach this point')
        }

        return dispatch(opts, handler)
      }
    },
    dns({ dualStack: false, affinity: 4 })
  ])

  const promise = client.request({
    ...requestOptions,
    origin: 'http://localhost:1234'
  })

  await t.rejects(promise, 'ECONNREFUSED')

  await t.completed
})

test('Should throw when on dual-stack disabled (6)', { skip: !ipv6Available }, async t => {
  t = tspl(t, { plan: 2 })

  let counter = 0
  const requestOptions = {
    method: 'GET',
    path: '/',
    headers: {
      'content-type': 'application/json'
    }
  }

  const client = new Agent().compose([
    dispatch => {
      return (opts, handler) => {
        ++counter
        const url = new URL(opts.origin)

        switch (counter) {
          case 1:
            // [::1] -> ::1
            t.equal(isIP(url.hostname.slice(1, 4)), 6)
            break

          default:
            t.fail('should not reach this point')
        }

        return dispatch(opts, handler)
      }
    },
    dns({ dualStack: false, affinity: 6 })
  ])

  const promise = client.request({
    ...requestOptions,
    origin: 'http://localhost:9999'
  })

  await t.rejects(promise, 'ECONNREFUSED')

  await t.completed
})

test('Should automatically resolve IPs (dual stack disabled - 4)', async t => {
  t = tspl(t, { plan: 6 })

  let counter = 0
  const server = createServer({ joinDuplicateHeaders: true })
  const requestOptions = {
    method: 'GET',
    path: '/',
    headers: {
      'content-type': 'application/json'
    }
  }

  server.on('request', (req, res) => {
    res.writeHead(200, { 'content-type': 'text/plain' })
    res.end('hello world!')
  })

  server.listen(0)

  await once(server, 'listening')

  const client = new Agent().compose([
    dispatch => {
      return (opts, handler) => {
        ++counter
        const url = new URL(opts.origin)

        switch (counter) {
          case 1:
            t.equal(isIP(url.hostname), 4)
            break

          case 2:
            // [::1] -> ::1
            t.equal(isIP(url.hostname), 4)
            break
          default:
            t.fail('should not reach this point')
        }

        return dispatch(opts, handler)
      }
    },
    dns({ dualStack: false })
  ])

  after(async () => {
    await client.close()
    server.close()

    await once(server, 'close')
  })

  const response = await client.request({
    ...requestOptions,
    origin: `http://localhost:${server.address().port}`
  })

  t.equal(response.statusCode, 200)
  t.equal(await response.body.text(), 'hello world!')

  const response2 = await client.request({
    ...requestOptions,
    origin: `http://localhost:${server.address().port}`
  })

  t.equal(response2.statusCode, 200)
  t.equal(await response2.body.text(), 'hello world!')
})

test('Should automatically resolve IPs (dual stack disabled - 6)', { skip: !ipv6Available }, async t => {
  t = tspl(t, { plan: 6 })

  let counter = 0
  const server = createServer({ joinDuplicateHeaders: true })
  const requestOptions = {
    method: 'GET',
    path: '/',
    headers: {
      'content-type': 'application/json'
    }
  }

  server.on('request', (req, res) => {
    res.writeHead(200, { 'content-type': 'text/plain' })
    res.end('hello world!')
  })

  server.listen(0)

  await once(server, 'listening')

  const client = new Agent().compose([
    dispatch => {
      return (opts, handler) => {
        ++counter
        const url = new URL(opts.origin)

        switch (counter) {
          case 1:
            // [::1] -> ::1
            t.equal(isIP(url.hostname.slice(1, 4)), 6)
            break

          case 2:
            // [::1] -> ::1
            t.equal(isIP(url.hostname.slice(1, 4)), 6)
            break
          default:
            t.fail('should not reach this point')
        }

        return dispatch(opts, handler)
      }
    },
    dns({ dualStack: false, affinity: 6 })
  ])

  after(async () => {
    await client.close()
    server.close()

    await once(server, 'close')
  })

  const response = await client.request({
    ...requestOptions,
    origin: `http://localhost:${server.address().port}`
  })

  t.equal(response.statusCode, 200)
  t.equal(await response.body.text(), 'hello world!')

  const response2 = await client.request({
    ...requestOptions,
    origin: `http://localhost:${server.address().port}`
  })

  t.equal(response2.statusCode, 200)
  t.equal(await response2.body.text(), 'hello world!')
})

test('Should we handle TTL (4)', async t => {
  t = tspl(t, { plan: 10 })

  const clock = FakeTimers.install()
  let counter = 0
  let lookupCounter = 0
  const server = createServer({ joinDuplicateHeaders: true })
  const requestOptions = {
    method: 'GET',
    path: '/',
    headers: {
      'content-type': 'application/json'
    }
  }

  server.on('request', (req, res) => {
    res.writeHead(200, { 'content-type': 'text/plain' })
    res.end('hello world!')
  })

  server.listen(0, '127.0.0.1')

  await once(server, 'listening')

  const client = new Agent().compose([
    dispatch => {
      return (opts, handler) => {
        ++counter
        const url = new URL(opts.origin)

        switch (counter) {
          case 1:
            t.equal(isIP(url.hostname), 4)
            break

          case 2:
            t.equal(isIP(url.hostname), 4)
            break

          case 3:
            t.equal(isIP(url.hostname), 4)
            break
          default:
            t.fail('should not reach this point')
        }

        return dispatch(opts, handler)
      }
    },
    dns({
      dualStack: false,
      affinity: 4,
      maxTTL: 400,
      lookup: (origin, opts, cb) => {
        ++lookupCounter
        lookup(
          origin.hostname,
          { all: true, family: opts.affinity },
          cb
        )
      }
    })
  ])

  after(async () => {
    clock.uninstall()
    await client.close()
    server.close()

    await once(server, 'close')
  })

  const response = await client.request({
    ...requestOptions,
    origin: `http://localhost:${server.address().port}`
  })

  t.equal(response.statusCode, 200)
  t.equal(await response.body.text(), 'hello world!')

  clock.tick(200)

  const response2 = await client.request({
    ...requestOptions,
    origin: `http://localhost:${server.address().port}`
  })

  t.equal(response2.statusCode, 200)
  t.equal(await response2.body.text(), 'hello world!')

  clock.tick(300)

  const response3 = await client.request({
    ...requestOptions,
    origin: `http://localhost:${server.address().port}`
  })

  t.equal(response3.statusCode, 200)
  t.equal(await response3.body.text(), 'hello world!')

  t.equal(lookupCounter, 2)
})

test('Should we handle TTL (6)', { skip: !ipv6Available }, async t => {
  t = tspl(t, { plan: 10 })

  const clock = FakeTimers.install()
  let counter = 0
  let lookupCounter = 0
  const server = createServer({ joinDuplicateHeaders: true })
  const requestOptions = {
    method: 'GET',
    path: '/',
    headers: {
      'content-type': 'application/json'
    }
  }

  server.on('request', (req, res) => {
    res.writeHead(200, { 'content-type': 'text/plain' })
    res.end('hello world!')
  })

  server.listen(0, '::1')

  await once(server, 'listening')

  const client = new Agent().compose([
    dispatch => {
      return (opts, handler) => {
        ++counter
        const url = new URL(opts.origin)

        switch (counter) {
          case 1:
            // [::1] -> ::1
            t.equal(isIP(url.hostname.slice(1, 4)), 6)
            break

          case 2:
            // [::1] -> ::1
            t.equal(isIP(url.hostname.slice(1, 4)), 6)
            break

          case 3:
            // [::1] -> ::1
            t.equal(isIP(url.hostname.slice(1, 4)), 6)
            break
          default:
            t.fail('should not reach this point')
        }

        return dispatch(opts, handler)
      }
    },
    dns({
      dualStack: false,
      affinity: 6,
      maxTTL: 400,
      lookup: (origin, opts, cb) => {
        ++lookupCounter
        lookup(
          origin.hostname,
          { all: true, family: opts.affinity },
          cb
        )
      }
    })
  ])

  after(async () => {
    clock.uninstall()
    await client.close()
    server.close()

    await once(server, 'close')
  })

  const response = await client.request({
    ...requestOptions,
    origin: `http://localhost:${server.address().port}`
  })

  t.equal(response.statusCode, 200)
  t.equal(await response.body.text(), 'hello world!')

  clock.tick(200)

  const response2 = await client.request({
    ...requestOptions,
    origin: `http://localhost:${server.address().port}`
  })

  t.equal(response2.statusCode, 200)
  t.equal(await response2.body.text(), 'hello world!')

  clock.tick(300)

  const response3 = await client.request({
    ...requestOptions,
    origin: `http://localhost:${server.address().port}`
  })

  t.equal(response3.statusCode, 200)
  t.equal(await response3.body.text(), 'hello world!')
  t.equal(lookupCounter, 2)
})

test('Should set lowest TTL between resolved and option maxTTL', async t => {
  t = tspl(t, { plan: 9 })

  const clock = FakeTimers.install()
  let lookupCounter = 0
  const server = createServer({ joinDuplicateHeaders: true })
  const requestOptions = {
    method: 'GET',
    path: '/',
    headers: {
      'content-type': 'application/json'
    }
  }

  server.on('request', (req, res) => {
    res.writeHead(200, { 'content-type': 'text/plain' })
    res.end('hello world!')
  })

  server.listen(0, '127.0.0.1')

  await once(server, 'listening')

  const client = new Agent().compose(
    dns({
      dualStack: false,
      affinity: 4,
      maxTTL: 200,
      lookup: (origin, opts, cb) => {
        ++lookupCounter
        cb(null, [
          {
            address: '127.0.0.1',
            family: 4,
            ttl: lookupCounter === 1 ? 50 : 500
          }
        ])
      }
    })
  )

  after(async () => {
    clock.uninstall()
    await client.close()
    server.close()

    await once(server, 'close')
  })

  const response = await client.request({
    ...requestOptions,
    origin: `http://localhost:${server.address().port}`
  })

  t.equal(response.statusCode, 200)
  t.equal(await response.body.text(), 'hello world!')

  clock.tick(100)

  // 100ms: lookup since ttl = Math.min(50, maxTTL: 200)
  const response2 = await client.request({
    ...requestOptions,
    origin: `http://localhost:${server.address().port}`
  })

  t.equal(response2.statusCode, 200)
  t.equal(await response2.body.text(), 'hello world!')

  clock.tick(100)

  // 100ms: cached since ttl = Math.min(500, maxTTL: 200)
  const response3 = await client.request({
    ...requestOptions,
    origin: `http://localhost:${server.address().port}`
  })

  t.equal(response3.statusCode, 200)
  t.equal(await response3.body.text(), 'hello world!')

  clock.tick(150)

  // 250ms: lookup since ttl = Math.min(500, maxTTL: 200)
  const response4 = await client.request({
    ...requestOptions,
    origin: `http://localhost:${server.address().port}`
  })

  t.equal(response4.statusCode, 200)
  t.equal(await response4.body.text(), 'hello world!')

  t.equal(lookupCounter, 3)
})

test('Should use all dns entries (dual stack)', async t => {
  t = tspl(t, { plan: 16 })

  let counter = 0
  let lookupCounter = 0
  const server = createServer({ joinDuplicateHeaders: true })
  const requestOptions = {
    method: 'GET',
    path: '/',
    headers: {
      'content-type': 'application/json'
    }
  }

  server.on('request', (req, res) => {
    res.writeHead(200, { 'content-type': 'text/plain' })
    res.end('hello world!')
  })

  server.listen(0)

  await once(server, 'listening')

  const client = new Agent().compose([
    dispatch => {
      return (opts, handler) => {
        ++counter
        const url = new URL(opts.origin)
        switch (counter) {
          case 1:
            t.equal(url.hostname, '1.1.1.1')
            break

          case 2:
            t.equal(url.hostname, '[::1]')
            break

          case 3:
            t.equal(url.hostname, '2.2.2.2')
            break

          case 4:
            t.equal(url.hostname, '[::2]')
            break

          case 5:
            t.equal(url.hostname, '1.1.1.1')
            break
          default:
            t.fail('should not reach this point')
        }

        url.hostname = '127.0.0.1'
        opts.origin = url.toString()
        return dispatch(opts, handler)
      }
    },
    dns({
      lookup (origin, opts, cb) {
        lookupCounter++
        cb(null, [
          { address: '::1', family: 6 },
          { address: '::2', family: 6 },
          { address: '1.1.1.1', family: 4 },
          { address: '2.2.2.2', family: 4 }
        ])
      }
    })
  ])

  after(async () => {
    await client.close()
    server.close()

    await once(server, 'close')
  })

  for (let i = 0; i < 5; i++) {
    const response = await client.request({
      ...requestOptions,
      origin: `http://localhost:${server.address().port}`
    })

    t.equal(response.statusCode, 200)
    t.equal(await response.body.text(), 'hello world!')
  }

  t.equal(lookupCounter, 1)
})

test('Should use all dns entries (dual stack disabled - 4)', async t => {
  t = tspl(t, { plan: 10 })

  let counter = 0
  let lookupCounter = 0
  const server = createServer({ joinDuplicateHeaders: true })
  const requestOptions = {
    method: 'GET',
    path: '/',
    headers: {
      'content-type': 'application/json'
    }
  }

  server.on('request', (req, res) => {
    res.writeHead(200, { 'content-type': 'text/plain' })
    res.end('hello world!')
  })

  server.listen(0)

  await once(server, 'listening')

  const client = new Agent().compose([
    dispatch => {
      return (opts, handler) => {
        ++counter
        const url = new URL(opts.origin)

        switch (counter) {
          case 1:
            t.equal(url.hostname, '1.1.1.1')
            break

          case 2:
            t.equal(url.hostname, '2.2.2.2')
            break

          case 3:
            t.equal(url.hostname, '1.1.1.1')
            break
          default:
            t.fail('should not reach this point')
        }

        url.hostname = '127.0.0.1'
        opts.origin = url.toString()
        return dispatch(opts, handler)
      }
    },
    dns({
      dualStack: false,
      lookup (origin, opts, cb) {
        lookupCounter++
        cb(null, [
          { address: '1.1.1.1', family: 4 },
          { address: '2.2.2.2', family: 4 }
        ])
      }
    })
  ])

  after(async () => {
    await client.close()
    server.close()

    await once(server, 'close')
  })

  const response1 = await client.request({
    ...requestOptions,
    origin: `http://localhost:${server.address().port}`
  })

  t.equal(response1.statusCode, 200)
  t.equal(await response1.body.text(), 'hello world!')

  const response2 = await client.request({
    ...requestOptions,
    origin: `http://localhost:${server.address().port}`
  })

  t.equal(response2.statusCode, 200)
  t.equal(await response2.body.text(), 'hello world!')

  const response3 = await client.request({
    ...requestOptions,
    origin: `http://localhost:${server.address().port}`
  })

  t.equal(response3.statusCode, 200)
  t.equal(await response3.body.text(), 'hello world!')

  t.equal(lookupCounter, 1)
})

test('Should use all dns entries (dual stack disabled - 6)', async t => {
  t = tspl(t, { plan: 10 })

  let counter = 0
  let lookupCounter = 0
  const server = createServer({ joinDuplicateHeaders: true })
  const requestOptions = {
    method: 'GET',
    path: '/',
    headers: {
      'content-type': 'application/json'
    }
  }

  server.on('request', (req, res) => {
    res.writeHead(200, { 'content-type': 'text/plain' })
    res.end('hello world!')
  })

  server.listen(0)

  await once(server, 'listening')

  const client = new Agent().compose([
    dispatch => {
      return (opts, handler) => {
        ++counter
        const url = new URL(opts.origin)

        switch (counter) {
          case 1:
            t.equal(url.hostname, '[::1]')
            break

          case 2:
            t.equal(url.hostname, '[::2]')
            break

          case 3:
            t.equal(url.hostname, '[::1]')
            break
          default:
            t.fail('should not reach this point')
        }

        url.hostname = '127.0.0.1'
        opts.origin = url.toString()
        return dispatch(opts, handler)
      }
    },
    dns({
      dualStack: false,
      affinity: 6,
      lookup (origin, opts, cb) {
        lookupCounter++
        cb(null, [
          { address: '::1', family: 6 },
          { address: '::2', family: 6 }
        ])
      }
    })
  ])

  after(async () => {
    await client.close()
    server.close()

    await once(server, 'close')
  })

  const response1 = await client.request({
    ...requestOptions,
    origin: `http://localhost:${server.address().port}`
  })

  t.equal(response1.statusCode, 200)
  t.equal(await response1.body.text(), 'hello world!')

  const response2 = await client.request({
    ...requestOptions,
    origin: `http://localhost:${server.address().port}`
  })

  t.equal(response2.statusCode, 200)
  t.equal(await response2.body.text(), 'hello world!')

  const response3 = await client.request({
    ...requestOptions,
    origin: `http://localhost:${server.address().port}`
  })

  t.equal(response3.statusCode, 200)
  t.equal(await response3.body.text(), 'hello world!')

  t.equal(lookupCounter, 1)
})

test('Should handle single family resolved (dual stack)', async t => {
  t = tspl(t, { plan: 7 })

  const clock = FakeTimers.install()
  let counter = 0
  let lookupCounter = 0
  const server = createServer({ joinDuplicateHeaders: true })
  const requestOptions = {
    method: 'GET',
    path: '/',
    headers: {
      'content-type': 'application/json'
    }
  }

  server.on('request', (req, res) => {
    res.writeHead(200, { 'content-type': 'text/plain' })
    res.end('hello world!')
  })

  server.listen(0)

  await once(server, 'listening')

  const client = new Agent().compose([
    dispatch => {
      return (opts, handler) => {
        ++counter
        const url = new URL(opts.origin)

        switch (counter) {
          case 1:
            t.equal(isIP(url.hostname), 4)
            break

          case 2:
            // [::1] -> ::1
            t.equal(isIP(url.hostname.slice(1, 4)), 6)
            break
          default:
            t.fail('should not reach this point')
        }

        return dispatch(opts, handler)
      }
    },
    dns({
      lookup (origin, opts, cb) {
        lookupCounter++
        if (lookupCounter === 1) {
          cb(null, [
            { address: '127.0.0.1', family: 4, ttl: 50 }
          ])
        } else {
          cb(null, [
            { address: '::1', family: 6, ttl: 50 }
          ])
        }
      }
    })
  ])

  after(async () => {
    clock.uninstall()
    await client.close()
    server.close()

    await once(server, 'close')
  })

  const response = await client.request({
    ...requestOptions,
    origin: `http://localhost:${server.address().port}`
  })

  t.equal(response.statusCode, 200)
  t.equal(await response.body.text(), 'hello world!')

  clock.tick(100)

  const response2 = await client.request({
    ...requestOptions,
    origin: `http://localhost:${server.address().port}`
  })

  t.equal(response2.statusCode, 200)
  t.equal(await response2.body.text(), 'hello world!')

  t.equal(lookupCounter, 2)
})

test('Should prefer affinity (dual stack - 4)', async t => {
  t = tspl(t, { plan: 10 })

  const clock = FakeTimers.install()
  let counter = 0
  let lookupCounter = 0
  const server = createServer({ joinDuplicateHeaders: true })
  const requestOptions = {
    method: 'GET',
    path: '/',
    headers: {
      'content-type': 'application/json'
    }
  }

  server.on('request', (req, res) => {
    res.writeHead(200, { 'content-type': 'text/plain' })
    res.end('hello world!')
  })

  server.listen(0)

  await once(server, 'listening')

  const client = new Agent().compose([
    dispatch => {
      return (opts, handler) => {
        ++counter
        const url = new URL(opts.origin)

        switch (counter) {
          case 1:
            t.equal(url.hostname, '1.1.1.1')
            break

          case 2:
            t.equal(url.hostname, '2.2.2.2')
            break

          case 3:
            t.equal(url.hostname, '1.1.1.1')
            break
          default:
            t.fail('should not reach this point')
        }

        url.hostname = '127.0.0.1'
        opts.origin = url.toString()
        return dispatch(opts, handler)
      }
    },
    dns({
      affinity: 4,
      lookup (origin, opts, cb) {
        lookupCounter++
        cb(null, [
          { address: '1.1.1.1', family: 4 },
          { address: '2.2.2.2', family: 4 },
          { address: '::1', family: 6 },
          { address: '::2', family: 6 }
        ])
      }
    })
  ])

  after(async () => {
    clock.uninstall()
    await client.close()
    server.close()

    await once(server, 'close')
  })

  const response = await client.request({
    ...requestOptions,
    origin: `http://localhost:${server.address().port}`
  })

  t.equal(response.statusCode, 200)
  t.equal(await response.body.text(), 'hello world!')

  clock.tick(100)

  const response2 = await client.request({
    ...requestOptions,
    origin: `http://localhost:${server.address().port}`
  })

  t.equal(response2.statusCode, 200)
  t.equal(await response2.body.text(), 'hello world!')

  const response3 = await client.request({
    ...requestOptions,
    origin: `http://localhost:${server.address().port}`
  })

  t.equal(response3.statusCode, 200)
  t.equal(await response3.body.text(), 'hello world!')

  t.equal(lookupCounter, 1)
})

test('Should prefer affinity (dual stack - 6)', async t => {
  t = tspl(t, { plan: 10 })

  const clock = FakeTimers.install()
  let counter = 0
  let lookupCounter = 0
  const server = createServer({ joinDuplicateHeaders: true })
  const requestOptions = {
    method: 'GET',
    path: '/',
    headers: {
      'content-type': 'application/json'
    }
  }

  server.on('request', (req, res) => {
    res.writeHead(200, { 'content-type': 'text/plain' })
    res.end('hello world!')
  })

  server.listen(0)

  await once(server, 'listening')

  const client = new Agent().compose([
    dispatch => {
      return (opts, handler) => {
        ++counter
        const url = new URL(opts.origin)

        switch (counter) {
          case 1:
            t.equal(url.hostname, '[::1]')
            break

          case 2:
            t.equal(url.hostname, '[::2]')
            break

          case 3:
            t.equal(url.hostname, '[::1]')
            break
          default:
            t.fail('should not reach this point')
        }

        url.hostname = '127.0.0.1'
        opts.origin = url.toString()
        return dispatch(opts, handler)
      }
    },
    dns({
      affinity: 6,
      lookup (origin, opts, cb) {
        lookupCounter++
        cb(null, [
          { address: '1.1.1.1', family: 4 },
          { address: '2.2.2.2', family: 4 },
          { address: '::1', family: 6 },
          { address: '::2', family: 6 }
        ])
      }
    })
  ])

  after(async () => {
    clock.uninstall()
    await client.close()
    server.close()

    await once(server, 'close')
  })

  const response = await client.request({
    ...requestOptions,
    origin: `http://localhost:${server.address().port}`
  })

  t.equal(response.statusCode, 200)
  t.equal(await response.body.text(), 'hello world!')

  clock.tick(100)

  const response2 = await client.request({
    ...requestOptions,
    origin: `http://localhost:${server.address().port}`
  })

  t.equal(response2.statusCode, 200)
  t.equal(await response2.body.text(), 'hello world!')

  const response3 = await client.request({
    ...requestOptions,
    origin: `http://localhost:${server.address().port}`
  })

  t.equal(response3.statusCode, 200)
  t.equal(await response3.body.text(), 'hello world!')

  t.equal(lookupCounter, 1)
})

test('Should use resolved ports (4)', async t => {
  t = tspl(t, { plan: 5 })

  let lookupCounter = 0
  const server1 = createServer({ joinDuplicateHeaders: true })
  const server2 = createServer({ joinDuplicateHeaders: true })
  const requestOptions = {
    method: 'GET',
    path: '/',
    headers: {
      'content-type': 'application/json'
    }
  }

  server1.on('request', (req, res) => {
    res.writeHead(200, { 'content-type': 'text/plain' })
    res.end('hello world!')
  })

  server1.listen(0)

  server2.on('request', (req, res) => {
    res.writeHead(200, { 'content-type': 'text/plain' })
    res.end('hello world! (x2)')
  })
  server2.listen(0)

  await Promise.all([once(server1, 'listening'), once(server2, 'listening')])

  const client = new Agent().compose([
    dns({
      lookup (origin, opts, cb) {
        lookupCounter++
        cb(null, [
          { address: '127.0.0.1', family: 4, port: server1.address().port },
          { address: '127.0.0.1', family: 4, port: server2.address().port }
        ])
      }
    })
  ])

  after(async () => {
    await client.close()
    server1.close()
    server2.close()

    await Promise.all([once(server1, 'close'), once(server2, 'close')])
  })

  const response = await client.request({
    ...requestOptions,
    origin: 'http://localhost'
  })

  t.equal(response.statusCode, 200)
  t.equal(await response.body.text(), 'hello world!')

  const response2 = await client.request({
    ...requestOptions,
    origin: 'http://localhost'
  })

  t.equal(response2.statusCode, 200)
  t.equal(await response2.body.text(), 'hello world! (x2)')

  t.equal(lookupCounter, 1)
})

test('Should use resolved ports (6)', async t => {
  t = tspl(t, { plan: 5 })

  let lookupCounter = 0
  const server1 = createServer({ joinDuplicateHeaders: true })
  const server2 = createServer({ joinDuplicateHeaders: true })
  const requestOptions = {
    method: 'GET',
    path: '/',
    headers: {
      'content-type': 'application/json'
    }
  }

  server1.on('request', (req, res) => {
    res.writeHead(200, { 'content-type': 'text/plain' })
    res.end('hello world!')
  })

  server1.listen(0, '::1')

  server2.on('request', (req, res) => {
    res.writeHead(200, { 'content-type': 'text/plain' })
    res.end('hello world! (x2)')
  })
  server2.listen(0, '::1')

  await Promise.all([once(server1, 'listening'), once(server2, 'listening')])

  const client = new Agent().compose([
    dns({
      lookup (origin, opts, cb) {
        lookupCounter++
        cb(null, [
          { address: '::1', family: 6, port: server1.address().port },
          { address: '::1', family: 6, port: server2.address().port }
        ])
      }
    })
  ])

  after(async () => {
    await client.close()
    server1.close()
    server2.close()

    await Promise.all([once(server1, 'close'), once(server2, 'close')])
  })

  const response = await client.request({
    ...requestOptions,
    origin: 'http://localhost'
  })

  t.equal(response.statusCode, 200)
  t.equal(await response.body.text(), 'hello world!')

  const response2 = await client.request({
    ...requestOptions,
    origin: 'http://localhost'
  })

  t.equal(response2.statusCode, 200)
  t.equal(await response2.body.text(), 'hello world! (x2)')

  t.equal(lookupCounter, 1)
})

test('Should handle max cached items', async t => {
  t = tspl(t, { plan: 9 })

  let counter = 0
  const server1 = createServer({ joinDuplicateHeaders: true })
  const server2 = createServer({ joinDuplicateHeaders: true })
  const requestOptions = {
    method: 'GET',
    path: '/',
    headers: {
      'content-type': 'application/json'
    }
  }

  server1.on('request', (req, res) => {
    res.writeHead(200, { 'content-type': 'text/plain' })
    res.end('hello world!')
  })

  server1.listen(0)

  server2.on('request', (req, res) => {
    res.writeHead(200, { 'content-type': 'text/plain' })
    res.end('hello world! (x2)')
  })
  server2.listen(0)

  await Promise.all([once(server1, 'listening'), once(server2, 'listening')])

  const client = new Agent().compose([
    dispatch => {
      return (opts, handler) => {
        ++counter
        const url = new URL(opts.origin)

        switch (counter) {
          case 1:
            t.equal(isIP(url.hostname), 4)
            break

          case 2:
            // [::1] -> ::1
            t.equal(isIP(url.hostname.slice(1, 4)), 6)
            break

          case 3:
            t.equal(url.hostname, 'developer.mozilla.org')
            // Rewrite origin to avoid reaching internet
            opts.origin = `http://127.0.0.1:${server2.address().port}`
            break
          default:
            t.fails('should not reach this point')
        }

        return dispatch(opts, handler)
      }
    },
    dns({
      maxItems: 1,
      lookup: (_origin, _opts, cb) => {
        cb(null, [
          {
            address: '::1',
            family: 6
          },
          {
            address: '127.0.0.1',
            family: 4
          }
        ])
      }
    })
  ])

  after(async () => {
    await client.close()
    server1.close()
    server2.close()

    await Promise.all([once(server1, 'close'), once(server2, 'close')])
  })

  const response = await client.request({
    ...requestOptions,
    origin: `http://localhost:${server1.address().port}`
  })

  t.equal(response.statusCode, 200)
  t.equal(await response.body.text(), 'hello world!')

  const response2 = await client.request({
    ...requestOptions,
    origin: `http://localhost:${server1.address().port}`
  })

  t.equal(response2.statusCode, 200)
  t.equal(await response2.body.text(), 'hello world!')

  const response3 = await client.request({
    ...requestOptions,
    origin: 'https://developer.mozilla.org'
  })

  t.equal(response3.statusCode, 200)
  t.equal(await response3.body.text(), 'hello world! (x2)')
})

test('Should support external storage', async t => {
  t = tspl(t, { plan: 9 })

  let counter = 0
  const server1 = createServer({ joinDuplicateHeaders: true })
  const server2 = createServer({ joinDuplicateHeaders: true })
  const requestOptions = {
    method: 'GET',
    path: '/',
    headers: {
      'content-type': 'application/json'
    }
  }

  server1.on('request', (req, res) => {
    res.writeHead(200, { 'content-type': 'text/plain' })
    res.end('hello world!')
  })

  server1.listen(0)

  server2.on('request', (req, res) => {
    res.writeHead(200, { 'content-type': 'text/plain' })
    res.end('hello world! (x2)')
  })
  server2.listen(0)

  await Promise.all([once(server1, 'listening'), once(server2, 'listening')])

  const cache = new Map()
  const storage = {
    get (origin) {
      return cache.get(origin)
    },
    set (origin, records) {
      cache.set(origin, records)
    },
    delete (origin) {
      cache.delete(origin)
    },
    // simulate internal DNSStorage behaviour with `maxItems: 1` parameter
    full () {
      return cache.size === 1
    }
  }

  const client = new Agent().compose([
    dispatch => {
      return (opts, handler) => {
        ++counter
        const url = new URL(opts.origin)

        switch (counter) {
          case 1:
            t.equal(isIP(url.hostname), 4)
            break

          case 2:
            // [::1] -> ::1
            t.equal(isIP(url.hostname.slice(1, 4)), 6)
            break

          case 3:
            t.equal(url.hostname, 'developer.mozilla.org')
            // Rewrite origin to avoid reaching internet
            opts.origin = `http://127.0.0.1:${server2.address().port}`
            break
          default:
            t.fails('should not reach this point')
        }

        return dispatch(opts, handler)
      }
    },
    dns({
      storage,
      lookup: (_origin, _opts, cb) => {
        cb(null, [
          {
            address: '::1',
            family: 6
          },
          {
            address: '127.0.0.1',
            family: 4
          }
        ])
      }
    })
  ])

  after(async () => {
    await client.close()
    server1.close()
    server2.close()

    await Promise.all([once(server1, 'close'), once(server2, 'close')])
  })

  const response = await client.request({
    ...requestOptions,
    origin: `http://localhost:${server1.address().port}`
  })

  t.equal(response.statusCode, 200)
  t.equal(await response.body.text(), 'hello world!')

  const response2 = await client.request({
    ...requestOptions,
    origin: `http://localhost:${server1.address().port}`
  })

  t.equal(response2.statusCode, 200)
  t.equal(await response2.body.text(), 'hello world!')

  const response3 = await client.request({
    ...requestOptions,
    origin: 'https://developer.mozilla.org'
  })

  t.equal(response3.statusCode, 200)
  t.equal(await response3.body.text(), 'hello world! (x2)')
})

test('retry once with dual-stack', async t => {
  t = tspl(t, { plan: 2 })

  const requestOptions = {
    method: 'GET',
    path: '/',
    headers: {
      'content-type': 'application/json'
    }
  }

  let counter = 0
  const client = new Agent().compose([
    dispatch => {
      return (opts, handler) => {
        counter++
        return dispatch(opts, handler)
      }
    },
    dns({
      lookup: (_origin, _opts, cb) => {
        cb(null, [
          {
            address: '127.0.0.1',
            port: 3669,
            family: 4,
            ttl: 1000
          },
          {
            address: '::1',
            port: 3669,
            family: 6,
            ttl: 1000
          }
        ])
      }
    })
  ])

  after(async () => {
    await client.close()
  })

  await t.rejects(client.request({
    ...requestOptions,
    origin: 'http://localhost'
  }), 'ECONNREFUSED')

  t.equal(counter, 2)
})

test('Should handle ENOTFOUND response error', async t => {
  t = tspl(t, { plan: 3 })
  let lookupCounter = 0

  const requestOptions = {
    method: 'GET',
    path: '/',
    origin: 'http://localhost'
  }

  const client = new Agent().compose([
    dns({
      lookup (origin, opts, cb) {
        lookupCounter++
        if (lookupCounter === 1) {
          const err = new Error('test error')
          err.code = 'ENOTFOUND'
          cb(err)
        } else {
          // Causes InformationalError
          cb(null, [])
        }
      }
    })
  ])

  after(async () => {
    await client.close()
  })

  let error1
  try {
    await client.request(requestOptions)
  } catch (err) {
    error1 = err
  }
  t.equal(error1.code, 'ENOTFOUND')

  // Test that the records in the dns interceptor were deleted after the
  // previous request
  let error2
  try {
    await client.request(requestOptions)
  } catch (err) {
    error2 = err
  }
  t.equal(error2.name, 'InformationalError')

  t.equal(lookupCounter, 2)
})

test('#3937 - Handle host correctly', async t => {
  t = tspl(t, { plan: 10 })

  const hostsnames = []
  const server = createServer({ joinDuplicateHeaders: true })
  const requestOptions = {
    method: 'GET',
    path: '/',
    headers: {
      'content-type': 'application/json'
    }
  }

  server.on('request', (req, res) => {
    t.equal(req.headers.host, `localhost:${server.address().port}`)

    res.writeHead(200, { 'content-type': 'text/plain' })
    res.end('hello world!')
  })

  server.listen(0)

  await once(server, 'listening')

  const client = new Agent().compose([
    dispatch => {
      return (opts, handler) => {
        const url = new URL(opts.origin)

        t.equal(hostsnames.includes(url.hostname), false)

        if (url.hostname[0] === '[') {
          // [::1] -> ::1
          t.equal(isIP(url.hostname.slice(1, 4)), 6)
        } else {
          t.equal(isIP(url.hostname), 4)
        }

        hostsnames.push(url.hostname)

        return dispatch(opts, handler)
      }
    },
    dns({
      lookup: (_origin, _opts, cb) => {
        cb(null, [
          {
            address: '::1',
            family: 6
          },
          {
            address: '127.0.0.1',
            family: 4
          }
        ])
      }
    })
  ])

  after(async () => {
    await client.close()
    server.close()

    await once(server, 'close')
  })

  const response = await client.request({
    ...requestOptions,
    origin: `http://localhost:${server.address().port}`
  })

  t.equal(response.statusCode, 200)
  t.equal(await response.body.text(), 'hello world!')

  const response2 = await client.request({
    ...requestOptions,
    origin: `http://localhost:${server.address().port}`
  })

  t.equal(response2.statusCode, 200)
  t.equal(await response2.body.text(), 'hello world!')
})

test('#4444 - Should preserve tuple-style headers', async t => {
  t = tspl(t, { plan: 5 })

  const server = createServer({ joinDuplicateHeaders: true })

  server.on('request', (req, res) => {
    t.equal(req.headers.host, `localhost:${server.address().port}`)
    t.equal(req.headers.foo, 'bar')
    t.equal(req.headers['0'], undefined)

    res.writeHead(200, { 'content-type': 'text/plain' })
    res.end('hello world!')
  })

  server.listen(0)

  await once(server, 'listening')

  const client = new Agent().compose([
    dns({
      lookup: (_origin, _opts, cb) => {
        cb(null, [
          {
            address: '::1',
            family: 6
          },
          {
            address: '127.0.0.1',
            family: 4
          }
        ])
      }
    })
  ])

  after(async () => {
    await client.close()
    server.close()

    await once(server, 'close')
  })

  const response = await request(`http://localhost:${server.address().port}`, {
    dispatcher: client,
    headers: [['foo', 'bar']]
  })

  t.equal(response.statusCode, 200)
  t.equal(await response.body.text(), 'hello world!')
})

test('#4444 - Should preserve iterable headers', async t => {
  t = tspl(t, { plan: 5 })

  const server = createServer({ joinDuplicateHeaders: true })

  server.on('request', (req, res) => {
    t.equal(req.headers.host, `localhost:${server.address().port}`)
    t.equal(req.headers.foo, 'bar')
    t.equal(req.headers['0'], undefined)

    res.writeHead(200, { 'content-type': 'text/plain' })
    res.end('hello world!')
  })

  server.listen(0)

  await once(server, 'listening')

  const client = new Agent().compose([
    dns({
      lookup: (_origin, _opts, cb) => {
        cb(null, [
          {
            address: '::1',
            family: 6
          },
          {
            address: '127.0.0.1',
            family: 4
          }
        ])
      }
    })
  ])

  after(async () => {
    await client.close()
    server.close()

    await once(server, 'close')
  })

  const response = await request(`http://localhost:${server.address().port}`, {
    dispatcher: client,
    headers: new Map([['foo', 'bar']])
  })

  t.equal(response.statusCode, 200)
  t.equal(await response.body.text(), 'hello world!')
})

test('#5522 - Should not throw when dns re-dispatches into a composed cache interceptor', async t => {
  t = tspl(t, { plan: 2 })

  const server = createServer()

  server.on('request', (req, res) => {
    res.writeHead(200, { 'content-type': 'text/plain' })
    res.end('hello world!')
  })

  server.listen(0)

  await once(server, 'listening')

  // dns is the outer interceptor here, so its re-dispatch after resolving
  // lands back in cache's normalizeHeaders() with a flat header array.
  const client = new Agent().compose(cache(), dns({
    lookup: (_origin, _opts, cb) => {
      cb(null, [
        {
          address: '127.0.0.1',
          family: 4
        }
      ])
    }
  }))

  after(async () => {
    await client.close()
    server.close()

    await once(server, 'close')
  })

  const response = await request(`http://localhost:${server.address().port}`, {
    dispatcher: client,
    headers: []
  })

  t.equal(response.statusCode, 200)
  t.equal(await response.body.text(), 'hello world!')
})

test('#5522 - Should not throw when dns re-dispatches into a composed deduplicate interceptor', async t => {
  t = tspl(t, { plan: 2 })

  const server = createServer()

  server.on('request', (req, res) => {
    res.writeHead(200, { 'content-type': 'text/plain' })
    res.end('hello world!')
  })

  server.listen(0)

  await once(server, 'listening')

  // Same root cause as the cache-interceptor case above, but through
  // deduplicate() -- it shares the same normalizeHeaders()/makeCacheKey()
  // utility from lib/util/cache.js, so it was equally affected.
  const client = new Agent().compose(deduplicate(), dns({
    lookup: (_origin, _opts, cb) => {
      cb(null, [
        {
          address: '127.0.0.1',
          family: 4
        }
      ])
    }
  }))

  after(async () => {
    await client.close()
    server.close()

    await once(server, 'close')
  })

  const response = await request(`http://localhost:${server.address().port}`, {
    dispatcher: client,
    headers: []
  })

  t.equal(response.statusCode, 200)
  t.equal(await response.body.text(), 'hello world!')
})

test('#3951 - Should handle lookup errors correctly', async t => {
  const suite = tspl(t, { plan: 1 })

  const requestOptions = {
    method: 'GET',
    path: '/',
    headers: {
      'content-type': 'application/json'
    }
  }

  const client = new Agent().compose([
    dns({
      lookup: (_origin, _opts, cb) => {
        cb(new Error('lookup error'))
      }
    })
  ])

  suite.rejects(client.request({
    ...requestOptions,
    origin: 'http://localhost'
  }), new Error('lookup error'))
})

test('#4234 - Should pass Client requests without origin through', async t => {
  t = tspl(t, { plan: 4 })

  const server = createServer({ joinDuplicateHeaders: true })
  server.on('request', (req, res) => {
    t.equal(req.headers.host, `localhost:${server.address().port}`)
    res.end('hello world!')
  })

  server.listen(0)
  await once(server, 'listening')

  let lookupCount = 0
  const client = new Client(`http://localhost:${server.address().port}`).compose(dns({
    lookup: (_origin, _opts, cb) => {
      lookupCount++
      cb(null, [{ address: '127.0.0.1', family: 4 }])
    }
  }))

  after(async () => {
    await client.close()
    server.close()
    await once(server, 'close')
  })

  const response = await client.request({
    method: 'GET',
    path: '/'
  })

  t.equal(response.statusCode, 200)
  t.equal(await response.body.text(), 'hello world!')
  t.equal(lookupCount, 1)
})

test('#4234 - Should pass Pool requests without origin through', async t => {
  t = tspl(t, { plan: 4 })

  const server = createServer({ joinDuplicateHeaders: true })
  server.on('request', (req, res) => {
    t.equal(req.headers.host, `localhost:${server.address().port}`)
    res.end('hello world!')
  })

  server.listen(0)
  await once(server, 'listening')

  let lookupCount = 0
  const pool = new Pool(`http://localhost:${server.address().port}`).compose(dns({
    lookup: (_origin, _opts, cb) => {
      lookupCount++
      cb(null, [{ address: '127.0.0.1', family: 4 }])
    }
  }))

  after(async () => {
    await pool.close()
    server.close()
    await once(server, 'close')
  })

  const response = await pool.request({
    method: 'GET',
    path: '/'
  })

  t.equal(response.statusCode, 200)
  t.equal(await response.body.text(), 'hello world!')
  t.equal(lookupCount, 1)
})
