var should = require('chai').should()
  , assert = require('chai').assert
  , BinarySearchTree = require('../index').BinarySearchTree
  , _ = require('underscore')
  , customUtils = require('../lib/customUtils')
  ;


describe('Binary search tree', function () {

  it('Upon creation, left, right are null, key and data can be set', function () {
    var bst = new BinarySearchTree();
    assert.isNull(bst.left);
    assert.isNull(bst.right);
    bst.hasOwnProperty('key').should.equal(false);
    bst.data.length.should.equal(0);

    bst = new BinarySearchTree({ key: 6, value: 'ggg' });
    assert.isNull(bst.left);
    assert.isNull(bst.right);
    bst.key.should.equal(6);
    bst.data.length.should.equal(1);
    bst.data[0].should.equal('ggg');
  });

  describe('Sanity checks', function () {

    it('Can get maxkey and minkey descendants', function () {
      var t = new BinarySearchTree({ key: 10 })
        , l = new BinarySearchTree({ key: 5 })
        , r = new BinarySearchTree({ key: 15 })
        , ll = new BinarySearchTree({ key: 3 })
        , lr = new BinarySearchTree({ key: 8 })
        , rl = new BinarySearchTree({ key: 11 })
        , rr = new BinarySearchTree({ key: 42 })
        ;

      t.left = l; t.right = r;
      l.left = ll; l.right = lr;
      r.left = rl; r.right = rr;

      // Getting min and max key descendants
      t.getMinKeyDescendant().key.should.equal(3);
      t.getMaxKeyDescendant().key.should.equal(42);

      t.left.getMinKeyDescendant().key.should.equal(3);
      t.left.getMaxKeyDescendant().key.should.equal(8);

      t.right.getMinKeyDescendant().key.should.equal(11);
      t.right.getMaxKeyDescendant().key.should.equal(42);

      t.right.left.getMinKeyDescendant().key.should.equal(11);
      t.right.left.getMaxKeyDescendant().key.should.equal(11);

      // Getting min and max keys
      t.getMinKey().should.equal(3);
      t.getMaxKey().should.equal(42);

      t.left.getMinKey().should.equal(3);
      t.left.getMaxKey().should.equal(8);

      t.right.getMinKey().should.equal(11);
      t.right.getMaxKey().should.equal(42);

      t.right.left.getMinKey().should.equal(11);
      t.right.left.getMaxKey().should.equal(11);
    });

    it('Can check a condition against every node in a tree', function () {
      var t = new BinarySearchTree({ key: 10 })
        , l = new BinarySearchTree({ key: 6 })
        , r = new BinarySearchTree({ key: 16 })
        , ll = new BinarySearchTree({ key: 4 })
        , lr = new BinarySearchTree({ key: 8 })
        , rl = new BinarySearchTree({ key: 12 })
        , rr = new BinarySearchTree({ key: 42 })
        ;

      t.left = l; t.right = r;
      l.left = ll; l.right = lr;
      r.left = rl; r.right = rr;

      function test (k, v) { if (k % 2 !== 0) { throw 'Key is not even'; } }

      t.checkAllNodesFullfillCondition(test);

      [l, r, ll, lr, rl, rr].forEach(function (node) {
        node.key += 1;
        (function () { t.checkAllNodesFullfillCondition(test); }).should.throw();
        node.key -= 1;
      });

      t.checkAllNodesFullfillCondition(test);
    });

    it('Can check that a tree verifies node ordering', function () {
      var t = new BinarySearchTree({ key: 10 })
        , l = new BinarySearchTree({ key: 5 })
        , r = new BinarySearchTree({ key: 15 })
        , ll = new BinarySearchTree({ key: 3 })
        , lr = new BinarySearchTree({ key: 8 })
        , rl = new BinarySearchTree({ key: 11 })
        , rr = new BinarySearchTree({ key: 42 })
        ;

      t.left = l; t.right = r;
      l.left = ll; l.right = lr;
      r.left = rl; r.right = rr;

      t.checkNodeOrdering();

      // Let's be paranoid and check all cases...
      l.key = 12;
      (function () { t.checkNodeOrdering(); }).should.throw();
      l.key = 5;

      r.key = 9;
      (function () { t.checkNodeOrdering(); }).should.throw();
      r.key = 15;

      ll.key = 6;
      (function () { t.checkNodeOrdering(); }).should.throw();
      ll.key = 11;
      (function () { t.checkNodeOrdering(); }).should.throw();
      ll.key = 3;

      lr.key = 4;
      (function () { t.checkNodeOrdering(); }).should.throw();
      lr.key = 11;
      (function () { t.checkNodeOrdering(); }).should.throw();
      lr.key = 8;

      rl.key = 16;
      (function () { t.checkNodeOrdering(); }).should.throw();
      rl.key = 9;
      (function () { t.checkNodeOrdering(); }).should.throw();
      rl.key = 11;

      rr.key = 12;
      (function () { t.checkNodeOrdering(); }).should.throw();
      rr.key = 7;
      (function () { t.checkNodeOrdering(); }).should.throw();
      rr.key = 10.5;
      (function () { t.checkNodeOrdering(); }).should.throw();
      rr.key = 42;

      t.checkNodeOrdering();
    });

    it('Checking if a tree\'s internal pointers (i.e. parents) are correct', function () {
      var t = new BinarySearchTree({ key: 10 })
        , l = new BinarySearchTree({ key: 5 })
        , r = new BinarySearchTree({ key: 15 })
        , ll = new BinarySearchTree({ key: 3 })
        , lr = new BinarySearchTree({ key: 8 })
        , rl = new BinarySearchTree({ key: 11 })
        , rr = new BinarySearchTree({ key: 42 })
        ;

      t.left = l; t.right = r;
      l.left = ll; l.right = lr;
      r.left = rl; r.right = rr;

      (function () { t.checkInternalPointers(); }).should.throw();
      l.parent = t;
      (function () { t.checkInternalPointers(); }).should.throw();
      r.parent = t;
      (function () { t.checkInternalPointers(); }).should.throw();
      ll.parent = l;
      (function () { t.checkInternalPointers(); }).should.throw();
      lr.parent = l;
      (function () { t.checkInternalPointers(); }).should.throw();
      rl.parent = r;
      (function () { t.checkInternalPointers(); }).should.throw();
      rr.parent = r;

      t.checkInternalPointers();
    });

    it('Can get the number of inserted keys', function () {
      var bst = new BinarySearchTree();

      bst.getNumberOfKeys().should.equal(0);
      bst.insert(10);
      bst.getNumberOfKeys().should.equal(1);
      bst.insert(5);
      bst.getNumberOfKeys().should.equal(2);
      bst.insert(3);
      bst.getNumberOfKeys().should.equal(3);
      bst.insert(8);
      bst.getNumberOfKeys().should.equal(4);
      bst.insert(15);
      bst.getNumberOfKeys().should.equal(5);
      bst.insert(12);
      bst.getNumberOfKeys().should.equal(6);
      bst.insert(37);
      bst.getNumberOfKeys().should.equal(7);
    });

  });

  describe('Insertion', function () {

    it('Insert at the root if its the first insertion', function () {
      var bst = new BinarySearchTree();

      bst.insert(10, 'some data');

      bst.checkIsBST();
      bst.key.should.equal(10);
      _.isEqual(bst.data, ['some data']).should.equal(true);
      assert.isNull(bst.left);
      assert.isNull(bst.right);
    });

    it("Insert on the left if key is less than the root's", function () {
      var bst = new BinarySearchTree();

      bst.insert(10, 'some data');
      bst.insert(7, 'some other data');

      bst.checkIsBST();
      assert.isNull(bst.right);
      bst.left.key.should.equal(7);
      _.isEqual(bst.left.data, ['some other data']).should.equal(true);
      assert.isNull(bst.left.left);
      assert.isNull(bst.left.right);
    });

    it("Insert on the right if key is greater than the root's", function () {
      var bst = new BinarySearchTree();

      bst.insert(10, 'some data');
      bst.insert(14, 'some other data');

      bst.checkIsBST();
      assert.isNull(bst.left);
      bst.right.key.should.equal(14);
      _.isEqual(bst.right.data, ['some other data']).should.equal(true);
      assert.isNull(bst.right.left);
      assert.isNull(bst.right.right);
    });

    it("Recursive insertion on the left works", function () {
      var bst = new BinarySearchTree();

      bst.insert(10, 'some data');
      bst.insert(7, 'some other data');
      bst.insert(1, 'hello');
      bst.insert(9, 'world');

      bst.checkIsBST();
      assert.isNull(bst.right);
      bst.left.key.should.equal(7);
      _.isEqual(bst.left.data, ['some other data']).should.equal(true);

      bst.left.left.key.should.equal(1);
      _.isEqual(bst.left.left.data, ['hello']).should.equal(true);

      bst.left.right.key.should.equal(9);
      _.isEqual(bst.left.right.data, ['world']).should.equal(true);
    });

    it("Recursive insertion on the right works", function () {
      var bst = new BinarySearchTree();

      bst.insert(10, 'some data');
      bst.insert(17, 'some other data');
      bst.insert(11, 'hello');
      bst.insert(19, 'world');

      bst.checkIsBST();
      assert.isNull(bst.left);
      bst.right.key.should.equal(17);
      _.isEqual(bst.right.data, ['some other data']).should.equal(true);

      bst.right.left.key.should.equal(11);
      _.isEqual(bst.right.left.data, ['hello']).should.equal(true);

      bst.right.right.key.should.equal(19);
      _.isEqual(bst.right.right.data, ['world']).should.equal(true);
    });

    it('If uniqueness constraint not enforced, we can insert different data for same key', function () {
      var bst = new BinarySearchTree();

      bst.insert(10, 'some data');
      bst.insert(3, 'hello');
      bst.insert(3, 'world');

      bst.checkIsBST();
      bst.left.key.should.equal(3);
      _.isEqual(bst.left.data, ['hello', 'world']).should.equal(true);

      bst.insert(12, 'a');
      bst.insert(12, 'b');

      bst.checkIsBST();
      bst.right.key.should.equal(12);
      _.isEqual(bst.right.data, ['a', 'b']).should.equal(true);
    });

    it('If uniqueness constraint is enforced, we cannot insert different data for same key', function () {
      var bst = new BinarySearchTree({ unique: true });

      bst.insert(10, 'some data');
      bst.insert(3, 'hello');
      try {
        bst.insert(3, 'world');
      } catch (e) {
        e.errorType.should.equal('uniqueViolated');
        e.key.should.equal(3);
      }

      bst.checkIsBST();
      bst.left.key.should.equal(3);
      _.isEqual(bst.left.data, ['hello']).should.equal(true);

      bst.insert(12, 'a');
      try {
        bst.insert(12, 'world');
      } catch (e) {
        e.errorType.should.equal('uniqueViolated');
        e.key.should.equal(12);
      }

      bst.checkIsBST();
      bst.right.key.should.equal(12);
      _.isEqual(bst.right.data, ['a']).should.equal(true);
    });

    it('Can insert 0 or the empty string', function () {
      var bst = new BinarySearchTree();

      bst.insert(0, 'some data');

      bst.checkIsBST();
      bst.key.should.equal(0);
      _.isEqual(bst.data, ['some data']).should.equal(true);
      assert.isNull(bst.left);
      assert.isNull(bst.right);

      bst = new BinarySearchTree();

      bst.insert('', 'some other data');

      bst.checkIsBST();
      bst.key.should.equal('');
      _.isEqual(bst.data, ['some other data']).should.equal(true);
      assert.isNull(bst.left);
      assert.isNull(bst.right);
    });

    it('Can insert a lot of keys and still get a BST (sanity check)', function () {
      var bst = new BinarySearchTree({ unique: true });

      customUtils.getRandomArray(100).forEach(function (n) {
        bst.insert(n, 'some data');
      });

      bst.checkIsBST();
    });

    it('All children get a pointer to their parent, the root doesnt', function () {
      var bst = new BinarySearchTree();

      bst.insert(10, 'root');
      bst.insert(5, 'yes');
      bst.insert(15, 'no');

      bst.checkIsBST();

      assert.isNull(bst.parent);
      bst.left.parent.should.equal(bst);
      bst.right.parent.should.equal(bst);
    });

  });   // ==== End of 'Insertion' ==== //


  describe('Search', function () {

    it('Can find data in a BST', function () {
      var bst = new BinarySearchTree()
        , i;

      customUtils.getRandomArray(100).forEach(function (n) {
        bst.insert(n, 'some data for ' + n);
      });

      bst.checkIsBST();

      for (i = 0; i < 100; i += 1) {
        _.isEqual(bst.search(i), ['some data for ' + i]).should.equal(true);
      }
    });

    it('If no data can be found, return an empty array', function () {
      var bst = new BinarySearchTree();

      customUtils.getRandomArray(100).forEach(function (n) {
        if (n !== 63) {
          bst.insert(n, 'some data for ' + n);
        }
      });

      bst.checkIsBST();

      bst.search(-2).length.should.equal(0);
      bst.search(100).length.should.equal(0);
      bst.search(101).length.should.equal(0);
      bst.search(63).length.should.equal(0);
    });

    it('Can search for data between two bounds', function () {
      var bst = new BinarySearchTree();

      [10, 5, 15, 3, 8, 13, 18].forEach(function (k) {
        bst.insert(k, 'data ' + k);
      });

      assert.deepEqual(bst.betweenBounds({ $gte: 8, $lte: 15 }), ['data 8', 'data 10', 'data 13', 'data 15']);
      assert.deepEqual(bst.betweenBounds({ $gt: 8, $lt: 15 }), ['data 10', 'data 13']);
    });

    it('Bounded search can handle cases where query contains both $lt and $lte, or both $gt and $gte', function () {
      var bst = new BinarySearchTree();

      [10, 5, 15, 3, 8, 13, 18].forEach(function (k) {
        bst.insert(k, 'data ' + k);
      });

      assert.deepEqual(bst.betweenBounds({ $gt:8, $gte: 8, $lte: 15 }), ['data 10', 'data 13', 'data 15']);
      assert.deepEqual(bst.betweenBounds({ $gt:5, $gte: 8, $lte: 15 }), ['data 8', 'data 10', 'data 13', 'data 15']);
      assert.deepEqual(bst.betweenBounds({ $gt:8, $gte: 5, $lte: 15 }), ['data 10', 'data 13', 'data 15']);

      assert.deepEqual(bst.betweenBounds({ $gte: 8, $lte: 15, $lt: 15 }), ['data 8', 'data 10', 'data 13']);
      assert.deepEqual(bst.betweenBounds({ $gte: 8, $lte: 18, $lt: 15 }), ['data 8', 'data 10', 'data 13']);
      assert.deepEqual(bst.betweenBounds({ $gte: 8, $lte: 15, $lt: 18 }), ['data 8', 'data 10', 'data 13', 'data 15']);
    });

    it('Bounded search can work when one or both boundaries are missing', function () {
      var bst = new BinarySearchTree();

      [10, 5, 15, 3, 8, 13, 18].forEach(function (k) {
        bst.insert(k, 'data ' + k);
      });

      assert.deepEqual(bst.betweenBounds({ $gte: 11 }), ['data 13', 'data 15', 'data 18']);
      assert.deepEqual(bst.betweenBounds({ $lte: 9 }), ['data 3', 'data 5', 'data 8']);
    });

  });   /// ==== End of 'Search' ==== //


  describe('Deletion', function () {

    it('Deletion does nothing on an empty tree', function () {
      var bst = new BinarySearchTree()
        , bstu = new BinarySearchTree({ unique: true });

      bst.getNumberOfKeys().should.equal(0);
      bstu.getNumberOfKeys().should.equal(0);

      bst.delete(5);
      bstu.delete(5);

      bst.hasOwnProperty('key').should.equal(false);
      bstu.hasOwnProperty('key').should.equal(false);

      bst.data.length.should.equal(0);
      bstu.data.length.should.equal(0);

      bst.getNumberOfKeys().should.equal(0);
      bstu.getNumberOfKeys().should.equal(0);
    });

    it('Deleting a non-existent key doesnt have any effect', function () {
      var bst = new BinarySearchTree();

      [10, 5, 3, 8, 15, 12, 37].forEach(function (k) {
        bst.insert(k, 'some ' + k);
      });

      function checkBst () {
        [10, 5, 3, 8, 15, 12, 37].forEach(function (k) {
          _.isEqual(bst.search(k), ['some ' + k]).should.equal(true);
        });
      }

      checkBst();
      bst.getNumberOfKeys().should.equal(7);

      bst.delete(2);
      checkBst(); bst.checkIsBST(); bst.getNumberOfKeys().should.equal(7);
      bst.delete(4);
      checkBst(); bst.checkIsBST(); bst.getNumberOfKeys().should.equal(7);
      bst.delete(9);
      checkBst(); bst.checkIsBST(); bst.getNumberOfKeys().should.equal(7);
      bst.delete(6);
      checkBst(); bst.checkIsBST(); bst.getNumberOfKeys().should.equal(7);
      bst.delete(11);
      checkBst(); bst.checkIsBST(); bst.getNumberOfKeys().should.equal(7);
      bst.delete(14);
      checkBst(); bst.checkIsBST(); bst.getNumberOfKeys().should.equal(7);
      bst.delete(20);
      checkBst(); bst.checkIsBST(); bst.getNumberOfKeys().should.equal(7);
      bst.delete(200);
      checkBst(); bst.checkIsBST(); bst.getNumberOfKeys().should.equal(7);
    });

    it('Able to delete the root if it is also a leaf', function () {
      var bst = new BinarySearchTree();

      bst.insert(10, 'hello');
      bst.key.should.equal(10);
      _.isEqual(bst.data, ['hello']).should.equal(true);
      bst.getNumberOfKeys().should.equal(1);

      bst.delete(10);
      bst.hasOwnProperty('key').should.equal(false);
      bst.data.length.should.equal(0);
      bst.getNumberOfKeys().should.equal(0);
    });

    it('Able to delete leaf nodes that are non-root', function () {
      var bst;

      function recreateBst () {
        bst = new BinarySearchTree();

        // With this insertion order the tree is well balanced
        // So we know the leaves are 3, 8, 12, 37
        [10, 5, 3, 8, 15, 12, 37].forEach(function (k) {
          bst.insert(k, 'some ' + k);
        });

        bst.getNumberOfKeys().should.equal(7);
      }

      function checkOnlyOneWasRemoved (theRemoved) {
        [10, 5, 3, 8, 15, 12, 37].forEach(function (k) {
          if (k === theRemoved) {
            bst.search(k).length.should.equal(0);
          } else {
            _.isEqual(bst.search(k), ['some ' + k]).should.equal(true);
          }
        });

        bst.getNumberOfKeys().should.equal(6);
      }

      recreateBst();
      bst.delete(3);
      bst.checkIsBST();
      checkOnlyOneWasRemoved(3);
      assert.isNull(bst.left.left);

      recreateBst();
      bst.delete(8);
      bst.checkIsBST();
      checkOnlyOneWasRemoved(8);
      assert.isNull(bst.left.right);

      recreateBst();
      bst.delete(12);
      bst.checkIsBST();
      checkOnlyOneWasRemoved(12);
      assert.isNull(bst.right.left);

      recreateBst();
      bst.delete(37);
      bst.checkIsBST();
      checkOnlyOneWasRemoved(37);
      assert.isNull(bst.right.right);
    });

    it('Able to delete the root if it has only one child', function () {
      var bst;

      // Root has only one child, on the left
      bst = new BinarySearchTree();
      [10, 5, 3, 6].forEach(function (k) {
        bst.insert(k, 'some ' + k);
      });
      bst.getNumberOfKeys().should.equal(4);
      bst.delete(10);
      bst.checkIsBST();
      bst.getNumberOfKeys().should.equal(3);
      [5, 3, 6].forEach(function (k) {
        _.isEqual(bst.search(k), ['some ' + k]).should.equal(true);
      });
      bst.search(10).length.should.equal(0);

      // Root has only one child, on the right
      bst = new BinarySearchTree();
      [10, 15, 13, 16].forEach(function (k) {
        bst.insert(k, 'some ' + k);
      });
      bst.getNumberOfKeys().should.equal(4);
      bst.delete(10);
      bst.checkIsBST();
      bst.getNumberOfKeys().should.equal(3);
      [15, 13, 16].forEach(function (k) {
        _.isEqual(bst.search(k), ['some ' + k]).should.equal(true);
      });
      bst.search(10).length.should.equal(0);
    });

    it('Able to delete non root nodes that have only one child', function () {
      var bst;

      function recreateBst () {
        bst = new BinarySearchTree();

        [10, 5, 15, 3, 1, 4, 20, 17, 25].forEach(function (k) {
          bst.insert(k, 'some ' + k);
        });

        bst.getNumberOfKeys().should.equal(9);
      }

      function checkOnlyOneWasRemoved (theRemoved) {
        [10, 5, 15, 3, 1, 4, 20, 17, 25].forEach(function (k) {
          if (k === theRemoved) {
            bst.search(k).length.should.equal(0);
          } else {
            _.isEqual(bst.search(k), ['some ' + k]).should.equal(true);
          }
        });

        bst.getNumberOfKeys().should.equal(8);
      }

      recreateBst();
      bst.delete(5);
      bst.checkIsBST();
      checkOnlyOneWasRemoved(5);

      recreateBst();
      bst.delete(15);
      bst.checkIsBST();
      checkOnlyOneWasRemoved(15);
    });

    it('Can delete the root if it has 2 children', function () {
      var bst;

      bst = new BinarySearchTree();
      [10, 5, 3, 8, 15, 12, 37].forEach(function (k) {
        bst.insert(k, 'some ' + k);
      });
      bst.getNumberOfKeys().should.equal(7);
      bst.delete(10);
      bst.checkIsBST();
      bst.getNumberOfKeys().should.equal(6);
      [5, 3, 8, 15, 12, 37].forEach(function (k) {
        _.isEqual(bst.search(k), ['some ' + k]).should.equal(true);
      });
      bst.search(10).length.should.equal(0);
    });

    it('Can delete a non-root node that has two children', function () {
      var bst;

      bst = new BinarySearchTree();
      [10, 5, 3, 1, 4, 8, 6, 9, 15, 12, 11, 13, 20, 19, 42].forEach(function (k) {
        bst.insert(k, 'some ' + k);
      });
      bst.getNumberOfKeys().should.equal(15);
      bst.delete(5);
      bst.checkIsBST();
      bst.getNumberOfKeys().should.equal(14);
      [10, 3, 1, 4, 8, 6, 9, 15, 12, 11, 13, 20, 19, 42].forEach(function (k) {
        _.isEqual(bst.search(k), ['some ' + k]).should.equal(true);
      });
      bst.search(5).length.should.equal(0);

      bst = new BinarySearchTree();
      [10, 5, 3, 1, 4, 8, 6, 9, 15, 12, 11, 13, 20, 19, 42].forEach(function (k) {
        bst.insert(k, 'some ' + k);
      });
      bst.getNumberOfKeys().should.equal(15);
      bst.delete(15);
      bst.checkIsBST();
      bst.getNumberOfKeys().should.equal(14);
      [10, 5, 3, 1, 4, 8, 6, 9, 12, 11, 13, 20, 19, 42].forEach(function (k) {
        _.isEqual(bst.search(k), ['some ' + k]).should.equal(true);
      });
      bst.search(15).length.should.equal(0);
    });

    it('If no value is provided, it will delete the entire node even if there are multiple pieces of data', function () {
      var bst = new BinarySearchTree();

      bst.insert(10, 'yes');
      bst.insert(5, 'hello');
      bst.insert(3, 'yes');
      bst.insert(5, 'world');
      bst.insert(8, 'yes');

      assert.deepEqual(bst.search(5), ['hello', 'world']);
      bst.getNumberOfKeys().should.equal(4);

      bst.delete(5);
      bst.search(5).length.should.equal(0);
      bst.getNumberOfKeys().should.equal(3);
    });

    it('Can remove only one value from an array', function () {
      var bst = new BinarySearchTree();

      bst.insert(10, 'yes');
      bst.insert(5, 'hello');
      bst.insert(3, 'yes');
      bst.insert(5, 'world');
      bst.insert(8, 'yes');

      assert.deepEqual(bst.search(5), ['hello', 'world']);
      bst.getNumberOfKeys().should.equal(4);

      bst.delete(5, 'hello');
      assert.deepEqual(bst.search(5), ['world']);
      bst.getNumberOfKeys().should.equal(4);
    });

    it('Removes nothing if value doesnt match', function () {
      var bst = new BinarySearchTree();

      bst.insert(10, 'yes');
      bst.insert(5, 'hello');
      bst.insert(3, 'yes');
      bst.insert(5, 'world');
      bst.insert(8, 'yes');

      assert.deepEqual(bst.search(5), ['hello', 'world']);
      bst.getNumberOfKeys().should.equal(4);

      bst.delete(5, 'nope');
      assert.deepEqual(bst.search(5), ['hello', 'world']);
      bst.getNumberOfKeys().should.equal(4);
    });

    it('If value provided but node contains only one value, remove entire node', function () {
      var bst = new BinarySearchTree();

      bst.insert(10, 'yes');
      bst.insert(5, 'hello');
      bst.insert(3, 'yes2');
      bst.insert(5, 'world');
      bst.insert(8, 'yes3');

      assert.deepEqual(bst.search(3), ['yes2']);
      bst.getNumberOfKeys().should.equal(4);

      bst.delete(3, 'yes2');
      bst.search(3).length.should.equal(0);
      bst.getNumberOfKeys().should.equal(3);
    });

    it('Can remove the root from a tree with height 2 when the root has two children (special case)', function () {
      var bst = new BinarySearchTree();

      bst.insert(10, 'maybe');
      bst.insert(5, 'no');
      bst.insert(15, 'yes');
      bst.getNumberOfKeys().should.equal(3);

      bst.delete(10);
      bst.checkIsBST();
      bst.getNumberOfKeys().should.equal(2);
      assert.deepEqual(bst.search(5), ['no']);
      assert.deepEqual(bst.search(15), ['yes']);
    });

    it('Can remove the root from a tree with height 3 when the root has two children (special case where the two children themselves have children)', function () {
      var bst = new BinarySearchTree();

      bst.insert(10, 'maybe');
      bst.insert(5, 'no');
      bst.insert(15, 'yes');
      bst.insert(2, 'no');
      bst.insert(35, 'yes');
      bst.getNumberOfKeys().should.equal(5);

      bst.delete(10);
      bst.checkIsBST();
      bst.getNumberOfKeys().should.equal(4);
      assert.deepEqual(bst.search(5), ['no']);
      assert.deepEqual(bst.search(15), ['yes']);
    });

  });   // ==== End of 'Deletion' ==== //


  it('Can use undefined as key and value', function () {
    function compareKeys (a, b) {
      if (a === undefined && b === undefined) { return 0; }
      if (a === undefined) { return -1; }
      if (b === undefined) { return 1; }

      if (a < b) { return -1; }
      if (a > b) { return 1; }
      if (a === b) { return 0; }
    }

    var bst = new BinarySearchTree({ compareKeys: compareKeys });

    bst.insert(2, undefined);
    bst.checkIsBST();
    bst.getNumberOfKeys().should.equal(1);
    assert.deepEqual(bst.search(2), [undefined]);
    assert.deepEqual(bst.search(undefined), []);

    bst.insert(undefined, 'hello');
    bst.checkIsBST();
    bst.getNumberOfKeys().should.equal(2);
    assert.deepEqual(bst.search(2), [undefined]);
    assert.deepEqual(bst.search(undefined), ['hello']);

    bst.insert(undefined, 'world');
    bst.checkIsBST();
    bst.getNumberOfKeys().should.equal(2);
    assert.deepEqual(bst.search(2), [undefined]);
    assert.deepEqual(bst.search(undefined), ['hello', 'world']);

    bst.insert(4, undefined);
    bst.checkIsBST();
    bst.getNumberOfKeys().should.equal(3);
    assert.deepEqual(bst.search(2), [undefined]);
    assert.deepEqual(bst.search(4), [undefined]);
    assert.deepEqual(bst.search(undefined), ['hello', 'world']);

    bst.delete(undefined, 'hello');
    bst.checkIsBST();
    bst.getNumberOfKeys().should.equal(3);
    assert.deepEqual(bst.search(2), [undefined]);
    assert.deepEqual(bst.search(4), [undefined]);
    assert.deepEqual(bst.search(undefined), ['world']);

    bst.delete(undefined);
    bst.checkIsBST();
    bst.getNumberOfKeys().should.equal(2);
    assert.deepEqual(bst.search(2), [undefined]);
    assert.deepEqual(bst.search(4), [undefined]);
    assert.deepEqual(bst.search(undefined), []);

    bst.delete(2, undefined);
    bst.checkIsBST();
    bst.getNumberOfKeys().should.equal(1);
    assert.deepEqual(bst.search(2), []);
    assert.deepEqual(bst.search(4), [undefined]);
    assert.deepEqual(bst.search(undefined), []);

    bst.delete(4);
    bst.checkIsBST();
    bst.getNumberOfKeys().should.equal(0);
    assert.deepEqual(bst.search(2), []);
    assert.deepEqual(bst.search(4), []);
    assert.deepEqual(bst.search(undefined), []);
  });


  it('Can use null as key and value', function () {
    function compareKeys (a, b) {
      if (a === null && b === null) { return 0; }
      if (a === null) { return -1; }
      if (b === null) { return 1; }

      if (a < b) { return -1; }
      if (a > b) { return 1; }
      if (a === b) { return 0; }
    }

    var bst = new BinarySearchTree({ compareKeys: compareKeys });

    bst.insert(2, null);
    bst.checkIsBST();
    bst.getNumberOfKeys().should.equal(1);
    assert.deepEqual(bst.search(2), [null]);
    assert.deepEqual(bst.search(null), []);

    bst.insert(null, 'hello');
    bst.checkIsBST();
    bst.getNumberOfKeys().should.equal(2);
    assert.deepEqual(bst.search(2), [null]);
    assert.deepEqual(bst.search(null), ['hello']);

    bst.insert(null, 'world');
    bst.checkIsBST();
    bst.getNumberOfKeys().should.equal(2);
    assert.deepEqual(bst.search(2), [null]);
    assert.deepEqual(bst.search(null), ['hello', 'world']);

    bst.insert(4, null);
    bst.checkIsBST();
    bst.getNumberOfKeys().should.equal(3);
    assert.deepEqual(bst.search(2), [null]);
    assert.deepEqual(bst.search(4), [null]);
    assert.deepEqual(bst.search(null), ['hello', 'world']);

    bst.delete(null, 'hello');
    bst.checkIsBST();
    bst.getNumberOfKeys().should.equal(3);
    assert.deepEqual(bst.search(2), [null]);
    assert.deepEqual(bst.search(4), [null]);
    assert.deepEqual(bst.search(null), ['world']);

    bst.delete(null);
    bst.checkIsBST();
    bst.getNumberOfKeys().should.equal(2);
    assert.deepEqual(bst.search(2), [null]);
    assert.deepEqual(bst.search(4), [null]);
    assert.deepEqual(bst.search(null), []);

    bst.delete(2, null);
    bst.checkIsBST();
    bst.getNumberOfKeys().should.equal(1);
    assert.deepEqual(bst.search(2), []);
    assert.deepEqual(bst.search(4), [null]);
    assert.deepEqual(bst.search(null), []);

    bst.delete(4);
    bst.checkIsBST();
    bst.getNumberOfKeys().should.equal(0);
    assert.deepEqual(bst.search(2), []);
    assert.deepEqual(bst.search(4), []);
    assert.deepEqual(bst.search(null), []);
  });


  describe('Execute on every node (=tree traversal)', function () {

    it('Can execute a function on every node', function () {
      var bst = new BinarySearchTree()
        , keys = []
        , executed = 0
        ;

      bst.insert(10, 'yes');
      bst.insert(5, 'hello');
      bst.insert(3, 'yes2');
      bst.insert(8, 'yes3');
      bst.insert(15, 'yes3');
      bst.insert(159, 'yes3');
      bst.insert(11, 'yes3');

      bst.executeOnEveryNode(function (node) {
        keys.push(node.key);
        executed += 1;
      });

      assert.deepEqual(keys, [3, 5, 8, 10, 11, 15, 159]);
      executed.should.equal(7);
    });

  });   // ==== End of 'Execute on every node' ==== //


  // This test performs several inserts and deletes at random, always checking the content
  // of the tree are as expected and the binary search tree constraint is respected
  // This test is important because it can catch bugs other tests can't
  // By their nature, BSTs can be hard to test (many possible cases, bug at one operation whose
  // effect begins to be felt only after several operations etc.)
  describe('Randomized test (takes much longer than the rest of the test suite)', function () {
    var bst = new BinarySearchTree()
      , data = {};

    // Check a bst against a simple key => [data] object
    function checkDataIsTheSame (bst, data) {
      var bstDataElems = [];

      // bstDataElems is a simple array containing every piece of data in the tree
      bst.executeOnEveryNode(function (node) {
        var i;
        for (i = 0; i < node.data.length; i += 1) {
          bstDataElems.push(node.data[i]);
        }
      });

      // Number of key and number of pieces of data match
      bst.getNumberOfKeys().should.equal(Object.keys(data).length);
      _.reduce(_.map(data, function (d) { return d.length; }), function (memo, n) { return memo + n; }, 0).should.equal(bstDataElems.length);

      // Compare data
      Object.keys(data).forEach(function (key) {
        checkDataEquality(bst.search(key), data[key]);
      });
    }

    // Check two pieces of data coming from the bst and data are the same
    function checkDataEquality (fromBst, fromData) {
      if (fromBst.length === 0) {
        if (fromData) { fromData.length.should.equal(0); }
      }

      assert.deepEqual(fromBst, fromData);
    }

    // Tests the tree structure (deletions concern the whole tree, deletion of some data in a node is well tested above)
    it('Inserting and deleting entire nodes', function () {
      // You can skew to be more insertive or deletive, to test all cases
      function launchRandomTest (nTests, proba) {
        var i, key, dataPiece, possibleKeys;

        for (i = 0; i < nTests; i += 1) {
          if (Math.random() > proba) {   // Deletion
            possibleKeys = Object.keys(data);

            if (possibleKeys.length > 0) {
              key = possibleKeys[Math.floor(possibleKeys.length * Math.random()).toString()];
            } else {
              key = Math.floor(70 * Math.random()).toString();
            }

            delete data[key];
            bst.delete(key);
          } else {   // Insertion
            key = Math.floor(70 * Math.random()).toString();
            dataPiece = Math.random().toString().substring(0, 6);
            bst.insert(key, dataPiece);
            if (data[key]) {
              data[key].push(dataPiece);
            } else {
              data[key] = [dataPiece];
            }
          }

          // Check the bst constraint are still met and the data is correct
          bst.checkIsBST();
          checkDataIsTheSame(bst, data);
        }
      }

      launchRandomTest(1000, 0.65);
      launchRandomTest(2000, 0.35);
    });

  });   // ==== End of 'Randomized test' ==== //



});