
local bh = require('binaryheap')

local data = {
  { value = 98, payload = "pos08" }, -- 1
  { value = 28, payload = "pos05" }, -- 2
  { value = 36, payload = "pos06" }, -- 3
  { value = 48, payload = "pos09" }, -- 4
  { value = 68, payload = "pos10" }, -- 5
  { value = 58, payload = "pos13" }, -- 6
  { value = 80, payload = "pos15" }, -- 7
  { value = 46, payload = "pos04" }, -- 8
  { value = 19, payload = "pos03" }, -- 9
  { value = 66, payload = "pos11" }, -- 10
  { value = 22, payload = "pos02" }, -- 11
  { value = 60, payload = "pos12" }, -- 12
  { value = 15, payload = "pos01" }, -- 13
  { value = 83, payload = "pos14" }, -- 14
  { value = 59, payload = "pos07" }, -- 15
}

local sort = function(t)
  table.sort(t, function(a,b) return (a.value < b.value) end)
  return t
end

local function check(heap)
  for pos = 2, #heap.values do
    local parent = math.floor(pos / 2)
    assert(not heap.lt(heap.values[pos], heap.values[parent]))
  end
  if heap.payloads then
    for pos in ipairs(heap.values) do
      local payload = heap.payloads[pos]
      assert(heap.reverse[payload] == pos)
    end
  end
end

local function newheap()
  -- create a heap with data
  local heap = bh.minUnique()
  for _, node in ipairs(data) do
    heap:insert(node.value,node.payload)
    check(heap)
  end

  -- create a sorted list with data, sorted by 'value'
  local sorted = {}
  for k,v in pairs(data) do sorted[k] = v end
  sort(sorted)
  -- create a reverse list of the sorted table; returns sorted-index, based on 'value'
  local sreverse = {}
  for i,v in ipairs(sorted) do
    sreverse[v.value] = i
  end
  return heap, sorted, sreverse
end

local function testheap(heap, sorted)
  while sorted[1] do
    local value1, payload1
    if heap.reverse then
      -- it is a unique heap
      payload1, value1 = heap:pop()
    else
      -- it is a plain heap
      value1, payload1 = heap:pop()
    end
    local value2, payload2 = sorted[1].value, sorted[1].payload
    table.remove(sorted, 1)
    assert.are.equal(payload1, payload2)
    assert.are.equal(value1, value2)
  end
end

