'use strict';

const test = require('tape');

const SymbolTree = require('..');

function o() {
        // return an object that is unique in a deepEqual check

        return {
                unique: Symbol(),
        };
}

test('test case internal prerequisite', (t) => {
        const a = o();
        t.notDeepEqual([o()], [o()]);
        t.deepEqual([a], [a]);
        t.end();
});

test('initialize', (t) => {
        const tree = new SymbolTree();
        const obj = {foo: 'bar'};

        t.equal(obj, tree.initialize(obj));
        t.deepEqual(['foo'], Object.getOwnPropertyNames(obj),
                'initialize() should not introduce any enumerable properties');

        t.end();
});

test('unassociated object', (t) => {
        const tree = new SymbolTree();
        const a = o();

        t.equal(false, tree.hasChildren(a));
        t.equal(null, tree.firstChild(a));
        t.equal(null, tree.lastChild(a));
        t.equal(null, tree.previousSibling(a));
        t.equal(null, tree.nextSibling(a));
        t.equal(null, tree.parent(a));

        t.end();
});

test('insertBefore without parent or siblings', (t) => {
        const tree = new SymbolTree();
        const a = o();
        const b = o();

        t.equal(a, tree.insertBefore(b, a));

        t.equal(false, tree.hasChildren(a));
        t.equal(null, tree.firstChild(a));
        t.equal(null, tree.lastChild(a));
        t.equal(null, tree.parent(a));
        t.equal(false, tree.hasChildren(b));
        t.equal(null, tree.firstChild(b));
        t.equal(null, tree.lastChild(b));
        t.equal(null, tree.parent(b));

        t.equal(null, tree.previousSibling(a));
        t.equal(b, tree.nextSibling(a));
        t.equal(a, tree.previousSibling(b));
        t.equal(null, tree.nextSibling(b));

        t.end();
});

test('insertAfter without parent or siblings', (t) => {
        const tree = new SymbolTree();
        const a = o();
        const b = o();

        t.equal(b, tree.insertAfter(a, b));

        t.equal(false, tree.hasChildren(a));
        t.equal(null, tree.firstChild(a));
        t.equal(null, tree.lastChild(a));
        t.equal(null, tree.parent(a));
        t.equal(false, tree.hasChildren(b));
        t.equal(null, tree.firstChild(b));
        t.equal(null, tree.lastChild(b));
        t.equal(null, tree.parent(b));

        t.equal(null, tree.previousSibling(a));
        t.equal(b, tree.nextSibling(a));
        t.equal(a, tree.previousSibling(b));
        t.equal(null, tree.nextSibling(b));

        t.end();
});

test('prependChild without children', (t) => {
        const tree = new SymbolTree();
        const parent = o();
        const a = o();

        t.equal(a, tree.prependChild(parent, a));

        t.equal(false, tree.hasChildren(a));
        t.equal(null, tree.firstChild(a));
        t.equal(null, tree.lastChild(a));
        t.equal(null, tree.previousSibling(a));
        t.equal(null, tree.nextSibling(a));
        t.equal(parent, tree.parent(a));

        t.equal(true, tree.hasChildren(parent));
        t.equal(a, tree.firstChild(parent));
        t.equal(a, tree.lastChild(parent));
        t.equal(null, tree.previousSibling(a));
        t.equal(null, tree.nextSibling(parent));
        t.equal(null, tree.parent(parent));

        t.end();
});

test('appendChild without children', (t) => {
        const tree = new SymbolTree();
        const parent = o();
        const a = o();

        t.equal(a, tree.appendChild(parent, a));

        t.equal(false, tree.hasChildren(a));
        t.equal(null, tree.firstChild(a));
        t.equal(null, tree.lastChild(a));
        t.equal(null, tree.previousSibling(a));
        t.equal(null, tree.nextSibling(a));
        t.equal(parent, tree.parent(a));

        t.equal(true, tree.hasChildren(parent));
        t.equal(a, tree.firstChild(parent));
        t.equal(a, tree.lastChild(parent));
        t.equal(null, tree.previousSibling(a));
        t.equal(null, tree.nextSibling(parent));
        t.equal(null, tree.parent(parent));

        t.end();
});

