'use strict'
const net = require('net')
const co = require('co')
const expect = require('expect.js')

const describe = require('mocha').describe
const it = require('mocha').it
const before = require('mocha').before
const after = require('mocha').after

const Pool = require('../')

describe('connection timeout', () => {
  const connectionFailure = new Error('Temporary connection failure')

  before((done) => {
    this.server = net.createServer((socket) => {
      socket.on('data', () => {
        // discard any buffered data or the server wont terminate
      })
    })

    this.server.listen(() => {
      this.port = this.server.address().port
      done()
    })
  })

  after((done) => {
    this.server.close(done)
  })

  it('should callback with an error if timeout is passed', (done) => {
    const pool = new Pool({ connectionTimeoutMillis: 10, port: this.port, host: 'localhost' })
    pool.connect((err, client, release) => {
      expect(err).to.be.an(Error)
      expect(err.message).to.contain('timeout')
      expect(client).to.equal(undefined)
      expect(pool.idleCount).to.equal(0)
      done()
    })
  })

  it('should reject promise with an error if timeout is passed', (done) => {
    const pool = new Pool({ connectionTimeoutMillis: 10, port: this.port, host: 'localhost' })
    pool.connect().catch((err) => {
      expect(err).to.be.an(Error)
      expect(err.message).to.contain('timeout')
      expect(pool.idleCount).to.equal(0)
      done()
    })
  })

  it(
    'should handle multiple timeouts',
    co.wrap(
      function* () {
        const errors = []
        const pool = new Pool({ connectionTimeoutMillis: 1, port: this.port, host: 'localhost' })
        for (let i = 0; i < 15; i++) {
          try {
            yield pool.connect()
          } catch (e) {
            errors.push(e)
          }
        }
        expect(errors).to.have.length(15)
      }.bind(this)
    )
  )

  it('should timeout on checkout of used connection', (done) => {
    const pool = new Pool({ connectionTimeoutMillis: 100, max: 1 })
    pool.connect((err, client, release) => {
      expect(err).to.be(undefined)
      expect(client).to.not.be(undefined)
      pool.connect((err, client) => {
        expect(err).to.be.an(Error)
        expect(client).to.be(undefined)
        release()
        pool.end(done)
      })
    })
  })

  it('should not break further pending checkouts on a timeout', (done) => {
    const pool = new Pool({ connectionTimeoutMillis: 200, max: 1 })
    pool.connect((err, client, releaseOuter) => {
      expect(err).to.be(undefined)

      pool.connect((err, client) => {
        expect(err).to.be.an(Error)
        expect(client).to.be(undefined)
        releaseOuter()
      })

      setTimeout(() => {
        pool.connect((err, client, releaseInner) => {
          expect(err).to.be(undefined)
          expect(client).to.not.be(undefined)
          releaseInner()
          pool.end(done)
        })
      }, 100)
    })
  })

  it('should timeout on query if all clients are busy', (done) => {
    const pool = new Pool({ connectionTimeoutMillis: 100, max: 1 })
    pool.connect((err, client, release) => {
      expect(err).to.be(undefined)
      expect(client).to.not.be(undefined)
      pool.query('select now()', (err, result) => {
        expect(err).to.be.an(Error)
        expect(result).to.be(undefined)
        release()
        pool.end(done)
      })
    })
  })

  it('should recover from timeout errors', (done) => {
    const pool = new Pool({ connectionTimeoutMillis: 100, max: 1 })
    pool.connect((err, client, release) => {
      expect(err).to.be(undefined)
      expect(client).to.not.be(undefined)
      pool.query('select now()', (err, result) => {
        expect(err).to.be.an(Error)
        expect(result).to.be(undefined)
        release()
        pool.query('select $1::text as name', ['brianc'], (err, res) => {
          expect(err).to.be(undefined)
          expect(res.rows).to.have.length(1)
          pool.end(done)
        })
      })
    })
  })

  it('continues processing after a connection failure', (done) => {
    const Client = require('pg').Client
    const orgConnect = Client.prototype.connect
    let called = false

    Client.prototype.connect = function (cb) {
      // Simulate a failure on first call
      if (!called) {
        called = true

        return setTimeout(() => {
          cb(connectionFailure)
        }, 100)
      }
      // And pass-through the second call
      orgConnect.call(this, cb)
    }

    const pool = new Pool({
      Client: Client,
      connectionTimeoutMillis: 1000,
      max: 1,
    })

    pool.connect((err, client, release) => {
      expect(err).to.be(connectionFailure)

      pool.query('select $1::text as name', ['brianc'], (err, res) => {
        expect(err).to.be(undefined)
        expect(res.rows).to.have.length(1)
        pool.end(done)
      })
    })
  })

  it('releases newly connected clients if the queued already timed out', (done) => {
    const Client = require('pg').Client

    const orgConnect = Client.prototype.connect

    let connection = 0

    Client.prototype.connect = function (cb) {
      // Simulate a failure on first call
      if (connection === 0) {
        connection++

        return setTimeout(() => {
          cb(connectionFailure)
        }, 300)
      }

      // And second connect taking > connection timeout
      if (connection === 1) {
        connection++

        return setTimeout(() => {
          orgConnect.call(this, cb)
        }, 1000)
      }

      orgConnect.call(this, cb)
    }

    const pool = new Pool({
      Client: Client,
      connectionTimeoutMillis: 1000,
      max: 1,
    })

    // Direct connect
    pool.connect((err, client, release) => {
      expect(err).to.be(connectionFailure)
    })

    // Queued
    let called = 0
    pool.connect((err, client, release) => {
      // Verify the callback is only called once
      expect(called++).to.be(0)
      expect(err).to.be.an(Error)

      pool.query('select $1::text as name', ['brianc'], (err, res) => {
        expect(err).to.be(undefined)
        expect(res.rows).to.have.length(1)
        pool.end(done)
      })
    })
  })
})
