'use strict'

const net = require('node:net')
const https = require('node:https')
const { once } = require('node:events')
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { request, ProxyAgent } = require('..')
const { ConnectTimeoutError, InvalidArgumentError } = require('../lib/core/errors')
const Socks5ProxyAgent = require('../lib/dispatcher/socks5-proxy-agent')
const { createServer } = require('node:http')
const { TestSocks5Server } = require('./fixtures/socks5-test-server')

function getPools (agent) {
  const poolsSymbol = Object.getOwnPropertySymbols(agent).find(symbol => symbol.description === 'pools')
  return agent[poolsSymbol]
}

async function createStalledSocks5Proxy ({ stallOnConnect = false } = {}) {
  const sockets = new Set()
  const server = net.createServer((socket) => {
    sockets.add(socket)
    socket.once('close', () => sockets.delete(socket))
    socket.on('error', () => {})

    let state = 'handshake'
    let buffer = Buffer.alloc(0)
    socket.on('data', (data) => {
      buffer = Buffer.concat([buffer, data])

      if (state === 'handshake') {
        if (buffer.length < 2 || buffer.length < 2 + buffer[1]) {
          return
        }
        buffer = buffer.subarray(2 + buffer[1])
        socket.write(Buffer.from([0x05, 0x00]))
        state = 'connect'
      } else if (state === 'connect') {
        if (buffer.length < 5) {
          return
        }

        const addressLength = buffer[3] === 0x01
          ? 4
          : buffer[3] === 0x04
            ? 16
            : 1 + buffer[4]
        const messageLength = 4 + addressLength + 2
        if (buffer.length < messageLength) {
          return
        }

        buffer = buffer.subarray(messageLength)
        if (!stallOnConnect) {
          socket.write(Buffer.from([0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0]))
        }
        state = 'connected'
      }
    })
  })
  await new Promise(resolve => server.listen(0, '127.0.0.1', resolve))
  return { server, sockets }
}

const tlsCerts = (() => {
  const forge = require('node-forge')
  const createCert = (cn, issuer, keyLength = 2048) => {
    const keys = forge.pki.rsa.generateKeyPair(keyLength)
    const cert = forge.pki.createCertificate()
    cert.publicKey = keys.publicKey
    cert.serialNumber = '' + Date.now()
    cert.validity.notBefore = new Date()
    cert.validity.notAfter = new Date()
    cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 10)
    const attrs = [{ name: 'commonName', value: cn }]
    cert.setSubject(attrs)
    const isCa = issuer === undefined
    cert.setExtensions([
      { name: 'basicConstraints', cA: isCa },
      { name: 'subjectAltName', altNames: [{ type: 2, value: cn }] }
    ])
    const alg = forge.md.sha256.create()
    if (issuer !== undefined) {
      cert.setIssuer(issuer.certificate.subject.attributes)
      cert.sign(issuer.privateKey, alg)
    } else {
      cert.setIssuer(attrs)
      cert.sign(keys.privateKey, alg)
    }
    return { privateKey: keys.privateKey, publicKey: keys.publicKey, certificate: cert }
  }
  const root = createCert('socks5-test-ca')
  const server = createCert('localhost', root)
  return {
    root: { crt: forge.pki.certificateToPem(root.certificate) },
    server: {
      key: forge.pki.privateKeyToPem(server.privateKey),
      crt: forge.pki.certificateToPem(server.certificate)
    }
  }
})()

test('Socks5ProxyAgent - constructor validation', async (t) => {
  const p = tspl(t, { plan: 4 })

  p.throws(() => {
    // eslint-disable-next-line no-new
    new Socks5ProxyAgent()
  }, InvalidArgumentError, 'should throw when proxy URL is not provided')

  p.throws(() => {
    // eslint-disable-next-line no-new
    new Socks5ProxyAgent('http://localhost:1080')
  }, InvalidArgumentError, 'should throw when proxy URL protocol is not socks5')

  p.doesNotThrow(() => {
    // eslint-disable-next-line no-new
    new Socks5ProxyAgent('socks5://localhost:1080')
  }, 'should accept socks5:// URLs')

  p.doesNotThrow(() => {
    // eslint-disable-next-line no-new
    new Socks5ProxyAgent('socks://localhost:1080')
  }, 'should accept socks:// URLs for compatibility')

  await p.completed
})