test('prependChild with children', (t) => {
        const tree = new SymbolTree();
        const parent = o();
        const a = o();
        const b = o();

        tree.prependChild(parent, b);
        tree.prependChild(parent, a);

        t.equal(true, tree.hasChildren(parent));
        t.equal(a, tree.firstChild(parent));
        t.equal(b, tree.lastChild(parent));

        t.equal(parent, tree.parent(a));
        t.equal(null, tree.previousSibling(a));
        t.equal(b, tree.nextSibling(a));

        t.equal(parent, tree.parent(b));
        t.equal(a, tree.previousSibling(b));
        t.equal(null, tree.nextSibling(b));
        t.end();
});

test('appendChild with children', (t) => {
        const tree = new SymbolTree();
        const parent = o();
        const a = o();
        const b = o();

        tree.appendChild(parent, a);
        tree.appendChild(parent, b);

        t.equal(true, tree.hasChildren(parent));
        t.equal(a, tree.firstChild(parent));
        t.equal(b, tree.lastChild(parent));

        t.equal(parent, tree.parent(a));
        t.equal(null, tree.previousSibling(a));
        t.equal(b, tree.nextSibling(a));

        t.equal(parent, tree.parent(b));
        t.equal(a, tree.previousSibling(b));
        t.equal(null, tree.nextSibling(b));
        t.end();
});

test('insertBefore with parent', (t) => {
        const tree = new SymbolTree();
        const parent = o();
        const a = o();
        const b = o();

        tree.prependChild(parent, b);
        tree.insertBefore(b, a);

        t.equal(true, tree.hasChildren(parent));
        t.equal(a, tree.firstChild(parent));
        t.equal(b, tree.lastChild(parent));

        t.equal(parent, tree.parent(a));
        t.equal(null, tree.previousSibling(a));
        t.equal(b, tree.nextSibling(a));

        t.equal(parent, tree.parent(b));
        t.equal(a, tree.previousSibling(b));
        t.equal(null, tree.nextSibling(b));
        t.end();
});

test('insertAfter with parent', (t) => {
        const tree = new SymbolTree();
        const parent = o();
        const a = o();
        const b = o();

        tree.appendChild(parent, a);
        tree.insertAfter(a, b);

        t.equal(true, tree.hasChildren(parent));
        t.equal(a, tree.firstChild(parent));
        t.equal(b, tree.lastChild(parent));

        t.equal(parent, tree.parent(a));
        t.equal(null, tree.previousSibling(a));
        t.equal(b, tree.nextSibling(a));

        t.equal(parent, tree.parent(b));
        t.equal(a, tree.previousSibling(b));
        t.equal(null, tree.nextSibling(b));
        t.end();
});

test('insertBefore with siblings', (t) => {
        const tree = new SymbolTree();
        const a = o();
        const b = o();
        const c = o();

        tree.insertBefore(c, a);
        tree.insertBefore(c, b);

        t.equal(null, tree.previousSibling(a));
        t.equal(b, tree.nextSibling(a));

        t.equal(a, tree.previousSibling(b));
        t.equal(c, tree.nextSibling(b));

        t.equal(b, tree.previousSibling(c));
        t.equal(null, tree.nextSibling(c));

        t.end();
});

test('insertAfter with siblings', (t) => {
        const tree = new SymbolTree();
        const a = o();
        const b = o();
        const c = o();

        tree.insertAfter(a, c);
        tree.insertAfter(a, b);

        t.equal(null, tree.previousSibling(a));
        t.equal(b, tree.nextSibling(a));

        t.equal(a, tree.previousSibling(b));
        t.equal(c, tree.nextSibling(b));

        t.equal(b, tree.previousSibling(c));
        t.equal(null, tree.nextSibling(c));

        t.end();
});

test('remove with previous sibling', (t) => {
        const tree = new SymbolTree();
        const a = o();
        const b = o();

        tree.insertAfter(a, b);
        tree.remove(b);

        t.equal(null, tree.previousSibling(a));
        t.equal(null, tree.nextSibling(a));
        t.equal(null, tree.parent(a));

        t.equal(null, tree.previousSibling(b));
        t.equal(null, tree.nextSibling(b));
        t.equal(null, tree.parent(b));

        t.end();
});

