var test = require('tap').test
var LRU = require('../')

test('basic', function (t) {
  var cache = new LRU({ max: 10 })
  cache.set('key', 'value')
  t.equal(cache.get('key'), 'value')
  t.equal(cache.get('nada'), undefined)
  t.equal(cache.length, 1)
  t.equal(cache.max, 10)
  t.end()
})

test('least recently set', function (t) {
  var cache = new LRU(2)
  cache.set('a', 'A')
  cache.set('b', 'B')
  cache.set('c', 'C')
  t.equal(cache.get('c'), 'C')
  t.equal(cache.get('b'), 'B')
  t.equal(cache.get('a'), undefined)
  t.end()
})

test('lru recently gotten', function (t) {
  var cache = new LRU(2)
  cache.set('a', 'A')
  cache.set('b', 'B')
  cache.get('a')
  cache.set('c', 'C')
  t.equal(cache.get('c'), 'C')
  t.equal(cache.get('b'), undefined)
  t.equal(cache.get('a'), 'A')
  t.end()
})

test('del', function (t) {
  var cache = new LRU(2)
  cache.set('a', 'A')
  cache.del('a')
  t.equal(cache.get('a'), undefined)
  t.end()
})

test('max', function (t) {
  var cache = new LRU(3)

  // test changing the max, verify that the LRU items get dropped.
  cache.max = 100
  var i
  for (i = 0; i < 100; i++) cache.set(i, i)
  t.equal(cache.length, 100)
  for (i = 0; i < 100; i++) {
    t.equal(cache.get(i), i)
  }
  cache.max = 3
  t.equal(cache.length, 3)
  for (i = 0; i < 97; i++) {
    t.equal(cache.get(i), undefined)
  }
  for (i = 98; i < 100; i++) {
    t.equal(cache.get(i), i)
  }

  // now remove the max restriction, and try again.
  cache.max = 0
  for (i = 0; i < 100; i++) cache.set(i, i)
  t.equal(cache.length, 100)
  for (i = 0; i < 100; i++) {
    t.equal(cache.get(i), i)
  }
  // should trigger an immediate resize
  cache.max = 3
  t.equal(cache.length, 3)
  for (i = 0; i < 97; i++) {
    t.equal(cache.get(i), undefined)
  }
  for (i = 98; i < 100; i++) {
    t.equal(cache.get(i), i)
  }
  t.end()
})

test('reset', function (t) {
  var cache = new LRU(10)
  cache.set('a', 'A')
  cache.set('b', 'B')
  cache.reset()
  t.equal(cache.length, 0)
  t.equal(cache.max, 10)
  t.equal(cache.get('a'), undefined)
  t.equal(cache.get('b'), undefined)
  t.end()
})

test('basic with weighed length', function (t) {
  var cache = new LRU({
    max: 100,
    length: function (item, key) {
      t.isa(key, 'string')
      return item.size
    }
  })
  cache.set('key', { val: 'value', size: 50 })
  t.equal(cache.get('key').val, 'value')
  t.equal(cache.get('nada'), undefined)
  t.equal(cache.lengthCalculator(cache.get('key'), 'key'), 50)
  t.equal(cache.length, 50)
  t.equal(cache.max, 100)
  t.end()
})

test('weighed length item too large', function (t) {
  var cache = new LRU({
    max: 10,
    length: function (item) { return item.size }
  })
  t.equal(cache.max, 10)

  // should fall out immediately
  cache.set('key', { val: 'value', size: 50 })

  t.equal(cache.length, 0)
  t.equal(cache.get('key'), undefined)
  t.end()
})

test('least recently set with weighed length', function (t) {
  var cache = new LRU({
    max: 8,
    length: function (item) { return item.length }
  })
  cache.set('a', 'A')
  cache.set('b', 'BB')
  cache.set('c', 'CCC')
  cache.set('d', 'DDDD')
  t.equal(cache.get('d'), 'DDDD')
  t.equal(cache.get('c'), 'CCC')
  t.equal(cache.get('b'), undefined)
  t.equal(cache.get('a'), undefined)
  t.end()
})

test('lru recently gotten with weighed length', function (t) {
  var cache = new LRU({
    max: 8,
    length: function (item) { return item.length }
  })
  cache.set('a', 'A')
  cache.set('b', 'BB')
  cache.set('c', 'CCC')
  cache.get('a')
  cache.get('b')
  cache.set('d', 'DDDD')
  t.equal(cache.get('c'), undefined)
  t.equal(cache.get('d'), 'DDDD')
  t.equal(cache.get('b'), 'BB')
  t.equal(cache.get('a'), 'A')
  t.end()
})

test('lru recently updated with weighed length', function (t) {
  var cache = new LRU({
    max: 8,
    length: function (item) { return item.length }
  })
  cache.set('a', 'A')
  cache.set('b', 'BB')
  cache.set('c', 'CCC')
  t.equal(cache.length, 6) // CCC BB A
  cache.set('a', '+A')
  t.equal(cache.length, 7) // +A CCC BB
  cache.set('b', '++BB')
  t.equal(cache.length, 6) // ++BB +A
  t.equal(cache.get('c'), undefined)

  cache.set('c', 'oversized')
  t.equal(cache.length, 6) // ++BB +A
  t.equal(cache.get('c'), undefined)

  cache.set('a', 'oversized')
  t.equal(cache.length, 4) // ++BB
  t.equal(cache.get('a'), undefined)
  t.equal(cache.get('b'), '++BB')
  t.end()
})

