var assert = require('assert')
var poolModule = require('..')

module.exports = {
  'expands to max limit': function (beforeExit) {
    var createCount = 0
    var destroyCount = 0
    var borrowCount = 0

    var factory = {
      name: 'test1',
      create: function (callback) {
        callback(null, { count: ++createCount })
      },
      destroy: function (client) { destroyCount++ },
      max: 2,
      idleTimeoutMillis: 100
    }

    var pool = poolModule.Pool(factory)

    for (var i = 0; i < 10; i++) {
      var full = !pool.acquire(function () {
        return function (err, obj) {
          assert.ifError(err)
          assert.equal(typeof obj.count, 'number')
          setTimeout(function () {
            borrowCount++
            pool.release(obj)
          }, 100)
        }
      }())
      assert.ok((i < 1) ^ full)
    }

    beforeExit(function () {
      assert.equal(0, factory.min)
      assert.equal(2, createCount)
      assert.equal(2, destroyCount)
      assert.equal(10, borrowCount)
    })
  },

  'respects min limit': function (beforeExit) {
    var createCount = 0
    var destroyCount = 0

    var pool = poolModule.Pool({
      name: 'test-min',
      create: function (callback) {
        callback(null, { count: ++createCount })
      },
      destroy: function (client) { destroyCount++ },
      min: 1,
      max: 2,
      idleTimeoutMillis: 100
    })

    pool.drain(function () {
      pool.destroyAllNow()
    })

    beforeExit(function () {
      assert.equal(0, pool.availableObjectsCount())
      assert.equal(1, createCount)
      assert.equal(1, destroyCount)
    })
  },

  'min and max limit defaults': function (beforeExit) {
    var factory = {
      name: 'test-limit-defaults',
      create: function (callback) { callback(null, {}) },
      destroy: function (client) {},
      idleTimeoutMillis: 100
    }
    var pool = poolModule.Pool(factory)

    beforeExit(function () {
      assert.equal(1, pool.getMaxPoolSize())
      assert.equal(0, pool.getMinPoolSize())
    })
  },

  'malformed min and max limits are ignored': function (beforeExit) {
    var factory = {
      name: 'test-limit-defaults2',
      create: function (callback) { callback(null, {}) },
      destroy: function (client) {},
      idleTimeoutMillis: 100,
      min: 'asf',
      max: []
    }
    var pool = poolModule.Pool(factory)

    beforeExit(function () {
      assert.equal(1, pool.getMaxPoolSize())
      assert.equal(0, pool.getMinPoolSize())
    })
  },

  'min greater than max sets to max minus one': function (beforeExit) {
    var factory = {
      name: 'test-limit-defaults3',
      create: function (callback) { callback(null, {}) },
      destroy: function (client) {},
      idleTimeoutMillis: 100,
      min: 5,
      max: 3
    }
    var pool = poolModule.Pool(factory)
    pool.drain(function () {
      pool.destroyAllNow()
    })

    beforeExit(function () {
      assert.equal(3, pool.getMaxPoolSize())
      assert.equal(2, pool.getMinPoolSize())
    })
  },

  'supports priority on borrow': function (beforeExit) {
    var borrowTimeLow = 0
    var borrowTimeHigh = 0
    var borrowCount = 0
    var i

    var pool = poolModule.Pool({
      name: 'test2',
      create: function (callback) { callback() },
      destroy: function (client) {},
      max: 1,
      idleTimeoutMillis: 100,
      priorityRange: 2
    })

    for (i = 0; i < 10; i++) {
      pool.acquire((function (err, obj) {
        return function () {
          setTimeout(function () {
            assert.ifError(err)
            var t = new Date().getTime()
            if (t > borrowTimeLow) { borrowTimeLow = t }
            borrowCount++
            pool.release(obj)
          }, 50)
        }
      }()), 1)
    }

    for (i = 0; i < 10; i++) {
      pool.acquire((function (obj) {
        return function () {
          setTimeout(function () {
            var t = new Date().getTime()
            if (t > borrowTimeHigh) { borrowTimeHigh = t }
            borrowCount++
            pool.release(obj)
          }, 50)
        }
      }()), 0)
    }

    beforeExit(function () {
      assert.equal(20, borrowCount)
      assert.equal(true, borrowTimeLow > borrowTimeHigh)
    })
  },

  'removes correct object on reap': function (beforeExit) {
    var destroyed = []
    var clientCount = 0

    var pool = poolModule.Pool({
      name: 'test3',
      create: function (callback) { callback(null, { id: ++clientCount }) },
      destroy: function (client) { destroyed.push(client.id) },
      max: 2,
      idleTimeoutMillis: 100
    })

    pool.acquire(function (err, client) {
      assert.ifError(err)
      assert.equal(typeof client.id, 'number')
      // should be removed second
      setTimeout(function () { pool.release(client) }, 5)
    })
    pool.acquire(function (err, client) {
      assert.ifError(err)
      assert.equal(typeof client.id, 'number')
      // should be removed first
      pool.release(client)
    })

    setTimeout(function () {}, 102)

    beforeExit(function () {
      assert.equal(2, destroyed[0])
      assert.equal(1, destroyed[1])
    })
  },

  'tests drain': function (beforeExit) {
    var created = 0
    var destroyed = 0
    var count = 5
    var acquired = 0

    var pool = poolModule.Pool({
      name: 'test4',
      create: function (callback) { callback(null, {id: ++created}) },
      destroy: function (client) { destroyed += 1 },
      max: 2,
      idletimeoutMillis: 300000
    })

    for (var i = 0; i < count; i++) {
      pool.acquire(function (err, client) {
        assert.ifError(err)
        acquired += 1
        assert.equal(typeof client.id, 'number')
        setTimeout(function () { pool.release(client) }, 250)
      })
    }

    assert.notEqual(count, acquired)
    pool.drain(function () {
      assert.equal(count, acquired)
      // short circuit the absurdly long timeouts above.
      pool.destroyAllNow()
      beforeExit(function () {})
    })

    // subsequent calls to acquire should return an error.
    assert.throws(function () {
      pool.acquire(function (client) {})
    }, Error)
  },

  'handle creation errors': function (beforeExit) {
    var created = 0
    var pool = poolModule.Pool({
      name: 'test6',
      create: function (callback) {
        if (created < 5) {
          callback(new Error('Error occurred.'))
        } else {
          callback({ id: created })
        }
        created++
      },
      destroy: function (client) {},
      max: 1,
      idleTimeoutMillis: 1000
    })
    // ensure that creation errors do not populate the pool.
    for (var i = 0; i < 5; i++) {
      pool.acquire(function (err, client) {
        assert.ok(err instanceof Error)
        assert.ok(client === null)
      })
    }

    var called = false
    pool.acquire(function (err, client) {
      assert.ok(err === null)
      assert.equal(typeof client.id, 'number')
      called = true
    })
    beforeExit(function () {
      assert.ok(called)
      assert.equal(pool.waitingClientsCount(), 0)
    })
  },

  'handle creation errors for delayed creates': function (beforeExit) {
    var created = 0
    var pool = poolModule.Pool({
      name: 'test6',
      create: function (callback) {
        if (created < 5) {
          setTimeout(function () {
            callback(new Error('Error occurred.'))
          }, 0)
        } else {
          setTimeout(function () {
            callback({ id: created })
          }, 0)
        }
        created++
      },
      destroy: function (client) {},
      max: 1,
      idleTimeoutMillis: 1000
    })
    // ensure that creation errors do not populate the pool.
    for (var i = 0; i < 5; i++) {
      pool.acquire(function (err, client) {
        assert.ok(err instanceof Error)
        assert.ok(client === null)
      })
    }
    var called = false
    pool.acquire(function (err, client) {
      assert.ok(err === null)
      assert.equal(typeof client.id, 'number')
      called = true
    })
    beforeExit(function () {
      assert.ok(called)
      assert.equal(pool.waitingClientsCount(), 0)
    })
  },

  'pooled decorator should acquire and release': function (beforeExit) {
    var assertion_count = 0
    var destroyed_count = 0
    var pool = poolModule.Pool({
      name: 'test1',
      create: function (callback) { callback({id: Math.floor(Math.random() * 1000)}) },
      destroy: function (client) { destroyed_count += 1 },
      max: 1,
      idleTimeoutMillis: 100
    })

    var pooledFn = pool.pooled(function (client, cb) {
      assert.equal(typeof client.id, 'number')
      assert.equal(pool.getPoolSize(), 1)
      assertion_count += 2
      cb()
    })

    assert.equal(pool.getPoolSize(), 0)
    assertion_count += 1

    pooledFn(function (err) {
      if (err) { throw err }
      assert.ok(true)
      assertion_count += 1
    })

    beforeExit(function () {
      assert.equal(assertion_count, 4)
      assert.equal(destroyed_count, 1)
    })
  },

  'pooled decorator should pass arguments and return values': function (beforeExit) {
    var assertion_count = 0
    var pool = poolModule.Pool({
      name: 'test1',
      create: function (callback) { callback({id: Math.floor(Math.random() * 1000)}) },
      destroy: function (client) {},
      max: 1,
      idleTimeoutMillis: 100
    })

    var pooledFn = pool.pooled(function (client, arg1, arg2, cb) {
      assert.equal(arg1, 'First argument')
      assert.equal(arg2, 'Second argument')
      assertion_count += 2
      cb(null, 'First return', 'Second return')
    })

    pooledFn('First argument', 'Second argument', function (err, retVal1, retVal2) {
      if (err) { throw err }
      assert.equal(retVal1, 'First return')
      assert.equal(retVal2, 'Second return')
      assertion_count += 2
    })

    beforeExit(function () {
      assert.equal(assertion_count, 4)
    })
  },

  'pooled decorator should allow undefined callback': function (beforeExit) {
    var assertion_count = 0
    var pool = poolModule.Pool({
      name: 'test1',
      create: function (callback) { callback({id: Math.floor(Math.random() * 1000)}) },
      destroy: function (client) {},
      max: 1,
      idleTimeoutMillis: 100
    })

    var pooledFn = pool.pooled(function (client, arg, cb) {
      assert.equal(arg, 'Arg!')
      assertion_count += 1
      cb()
    })

    pooledFn('Arg!')

    beforeExit(function () {
      assert.equal(pool.getPoolSize(), 0)
      assert.equal(assertion_count, 1)
    })
  },

  'pooled decorator should forward pool errors': function (beforeExit) {
    var assertion_count = 0
    var pool = poolModule.Pool({
      name: 'test1',
      create: function (callback) { callback(new Error('Pool error')) },
      destroy: function (client) {},
      max: 1,
      idleTimeoutMillis: 100
    })

    var pooledFn = pool.pooled(function (cb) {
      assert.ok(false, "Pooled function shouldn't be called due to a pool error")
    })

    pooledFn(function (err, obj) {
      assert.equal(err.message, 'Pool error')
      assertion_count += 1
    })

    beforeExit(function () {
      assert.equal(assertion_count, 1)
    })
  },

  'getPoolSize': function (beforeExit) {
    var assertion_count = 0
    var pool = poolModule.Pool({
      name: 'test1',
      create: function (callback) { callback({id: Math.floor(Math.random() * 1000)}) },
      destroy: function (client) {},
      max: 2,
      idleTimeoutMillis: 100
    })

    assert.equal(pool.getPoolSize(), 0)
    assertion_count += 1
    pool.acquire(function (err, obj1) {
      if (err) { throw err }
      assert.equal(pool.getPoolSize(), 1)
      assertion_count += 1
      pool.acquire(function (err, obj2) {
        if (err) { throw err }
        assert.equal(pool.getPoolSize(), 2)
        assertion_count += 1

        pool.release(obj1)
        pool.release(obj2)

        pool.acquire(function (err, obj3) {
          if (err) { throw err }
          // should still be 2
          assert.equal(pool.getPoolSize(), 2)
          assertion_count += 1
          pool.release(obj3)
        })
      })
    })

    beforeExit(function () {
      assert.equal(assertion_count, 4)
    })
  },

  'availableObjectsCount': function (beforeExit) {
    var assertion_count = 0
    var pool = poolModule.Pool({
      name: 'test1',
      create: function (callback) { callback({id: Math.floor(Math.random() * 1000)}) },
      destroy: function (client) {},
      max: 2,
      idleTimeoutMillis: 100
    })

    assert.equal(pool.availableObjectsCount(), 0)
    assertion_count += 1
    pool.acquire(function (err, obj1) {
      if (err) { throw err }
      assert.equal(pool.availableObjectsCount(), 0)
      assertion_count += 1

      pool.acquire(function (err, obj2) {
        if (err) { throw err }
        assert.equal(pool.availableObjectsCount(), 0)
        assertion_count += 1

        pool.release(obj1)
        assert.equal(pool.availableObjectsCount(), 1)
        assertion_count += 1

        pool.release(obj2)
        assert.equal(pool.availableObjectsCount(), 2)
        assertion_count += 1

        pool.acquire(function (err, obj3) {
          if (err) { throw err }
          assert.equal(pool.availableObjectsCount(), 1)
          assertion_count += 1
          pool.release(obj3)

          assert.equal(pool.availableObjectsCount(), 2)
          assertion_count += 1
        })
      })
    })

    beforeExit(function () {
      assert.equal(assertion_count, 7)
    })
  },

  'logPassesLogLevel': function (beforeExit) {
    var loglevels = {'verbose': 0, 'info': 1, 'warn': 2, 'error': 3}
    var logmessages = {verbose: [], info: [], warn: [], error: []}
    var factory = {
      name: 'test1',
      create: function (callback) { callback(null, {id: Math.floor(Math.random() * 1000)}) },
      destroy: function (client) {},
      max: 2,
      idleTimeoutMillis: 100,
      log: function (msg, level) { testlog(msg, level) }
    }
    var testlog = function (msg, level) {
      assert.ok(level in loglevels)
      logmessages[level].push(msg)
    }
    var pool = poolModule.Pool(factory)

    var pool2 = poolModule.Pool({
      name: 'testNoLog',
      create: function (callback) { callback(null, {id: Math.floor(Math.random() * 1000)}) },
      destroy: function (client) {},
      max: 2,
      idleTimeoutMillis: 100
    })
    assert.equal(pool2.getName(), 'testNoLog')

    pool.acquire(function (err, obj) {
      assert.ifError(err)
      assert.equal(logmessages.verbose[0], 'createResource() - creating obj - count=1 min=0 max=2')
      assert.equal(logmessages.info[0], 'dispense() clients=1 available=0')
      logmessages.info = []
      logmessages.verbose = []
      pool2.borrow(function (err, obj) {
        assert.ifError(err)
        assert.equal(logmessages.info.length, 0)
        assert.equal(logmessages.verbose.length, 0)
        assert.equal(logmessages.warn.length, 0)
      })
    })
  },

  'removes from available objects on destroy': function (beforeExit) {
    var destroyCalled = false
    var factory = {
      name: 'test',
      create: function (callback) { callback(null, {}) },
      destroy: function (client) { destroyCalled = true },
      max: 2,
      idleTimeoutMillis: 100
    }

    var pool = poolModule.Pool(factory)
    pool.acquire(function (err, obj) {
      assert.ifError(err)
      pool.destroy(obj)
    })
    assert.equal(destroyCalled, true)
    assert.equal(pool.availableObjectsCount(), 0)
  },

  'removes from available objects on validation failure': function (beforeExit) {
    var destroyCalled = false
    var validateCalled = false
    var count = 0
    var factory = {
      name: 'test',
      create: function (callback) { callback(null, {count: count++}) },
      destroy: function (client) { destroyCalled = client.count },
      validate: function (client) { validateCalled = true; return client.count > 0 },
      max: 2,
      idleTimeoutMillis: 100
    }

    var pool = poolModule.Pool(factory)
    pool.acquire(function (err, obj) {
      assert.ifError(err)
      pool.release(obj)
      assert.equal(obj.count, 0)

      pool.acquire(function (err, obj) {
        assert.ifError(err)
        pool.release(obj)
        assert.equal(obj.count, 1)
      })
    })
    assert.equal(validateCalled, true)
    assert.equal(destroyCalled, 0)
    assert.equal(pool.availableObjectsCount(), 1)
  },

  'removes from available objects on async validation failure': function (beforeExit) {
    var destroyCalled = false
    var validateCalled = false
    var count = 0
    var factory = {
      name: 'test',
      create: function (callback) { callback(null, {count: count++}) },
      destroy: function (client) { destroyCalled = client.count },
      validateAsync: function (client, callback) { validateCalled = true; callback(client.count > 0) },
      max: 2,
      idleTimeoutMillis: 100
    }

    var pool = poolModule.Pool(factory)
    pool.acquire(function (err, obj) {
      assert.ifError(err)
      pool.release(obj)
      assert.equal(obj.count, 0)

      pool.acquire(function (err, obj) {
        assert.ifError(err)
        pool.release(obj)
        assert.equal(obj.count, 1)
      })
    })
    assert.equal(validateCalled, true)
    assert.equal(destroyCalled, 0)
    assert.equal(pool.availableObjectsCount(), 1)
  },

  'error on setting both validate functions': function (beforeExit) {
    var noop = function () {}
    var factory = {
      name: 'test',
      create: noop,
      destroy: noop,
      validate: noop,
      validateAsync: noop
    }

    try {
      poolModule.Pool(factory)
    } catch (err) {
      assert.equal(err.message, 'Only one of validate or validateAsync may be specified')
    }
  },

  'do schedule again if error occured when creating new Objects async': function (beforeExit) {
    var factory = {
      name: 'test',
      create: function (callback) {
        process.nextTick(function () {
          var err = new Error('Create Error')
          callback(err)
        })
      },
      destroy: function (client) {},
      max: 1,
      idleTimeoutMillis: 100
    }

    var getFlag = 0
    var pool = poolModule.Pool(factory)
    pool.acquire(function () {})
    pool.acquire(function (err, obj) {
      getFlag = 1
      assert(err)
      assert.equal(pool.availableObjectsCount(), 0)
    })

    beforeExit(function () {
      assert.equal(getFlag, 1)
    })
  },

  'returns only valid object to the pool': function (beforeExit) {
    var pool = poolModule.Pool({
      name: 'test',
      create: function (callback) {
        process.nextTick(function () {
          callback(null, { id: 'validId' })
        })
      },
      destroy: function (client) {},
      max: 1,
      idleTimeoutMillis: 100
    })

    pool.acquire(function (err, obj) {
      assert.ifError(err)
      assert.equal(pool.availableObjectsCount(), 0)
      assert.equal(pool.inUseObjectsCount(), 1)

      // Invalid release
      pool.release({})
      assert.equal(pool.availableObjectsCount(), 0)
      assert.equal(pool.inUseObjectsCount(), 1)

      // Valid release
      pool.release(obj)
      assert.equal(pool.availableObjectsCount(), 1)
      assert.equal(pool.inUseObjectsCount(), 0)
    })
  },

  'validate acquires object from the pool': function (beforeExit) {
    var pool = poolModule.Pool({
      name: 'test',
      create: function (callback) {
        process.nextTick(function () {
          callback(null, { id: 'validId' })
        })
      },
      validate: function (resource) {
        return true
      },
      destroy: function (client) {},
      max: 1,
      idleTimeoutMillis: 100
    })

    pool.acquire(function (err, obj) {
      assert.ifError(err)
      assert.equal(pool.availableObjectsCount(), 0)
      assert.equal(pool.inUseObjectsCount(), 1)
    })
  },

  'validateAsync acquires object from the pool': function (beforeExit) {
    var pool = poolModule.Pool({
      name: 'test',
      create: function (callback) {
        process.nextTick(function () {
          callback(null, { id: 'validId' })
        })
      },
      validateAsync: function (resource, callback) {
        callback(true)
      },
      destroy: function (client) {},
      max: 1,
      idleTimeoutMillis: 100
    })

    pool.acquire(function (err, obj) {
      assert.ifError(err)
      assert.equal(pool.availableObjectsCount(), 0)
      assert.equal(pool.inUseObjectsCount(), 1)
    })
  }
}