test('remove with next sibling', (t) => {
        const tree = new SymbolTree();
        const a = o();
        const b = o();

        tree.insertAfter(a, b);
        tree.remove(a);

        t.equal(null, tree.previousSibling(a));
        t.equal(null, tree.nextSibling(a));
        t.equal(null, tree.parent(a));

        t.equal(null, tree.previousSibling(b));
        t.equal(null, tree.nextSibling(b));
        t.equal(null, tree.parent(b));

        t.end();
});

test('remove with siblings', (t) => {
        const tree = new SymbolTree();
        const a = o();
        const b = o();
        const c = o();

        tree.insertAfter(a, b);
        tree.insertAfter(b, c);
        tree.remove(b);

        t.equal(null, tree.previousSibling(a));
        t.equal(c, tree.nextSibling(a));
        t.equal(null, tree.parent(a));

        t.equal(null, tree.previousSibling(b));
        t.equal(null, tree.nextSibling(b));
        t.equal(null, tree.parent(b));

        t.equal(a, tree.previousSibling(c));
        t.equal(null, tree.nextSibling(c));
        t.equal(null, tree.parent(c));

        t.end();
});

test('remove with parent', (t) => {
        const tree = new SymbolTree();
        const parent = o();
        const a = o();

        tree.prependChild(parent, a);
        tree.remove(a);

        t.equal(null, tree.parent(a));
        t.equal(null, tree.firstChild(parent));
        t.equal(null, tree.lastChild(parent));

        t.end();
});

test('remove with children', (t) => {
        const tree = new SymbolTree();
        const parent = o();
        const a = o();

        tree.prependChild(parent, a);
        tree.remove(parent);

        t.equal(parent, tree.parent(a));
        t.equal(a, tree.firstChild(parent));
        t.equal(a, tree.lastChild(parent));

        t.end();
});

test('remove with parent and siblings', (t) => {
        const tree = new SymbolTree();
        const parent = o();
        const a = o();
        const b = o();
        const c = o();

        tree.prependChild(parent, a);
        tree.insertAfter(a, b);
        tree.insertAfter(b, c);
        tree.remove(b);

        t.equal(a, tree.firstChild(parent));
        t.equal(c, tree.lastChild(parent));

        t.equal(null, tree.previousSibling(a));
        t.equal(c, tree.nextSibling(a));
        t.equal(parent, tree.parent(a));

        t.equal(null, tree.previousSibling(b));
        t.equal(null, tree.nextSibling(b));
        t.equal(null, tree.parent(b));

        t.equal(a, tree.previousSibling(c));
        t.equal(null, tree.nextSibling(c));
        t.equal(parent, tree.parent(c));

        t.end();
});

test('inserting an already associated object should fail', (t) => {
        const tree = new SymbolTree();
        const a = o();
        const b = o();

        tree.insertBefore(b, a);

        // jscs:disable requireBlocksOnNewline

        // `nextSibling` check
        t.throws(() => { tree.insertBefore(b, a); }, /already present/);
        t.throws(() => { tree.insertAfter(b, a); }, /already present/);
        t.throws(() => { tree.prependChild(b, a); }, /already present/);
        t.throws(() => { tree.appendChild(b, a); }, /already present/);

        // `previousSibling` check
        t.throws(() => { tree.insertBefore(a, b); }, /already present/);
        t.throws(() => { tree.insertAfter(a, b); }, /already present/);
        t.throws(() => { tree.prependChild(a, b); }, /already present/);
        t.throws(() => { tree.appendChild(a, b); }, /already present/);

        tree.remove(a);

        tree.prependChild(b, a);
        // `parent` check
        t.throws(() => { tree.insertBefore(b, a); }, /already present/);
        t.throws(() => { tree.insertAfter(b, a); }, /already present/);
        t.throws(() => { tree.prependChild(b, a); }, /already present/);
        t.throws(() => { tree.appendChild(b, a); }, /already present/);

        // jscs:enable requireBlocksOnNewline

        t.end();
});