test('Socks5ProxyAgent - timeout validation', async (t) => {
  const p = tspl(t, { plan: 8 })

  for (const connectTimeout of [-1, Infinity, '100']) {
    p.throws(() => {
      // eslint-disable-next-line no-new
      new Socks5ProxyAgent('socks5://localhost:1080', { connectTimeout })
    }, InvalidArgumentError)
  }

  for (const timeout of [-1, Infinity]) {
    p.throws(() => {
      // eslint-disable-next-line no-new
      new Socks5ProxyAgent('socks5://localhost:1080', { requestTls: { timeout } })
    }, InvalidArgumentError)
    p.throws(() => {
      // eslint-disable-next-line no-new
      new Socks5ProxyAgent('socks5://localhost:1080', { proxyTls: { timeout } })
    }, InvalidArgumentError)
  }

  p.doesNotThrow(() => {
    // eslint-disable-next-line no-new
    new Socks5ProxyAgent('socks5://localhost:1080', { connectTimeout: 0 })
  })

  await p.completed
})

test('Socks5ProxyAgent - uses custom connector for proxy connection', async (t) => {
  const p = tspl(t, { plan: 3 })

  const server = createServer((req, res) => {
    res.writeHead(200, { 'content-type': 'application/json' })
    res.end(JSON.stringify({ ok: true }))
  })

  await new Promise((resolve) => {
    server.listen(0, resolve)
  })
  const serverPort = server.address().port

  const socksServer = new TestSocks5Server()
  const socksAddress = await socksServer.listen()
  let connectorCalled = false

  const proxyWrapper = new Socks5ProxyAgent(`socks5://localhost:${socksAddress.port}`, {
    connect (opts, callback) {
      connectorCalled = true
      const socket = net.connect({
        host: opts.hostname,
        port: opts.port
      })
      socket.once('connect', () => callback(null, socket))
      socket.once('error', callback)
      return socket
    }
  })

  const response = await request(`http://localhost:${serverPort}/test`, {
    dispatcher: proxyWrapper
  })

  p.equal(response.statusCode, 200, 'should get 200 status code')
  p.deepEqual(await response.body.json(), { ok: true }, 'should get correct response body')
  p.equal(connectorCalled, true, 'should use custom connector for proxy connection')

  t.after(async () => {
    if (proxyWrapper) {
      await proxyWrapper.close()
    }
    await socksServer.close()
    server.close()
  })

  await p.completed
})

test('Socks5ProxyAgent - dispatch returns boolean backpressure signal', async (t) => {
  const p = tspl(t, { plan: 1 })
  const proxyWrapper = new Socks5ProxyAgent('socks5://localhost:9999')

  const ret = proxyWrapper.dispatch({
    origin: 'http://example.com',
    path: '/',
    method: 'GET'
  }, {
    onRequestStart () {},
    onResponseStart () {},
    onResponseData () {},
    onResponseEnd () {},
    onResponseError () {}
  })

  p.equal(typeof ret, 'boolean')

  await proxyWrapper.destroy()
  await p.completed
})

test('Socks5ProxyAgent - basic HTTP connection', async (t) => {
  const p = tspl(t, { plan: 2 })

  // Create target HTTP server
  const server = createServer((req, res) => {
    res.writeHead(200, { 'content-type': 'application/json' })
    res.end(JSON.stringify({ message: 'Hello from target server', path: req.url }))
  })

  // Start target server
  await new Promise((resolve) => {
    server.listen(0, resolve)
  })
  const serverPort = server.address().port

  // Create SOCKS5 proxy server
  const socksServer = new TestSocks5Server()
  const socksAddress = await socksServer.listen()

  after(() => socksServer.close())
  after(() => server.close())

  // Create Socks5ProxyAgent
  const proxyWrapper = new Socks5ProxyAgent(`socks5://localhost:${socksAddress.port}`)

  // Make request through SOCKS5 proxy
  const response = await request(`http://localhost:${serverPort}/test`, {
    dispatcher: proxyWrapper
  })

  p.equal(response.statusCode, 200, 'should get 200 status code')

  const body = await response.body.json()
  p.deepEqual(body, {
    message: 'Hello from target server',
    path: '/test'
  }, 'should get correct response body')

  await p.completed
})

