'use strict'

const { test } = require('tap')
const { Client, errors } = require('..')
const { createServer } = require('http')
const { kSocket, kSize } = require('../lib/core/symbols')

test('close waits for queued requests to finish', (t) => {
  t.plan(16)

  const server = createServer()

  server.on('request', (req, res) => {
    t.pass('request received')
    res.end('hello')
  })
  t.teardown(server.close.bind(server))

  server.listen(0, () => {
    const client = new Client(`http://localhost:${server.address().port}`)
    t.teardown(client.destroy.bind(client))

    client.request({ path: '/', method: 'GET' }, function (err, data) {
      onRequest(err, data)

      client.request({ path: '/', method: 'GET' }, onRequest)
      client.request({ path: '/', method: 'GET' }, onRequest)
      client.request({ path: '/', method: 'GET' }, onRequest)

      // needed because the next element in the queue will be called
      // after the current function completes
      process.nextTick(function () {
        client.close()
      })
    })
  })

  function onRequest (err, { statusCode, headers, body }) {
    t.error(err)
    t.equal(statusCode, 200)
    const bufs = []
    body.on('data', (buf) => {
      bufs.push(buf)
    })
    body.on('end', () => {
      t.equal('hello', Buffer.concat(bufs).toString('utf8'))
    })
  }
})

test('destroy invoked all pending callbacks', (t) => {
  t.plan(4)

  const server = createServer()

  server.on('request', (req, res) => {
    res.write('hello')
  })
  t.teardown(server.close.bind(server))

  server.listen(0, () => {
    const client = new Client(`http://localhost:${server.address().port}`, {
      pipelining: 2
    })
    t.teardown(client.destroy.bind(client))

    client.request({ path: '/', method: 'GET' }, (err, data) => {
      t.error(err)
      data.body.on('error', (err) => {
        t.ok(err)
      }).resume()
      client.destroy()
    })
    client.request({ path: '/', method: 'GET' }, (err) => {
      t.type(err, errors.ClientDestroyedError)
    })
    client.request({ path: '/', method: 'GET' }, (err) => {
      t.type(err, errors.ClientDestroyedError)
    })
  })
})

test('destroy invoked all pending callbacks ticked', (t) => {
  t.plan(4)

  const server = createServer()

  server.on('request', (req, res) => {
    res.write('hello')
  })
  t.teardown(server.close.bind(server))

  server.listen(0, () => {
    const client = new Client(`http://localhost:${server.address().port}`, {
      pipelining: 2
    })
    t.teardown(client.destroy.bind(client))

    let ticked = false
    client.request({ path: '/', method: 'GET' }, (err) => {
      t.equal(ticked, true)
      t.type(err, errors.ClientDestroyedError)
    })
    client.request({ path: '/', method: 'GET' }, (err) => {
      t.equal(ticked, true)
      t.type(err, errors.ClientDestroyedError)
    })
    client.destroy()
    ticked = true
  })
})

test('close waits until socket is destroyed', (t) => {
  t.plan(4)

  const server = createServer((req, res) => {
    res.end(req.url)
  })
  t.teardown(server.close.bind(server))

  server.listen(0, () => {
    const client = new Client(`http://localhost:${server.address().port}`)
    t.teardown(client.destroy.bind(client))

    makeRequest()

    client.once('connect', () => {
      let done = false
      client[kSocket].on('close', () => {
        done = true
      })
      client.close((err) => {
        t.error(err)
        t.equal(client.closed, true)
        t.equal(done, true)
      })
    })

    function makeRequest () {
      client.request({ path: '/', method: 'GET' }, (err, data) => {
        t.error(err)
      })
      return client[kSize] <= client.pipelining
    }
  })
})

test('close should still reconnect', (t) => {
  t.plan(6)

  const server = createServer((req, res) => {
    res.end(req.url)
  })
  t.teardown(server.close.bind(server))

  server.listen(0, () => {
    const client = new Client(`http://localhost:${server.address().port}`)
    t.teardown(client.destroy.bind(client))

    t.ok(makeRequest())
    t.ok(!makeRequest())

    client.close((err) => {
      t.error(err)
      t.equal(client.closed, true)
    })
    client.once('connect', () => {
      client[kSocket].destroy()
    })

    function makeRequest () {
      client.request({ path: '/', method: 'GET' }, (err, data) => {
        t.error(err)
        data.body.resume()
      })
      return client[kSize] <= client.pipelining
    }
  })
})