test('Multiple SymbolTree instances should not conflict', (t) => {
        const tree1 = new SymbolTree();
        const tree2 = new SymbolTree();
        const a = o();
        const b = o();

        tree1.insertBefore(b, a);
        tree2.insertBefore(a, b);

        t.equal(null, tree1.previousSibling(a));
        t.equal(b, tree1.nextSibling(a));
        t.equal(a, tree1.previousSibling(b));
        t.equal(null, tree1.nextSibling(b));

        t.equal(null, tree2.previousSibling(b));
        t.equal(a, tree2.nextSibling(b));
        t.equal(b, tree2.previousSibling(a));
        t.equal(null, tree2.nextSibling(a));

        t.end();
});

test('lastInclusiveDescendant', (t) => {
        const tree = new SymbolTree();
        const a = o();
        const aa = o();
        const ab = o();
        const aba = o();
        const abaa = o();
        const b = o();

        tree.appendChild(a, aa);
        tree.appendChild(a, ab);
        tree.appendChild(ab, aba);
        tree.appendChild(aba, abaa);
        tree.insertAfter(a, b);

        t.equal(abaa, tree.lastInclusiveDescendant(a));

        t.end();
});

test('look up preceding with a previous sibling', (t) => {
        const tree = new SymbolTree();
        const a = o();
        const b = o();

        tree.insertAfter(a, b);

        t.equal(null, tree.preceding(a));
        t.equal(a, tree.preceding(b));

        t.end();
});

test('look up preceding with a previous sibling with a child', (t) => {
        const tree = new SymbolTree();
        const a = o();
        const aa = o();
        const ab = o();
        const b = o();

        tree.appendChild(a, aa);
        tree.appendChild(a, ab);
        tree.insertAfter(a, b);

        t.equal(null, tree.preceding(a));
        t.equal(a, tree.preceding(aa));
        t.equal(aa, tree.preceding(ab));
        t.equal(ab, tree.preceding(b));

        t.end();
});

test('look up preceding with a previous sibling with a descendants', (t) => {
        const tree = new SymbolTree();
        const a = o();
        const aa = o();
        const ab = o();
        const aba = o();
        const abaa = o();
        const b = o();

        tree.appendChild(a, aa);
        tree.appendChild(a, ab);
        tree.appendChild(ab, aba);
        tree.appendChild(aba, abaa);
        tree.insertAfter(a, b);

        t.equal(abaa, tree.preceding(b));

        t.end();
});

test('look up preceding using a specified root', (t) => {
        const tree = new SymbolTree();
        const a = o();
        const aa = o();

        tree.appendChild(a, aa);

        t.equal(null, tree.preceding(a, {root: a}));
        t.equal(a, tree.preceding(aa, {root: a}));
        t.equal(null, tree.preceding(aa, {root: aa}));

        t.end();
});

test('following with a child', (t) => {
        const tree = new SymbolTree();
        const a = o();
        const aa = o();

        tree.appendChild(a, aa);

        t.equal(aa, tree.following(a));
        t.equal(null, tree.following(aa));

        t.end();
});

test('following with a nextSibling sibling', (t) => {
        const tree = new SymbolTree();
        const a = o();
        const b = o();

        tree.insertAfter(a, b);

        t.equal(b, tree.following(a));
        t.equal(null, tree.following(b));

        t.end();
});

test('following with sibling of parent', (t) => {
        const tree = new SymbolTree();
        const a = o();
        const aa = o();
        const b = o();

        tree.appendChild(a, aa);
        tree.insertAfter(a, b);

        t.equal(b, tree.following(aa));

        t.end();
});

test('following with sibling of grandparent', (t) => {
        const tree = new SymbolTree();
        const a = o();
        const aa = o();
        const aaa = o();
        const b = o();

        tree.appendChild(a, aa);
        tree.appendChild(aa, aaa);
        tree.insertAfter(a, b);

        t.equal(b, tree.following(aaa));

        t.end();
});