test('Socks5ProxyAgent - removes an origin pool after disconnect', async (t) => {
  const p = tspl(t, { plan: 2 })
  const server = createServer((req, res) => {
    res.setHeader('connection', 'close')
    res.end('OK')
  })
  const targetSocketClosed = new Promise((resolve) => {
    server.once('connection', (socket) => socket.once('close', resolve))
  })
  await new Promise(resolve => server.listen(0, resolve))
  t.after(() => server.close())

  const socksServer = new TestSocks5Server()
  const socksAddress = await socksServer.listen()
  t.after(() => socksServer.close())

  const proxyWrapper = new Socks5ProxyAgent(`socks5://localhost:${socksAddress.port}`)
  t.after(() => proxyWrapper.destroy())

  const responsePromise = request(`http://localhost:${server.address().port}/`, {
    dispatcher: proxyWrapper
  })
  const pool = getPools(proxyWrapper).values().next().value
  const disconnected = once(pool, 'disconnect')
  const response = await responsePromise
  p.equal(await response.body.text(), 'OK')

  await targetSocketClosed
  await disconnected
  p.strictEqual(getPools(proxyWrapper).size, 0, 'should remove the disconnected origin pool')

  await p.completed
})

test.skip('Socks5ProxyAgent - HTTPS connection', async (t) => {
  // Skip HTTPS test for now - TLS option passing needs additional work
  t.skip('HTTPS test requires TLS option refinement')
})

test('Socks5ProxyAgent - times out a stalled TLS upgrade', { timeout: 3000 }, async (t) => {
  const p = tspl(t, { plan: 4 })
  const { server: stalledProxy, sockets: proxySockets } = await createStalledSocks5Proxy()

  let connectorSocket
  const agent = new Socks5ProxyAgent(`socks5://127.0.0.1:${stalledProxy.address().port}`, {
    connectTimeout: 100,
    connect (opts, callback) {
      const socket = net.connect({ host: opts.hostname, port: opts.port })
      connectorSocket = socket
      socket.once('connect', () => callback(null, socket))
      socket.once('error', callback)
      return socket
    }
  })

  t.after(async () => {
    await agent.destroy()
    for (const socket of proxySockets) {
      socket.destroy()
    }
    await new Promise(resolve => stalledProxy.close(resolve))
  })

  try {
    await request('https://example.invalid/', { dispatcher: agent })
    p.fail('should have thrown an error')
  } catch (err) {
    p.ok(err instanceof ConnectTimeoutError)
    p.strictEqual(err.code, 'UND_ERR_CONNECT_TIMEOUT')
  }

  p.ok(connectorSocket.destroyed, 'timed out TLS upgrade should destroy the tunnel socket')
  p.strictEqual(getPools(agent).size, 0, 'failed TLS upgrade should remove the origin pool')

  await p.completed
})

test('Socks5ProxyAgent - times out a stalled CONNECT response', { timeout: 3000 }, async (t) => {
  const p = tspl(t, { plan: 3 })
  const { server: stalledProxy, sockets: proxySockets } = await createStalledSocks5Proxy({
    stallOnConnect: true
  })
  const agent = new Socks5ProxyAgent(`socks5://127.0.0.1:${stalledProxy.address().port}`, {
    connectTimeout: 100
  })

  t.after(async () => {
    await agent.destroy()
    for (const socket of proxySockets) {
      socket.destroy()
    }
    await new Promise(resolve => stalledProxy.close(resolve))
  })

  try {
    await request('http://example.invalid/', { dispatcher: agent })
    p.fail('should have thrown an error')
  } catch (err) {
    p.ok(err instanceof ConnectTimeoutError)
    p.strictEqual(err.code, 'UND_ERR_CONNECT_TIMEOUT')
  }

  p.strictEqual(getPools(agent).size, 0, 'failed CONNECT should remove the origin pool')
  await p.completed
})

test('ProxyAgent forwards connectTimeout to SOCKS5 TLS upgrades', { timeout: 3000 }, async (t) => {
  const p = tspl(t, { plan: 2 })
  const { server: stalledProxy, sockets: proxySockets } = await createStalledSocks5Proxy()
  const agent = new ProxyAgent({
    uri: `socks5://127.0.0.1:${stalledProxy.address().port}`,
    connectTimeout: 100
  })

  t.after(async () => {
    await agent.destroy()
    for (const socket of proxySockets) {
      socket.destroy()
    }
    await new Promise(resolve => stalledProxy.close(resolve))
  })

  try {
    await request('https://example.invalid/', { dispatcher: agent })
    p.fail('should have thrown an error')
  } catch (err) {
    p.ok(err instanceof ConnectTimeoutError)
    p.strictEqual(err.code, 'UND_ERR_CONNECT_TIMEOUT')
  }

  await p.completed
})

