import RedBlackTree from '../src/Util/Tree.js'
import ID from '../src/Util/ID/ID.js'
import Chance from 'chance'
import { test, proxyConsole } from 'cutest'

proxyConsole()

var numberOfRBTreeTests = 10000

function checkRedNodesDoNotHaveBlackChildren (t, tree) {
  let correct = true
  function traverse (n) {
    if (n == null) {
      return
    }
    if (n.isRed()) {
      if (n.left != null) {
        correct = correct && !n.left.isRed()
      }
      if (n.right != null) {
        correct = correct && !n.right.isRed()
      }
    }
    traverse(n.left)
    traverse(n.right)
  }
  traverse(tree.root)
  t.assert(correct, 'Red nodes do not have black children')
}

function checkBlackHeightOfSubTreesAreEqual (t, tree) {
  let correct = true
  function traverse (n) {
    if (n == null) {
      return 0
    }
    var sub1 = traverse(n.left)
    var sub2 = traverse(n.right)
    if (sub1 !== sub2) {
      correct = false
    }
    if (n.isRed()) {
      return sub1
    } else {
      return sub1 + 1
    }
  }
  traverse(tree.root)
  t.assert(correct, 'Black-height of sub-trees are equal')
}

function checkRootNodeIsBlack (t, tree) {
  t.assert(tree.root == null || tree.root.isBlack(), 'root node is black')
}

test('RedBlack Tree', async function redBlackTree (t) {
  let tree = new RedBlackTree()
  tree.put({_id: new ID(8433, 0)})
  tree.put({_id: new ID(12844, 0)})
  tree.put({_id: new ID(1795, 0)})
  tree.put({_id: new ID(30302, 0)})
  tree.put({_id: new ID(64287)})
  tree.delete(new ID(8433, 0))
  tree.put({_id: new ID(28996)})
  tree.delete(new ID(64287))
  tree.put({_id: new ID(22721)})
  checkRootNodeIsBlack(t, tree)
  checkBlackHeightOfSubTreesAreEqual(t, tree)
  checkRedNodesDoNotHaveBlackChildren(t, tree)
})

test(`random tests (${numberOfRBTreeTests})`, async function random (t) {
  let chance = new Chance(t.getSeed() * 1000000000)
  let tree = new RedBlackTree()
  let elements = []
  for (var i = 0; i < numberOfRBTreeTests; i++) {
    if (chance.bool({likelihood: 80})) {
      // 80% chance to insert an element
      let obj = new ID(chance.integer({min: 0, max: numberOfRBTreeTests}), chance.integer({min: 0, max: 1}))
      let nodeExists = tree.find(obj)
      if (nodeExists === null) {
        if (elements.some(e => e.equals(obj))) {
          t.assert(false, 'tree and elements contain different results')
        }
        elements.push(obj)
        tree.put({_id: obj})
      }
    } else if (elements.length > 0) {
      // ~20% chance to delete an element
      var elem = chance.pickone(elements)
      elements = elements.filter(function (e) {
        return !e.equals(elem)
      })
      tree.delete(elem)
    }
  }
  checkRootNodeIsBlack(t, tree)
  checkBlackHeightOfSubTreesAreEqual(t, tree)
  checkRedNodesDoNotHaveBlackChildren(t, tree)
  // TEST if all nodes exist
  let allNodesExist = true
  for (let id of elements) {
    let node = tree.find(id)
    if (!node._id.equals(id)) {
      allNodesExist = false
    }
  }
  t.assert(allNodesExist, 'All inserted nodes exist')
  // TEST lower bound search
  let findAllNodesWithLowerBoundSerach = true
  for (let id of elements) {
    let node = tree.findWithLowerBound(id)
    if (!node._id.equals(id)) {
      findAllNodesWithLowerBoundSerach = false
    }
  }
  t.assert(
    findAllNodesWithLowerBoundSerach,
    'Find every object with lower bound search'
  )
  // TEST iteration (with lower bound search)
  let lowerBound = chance.pickone(elements)
  let expectedResults = elements.filter((e, pos) =>
    (lowerBound.lessThan(e) || e.equals(lowerBound)) &&
    elements.indexOf(e) === pos
  ).length
  let actualResults = 0
  tree.iterate(lowerBound, null, function (val) {
    if (val == null) {
      t.assert(false, 'val is undefined!')
    }
    actualResults++
  })
  t.assert(
    expectedResults === actualResults,
    'Iterating over a tree with lower bound yields the right amount of results'
  )

  expectedResults = elements.filter((e, pos) =>
    elements.indexOf(e) === pos
  ).length
  actualResults = 0
  tree.iterate(null, null, function (val) {
    if (val == null) {
      t.assert(false, 'val is undefined!')
    }
    actualResults++
  })
  t.assert(
    expectedResults === actualResults,
    'iterating over a tree without bounds yields the right amount of results'
  )

  let upperBound = chance.pickone(elements)
  expectedResults = elements.filter((e, pos) =>
    (e.lessThan(upperBound) || e.equals(upperBound)) &&
    elements.indexOf(e) === pos
  ).length
  actualResults = 0
  tree.iterate(null, upperBound, function (val) {
    if (val == null) {
      t.assert(false, 'val is undefined!')
    }
    actualResults++
  })
  t.assert(
    expectedResults === actualResults,
    'iterating over a tree with upper bound yields the right amount of results'
  )

  upperBound = chance.pickone(elements)
  lowerBound = chance.pickone(elements)
  if (upperBound.lessThan(lowerBound)) {
    [lowerBound, upperBound] = [upperBound, lowerBound]
  }
  expectedResults = elements.filter((e, pos) =>
    (lowerBound.lessThan(e) || e.equals(lowerBound)) &&
    (e.lessThan(upperBound) || e.equals(upperBound)) &&
    elements.indexOf(e) === pos
  ).length
  actualResults = 0
  tree.iterate(lowerBound, upperBound, function (val) {
    if (val == null) {
      t.assert(false, 'val is undefined!')
    }
    actualResults++
  })
  t.assert(
    expectedResults === actualResults,
    'iterating over a tree with upper bound yields the right amount of results'
  )
})