test('set returns proper booleans', function (t) {
  var cache = new LRU({
    max: 5,
    length: function (item) { return item.length }
  })

  t.equal(cache.set('a', 'A'), true)

  // should return false for max exceeded
  t.equal(cache.set('b', 'donuts'), false)

  t.equal(cache.set('b', 'B'), true)
  t.equal(cache.set('c', 'CCCC'), true)
  t.end()
})

test('drop the old items', function (t) {
  var n = process.env.CI ? 1000 : 100
  var cache = new LRU({
    max: 5,
    maxAge: n * 2
  })

  cache.set('a', 'A')

  setTimeout(function () {
    cache.set('b', 'b')
    t.equal(cache.get('a'), 'A')
  }, n)

  setTimeout(function () {
    cache.set('c', 'C')
    // timed out
    t.notOk(cache.get('a'))
  }, n * 3)

  setTimeout(function () {
    t.notOk(cache.get('b'))
    t.equal(cache.get('c'), 'C')
  }, n * 4)

  setTimeout(function () {
    t.notOk(cache.get('c'))
    t.end()
  }, n * 6)
})

test('manual pruning', function (t) {
  var cache = new LRU({
    max: 5,
    maxAge: 50
  })

  cache.set('a', 'A')
  cache.set('b', 'b')
  cache.set('c', 'C')

  setTimeout(function () {
    cache.prune()

    t.notOk(cache.get('a'))
    t.notOk(cache.get('b'))
    t.notOk(cache.get('c'))

    t.end()
  }, 100)
})

test('individual item can have its own maxAge', function (t) {
  var cache = new LRU({
    max: 5,
    maxAge: 50
  })

  cache.set('a', 'A', 20)
  setTimeout(function () {
    t.notOk(cache.get('a'))
    t.end()
  }, 25)
})

test('individual item can have its own maxAge > cache', function (t) {
  var cache = new LRU({
    max: 5,
    maxAge: 20
  })

  cache.set('a', 'A', 50)
  setTimeout(function () {
    t.equal(cache.get('a'), 'A')
    t.end()
  }, 25)
})

test('disposal function', function (t) {
  var disposed = false
  var cache = new LRU({
    max: 1,
    dispose: function (k, n) {
      disposed = n
    }
  })

  cache.set(1, 1)
  cache.set(2, 2)
  t.equal(disposed, 1)
  cache.set(2, 10)
  t.equal(disposed, 2)
  cache.set(3, 3)
  t.equal(disposed, 10)
  cache.reset()
  t.equal(disposed, 3)
  t.end()
})

test('no dispose on set', function (t) {
  var disposed = false
  var cache = new LRU({
    max: 1,
    noDisposeOnSet: true,
    dispose: function (k, n) {
      disposed = n
    }
  })

  cache.set(1, 1)
  cache.set(1, 10)
  t.equal(disposed, false)
  t.end()
})

test('disposal function on too big of item', function (t) {
  var disposed = false
  var cache = new LRU({
    max: 1,
    length: function (k) {
      return k.length
    },
    dispose: function (k, n) {
      disposed = n
    }
  })
  var obj = [ 1, 2 ]

  t.equal(disposed, false)
  cache.set('obj', obj)
  t.equal(disposed, obj)
  t.end()
})

test('has()', function (t) {
  var cache = new LRU({
    max: 1,
    maxAge: 10
  })

  cache.set('foo', 'bar')
  t.equal(cache.has('foo'), true)
  cache.set('blu', 'baz')
  t.equal(cache.has('foo'), false)
  t.equal(cache.has('blu'), true)
  setTimeout(function () {
    t.equal(cache.has('blu'), false)
    t.end()
  }, 15)
})

test('stale', function (t) {
  var cache = new LRU({
    maxAge: 10,
    stale: true
  })

  t.equal(cache.allowStale, true)
  cache.allowStale = false
  t.equal(cache.allowStale, false)
  cache.allowStale = true
  t.equal(cache.allowStale, true)

  cache.set('foo', 'bar')
  t.equal(cache.get('foo'), 'bar')
  t.equal(cache.has('foo'), true)
  setTimeout(function () {
    t.equal(cache.has('foo'), false)
    t.equal(cache.get('foo'), 'bar')
    t.equal(cache.get('foo'), undefined)
    t.end()
  }, 15)
})

test('lru update via set', function (t) {
  var cache = new LRU({ max: 2 })

  cache.set('foo', 1)
  cache.set('bar', 2)
  cache.del('bar')
  cache.set('baz', 3)
  cache.set('qux', 4)

  t.equal(cache.get('foo'), undefined)
  t.equal(cache.get('bar'), undefined)
  t.equal(cache.get('baz'), 3)
  t.equal(cache.get('qux'), 4)
  t.end()
})