test('Socks5ProxyAgent - with authentication', async (t) => {
  const p = tspl(t, { plan: 2 })

  // Create target HTTP server
  const server = createServer((req, res) => {
    res.writeHead(200, { 'content-type': 'application/json' })
    res.end(JSON.stringify({ message: 'Authenticated request successful' }))
  })

  // Start target server
  await new Promise((resolve) => {
    server.listen(0, resolve)
  })
  const serverPort = server.address().port

  // Create SOCKS5 proxy server with auth
  const socksServer = new TestSocks5Server({
    requireAuth: true,
    credentials: { username: 'testuser', password: 'testpass' }
  })
  const socksAddress = await socksServer.listen()

  after(() => socksServer.close())
  after(() => server.close())

  // Create Socks5ProxyAgent with auth
  const proxyWrapper = new Socks5ProxyAgent(`socks5://testuser:testpass@localhost:${socksAddress.port}`)

  // Make request through SOCKS5 proxy
  const response = await request(`http://localhost:${serverPort}/auth-test`, {
    dispatcher: proxyWrapper
  })

  p.equal(response.statusCode, 200, 'should get 200 status code')

  const body = await response.body.json()
  p.deepEqual(body, {
    message: 'Authenticated request successful'
  }, 'should get correct response body')

  await p.completed
})

test('Socks5ProxyAgent - authentication with options', async (t) => {
  const p = tspl(t, { plan: 2 })

  // Create target HTTP server
  const server = createServer((req, res) => {
    res.writeHead(200, { 'content-type': 'application/json' })
    res.end(JSON.stringify({ message: 'Options auth successful' }))
  })

  // Start target server
  await new Promise((resolve) => {
    server.listen(0, resolve)
  })
  const serverPort = server.address().port

  // Create SOCKS5 proxy server with auth
  const socksServer = new TestSocks5Server({
    requireAuth: true,
    credentials: { username: 'optuser', password: 'optpass' }
  })
  const socksAddress = await socksServer.listen()

  after(() => socksServer.close())
  after(() => server.close())

  // Create Socks5ProxyAgent with auth in options
  const proxyWrapper = new Socks5ProxyAgent(`socks5://localhost:${socksAddress.port}`, {
    username: 'optuser',
    password: 'optpass'
  })

  // Make request through SOCKS5 proxy
  const response = await request(`http://localhost:${serverPort}/options-auth`, {
    dispatcher: proxyWrapper
  })

  p.equal(response.statusCode, 200, 'should get 200 status code')

  const body = await response.body.json()
  p.deepEqual(body, {
    message: 'Options auth successful'
  }, 'should get correct response body')

  await p.completed
})

test('Socks5ProxyAgent - multiple requests through same proxy', async (t) => {
  const p = tspl(t, { plan: 4 })

  // Create target HTTP server
  let requestCount = 0
  const server = createServer((req, res) => {
    requestCount++
    res.writeHead(200, { 'content-type': 'application/json' })
    res.end(JSON.stringify({ message: `Request ${requestCount}`, path: req.url }))
  })

  // Start target server
  await new Promise((resolve) => {
    server.listen(0, resolve)
  })
  const serverPort = server.address().port

  // Create SOCKS5 proxy server
  const socksServer = new TestSocks5Server()
  const socksAddress = await socksServer.listen()

  after(() => socksServer.close())
  after(() => server.close())

  // Create Socks5ProxyAgent
  const proxyWrapper = new Socks5ProxyAgent(`socks5://localhost:${socksAddress.port}`)

  // Make first request
  const response1 = await request(`http://localhost:${serverPort}/request1`, {
    dispatcher: proxyWrapper
  })

  p.equal(response1.statusCode, 200, 'should get 200 status code for first request')
  const body1 = await response1.body.json()
  p.deepEqual(body1, { message: 'Request 1', path: '/request1' }, 'should get correct response body for first request')

  // Make second request through same proxy
  const response2 = await request(`http://localhost:${serverPort}/request2`, {
    dispatcher: proxyWrapper
  })

  p.equal(response2.statusCode, 200, 'should get 200 status code for second request')
  const body2 = await response2.body.json()
  p.deepEqual(body2, { message: 'Request 2', path: '/request2' }, 'should get correct response body for second request')

  await p.completed
})

