/*\
title: test-linked-list.js
type: application/javascript
tags: [[$:/tags/test-spec]]

Tests the utils.LinkedList class.

LinkedList was built to behave exactly as $tw.utils.pushTop and
Array.prototype.push would behave with an array.

Many of these tests function by performing operations on a paired set of
an array and LinkedList. It uses equivalent actions on both.
Then we confirm that the two come out functionally identical.

NOTE TO FURTHER LINKED LIST DEVELOPERS:

	If you want to add new functionality, like 'shift' or 'unshift', you'll
	probably need to deal with the fact that Linked List will insert undefined
	as a first entry into an item's 'prev' array when it's at the front of
	the list, but it doesn't do the same for the 'next' array when it's at
	the end. I think you'll probably be better off preventing 'prev' from ever
	adding undefined.
\*/

"use strict";

describe("LinkedList class tests", function() {

	// creates and initializes a new {array, list} pair for testing
	function newPair(initialArray) {
		var pair = {array: [], list: new $tw.utils.LinkedList()};
		if(initialArray) {
			push(pair, initialArray);
		}
		return pair;
	};

	// pushTops a value or array of values into both the array and linked list.
	function pushTop(pair, valueOrValues) {
		pair.list.pushTop(valueOrValues);
		$tw.utils.pushTop(pair.array, valueOrValues);
		return pair;
	};

	// pushes values into both the array and the linked list.
	function push(pair, values) {
		pair.list.push(values);
		pair.array.push.apply(pair.array, values);
		return pair;
	};

	// operates a remove action on an array and a linked list in parallel.
	function remove(pair, valueOrValues) {
		pair.list.remove(valueOrValues);
		$tw.utils.removeArrayEntries(pair.array, valueOrValues);
		return pair;
	};

	// This returns an array in reverse using a LinkList's prev member. Thus
	// testing that prev is not corrupt. It doesn't exist in the LinkList module
	// itself to avoid full support for it. Maybe that will change later.
	function toReverseArray(list) {
		var visits = Object.create(null),
			value = list.prev.get(null),
			array = [];
		while(value !== null) {
			array.push(value);
			var prev = list.prev.get(value);
			if(Array.isArray(prev)) {
				var i = (visits[value] || prev.length) - 1;
				visits[value] = i;
				value = prev[i];
			} else {
				value = prev;
			}
		}
		return array;
	};

	// compares an array and a linked list to make sure they match up
	function compare(pair) {
		var forward = pair.list.toArray();
		expect(forward).toEqual(pair.array);
		expect(pair.list.length).toBe(pair.array.length);
		// Now we reverse the linked list and test it back to front, thus
		// confirming that the list.prev isn't corrupt.
		expect(toReverseArray(pair.list)).toEqual(forward.reverse());
		return pair;
	};

	it("can pushTop", function() {
		var pair = newPair(["A", "B", "C"]);
		// singles
		pushTop(pair, "X");
		pushTop(pair, "B");
		compare(pair); // ACXB
		//arrays
		pushTop(pair, ["X", "A", "G", "A"]);
		// If the pushedTopped list has duplicates, they go in unempeded.
		compare(pair); // CBXAGA
	});

	it("can pushTop with tricky duplicates", function() {
		var pair = newPair(["A", "B", "A", "C", "A", "e"]);
		// If the original list contains duplicates, only one instance is cut
		compare(pushTop(pair, "A")); // BACAeA

		// And the Llist properly knows the next 'A' to cut if pushed again
		compare(pushTop(pair, ["X", "A"])); // BCAeAXA

		// One last time, to make sure we maintain the linked chain of copies
		compare(pushTop(pair, "A")); // BCeAXAA
	});

	it("can pushTop a single-value list with itself", function() {
		// This simple case actually requires special handling in LinkedList.
		compare(pushTop(newPair(["A"]), "A")); // A
	});

	it("can remove all instances of a multi-instance value", function() {
		var pair = compare(remove(newPair(["A", "A"]), ["A", "A"])); //
		// Now add 'A' back in, since internally it might be using arrays,
		// even though those arrays must be empty.
		compare(pushTop(pair, "A")); // A
		// Same idea, but push something else before readding 'A'
		compare(pushTop(remove(newPair(["A", "A"]), ["A", "A"]), ["B", "A"])); // BA

		// Again, but this time with other values mixed in
		compare(remove(newPair(["B", "A", "A", "C"]), ["A", "A"])); // BC;
		// And again, but this time with value inbetween too.
		compare(remove(newPair(["B", "A", "X", "Y", "Z", "A", "C"]), ["A", "A"])); // BXYZC

		// One last test, where removing a pair from the end could corrupt
		// list.last.
		pair = remove(newPair(["D", "C", "A", "A"]), ["A", "A"]);
		// But I can't figure out another way to test this. It's wrong
		// for list.last to be anything other than a string, but I
		// can't figure out how to make that corruption manifest a problem.
		// So I dig into its private members. Bleh...
		expect(typeof pair.list.prev.get(null)).toBe("string");
	});

	it("can pushTop value linked to by a repeat item", function() {
		var pair = newPair(["A", "B", "A", "C", "A", "C", "D"]);
		// This is tricky because that 'C' is referenced by a second 'A'
		// It WAS a crash before
		pushTop(pair, "C");
		compare(pair); // ABAACDC
	});

	it("can pushTop last value after pair", function() {
		// The 'next' ptrs for A would be polluted with an extraneous
		// undefined after the pop, which would make pushing the 'X'
		// back on problematic.
		compare(pushTop(newPair(["A", "A", "X"]), "X")); // AACX
		// And lets try a few other manipulations around pairs
		compare(pushTop(newPair(["A", "A", "X", "C"]), "X")); // AACX
		compare(pushTop(newPair(["X", "A", "A"]), "X")); // AAX
		compare(pushTop(newPair(["C", "X", "A", "A"]), "X")); // CAAX
	});

	it("can remove all instances of a multi-instance value #7059", function() {
		// Remove duplicate items when one or more items between the duplicates
		// are not removed and the first of those duplicates is not the first item.
		// These tests used to fail prior to the fix to #7059
		compare(remove(newPair(["A", "A", "C", "B", "A"]), ["A", "C", "A", "A"])); // B
		compare(remove(newPair(["A", "A", "C", "B", "A"]), ["C", "A", "A", "A"])); // B
		compare(remove(newPair(["A", "A", "C", "B", "A"]), ["A", "A", "A"])); // CB
		compare(remove(newPair(["A", "A", "C", "B", "A"]), ["A", "A", "A", "C"])); // B
		compare(remove(newPair(["A", "A", "B", "A"]), ["A", "A", "A"])); // B
		compare(remove(newPair(["A", "A", "B", "A"]), ["A", "A", "A", "B"])); //
		compare(remove(newPair(["C", "A", "B", "A"]), ["C", "A", "A"])); // B
		compare(remove(newPair(["C", "A", "B", "A", "C"]), ["C", "A", "A", "C"])); // B
		compare(remove(newPair(["B", "A", "B", "A"]), ["B", "A", "A"])); // B
	});

	it("can handle particularly nasty pushTop pitfall", function() {
		var pair = newPair(["A", "B", "A", "C"]);
		pushTop(pair, "A"); // BACA
		pushTop(pair, "X"); // BACAX
		remove(pair, "A");  // BCAX
		pushTop(pair, "A"); // BCXA
		remove(pair, "A");  // BCX

		// But! The way I initially coded the copy chains, a mystery A could
		// hang around.
		compare(pair); // BCX
	});

	it("can handle past-duplicate items when pushing", function() {
		var pair = newPair(["X", "Y", "A", "C", "A"]);
		// Removing an item, when it has a duplicat at the list's end
		remove(pair, "A");
		compare(pair); // XYCA
		// This actually caused an infinite loop once. So important test here.
		push(pair, ["A"]);
		compare(pair); // XYCAA
		pushTop(pair, "A"); // switch those last As
		compare(pair); // XYCAA
		remove(pair, ["A", "A"]); // Remove all As, then add them back
		pushTop(pair, ["A", "A"]);
		compare(pair); // XYCAA
	});

	it("can push", function() {
		var list = new $tw.utils.LinkedList();
		// singles
		expect(list.push("A")).toBe(1);
		expect(list.push("B")).toBe(2);
		// multiple args
		expect(list.push("C", "D", "E")).toBe(5);
		// array arg allowed
		expect(list.push(["F", "G"])).toBe(7);
		// No-op
		expect(list.push()).toBe(7);
		expect(list.toArray()).toEqual(["A", "B", "C", "D", "E", "F", "G"]);
	});

	it("can handle empty string", function() {
		compare(newPair(["", "", ""])); // ___
		compare(push(newPair([""]), [""])); // __
		compare(pushTop(newPair(["", "", ""]), ["A", ""])); // __A_
		compare(remove(newPair(["", "A"]), "A")); // _
		compare(push(newPair(["", "A"]), ["A"])); // _AA
		compare(remove(newPair(["A", ""]), "A")); // _
		compare(push(newPair(["A", ""]), ["A"])); // A_A

		// This one is tricky but precise. Remove 'B', and 'A' might mistake
		// it as being first in the list since it's before ''. 'C' would get
		// blasted from A's prev reference array.
		compare(remove(newPair(["C", "A", "", "B", "A"]), ["B", "A"])); // C_A
		// Same idea, but with A mistaking B for being at the list's end, and
		// thus removing C from its 'next' reference array.
		compare(remove(newPair(["A", "B", "", "A", "C"]), ["B", "A"])); // _AC
	});

	it("will throw if told to push non-strings", function() {
		var message = "Linked List only accepts string values, not ";
		var list = new $tw.utils.LinkedList();
		expect(() => list.push(undefined)).toThrow(message + "undefined");
		expect(() => list.pushTop(undefined)).toThrow(message + "undefined");
		expect(() => list.pushTop(["c", undefined])).toThrow(message + "undefined");
		expect(() => list.pushTop(5)).toThrow(message + "5");
		expect(() => list.pushTop(null)).toThrow(message + "null");

		// now lets do a quick test to make sure this exception
		// doesn't leave any side-effects
		// A.K.A Strong guarantee
		var pair = newPair(["A", "5", "B", "C"]);
		expect(() => pushTop(pair, 5)).toThrow(message + "5");
		compare(pair);
		expect(() => push(pair, ["D", 7])).toThrow(message + "7");
		compare(pair);
		expect(() => remove(pair, 5)).toThrow(message + "5");
		compare(pair);
		// This is the tricky one. 'A' and 'B' should not be removed or pushed.
		expect(() => pushTop(pair, ["A", "B", null])).toThrow(message + "null");
		compare(pair);
		expect(() => remove(pair, ["A", "B", null])).toThrow(message + "null");
		compare(pair);
	});

	it("can clear", function() {
		var list = new $tw.utils.LinkedList();
		list.push("A", "B", "C");
		list.clear();
		expect(list.toArray()).toEqual([]);
		expect(list.length).toBe(0);
	});

	it("can remove", function() {
		var list = new $tw.utils.LinkedList();
		list.push(["A", "x", "C", "x", "D", "x", "E", "x"]);
		// single
		list.remove("x");
		// arrays
		list.remove(["x", "A", "XXX", "x"]);
		expect(list.toArray()).toEqual(["C", "D", "E", "x"]);
	});

	it("can ignore removal of nonexistent items", function() {
		var pair = newPair(["A", "B", "C", "D"]);
		// single
		compare(remove(pair, "Z")); // ABCD

		// array
		compare(remove(pair, ["Z", "B", "X"])); // ACD
	});

	it("can iterate with each", function() {
		var list = new $tw.utils.LinkedList();
		list.push("0", "1", "2", "3");
		var counter = 0;
		list.each(function(value) {
			expect(value).toBe(counter.toString());
			counter = counter + 1;
		});
		expect(counter).toBe(4);
	});

	it("can iterate a list of the same item", function() {
		// Seems simple. Caused an infinite loop during development.
		compare(newPair(["A", "A"]));
	});
});