describe("[minUnique]", function()

  it("validates order of insertion", function()
    local h = newheap()
    assert.are.equal(h.payloads[1], data[13].payload)
    assert.are.equal(h.payloads[2], data[11].payload)
    assert.are.equal(h.payloads[3], data[9].payload)
    assert.are.equal(h.payloads[4], data[8].payload)
    assert.are.equal(h.payloads[5], data[2].payload)
    assert.are.equal(h.payloads[6], data[3].payload)
    assert.are.equal(h.payloads[7], data[15].payload)
    assert.are.equal(h.payloads[8], data[1].payload)
    assert.are.equal(h.payloads[9], data[4].payload)
    assert.are.equal(h.payloads[10], data[5].payload)
    assert.are.equal(h.payloads[11], data[10].payload)
    assert.are.equal(h.payloads[12], data[12].payload)
    assert.are.equal(h.payloads[13], data[6].payload)
    assert.are.equal(h.payloads[14], data[14].payload)
    assert.are.equal(h.payloads[15], data[7].payload)
  end)

  it("validates order of popping", function()
    testheap(newheap())
  end)

  it("peek()", function()
    local heap, sorted = newheap()
    local payload, value = heap:peek()
    -- correct values?
    assert.are.equal(value, sorted[1].value)
    assert.are.equal(payload, sorted[1].payload)
    -- are they still on the heap?
    assert.are.equal(value, heap.values[1])
    assert.are.equal(payload, heap.payloads[1])
  end)

  it("peekValue()", function()
    local h = bh.minUnique()
    h:insert(1, 11)
    assert.equal(1, h:peekValue(11))
    -- try again empty
    h:pop()
    assert.is_nil(h:peekValue(11))
  end)

  it("pop()", function()
    local h = bh.minUnique()
    h:insert(3, 13)
    h:insert(2, 12)
    h:insert(1, 11)
    -- try again empty
    local pl, v
    pl, v = h:pop()
    assert.equal(v, 1)
    assert.equal(pl, 11)
    pl, v = h:pop()
    assert.equal(v, 2)
    assert.equal(pl, 12)
    pl, v = h:pop()
    assert.equal(v, 3)
    assert.equal(pl, 13)
    pl, v = h:pop()
    assert.is_nil(v)
    assert.is_nil(pl)
end)

  describe("remove()", function()
    it("a middle item", function()
      local heap, sorted = newheap()
      local idx = 4
      local value = sorted[idx].value
      local payload = sorted[idx].payload
      local v, pl = heap:remove(payload)
      check(heap)
      -- did we get the right ones?
      assert.are.equal(value, v)
      assert.are.equal(payload, pl)
      assert.is.Nil(heap[payload])
      -- remove from test data and compare
      table.remove(sorted, idx)
      testheap(heap, sorted)
    end)

    it("the last item (of the array)", function()
      local heap, sorted = newheap()
      local idx = #heap.values
      local value = sorted[idx].value
      local payload = sorted[idx].payload
      local v, pl = heap:remove(payload)
      check(heap)
      -- did we get the right ones?
      assert.are.equal(value, v)
      assert.are.equal(payload, pl)
      assert.is.Nil(heap.reverse[payload])
      -- remove from test data and compare
      table.remove(sorted, idx)
      testheap(heap, sorted)
    end)

    it("non existing payload returns nil", function()
      local heap = newheap()
      local v, pl = heap:remove({})
      assert.is_nil(v)
      assert.is_nil(pl)
    end)

    it("nil payload returns nil", function()
      local heap = newheap()
      local v, pl = heap:remove(nil)
      assert.is_nil(v)
      assert.is_nil(pl)
    end)

    it("with repeated values", function()
      local h = bh.minUnique()
      h:insert(1, 11)
      check(h)
      h:insert(1, 12)
      check(h)
      local value, payload
      value, payload = h:remove(11)
      check(h)
      assert.equal(1, value)
      assert.equal(11, payload)
      payload, value = h:peek()
      assert.equal(1, value)
      assert.equal(12, payload)
      assert.same({1}, h.values)
      assert.same({12}, h.payloads)
      assert.same({[12]=1}, h.reverse)
    end)
  end)

  describe("insert()", function()
    it("a top item", function()
      local heap, sorted = newheap()
      local nvalue = sorted[1].value - 10
      local npayload = {}
      table.insert(sorted, 1, {})
      sorted[1].value = nvalue
      sorted[1].payload = npayload
      heap:insert(nvalue, npayload)
      check(heap)
      testheap(heap, sorted)
    end)

    it("a middle item", function()
      local heap, sorted = newheap()
      local nvalue = 57
      local npayload = {}
      table.insert(sorted, { value = nvalue, payload = npayload })
      sort(sorted)
      heap:insert(nvalue, npayload)
      check(heap)
      testheap(heap, sorted)
    end)

    it("a last item", function()
      local heap, sorted = newheap()
      local nvalue = sorted[#sorted].value + 10
      local npayload = {}
      table.insert(sorted, {})
      sorted[#sorted].value = nvalue
      sorted[#sorted].payload = npayload
      heap:insert(nvalue, npayload)
      check(heap)
      testheap(heap, sorted)
    end)

    it("a nil value throws an error", function()
      local heap = newheap()
      assert.has.error(function()
        heap:insert(nil, "something")
      end)
    end)

    it("a nil payload throws an error", function()
      local heap = newheap()
      assert.has.error(function()
        heap:insert(15, nil)
      end)
    end)

    it("a duplicate payload throws an error", function()
      local heap = newheap()
      local value = {}
      heap:insert(1, value)
      assert.has.error(function()
        heap:insert(2, value)
      end)
    end)
  end)

  describe("update()", function()
    it("a top item", function()
      local heap, sorted = newheap()
      local idx = 1
      local payload = sorted[idx].payload
      local nvalue = sorted[#sorted].value + 1 -- move to end with new value
      sorted[idx].value = nvalue
      sort(sorted)
      heap:update(payload, nvalue)
      check(heap)
      testheap(heap, sorted)
    end)

    it("a middle item", function()
      local heap, sorted = newheap()
      local idx = 4
      local payload = sorted[idx].payload
      local nvalue = sorted[idx].value * 2
      sorted[idx].value = nvalue
      sort(sorted)
      heap:update(payload, nvalue)
      check(heap)
      testheap(heap, sorted)
    end)

    it("a last item", function()
      local heap, sorted = newheap()
      local idx = #sorted
      local payload = sorted[idx].payload
      local nvalue = sorted[1].value - 1 -- move to top with new value
      sorted[idx].value = nvalue
      sort(sorted)
      heap:update(payload, nvalue)
      check(heap)
      testheap(heap, sorted)
    end)

    it("a nil value throws an error", function()
      local heap, sorted = newheap()
      local idx = #sorted
      local payload = sorted[idx].payload
      assert.has.error(function()
        heap:update(payload, nil)
      end)
    end)

    it("an unknown payload throws an error", function()
      local heap = newheap()
      assert.has.error(function()
        heap:update({}, 10)
      end)
    end)
  end)

  describe("size()", function()
    it("returns number of elements", function()
      local h = bh.minUnique()
      assert.equal(0, h:size())
      h:insert(1, -1)
      assert.equal(1, h:size())
      h:insert(2, -2)
      assert.equal(2, h:size())
      h:insert(3, -3)
      assert.equal(3, h:size())
      h:insert(4, -4)
      assert.equal(4, h:size())
      h:insert(5, -5)
      assert.equal(5, h:size())
      h:pop()
      assert.equal(4, h:size())
      h:pop()
      assert.equal(3, h:size())
      h:pop()
      assert.equal(2, h:size())
      h:pop()
      assert.equal(1, h:size())
      h:pop()
      assert.equal(0, h:size())
    end)
  end)

  describe("valueByPayload()", function()
    it("gets value by payload", function()
      local h = bh.minUnique()
      h:insert(1, -1)
      h:insert(2, -2)
      h:insert(3, -3)
      h:insert(4, -4)
      h:insert(5, -5)
      assert.equal(1, h:valueByPayload((-1)))
      assert.equal(2, h:valueByPayload((-2)))
      assert.equal(3, h:valueByPayload((-3)))
      assert.equal(4, h:valueByPayload((-4)))
      assert.equal(5, h:valueByPayload((-5)))
      h:remove(-1)
      assert.falsy(h:valueByPayload((-1)))
    end)

    it("non existing payload returns nil", function()
      local h = bh.minUnique()
      h:insert(1, -1)
      h:insert(2, -2)
      h:insert(3, -3)
      h:insert(4, -4)
      h:insert(5, -5)
      assert.is_nil(h:valueByPayload({}))
    end)

    it("nil payload returns nil", function()
      local h = bh.minUnique()
      h:insert(1, -1)
      h:insert(2, -2)
      h:insert(3, -3)
      h:insert(4, -4)
      h:insert(5, -5)
      assert.is_nil(h:valueByPayload(nil))
    end)
  end)

  it("creates minUnique with custom less-than function", function()
    local h = bh.minUnique(function (a, b)
      return math.abs(a) < math.abs(b)
    end)
    h:insert(1, -1)
    check(h)
    h:insert(-2, 2)
    check(h)
    h:insert(3, -3)
    check(h)
    h:insert(-4, 4)
    check(h)
    h:insert(5, -5)
    check(h)
    local value, payload
    payload, value = h:peek()
    assert.equal(1, value)
    assert.equal(-1, payload)
    h:pop()
    check(h)
    payload, value = h:peek()
    assert.equal(-2, value)
    assert.equal(2, payload)
  end)
end)

describe("[maxUnique]", function()
  it("creates maxUnique with custom less-than function", function()
    local h = bh.maxUnique(function (a, b)
      return math.abs(a) > math.abs(b)
    end)
    h:insert(1, -1)
    check(h)
    h:insert(-2, 2)
    check(h)
    h:insert(3, -3)
    check(h)
    h:insert(-4, 4)
    check(h)
    h:insert(5, -5)
    check(h)
    local value, payload
    payload, value = h:peek()
    assert.equal(5, value)
    assert.equal(-5, payload)
    h:pop()
    check(h)
    payload, value = h:peek()
    assert.equal(-4, value)
    assert.equal(4, payload)
  end)
end)

describe("[minHeap]", function()
  it("creates minHeap", function()
    local h = bh.minHeap()
    check(h)
  end)

  describe("insert()", function()
    it("a number into minHeap", function()
      local h = bh.minHeap()
      h:insert(42)
      check(h)
    end)

    it("nil throws an error", function()
      local h = bh.minHeap()
      assert.has.error(function()
        h:insert(nil)
      end)
    end)
  end)

  describe("remove()", function()
    it("a position", function()
      local h = bh.minHeap()
      h:insert(42)
      h:insert(43)
      assert.equal(43, h:remove(2))
      check(h)
    end)

    it("a bad position returns nil", function()
      local h = bh.minHeap()
      h:insert(42)
      h:insert(43)
      assert.is_nil(h:remove(0))
      assert.is_nil(h:remove(3))
      check(h)
    end)
  end)

  describe("size()", function()
    it("returns number of elements", function()
      local h = bh.minHeap()
      assert.equal(0, h:size())
      h:insert(1)
      assert.equal(1, h:size())
      h:insert(2)
      assert.equal(2, h:size())
      h:insert(3)
      assert.equal(3, h:size())
      h:insert(4)
      assert.equal(4, h:size())
      h:insert(5)
      assert.equal(5, h:size())
      h:pop()
      assert.equal(4, h:size())
      h:pop()
      assert.equal(3, h:size())
      h:pop()
      assert.equal(2, h:size())
      h:pop()
      assert.equal(1, h:size())
      h:pop()
      assert.equal(0, h:size())
    end)
  end)

  describe("peek()", function()
    it("return nil in empty minHeap", function()
      local h = bh.minHeap()
      assert.is_nil(h:peek())
      check(h)
    end)

    it("minHeap of one element", function()
      local h = bh.minHeap()
      h:insert(42)
      check(h)
      local value, payload = h:peek()
      assert.equal(42, value)
      assert.falsy(payload)
    end)

    it("minHeap of two elements", function()
      local h = bh.minHeap()
      h:insert(42)
      check(h)
      h:insert(1)
      check(h)
      local value, payload = h:peek()
      assert.equal(1, value)
      assert.falsy(payload)
    end)

    it("minHeap of 10 elements", function()
      local h = bh.minHeap()
      h:insert(10)
      h:insert(7)
      h:insert(1)
      h:insert(5)
      h:insert(6)
      h:insert(9)
      h:insert(8)
      h:insert(4)
      h:insert(2)
      h:insert(3)
      check(h)
      local value, payload = h:peek()
      assert.equal(1, value)
      assert.falsy(payload)
    end)

    it("removes peek in minHeap of 5 elements", function()
      local h = bh.minHeap()
      h:insert(1)
      h:insert(2)
      h:insert(3)
      h:insert(4)
      h:insert(5)
      local value
      value = h:pop()
      check(h)
      assert.equal(1, value)
      value = h:peek()
      assert.equal(2, value)
    end)
  end)

  describe("update()", function()
    it("in minHeap of 5 elements (pos 2 -> pos 1)", function()
      local h = bh.minHeap()
      h:insert(1)
      h:insert(2)
      h:insert(3)
      h:insert(4)
      h:insert(5)
      check(h)
      h:update(2, -100)
      check(h)
      local value = h:peek()
      assert.equal(-100, value)
    end)

    it("in minHeap of 5 elements (pos 1 -> pos 2)", function()
      local h = bh.minHeap()
      h:insert(1)
      h:insert(2)
      h:insert(3)
      h:insert(4)
      h:insert(5)
      check(h)
      h:update(1, 100)
      check(h)
      local value = h:peek()
      assert.equal(2, value)
    end)

    it("nil throws an error", function()
      local h = bh.minHeap()
      h:insert(10)
      assert.has.error(function()
        h:update(nil)
      end)
    end)

    it("bad position throws an error", function()
      local h = bh.minHeap()
      h:insert(10)
      assert.has.error(function()
        h:update(0)
      end)
      assert.has.error(function()
        h:update(2)
      end)
    end)
  end)

  it("creates minHeap with custom less-than function", function()
    local h = bh.minHeap(function (a, b)
      return math.abs(a) < math.abs(b)
    end)
    h:insert(1)
    check(h)
    h:insert(-2)
    check(h)
    h:insert(3)
    check(h)
    h:insert(-4)
    check(h)
    h:insert(5)
    check(h)
    assert.equal(1, h:peek())
    h:pop()
    check(h)
    assert.equal(-2, h:peek())
  end)
end)


describe("[maxHeap]", function()
  it("creates maxHeap", function()
    local h = bh.maxHeap()
    check(h)
  end)

  describe("insert()", function()
    it("inserts a number into maxHeap", function()
      local h = bh.maxHeap()
      h:insert(42)
      check(h)
    end)

    it("nil throws an error", function()
      local h = bh.maxHeap()
      assert.has.error(function()
        h:insert(nil)
      end)
    end)
  end)

  describe("remove()", function()
    it("a position", function()
      local h = bh.maxHeap()
      h:insert(42)
      h:insert(43)
      assert.equal(42, h:remove(2))
      check(h)
    end)

    it("a bad position returns nil", function()
      local h = bh.maxHeap()
      h:insert(42)
      h:insert(43)
      assert.is_nil(h:remove(0))
      assert.is_nil(h:remove(3))
      check(h)
    end)
  end)

  describe("size()", function()
    it("returns number of elements", function()
      local h = bh.minHeap()
      assert.equal(0, h:size())
      h:insert(1)
      assert.equal(1, h:size())
      h:insert(2)
      assert.equal(2, h:size())
      h:insert(3)
      assert.equal(3, h:size())
      h:insert(4)
      assert.equal(4, h:size())
      h:insert(5)
      assert.equal(5, h:size())
      h:pop()
      assert.equal(4, h:size())
      h:pop()
      assert.equal(3, h:size())
      h:pop()
      assert.equal(2, h:size())
      h:pop()
      assert.equal(1, h:size())
      h:pop()
      assert.equal(0, h:size())
    end)
  end)

  describe("peek()", function()
    it("return nil in empty maxHeap", function()
      local h = bh.maxHeap()
      assert.is_nil(h:peek())
      check(h)
    end)

    it("maxHeap of one element", function()
      local h = bh.maxHeap()
      h:insert(42)
      check(h)
      local value = h:peek()
      assert.equal(42, value)
    end)

    it("maxHeap of two elements", function()
      local h = bh.maxHeap()
      h:insert(42)
      check(h)
      h:insert(1)
      check(h)
      local value = h:peek()
      assert.equal(42, value)
    end)

    it("maxHeap of 10 elements", function()
      local h = bh.maxHeap()
      h:insert(10)
      h:insert(7)
      h:insert(1)
      h:insert(5)
      h:insert(6)
      h:insert(9)
      h:insert(8)
      h:insert(4)
      h:insert(2)
      h:insert(3)
      check(h)
      local value = h:peek()
      assert.equal(10, value)
    end)

    it("removes peek in maxHeap of 5 elements", function()
      local h = bh.maxHeap()
      h:insert(1)
      h:insert(2)
      h:insert(3)
      h:insert(4)
      h:insert(5)
      check(h)
      local value
      value = h:pop()
      check(h)
      assert.equal(5, value)
      value = h:peek()
      assert.equal(4, value)
    end)
  end)

  describe("update()", function()
    it("in maxHeap of 5 elements (pos 2 -> pos 1)", function()
      local h = bh.maxHeap()
      h:insert(1)
      h:insert(2)
      h:insert(3)
      h:insert(4)
      h:insert(5)
      check(h)
      h:update(2, 100)
      check(h)
      local value = h:peek()
      assert.equal(100, value)
    end)

    it("in maxHeap of 5 elements (pos 1 -> pos 2)", function()
      local h = bh.maxHeap()
      h:insert(1)
      h:insert(2)
      h:insert(3)
      h:insert(4)
      h:insert(5)
      check(h)
      h:update(1, -100)
      check(h)
      local value = h:peek()
      assert.equal(4, value)
    end)

    it("nil throws an error", function()
      local h = bh.maxHeap()
      h:insert(10)
      assert.has.error(function()
        h:update(nil)
      end)
    end)

    it("bad position throws an error", function()
      local h = bh.maxHeap()
      h:insert(10)
      assert.has.error(function()
        h:update(0)
      end)
      assert.has.error(function()
        h:update(2)
      end)
    end)
end)

  it("creates maxHeap with custom greater-than function", function()
    local h = bh.maxHeap(function (a, b)
      return math.abs(a) > math.abs(b)
    end)
    h:insert(1)
    check(h)
    h:insert(-2)
    check(h)
    h:insert(3)
    check(h)
    h:insert(-4)
    check(h)
    h:insert(5)
    check(h)
    assert.equal(5, (h:peek()))
    h:pop()
    check(h)
    assert.equal(-4, (h:peek()))
  end)
end)