test('Socks5ProxyAgent - requests to different origins are routed correctly', async (t) => {
  const p = tspl(t, { plan: 4 })

  // Create two distinct target servers
  const serverA = createServer((req, res) => {
    res.writeHead(200, { 'content-type': 'application/json' })
    res.end(JSON.stringify({ server: 'A', path: req.url }))
  })
  const serverB = createServer((req, res) => {
    res.writeHead(200, { 'content-type': 'application/json' })
    res.end(JSON.stringify({ server: 'B', path: req.url }))
  })

  await new Promise((resolve) => serverA.listen(0, '127.0.0.1', resolve))
  await new Promise((resolve) => serverB.listen(0, '127.0.0.1', resolve))
  const portA = serverA.address().port
  const portB = serverB.address().port

  const socksServer = new TestSocks5Server()
  const socksAddress = await socksServer.listen()

  after(() => socksServer.close())
  after(() => serverA.close())
  after(() => serverB.close())

  const proxyWrapper = new Socks5ProxyAgent(`socks5://127.0.0.1:${socksAddress.port}`)

  // First request goes to server A — establishes a pool
  const respA = await request(`http://127.0.0.1:${portA}/a`, { dispatcher: proxyWrapper })
  p.equal(respA.statusCode, 200)
  p.deepEqual(await respA.body.json(), { server: 'A', path: '/a' })

  // Second request goes to server B — must NOT reuse the pool from origin A
  const respB = await request(`http://127.0.0.1:${portB}/b`, { dispatcher: proxyWrapper })
  p.equal(respB.statusCode, 200)
  p.deepEqual(await respB.body.json(), { server: 'B', path: '/b' }, 'request to origin B must reach server B, not server A')

  await p.completed
})

test('Socks5ProxyAgent - connection failure', async (t) => {
  const p = tspl(t, { plan: 2 })

  // Create Socks5ProxyAgent pointing to non-existent proxy
  const proxyWrapper = new Socks5ProxyAgent('socks5://localhost:9999')

  try {
    await request('http://example.com/', {
      dispatcher: proxyWrapper
    })
    p.fail('should have thrown an error')
  } catch (err) {
    p.ok(err, 'should throw error when SOCKS5 proxy is not available')
  }

  p.strictEqual(getPools(proxyWrapper).size, 0, 'should remove the failed origin pool')

  await p.completed
})

test('Socks5ProxyAgent - destroys socket when negotiation times out', async (t) => {
  const p = tspl(t, { plan: 3 })

  // SOCKS5 proxy that accepts the TCP connection but never replies to the greeting
  const stalledProxy = net.createServer(() => {})
  await new Promise((resolve) => {
    stalledProxy.listen(0, '127.0.0.1', resolve)
  })

  let connectorSocket
  const agent = new Socks5ProxyAgent(`socks5://127.0.0.1:${stalledProxy.address().port}`, {
    connectTimeout: 100,
    connect (opts, callback) {
      const socket = net.connect({ host: opts.hostname, port: opts.port })
      connectorSocket = socket
      socket.once('connect', () => callback(null, socket))
      socket.once('error', callback)
      return socket
    }
  })

  try {
    await request('http://example.invalid/', {
      dispatcher: agent
    })
    p.fail('should have thrown an error')
  } catch (err) {
    p.ok(err instanceof ConnectTimeoutError)
    p.strictEqual(err.code, 'UND_ERR_CONNECT_TIMEOUT')
  }

  await agent.close()
  p.ok(connectorSocket.destroyed, 'socket left by the timed out negotiation should be destroyed')

  t.after(async () => {
    stalledProxy.close()
  })

  await p.completed
})

test('Socks5ProxyAgent - proxy connection refused', async (t) => {
  const p = tspl(t, { plan: 1 })

  // Create target HTTP server
  const server = createServer((req, res) => {
    res.writeHead(200)
    res.end('OK')
  })

  await new Promise((resolve) => {
    server.listen(0, resolve)
  })
  const serverPort = server.address().port

  // Create SOCKS5 proxy server that simulates connection failure
  const socksServer = new TestSocks5Server({ simulateFailure: true })
  const socksAddress = await socksServer.listen()

  after(() => socksServer.close())
  after(() => server.close())

  try {
    const proxyWrapper = new Socks5ProxyAgent(`socks5://localhost:${socksAddress.port}`)

    await request(`http://localhost:${serverPort}/`, {
      dispatcher: proxyWrapper
    })
    p.fail('should have thrown an error')
  } catch (err) {
    p.ok(err, 'should throw error when SOCKS5 proxy refuses connection')
  }

  await p.completed
})