test('following using a specified root', (t) => {
        const tree = new SymbolTree();
        const a = o();
        const aa = o();
        const aaa = o();
        const b = o();

        tree.appendChild(a, aa);
        tree.appendChild(aa, aaa);
        tree.insertAfter(a, b);

        t.equal(null, tree.following(aaa, {root: aaa}));
        t.equal(null, tree.following(aaa, {root: aa}));
        t.equal(null, tree.following(aaa, {root: a}));
        t.equal(aa, tree.following(a, {root: a}));
        t.equal(aaa, tree.following(aa, {root: a}));

        t.end();
});

test('following with skipChildren', (t) => {
        const tree = new SymbolTree();
        const a = o();
        const aa = o();
        const b = o();

        tree.appendChild(a, aa);
        tree.insertAfter(a, b);

        t.equal(b, tree.following(a, {skipChildren: true}));

        t.end();
});

test('childrenToArray', (t) => {
        const tree = new SymbolTree();
        const a = o();
        const aa = o();
        const ab = o();
        const aba = o();
        const ac = o();
        const b = o();

        tree.appendChild(a, aa);
        tree.appendChild(a, ab);
        tree.appendChild(ab, aba);
        tree.appendChild(a, ac);
        tree.insertAfter(a, b);

        t.deepEqual([aa, ab, ac], tree.childrenToArray(a));

        const arr = ['a', 5];
        tree.childrenToArray(a, {array: arr});
        t.deepEqual(['a', 5, aa, ab, ac], arr);

        t.end();
});

test('childrenToArray with filter', (t) => {
        const tree = new SymbolTree();
        const a = o();
        const aa = o();
        const ab = o();
        const aba = o();
        const ac = o();
        const b = o();

        tree.appendChild(a, aa);
        tree.appendChild(a, ab);
        tree.appendChild(ab, aba);
        tree.appendChild(a, ac);
        tree.insertAfter(a, b);

        const filter = function(object) {
                t.equal(this, undefined);

                return object !== ab;
        };

        t.deepEqual([aa, ac], tree.childrenToArray(a, {filter: filter}));

        const thisArg = {a: 123};
        const filterThis = function(object) {
                t.equal(this, thisArg);

                return object !== ab;
        };

        t.deepEqual([aa, ac], tree.childrenToArray(a, {filter: filterThis, thisArg: thisArg}));

        t.end();
});

test('children iterator', (t) => {
        const tree = new SymbolTree();
        const a = o();
        const aa = o();
        const ab = o();
        const aba = o();
        const ac = o();
        const b = o();

        tree.appendChild(a, aa);
        tree.appendChild(a, ab);
        tree.appendChild(ab, aba);
        tree.appendChild(a, ac);
        tree.insertAfter(a, b);

        const results = [];

        for (const object of tree.childrenIterator(a)) {
                results.push(object);
        }
        t.deepEqual([aa, ab, ac], results);

        t.end();
});

test('children iterator reverse', (t) => {
        const tree = new SymbolTree();
        const a = o();
        const aa = o();
        const ab = o();
        const aba = o();
        const ac = o();
        const b = o();

        tree.appendChild(a, aa);
        tree.appendChild(a, ab);
        tree.appendChild(ab, aba);
        tree.appendChild(a, ac);
        tree.insertAfter(a, b);

        const results = [];

        for (const object of tree.childrenIterator(a, {reverse: true})) {
                results.push(object);
        }
        t.deepEqual([ac, ab, aa], results);

        t.end();
});

test('children iterator return value using a generator', (t) => {
        const tree = new SymbolTree();
        const a = o();
        const aa = o();
        const ab = o();
        const ac = o();

        tree.appendChild(a, aa);
        tree.appendChild(a, ab);
        tree.appendChild(a, ac);

        function* generator(it) {
                const returnValue = yield* it;
                t.equal(a, returnValue);
        }

        const results = [];

        for (const object of generator(tree.childrenIterator(a))) {
                results.push(object);
        }
        t.deepEqual([aa, ab, ac], results);

        t.end();
});