test('close should call callback once finished', (t) => {
  t.plan(6)

  const server = createServer((req, res) => {
    setImmediate(function () {
      res.end(req.url)
    })
  })
  t.teardown(server.close.bind(server))

  server.listen(0, () => {
    const client = new Client(`http://localhost:${server.address().port}`)
    t.teardown(client.destroy.bind(client))

    t.ok(makeRequest())
    t.ok(!makeRequest())

    client.close((err) => {
      t.error(err)
      t.equal(client.closed, true)
    })

    function makeRequest () {
      client.request({ path: '/', method: 'GET' }, (err, data) => {
        t.error(err)
        data.body.resume()
      })
      return client[kSize] <= client.pipelining
    }
  })
})

test('closed and destroyed errors', (t) => {
  t.plan(4)

  const client = new Client('http://localhost:4000')
  t.teardown(client.destroy.bind(client))

  client.request({ path: '/', method: 'GET' }, (err) => {
    t.ok(err)
  })
  client.close((err) => {
    t.error(err)
  })
  client.request({ path: '/', method: 'GET' }, (err) => {
    t.type(err, errors.ClientClosedError)
    client.destroy()
    client.request({ path: '/', method: 'GET' }, (err) => {
      t.type(err, errors.ClientDestroyedError)
    })
  })
})

test('close after and destroy should error', (t) => {
  t.plan(2)

  const client = new Client('http://localhost:4000')
  t.teardown(client.destroy.bind(client))

  client.destroy()
  client.close((err) => {
    t.type(err, errors.ClientDestroyedError)
  })
  client.close().catch((err) => {
    t.type(err, errors.ClientDestroyedError)
  })
})

test('close socket and reconnect after maxRequestsPerClient reached', (t) => {
  t.plan(5)

  const server = createServer((req, res) => {
    res.end(req.url)
  })

  t.teardown(server.close.bind(server))

  server.listen(0, async () => {
    let connections = 0
    server.on('connection', () => {
      connections++
    })
    const client = new Client(
      `http://localhost:${server.address().port}`,
      { maxRequestsPerClient: 2 }
    )
    t.teardown(client.destroy.bind(client))

    await t.resolves(makeRequest())
    await t.resolves(makeRequest())
    await t.resolves(makeRequest())
    await t.resolves(makeRequest())
    t.equal(connections, 2)

    function makeRequest () {
      return client.request({ path: '/', method: 'GET' })
    }
  })
})

test('close socket and reconnect after maxRequestsPerClient reached (async)', (t) => {
  t.plan(2)

  const server = createServer((req, res) => {
    res.end(req.url)
  })

  t.teardown(server.close.bind(server))

  server.listen(0, async () => {
    let connections = 0
    server.on('connection', () => {
      connections++
    })
    const client = new Client(
      `http://localhost:${server.address().port}`,
      { maxRequestsPerClient: 2 }
    )
    t.teardown(client.destroy.bind(client))

    await t.resolves(
      Promise.all([
        makeRequest(),
        makeRequest(),
        makeRequest(),
        makeRequest()
      ])
    )
    t.equal(connections, 2)

    function makeRequest () {
      return client.request({ path: '/', method: 'GET' })
    }
  })
})

test('should not close socket when no maxRequestsPerClient is provided', (t) => {
  t.plan(5)

  const server = createServer((req, res) => {
    res.end(req.url)
  })

  t.teardown(server.close.bind(server))

  server.listen(0, async () => {
    let connections = 0
    server.on('connection', () => {
      connections++
    })
    const client = new Client(`http://localhost:${server.address().port}`)
    t.teardown(client.destroy.bind(client))

    await t.resolves(makeRequest())
    await t.resolves(makeRequest())
    await t.resolves(makeRequest())
    await t.resolves(makeRequest())
    t.equal(connections, 1)

    function makeRequest () {
      return client.request({ path: '/', method: 'GET' })
    }
  })
})