test('Socks5ProxyAgent - close and destroy', async (t) => {
  const p = tspl(t, { plan: 2 })

  const proxyWrapper = new Socks5ProxyAgent('socks5://localhost:1080')

  // Test close
  await proxyWrapper.close()
  p.ok(true, 'should close without error')

  // Test destroy
  await proxyWrapper.destroy()
  p.ok(true, 'should destroy without error')

  await p.completed
})

test('Socks5ProxyAgent - URL parsing edge cases', async (t) => {
  const p = tspl(t, { plan: 3 })

  // Test with URL object
  const url = new URL('socks5://user:pass@proxy.example.com:1080')
  p.doesNotThrow(() => {
    // eslint-disable-next-line no-new
    new Socks5ProxyAgent(url)
  }, 'should accept URL object')

  // Test with encoded credentials
  p.doesNotThrow(() => {
    // eslint-disable-next-line no-new
    new Socks5ProxyAgent('socks5://user%40domain:p%40ss@localhost:1080')
  }, 'should handle URL-encoded credentials')

  // Test default port
  p.doesNotThrow(() => {
    // eslint-disable-next-line no-new
    new Socks5ProxyAgent('socks5://localhost')
  }, 'should use default port 1080')

  await p.completed
})

test('Socks5ProxyAgent - requestTls is honored for target HTTPS connection (GHSA-vmh5-mc38-953g)', async (t) => {
  const p = tspl(t, { plan: 2 })

  // HTTPS server with a cert signed by tlsCerts.root, NOT in Node's default trust store
  const server = https.createServer({
    key: tlsCerts.server.key,
    cert: tlsCerts.server.crt
  }, (req, res) => {
    res.writeHead(200, { 'content-type': 'application/json' })
    res.end(JSON.stringify({ ok: true }))
  })
  await new Promise(resolve => server.listen(0, '127.0.0.1', resolve))
  const serverPort = server.address().port

  // SOCKS5 server that forwards CONNECT to the local HTTPS server
  const socksServer = new TestSocks5Server()
  const socksAddress = await socksServer.listen()

  // Use Socks5ProxyAgent directly with requestTls
  const proxyAgent = new Socks5ProxyAgent(`socks5://127.0.0.1:${socksAddress.port}`, {
    requestTls: {
      ca: [tlsCerts.root.crt]
    }
  })

  after(async () => {
    await proxyAgent.close()
    server.close()
    await socksServer.close()
  })

  // The HTTPS request via SOCKS5 should succeed because requestTls.ca contains the root CA
  // that signed the server cert. Without honoring requestTls, Node would default to Mozilla
  // CA bundle and reject the cert.
  const response = await request(`https://localhost:${serverPort}/`, {
    dispatcher: proxyAgent
  })
  p.strictEqual(response.statusCode, 200, 'request should succeed when requestTls.ca is honored')
  const body = await response.body.json()
  p.deepStrictEqual(body, { ok: true })

  await p.completed
})

test('ProxyAgent forwards requestTls to Socks5ProxyAgent (GHSA-vmh5-mc38-953g)', async (t) => {
  const p = tspl(t, { plan: 2 })

  const server = https.createServer({
    key: tlsCerts.server.key,
    cert: tlsCerts.server.crt
  }, (req, res) => {
    res.writeHead(200, { 'content-type': 'application/json' })
    res.end(JSON.stringify({ ok: true }))
  })
  await new Promise(resolve => server.listen(0, '127.0.0.1', resolve))
  const serverPort = server.address().port

  const socksServer = new TestSocks5Server()
  const socksAddress = await socksServer.listen()

  // Use top-level ProxyAgent with socks5 URI + requestTls; the option must be forwarded.
  const proxyAgent = new ProxyAgent({
    uri: `socks5://127.0.0.1:${socksAddress.port}`,
    requestTls: {
      ca: [tlsCerts.root.crt]
    }
  })

  after(async () => {
    await proxyAgent.close()
    server.close()
    await socksServer.close()
  })

  const response = await request(`https://localhost:${serverPort}/`, {
    dispatcher: proxyAgent
  })
  p.strictEqual(response.statusCode, 200, 'request should succeed when ProxyAgent forwards requestTls')
  const body = await response.body.json()
  p.deepStrictEqual(body, { ok: true })

  await p.completed
})