test('previous sibling iterator', (t) => {
        const tree = new SymbolTree();
        const a = o();
        const aa = o();
        const ab = o();
        const aba = o();
        const ac = o();
        const ad = o();
        const ae = o();
        const b = o();

        tree.appendChild(a, aa);
        tree.appendChild(a, ab);
        tree.appendChild(ab, aba);
        tree.appendChild(a, ac);
        tree.appendChild(a, ad);
        tree.appendChild(a, ae);
        tree.insertAfter(a, b);

        const results = [];

        for (const object of tree.previousSiblingsIterator(ad)) {
                results.push(object);
        }
        t.deepEqual([ac, ab, aa], results);

        t.end();
});

test('nextSibling sibling iterator', (t) => {
        const tree = new SymbolTree();
        const a = o();
        const aa = o();
        const ab = o();
        const aba = o();
        const ac = o();
        const ad = o();
        const ae = o();
        const b = o();

        tree.appendChild(a, aa);
        tree.appendChild(a, ab);
        tree.appendChild(ab, aba);
        tree.appendChild(a, ac);
        tree.appendChild(a, ad);
        tree.appendChild(a, ae);
        tree.insertAfter(a, b);

        const results = [];

        for (const object of tree.nextSiblingsIterator(ab)) {
                results.push(object);
        }
        t.deepEqual([ac, ad, ae], results);

        t.end();
});

test('ancestorsToArray', (t) => {
        const tree = new SymbolTree();
        const a = o();
        const aa = o();
        const ab = o();
        const aba = o();
        const abaa = o();
        const b = o();

        tree.appendChild(a, aa);
        tree.appendChild(a, ab);
        tree.appendChild(ab, aba);
        tree.appendChild(aba, abaa);
        tree.insertAfter(a, b);

        t.deepEqual([abaa, aba, ab, a], tree.ancestorsToArray(abaa));
        t.deepEqual([aba, ab, a], tree.ancestorsToArray(aba));
        t.deepEqual([b], tree.ancestorsToArray(b));

        const arr = ['a', 5];
        tree.ancestorsToArray(abaa, {array: arr});
        t.deepEqual(['a', 5, abaa, aba, ab, a], arr);

        t.end();
});

test('ancestorsToArray with filter', (t) => {
        const tree = new SymbolTree();
        const a = o();
        const aa = o();
        const ab = o();
        const aba = o();
        const abaa = o();
        const b = o();

        tree.appendChild(a, aa);
        tree.appendChild(a, ab);
        tree.appendChild(ab, aba);
        tree.appendChild(aba, abaa);
        tree.insertAfter(a, b);

        const thisArg = {foo: 'bar'};
        const filter = function(object) {
                t.equal(this, thisArg);

                return object !== abaa && object !== ab;
        };

        t.deepEqual([aba, a], tree.ancestorsToArray(abaa, {filter: filter, thisArg: thisArg}));

        t.end();
});

test('ancestors iterator', (t) => {
        const tree = new SymbolTree();
        const a = o();
        const aa = o();
        const ab = o();
        const aba = o();
        const abaa = o();
        const b = o();

        tree.appendChild(a, aa);
        tree.appendChild(a, ab);
        tree.appendChild(ab, aba);
        tree.appendChild(aba, abaa);
        tree.insertAfter(a, b);

        const results = [];
        const iterator = tree.ancestorsIterator(abaa);

        for (const object of iterator) {
                results.push(object);
        }
        t.deepEqual([abaa, aba, ab, a], results);
        t.deepEqual({done: true, value: abaa}, iterator.next());
        t.deepEqual({done: true, value: abaa}, iterator.next()); // should keep returning done: true

        t.end();
});

test('treeToArray', (t) => {
        const tree = new SymbolTree();
        const a = o();
        const aa = o();
        const ab = o();
        const aba = o();
        const abaa = o();
        const b = o();

        tree.appendChild(a, aa);
        tree.appendChild(a, ab);
        tree.appendChild(ab, aba);
        tree.appendChild(aba, abaa);
        tree.insertAfter(a, b);

        t.deepEqual([a, aa, ab, aba, abaa], tree.treeToArray(a));

        const arr = ['a', 5];
        tree.treeToArray(a, {array: arr});
        t.deepEqual(['a', 5, a, aa, ab, aba, abaa], arr);

        t.end();
});