test('least recently set w/ peek', function (t) {
  var cache = new LRU(2)
  cache.set('a', 'A')
  cache.set('b', 'B')
  t.equal(cache.peek('a'), 'A')
  cache.set('c', 'C')
  t.equal(cache.get('c'), 'C')
  t.equal(cache.get('b'), 'B')
  t.equal(cache.get('a'), undefined)
  t.end()
})

test('pop the least used item', function (t) {
  var cache = new LRU(3)
  var last

  cache.set('a', 'A')
  cache.set('b', 'B')
  cache.set('c', 'C')

  t.equal(cache.length, 3)
  t.equal(cache.max, 3)

  // Ensure we pop a, c, b
  cache.get('b', 'B')

  last = cache.pop()
  t.equal(last.key, 'a')
  t.equal(last.value, 'A')
  t.equal(cache.length, 2)
  t.equal(cache.max, 3)

  last = cache.pop()
  t.equal(last.key, 'c')
  t.equal(last.value, 'C')
  t.equal(cache.length, 1)
  t.equal(cache.max, 3)

  last = cache.pop()
  t.equal(last.key, 'b')
  t.equal(last.value, 'B')
  t.equal(cache.length, 0)
  t.equal(cache.max, 3)

  last = cache.pop()
  t.equal(last, null)
  t.equal(cache.length, 0)
  t.equal(cache.max, 3)

  t.end()
})

test('get and set only accepts strings and numbers as keys', function (t) {
  var cache = new LRU()

  cache.set('key', 'value')
  cache.set(123, 456)

  t.equal(cache.get('key'), 'value')
  t.equal(cache.get(123), 456)

  t.end()
})

test('peek with wierd keys', function (t) {
  var cache = new LRU()

  cache.set('key', 'value')
  cache.set(123, 456)

  t.equal(cache.peek('key'), 'value')
  t.equal(cache.peek(123), 456)

  t.equal(cache.peek({
    toString: function () { return 'key' }
  }), undefined)

  t.end()
})

test('invalid length calc results in basic length', function (t) {
  var l = new LRU({ length: true })
  t.isa(l.lengthCalculator, 'function')
  l.lengthCalculator = 'not a function'
  t.isa(l.lengthCalculator, 'function')
  t.end()
})

test('change length calculator recalculates', function (t) {
  var l = new LRU({ max: 3 })
  l.set(2, 2)
  l.set(1, 1)
  l.lengthCalculator = function (key, val) {
    return key + val
  }
  t.equal(l.itemCount, 1)
  t.equal(l.get(2), undefined)
  t.equal(l.get(1), 1)
  l.set(0, 1)
  t.equal(l.itemCount, 2)
  l.lengthCalculator = function (key, val) {
    return key
  }
  t.equal(l.lengthCalculator(1, 10), 1)
  t.equal(l.lengthCalculator(10, 1), 10)
  l.lengthCalculator = { not: 'a function' }
  t.equal(l.lengthCalculator(1, 10), 1)
  t.equal(l.lengthCalculator(10, 1), 1)
  t.end()
})

test('delete non-existent item has no effect', function (t) {
  var l = new LRU({ max: 2 })
  l.set('foo', 1)
  l.set('bar', 2)
  l.del('baz')
  t.same(l.dumpLru().toArray().map(function (hit) {
    return hit.key
  }), [ 'bar', 'foo' ])
  t.end()
})

test('maxAge on list, cleared in forEach', function (t) {
  var l = new LRU({ stale: true })
  l.set('foo', 1)

  // hacky.  make it seem older.
  l.dumpLru().head.value.now = Date.now() - 100000

  t.equal(l.maxAge, 0)

  l.maxAge = 1

  var saw = false
  l.forEach(function (val, key) {
    saw = true
    t.equal(key, 'foo')
  })
  t.ok(saw)
  t.equal(l.length, 0)

  t.end()
})

test('bad max/maxAge options', t => {
  t.throws(() => new LRU({ maxAge: true }), 'maxAge must be a number')
  t.throws(() => { new LRU().maxAge = 'foo' }, 'maxAge must be a number')
  t.throws(() => new LRU({ max: true }), 'max must be a non-negative number')
  t.throws(() => { new LRU().max = 'foo' }, 'max must be a non-negative number')
  const c = new LRU({
    max: 2
  })
  t.throw(() => c.set('a', 'A', 'true'), 'maxAge must be a number')
  t.end()
})

test('update age on get', t => {
  const l = new LRU({ updateAgeOnGet: true, maxAge: 10 })
  l.set('foo', 'bar')
  const e1 = l.dump()[0].e
  // spin for 5ms
  for (let then = Date.now() + 5; then > Date.now(); );
  l.get('foo')
  const e2 = l.dump()[0].e
  // spin for 5ms
  for (let then = Date.now() + 5; then > Date.now(); );
  l.get('foo')
  const e3 = l.dump()[0].e
  t.ok(e1 < e2, 'time updated on first get')
  t.ok(e2 < e3, 'time updated on second get')
  t.end()
})