test('treeToArray with filter', (t) => {
        const tree = new SymbolTree();
        const a = o();
        const aa = o();
        const ab = o();
        const aba = o();
        const abaa = o();
        const b = o();

        tree.appendChild(a, aa);
        tree.appendChild(a, ab);
        tree.appendChild(ab, aba);
        tree.appendChild(aba, abaa);
        tree.insertAfter(a, b);

        const filter = function(object) {
                t.equal(this, undefined);

                return object !== a && object !== aba;
        };

        t.deepEqual([aa, ab, abaa], tree.treeToArray(a, {filter: filter}));

        const thisArg = {foo: 'bar'};
        const filterThis = function(object) {
                t.equal(this, thisArg);

                return object !== a && object !== aba;
        };

        t.deepEqual([aa, ab, abaa], tree.treeToArray(a, {filter: filterThis, thisArg: thisArg}));

        t.end();
});

test('tree iterator', (t) => {
        const tree = new SymbolTree();
        const a = o();
        const aa = o();
        const ab = o();
        const aba = o();
        const abaa = o();
        const ac = o();
        const b = o();

        tree.appendChild(a, aa);
        tree.appendChild(a, ab);
        tree.appendChild(ab, aba);
        tree.appendChild(aba, abaa);
        tree.appendChild(a, ac);
        tree.insertAfter(a, b);

        const results = [];
        const iterator = tree.treeIterator(a);

        for (const object of iterator) {
                results.push(object);
        }
        t.deepEqual([a, aa, ab, aba, abaa, ac], results);
        t.deepEqual({done: true, value: a}, iterator.next());
        t.deepEqual({done: true, value: a}, iterator.next()); // should keep returning done: true

        t.end();
});

test('tree iterator reverse', (t) => {
        const tree = new SymbolTree();
        const a = o();
        const aa = o();
        const ab = o();
        const aba = o();
        const abaa = o();
        const ac = o();
        const b = o();

        tree.appendChild(a, aa);
        tree.appendChild(a, ab);
        tree.appendChild(ab, aba);
        tree.appendChild(aba, abaa);
        tree.appendChild(a, ac);
        tree.insertAfter(a, b);

        const results = [];
        const iterator = tree.treeIterator(a, {reverse: true});

        for (const object of iterator) {
                results.push(object);
        }
        t.deepEqual([ac, abaa, aba, ab, aa, a], results);
        t.deepEqual({done: true, value: a}, iterator.next());
        t.deepEqual({done: true, value: a}, iterator.next()); // should keep returning done: true

        t.end();
});

test('look up the index of an object', (t) => {
        const tree = new SymbolTree();
        const a = o();
        const aa = o();
        const ab = o();
        const aba = o();
        const ac = o();
        const b = o();

        tree.appendChild(a, aa);
        tree.appendChild(a, ab);
        tree.appendChild(ab, aba);
        tree.appendChild(a, ac);
        tree.insertAfter(a, b);

        t.equal(-1, tree.index(a), 'should return -1 if an object has no parent');
        t.equal(0, tree.index(aa));
        t.equal(1, tree.index(ab));
        t.equal(0, tree.index(aba));
        t.equal(2, tree.index(ac));
        t.equal(-1, tree.index(b));

        t.end();
});

test('cached index', (t) => {
        const tree = new SymbolTree();
        const a = o();
        const aa = o();
        const ab = o();
        const aba = o();
        const ac = o();
        const b = o();

        tree.appendChild(a, aa);
        tree.appendChild(a, ab);
        tree.appendChild(ab, aba);
        tree.appendChild(a, ac);
        tree.insertAfter(a, b);

        // looking up ac, will also set the cached index for aa and ab, so check that those are valid
        t.equal(2, tree.index(ac));
        t.equal(1, tree.index(ab));
        t.equal(0, tree.index(aa));

        // removing something should invalidate the cache
        tree.remove(ab);
        t.equal(1, tree.index(ac));
        t.equal(-1, tree.index(ab));
        t.equal(0, tree.index(aa));

        // insertAfter should invalidate
        tree.insertAfter(aa, ab);
        t.equal(0, tree.index(aa));
        t.equal(1, tree.index(ab));
        t.equal(2, tree.index(ac));

        // insertBefore should invalidate
        const foo = o();
        tree.insertBefore(ab, foo);
        t.equal(0, tree.index(aa));
        t.equal(2, tree.index(ab));
        t.equal(3, tree.index(ac));

        t.end();
});

test('cached index warmed up by childrenToArray', (t) => {
        const tree = new SymbolTree();
        const a = o();
        const aa = o();
        const ab = o();
        const aba = o();
        const ac = o();
        const b = o();

        tree.appendChild(a, aa);
        tree.appendChild(a, ab);
        tree.appendChild(ab, aba);
        tree.appendChild(a, ac);
        tree.insertAfter(a, b);

        tree.childrenToArray(a);
        t.equal(0, tree.index(aa));
        t.equal(1, tree.index(ab));
        t.equal(2, tree.index(ac));

        tree.appendChild(a, o());
        t.equal(2, tree.index(ac));
        tree.childrenToArray(a);
        t.equal(2, tree.index(ac));

        t.end();
});

test('regression test: remove() should invalidate the child index cache', (t) => {
        const tree = new SymbolTree();
        const a = o();
        const aa = o();
        const b = o();
        const ba = o();
        const bb = o();

        tree.appendChild(a, aa);
        tree.appendChild(b, ba);
        tree.appendChild(b, bb);

        t.equal(0, tree.index(ba));
        tree.remove(ba);
        t.equal(-1, tree.index(ba));
        tree.appendChild(a, ba);
        t.equal(1, tree.index(ba));

        t.end();
});

test('children count', (t) => {
        // no need to test the caching since we already tested for that in childrenCount
        const tree = new SymbolTree();
        const a = o();
        const aa = o();
        const ab = o();
        const aba = o();
        const ac = o();
        const b = o();

        tree.appendChild(a, aa);
        tree.appendChild(a, ab);
        tree.appendChild(ab, aba);
        tree.appendChild(a, ac);
        tree.insertAfter(a, b);

        t.equal(3, tree.childrenCount(a), 'foo');
        t.equal(0, tree.childrenCount(aa));
        t.equal(1, tree.childrenCount(ab));
        t.equal(0, tree.childrenCount(b));

        t.end();
});


test('compare tree position', (t) => {
        const tree = new SymbolTree();
        const a = o();
        const aa = o();
        const aaa = o();
        const ab = o();
        const aba = o();
        const abaa = o();
        const ac = o();

        const b = o();
        const ba = o();

        tree.appendChild(a, aa);
        tree.appendChild(aa, aaa);
        tree.appendChild(a, ab);
        tree.appendChild(ab, aba);
        tree.appendChild(aba, abaa);
        tree.appendChild(a, ac);

        tree.insertAfter(a, b);
        tree.appendChild(b, ba);

        t.equal(0, tree.compareTreePosition(a, a), 'object equal');

        t.equal(1, tree.compareTreePosition(a, o()), 'object disconnected');
        t.equal(1, tree.compareTreePosition(a, b), 'object disconnected');

        t.equal(20, tree.compareTreePosition(a, aa), 'contained by & following');
        t.equal(10, tree.compareTreePosition(aa, a), 'contains & preceding');
        t.equal(20, tree.compareTreePosition(a, abaa), 'contained by & following');
        t.equal(10, tree.compareTreePosition(abaa, a), 'contains & preceding');

        t.equal(4, tree.compareTreePosition(aa, ab), 'following');
        t.equal(2, tree.compareTreePosition(ab, aa), 'preceding');
        t.equal(4, tree.compareTreePosition(aa, aba), 'following');
        t.equal(2, tree.compareTreePosition(aba, aa), 'preceding');
        t.equal(4, tree.compareTreePosition(aa, abaa), 'following');
        t.equal(2, tree.compareTreePosition(abaa, aa), 'preceding');
        t.equal(4, tree.compareTreePosition(aaa, abaa), 'following');
        t.equal(2, tree.compareTreePosition(abaa, aaa), 'preceding');

        t.end();
});
