define(["intern!object", "intern/chai!assert", "./resources/test.utils!"], function(registerSuite, assert, testutils) {
	"use strict";

	var ns = "An_observed_nameSpace",
		testHolder,
		containerId,
		timers,
		Observer,
		observer;

	registerSuite({
		name: "Observer",
		setup: function() {
			return testutils.setupHelper(["wc/Observer", "wc/timers"]).then(function(arr) {
				Observer = arr[0];
				timers = arr[1];
				observer = new Observer();
				testHolder = testutils.getTestHolder();
				containerId = testHolder.id;
			});
		},
		afterEach: function() {
			observer.reset();
			observer.reset(ns);
		},
		/* NOTE: The subscribe() tests rely on an assumption that notify() works as expected. */

		/* Why does subscribe() return the subscriber? Who knows; but it does so it gets tested. */
		testObserverSubscribeReturnsSubscriber: function() {
			function subscriber() {
				return true;
			}
			assert.strictEqual(observer.subscribe(subscriber), subscriber, "Observer.subscribe should return the subscribed function.");
		},
		testObserverSubscribe: function() {
			var wasNotified = false;

			function subscriber() {
				wasNotified = true;
			}

			observer.subscribe(subscriber);
			observer.notify();
			assert.isTrue(wasNotified, "The subscriber should be notified if it was correctly subscribed.");
		},
		testObserverSubscribePromise: function() {
			var wasNotified = false;

			function subscriber() {
				wasNotified = true;
				return Promise.resolve();
			}

			observer.subscribe(subscriber);
			observer.notify().then(function() {
				assert.isTrue(wasNotified, "The subscriber should be notified if it was correctly subscribed.");
			});
		},
		testObserverSubscribeStagedPromise: function() {
			var wasNotified = false,
				myObserver = new Observer(true);

			function subscriber() {
				wasNotified = true;
				return Promise.resolve();
			}

			myObserver.subscribe(subscriber);
			myObserver.notify().then(function() {
				assert.isTrue(wasNotified, "The subscriber should be notified if it was correctly subscribed.");
			});
		},
		testObserverSubscribeSubscriberNeedsArgs: function() {
			var wasNotified = false;
			function subscriber(arg1, arg2) {
				if (arg1 === "foo" && arg2 === "bar") {
					wasNotified = true;
				}
			}
			observer.subscribe(subscriber);
			observer.notify("foo", "bar");
			assert.isTrue(wasNotified, "Subscribe with args should honour notified args.");
		},
		testObserverSubscribeTwice: function() {
			var count = 0;

			function subscriber() {
				count++;
			}

			observer.subscribe(subscriber);
			observer.subscribe(subscriber);
			observer.notify();
			assert.strictEqual(count, 1, "Should not be able to subscribe more than once.");
		},
		testObserverSubscribeNoParams: function() {
			try {
				observer.subscribe();
				assert.fail(!null, null, "Expected exception: Subscribing without a subscriber should have failed.");  // should not get here
			} catch (e) {
				assert.isTrue(true, "Error expected.");
			}
		},
		/* Subscribing an object allows late binding of the subscriber function. The intent of this is that a method
		 * name is always passed to Observer.subscribe but lack of overloading in JS makes this impractical to enforce.
		 * This tests that subscribing an Object works. Later we test subscribing with a method name to testnotify of
		 * that method.*/
		testObserverSubscribeToNotAFunction: function() {
			var someObject = {
				prop1: null,
				prop2: function() {
					return true;
				} };

			assert.strictEqual(observer.subscribe(someObject), someObject, "We should be able to subscribe anything and have it returned.");
		},
		/* Subscriber groups are really an issue for notify(). This just tests that  the subscriber gets subscribed (by returning itself) and does not throw an exception. */
		testObserverSubscribeWithGroup: function() {
			function subscriber() { }
			assert.strictEqual(observer.subscribe(subscriber, { group: ns }), subscriber, "Subscribe with a group should return the subscriber.");
		},
		/* Tests of Observer context applied to a subscriber. The context supplied by a call to subscribe should be the "this" of the subscriber when notified. */
		testObserverSubscribeWithContext: function() {
			var actualContext,
				expectedContext = document.getElementById(containerId) || assert.fail(null, !null, "Cannot get context container.");

			function subscriber() {
				actualContext = this;
			}
			observer.subscribe(subscriber, { context: expectedContext });
			observer.notify();
			assert.strictEqual(actualContext, expectedContext, "Notify should have reset actualContext.");
		},
		testObserverSubscribeWithDifferentContexts: function() {
			var actualContext1,
				actualContext2,
				expectedContext1 = document.getElementById(containerId),
				expectedContext2 = { foo: "bar" };

			function subscriber1() {
				actualContext1 = this;
			}

			function subscriber2() {
				actualContext2 = this;
			}

			observer.subscribe(subscriber1, { context: expectedContext1 });
			observer.subscribe(subscriber2, { context: expectedContext2 });
			observer.notify();

			assert.strictEqual(actualContext1, expectedContext1, "Notify should have reset actualContext1.");
			assert.strictEqual(actualContext2, expectedContext2, "Notify should have reset actualContext2.");
		},
		testObserverSubscribeWithNullContext: function() {
			var actualContext,
				expectedContext = window.self;

			function subscriber() {
				actualContext = this;
			}
			observer.subscribe(subscriber, { context: null });  // pass nothing and we should get global context
			observer.notify();
			assert.strictEqual(actualContext, expectedContext, "Notify should have reset actualContext.");
		},
		testObserverSubscribeWithContextPassthru: function() {
			var actualContext,
				expectedContext = { foo: "bar" };

			function subscriber() {
				actualContext = this;
			}

			observer.subscribe(subscriber);  // if using "call" or "apply" and no context specified context should pass through
			observer.notify.call(expectedContext);
			assert.strictEqual(actualContext, expectedContext, "Notify should have reset actualContext.");
		},
		testObserverSubscribeWithMethodContext: function() {
			var actualContext,
				expectedContext;

			function Subscriber() {
				this.myMethod = function() {
					actualContext = this;
				};
			}

			expectedContext = new Subscriber();
			observer.subscribe(expectedContext, { method: "myMethod" }); // calling a method the context should be the object to which the method belongs
			observer.notify();
			assert.strictEqual(actualContext, expectedContext, "Notify should have reset actualContext.");
		},
		testObserverSubscribeWithMethodContextOverride: function() {
			var actualContext,
				expectedContext = document.getElementById(containerId);

			function Subscriber() {
				this.myMethod = function() {
					actualContext = this;
				};
			}
			observer.subscribe(new Subscriber(), { context: expectedContext, method: "myMethod" });
			observer.notify();
			assert.strictEqual(actualContext, expectedContext, "Notify should have reset actualContext");
		},
		/* Testing the priority parameter. */
		testObserverSubscribeImportance: function() {
			var amIImportant = true;

			function iAmImportant() {
				amIImportant = true;
			}
			function iAmNotImportant() {
				amIImportant = false;
			}

			// two subscribers, the second is important so gets notified first.
			observer.subscribe(iAmNotImportant, { priority: Observer.priority.MED });
			observer.subscribe(iAmImportant, { priority: Observer.priority.HIGH });
			observer.notify();
			assert.isFalse(amIImportant, "The important subscriber should be notified first, therefore amIImportant should be reset by the first subscriber");
		},
		testObserverSubscribeLowImportance: function() {
			var lastcaller = 1;

			observer.subscribe(function() {
				lastcaller = 2;
			}, { priority: Observer.priority.LOW });
			observer.subscribe(function() {
				lastcaller = 3;
			});
			observer.notify();
			assert.strictEqual(lastcaller, 2, "The low priority subscriber should be called last.");
		},
		testObserverSubscribeHighImportance: function() {
			var firstcaller;

			observer.subscribe(function() {
				firstcaller = firstcaller || 1;
			});
			observer.subscribe(function() {
				firstcaller = firstcaller || 2;
			}, { priority: Observer.priority.HIGH });
			observer.subscribe(function() {
				firstcaller = firstcaller || 3;
			});
			observer.notify();
			assert.strictEqual(firstcaller, 2, "The High priority subscriber should be called first.");
		},/* The important parameter is boolean and therefore should be able to be set by another function */
		testObserverSubscribeImportanceFunction: function() {
			var amIImportant = true;

			function iAmImportant() {
				amIImportant = true;
			}
			function iAmNotImportant() {
				amIImportant = false;
			}

			function setImportanceParameter(foo) {
				if (typeof foo === "string") {
					return true;
				}
				return false;
			}
			observer.subscribe(iAmNotImportant, { priority: setImportanceParameter({ p1: "empty" }) });
			observer.subscribe(iAmImportant, { priority: setImportanceParameter("anything") });
			observer.notify();
			assert.isFalse(amIImportant, "The important subscriber should be notified first, therefore amIImportant should be reset by the first subscriber");
		},
		testImportanceWithPromise: function () {
			var expected = ["high", "medium", "low"],
				order = [];
			observer.subscribe(function() {
				order.push("medium");
			});
			observer.subscribe(function() {
				order.push("low");
			}, { priority: Observer.priority.LOW });
			observer.subscribe(function() {
				order.push("high");
			}, { priority: Observer.priority.HIGH });
			return observer.notify().then(function() {
				assert.equal(order.join(), expected.join());
			});
		},
		testImportanceWithPromiseRejectsAndErrors: function () {
			var expected = ["high", "high", "medium", "medium", "low", "low"],
				order = [];
			observer.subscribe(function() {
				order.push("medium");
				return Promise.reject("Observer testing reject in medium subscriber");
			});
			observer.subscribe(function() {
				order.push("medium");
				throw new Error("Observer testing error in medium subscriber");
			});
			observer.subscribe(function() {
				order.push("low");
				throw new Error("Observer testing error in low subscriber");
			}, { priority: Observer.priority.LOW });
			observer.subscribe(function() {
				order.push("low");
				return Promise.reject("Observer testing reject in low subscriber");
			}, { priority: Observer.priority.LOW });
			observer.subscribe(function() {
				order.push("high");
				throw new Error("Observer testing error in high subscriber");
			}, { priority: Observer.priority.HIGH });
			observer.subscribe(function() {
				order.push("high");
				return Promise.reject("Observer testing reject in high subscriber");
			}, { priority: Observer.priority.HIGH });
			return observer.notify().then(function() {
				assert.equal(order.join(), expected.join());
			});
		},
		testStagedImportance: function () {
			var myObserver = new Observer(true),
				expected = ["high", "medium", "low"],
				order = [];
			myObserver.subscribe(function() {
				order.push("medium");
			});
			myObserver.subscribe(function() {
				order.push("low");
			}, { priority: Observer.priority.LOW });
			myObserver.subscribe(function() {
				order.push("high");
			}, { priority: Observer.priority.HIGH });
			return myObserver.notify().then(function() {
				assert.equal(order.join(), expected.join());
			});
		},
		testStagedImportanceSubscriberThrowsErrors: function () {
			var myObserver = new Observer(true),
				expected = ["high", "high", "medium", "medium", "low", "low"],
				order = [];
			myObserver.subscribe(function() {
				order.push("medium");
				throw new Error("Observer testing error in medium subscriber");
			});
			myObserver.subscribe(function() {
				order.push("medium");
				throw new Error("Observer testing error in medium subscriber");
			});
			myObserver.subscribe(function() {
				order.push("low");
			}, { priority: Observer.priority.LOW });
			myObserver.subscribe(function() {
				order.push("low");
			}, { priority: Observer.priority.LOW });
			myObserver.subscribe(function() {
				order.push("high");
				throw new Error("Observer testing error in high subscriber");
			}, { priority: Observer.priority.HIGH });
			myObserver.subscribe(function() {
				order.push("high");
				throw new Error("Observer testing error in high subscriber");
			}, { priority: Observer.priority.HIGH });
			return myObserver.notify().then(function() {
				assert.equal(order.join(), expected.join());
			});
		},
		testStagedImportanceWaits: function () {
			var myObserver = new Observer(true),
				expected = ["high", "medium", "low"],
				order = [];
			/*
				Even if the low subscribers are very fast and the high subscribers
				very slow, each stage will complete in order if "staged" is true.
			 */
			myObserver.subscribe(function() {
				return new Promise(function(win) {
					timers.setTimeout(function() {
						order.push("medium");
						win();
					}, 20);
				});
			});
			myObserver.subscribe(function() {
				order.push("low");
			}, { priority: Observer.priority.LOW });
			myObserver.subscribe(function() {
				return new Promise(function(win) {
					timers.setTimeout(function() {
						order.push("high");
						win();
					}, 40);
				});
			}, { priority: Observer.priority.HIGH });

			return myObserver.notify().then(function() {
				assert.equal(order.join(), expected.join(), "Subscribers should wait for the previous stage to complete");
			});
		},
		testStagedImportanceRandomWaits: function () {
			var myObserver = new Observer(true),
				expected = ["high", "medium", "low"],
				delays = [getRandomInt(0, 50), getRandomInt(0, 50), getRandomInt(0, 50)],
				order = [];
			/*
			 Same as test above but mix up the times
			 */
			myObserver.subscribe(function() {
				return new Promise(function(win) {
					timers.setTimeout(function() {
						order.push("medium");
						win();
					}, delays[0]);
				});
			});
			myObserver.subscribe(function() {
				return new Promise(function(win) {
					timers.setTimeout(function() {
						order.push("low");
						win();
					}, delays[1]);
				});
			}, { priority: Observer.priority.LOW });
			myObserver.subscribe(function() {
				return new Promise(function(win) {
					timers.setTimeout(function() {
						order.push("high");
						win();
					}, delays[2]);
				});
			}, { priority: Observer.priority.HIGH });

			return myObserver.notify().then(function() {
				assert.equal(order.join(), expected.join(),
					"Subscribers should wait for the previous stage to complete " + delays.join());
			});
		},
		/* Testing the method parameter */
		testObserverSubscribeWithMethod: function() {
			var calledIn = null,
				objectSubscriber;

			function ObjectSubscriber() {
				this.doSubscribe = function() {
					calledIn = this;
				};
			}

			objectSubscriber = new ObjectSubscriber();
			observer.subscribe(objectSubscriber, { method: "doSubscribe" });
			observer.notify();
			assert.isNotNull(calledIn, "Subscribed method should have been called.");
		},
		testObserverSubscribeWithMethodKeepsContext: function() {
			var calledIn,
				objectSubscriber;

			function ObjectSubscriber() {
				this.doSubscribe = function() {
					calledIn = this;
				};
			}

			objectSubscriber = new ObjectSubscriber();
			observer.subscribe(objectSubscriber, { method: "doSubscribe" });
			observer.notify();
			assert.strictEqual(calledIn, objectSubscriber, "Context should have been kept when using a subscriber Object with method name.");
		},
		/* subscribe()'s method param does not come into play until notify() is called so it may contain pretty much
		 * anything but the various exceptions should be tested in notify() tests.*/
		testObserverSubscribeMethodUndefinedFunction: function() {
			var iHaveBeenCalled = false,
				hasSubscribed = false,
				objectSubscriber;

			function ObjectSubscriber() {
				iHaveBeenCalled = true;
				this.doSubscribe = function() {
					hasSubscribed = true;
				};
			}

			objectSubscriber = new ObjectSubscriber();
			observer.subscribe(objectSubscriber, { method: "listSubscribe" });
			assert.isTrue(iHaveBeenCalled, "Subscribe should have instantiated a subscriber object");
			observer.notify();
			assert.isFalse(hasSubscribed, "I should not have been subscribed");
		},
		/* unsubscribe tests */
		/* If there is more than one subscriber (in a group) unsubscribe expects an arg. */
		testObserverUnubscribeNoParamsMultiSubscribers: function() {
			var isSubscribed = false;

			function subscriber() {
				isSubscribed = true;
			}
			function dummySubscriber() { }

			try {
				observer.subscribe(subscriber);
				observer.subscribe(dummySubscriber);
				observer.unsubscribe();  // Call unsubscribe() with no args
				assert.fail(!null, null, "Error expected when unsubscribe called withoput args and more than one subscriber in group");
			} catch (error) {  // expected error
				observer.notify();
			} finally {
				assert.isTrue(isSubscribed, "Notify called and unsubscribe should have failed.");
			}
		},
		/* TODO: this is inconsistent and MUST be fixed. unsubscribe with no args should either always throw an illegal
		 * argument exception (as above) or NEVER (as here). My preference is for the former.*/
		testObserverUnubscribeNoParamsSingleSubscriber: function() {
			var isSubscribed = false;

			function subscriber() {
				isSubscribed = true;
			}

			observer.subscribe(subscriber);
			try {
				observer.unsubscribe();  // Call unsubscribe() with no params does not unsubscribe anything
				observer.notify();  // should reach here as no error thrown.
			} catch (error) {
				assert.fail(error, null, "No error expected when unsubscribe called without params and exactly one subscriber in group");
			} finally {
				assert.isTrue(isSubscribed, "Notify called and unsubscribe should have failed.");
			}
		},
		testObserverUnubscribeNoGroupReturnsSubscriber: function() {
			var theSubscriber = null,
				unsubSubscriber = null;

			function subscriber() {
				return true;
			}

			theSubscriber = observer.subscribe(subscriber);
			unsubSubscriber = observer.unsubscribe(theSubscriber);
			assert.strictEqual(unsubSubscriber, theSubscriber, "unsubscribe() should return the subscriber.");
		},
		testObserverUnubscribeNoGroup: function() {
			var isSubscribed = false;

			function subscriber() {
				isSubscribed = true;
			}

			observer.subscribe(subscriber);
			observer.unsubscribe(subscriber);

			// if we now call notify having unsubscribed then wasSubscribed should not be changed
			observer.notify();
			assert.isFalse(isSubscribed, "notify called after unsubscribe should not change value of isSubscribed");
		},
		testObserverUnubscribeWithGroup: function() {
			var theSubscriber = null;

			function subscriber() {
				return true;
			}

			theSubscriber = observer.subscribe(subscriber, { group: ns });
			assert.strictEqual(observer.unsubscribe(subscriber, ns), theSubscriber, "unsubscribe with group should return the subscriber.");
		},
		testObserverUnsubscribeWithGroupMismatchNotSubscriber: function() {
			/* note: this depends on working observer.filter (see below) */
			var theSubscriber = null;

			function subscriber() {
				return true;
			}

			theSubscriber = observer.subscribe(subscriber, { group: ns });
			assert.notEqual(observer.unsubscribe(subscriber), theSubscriber, "unsubscribe should not return the subscriber if not using the same group.");
		},
		testObserverUnsubscribeWithGroupMismatchIsNull: function() {
			/* note: this depends on working observer.filter (see below) */
			function subscriber() {
				return true;
			}

			observer.subscribe(subscriber, { group: ns });
			assert.isNull(observer.unsubscribe(subscriber), "unsubscribe when subscribe used a different group should return null.");
		},
		testObserverUnsubscribeWithGroupMismatch: function() {
			/* note: this depends on working observer.filter (see below) */
			var isSubscribed = false;

			function subscriber() {
				isSubscribed = true;
			}

			observer.subscribe(subscriber, { group: ns });
			observer.unsubscribe(subscriber); // should be nothing
			observer.setFilter(ns);
			observer.notify();
			assert.isTrue(isSubscribed, "Subscriber should have been notified since have not unsubscribed with the correct group.");
		},
		/* NOTIFY tests */
		testObserverNotify: function() {
			// NOTE: most subscribe/unsubscribe tests rely on a working notify() so there is some double-up.
			// this is same test as testObserverSubscribeSubscriberNeedsArgs() and is here for completeness.
			var wasNotified = false;

			function subscriber(arg1, arg2) {
				if (arg1 === "foo" && arg2 === "bar") {
					wasNotified = true;
				}
			}

			observer.subscribe(subscriber);
			observer.notify("foo", "bar");
			assert.isTrue(wasNotified);
		},
		testObserverNotifyOrderWithMultipleImportantAndNotImportant: function() {
			var idx = 0,
				result = { },
				expected = { 0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5 };

			observer.subscribe(curriedSubscriber(2), { priority: Observer.priority.MED });
			observer.subscribe(curriedSubscriber(5), { priority: Observer.priority.LOW });
			observer.subscribe(curriedSubscriber(0), { priority: Observer.priority.HIGH });
			observer.subscribe(curriedSubscriber(3));
			observer.subscribe(curriedSubscriber(4));
			observer.subscribe(curriedSubscriber(1), { priority: Observer.priority.HIGH });

			observer.notify();
			assert.deepEqual(expected, result);

			function curriedSubscriber(sauce) {
				return function () {
					result[idx++] = sauce;
				};
			}
		},
		/* Check that adding order is honored across groups. */
		testObserverNotifyOrderWithDifferentGroups: function() {
			var idx = 0,
				filter = observer.getGroupAsWildcardFilter("oscar.mike.foxtrot.golf"),
				ns1 = "oscar.mike.foxtrot.golf",
				ns2 = "oscar.mike.*.golf",
				ns3 = "whisky.tango.foxtrot",
				result = { },
				expected = {
					0: 0,
					1: 1,
					2: 3,
					3: 5 };

			function curriedSubscriber(sauce) {
				return function () {
					result[idx++] = sauce;
				};
			}
			/* The try/finally is only here to ensure proper cleanup of the various groups. */
			try {
				observer.subscribe(curriedSubscriber(0), { group: ns1 });
				observer.subscribe(curriedSubscriber(1), { group: ns2 });
				observer.subscribe(curriedSubscriber(2), { group: ns3 });
				observer.subscribe(curriedSubscriber(3), { group: ns2 });
				observer.subscribe(curriedSubscriber(4), { group: ns3 });
				observer.subscribe(curriedSubscriber(5), { group: ns1 });

				observer.setFilter(filter);
				observer.notify().then(function() {
					assert.deepEqual(result, expected);
				});
			} finally {
				assert.deepEqual(result, expected);
				observer.reset(ns1);
				observer.reset(ns2);
				observer.reset(ns3);
			}
		},
		/* This is an advanced and critically important test to ensure that the order in which subscribers are added is
		 * honored even when multiple groups are involved (by means of a wildcard subscriber). AS WELL AS ensuring
		 * subscriber priority is also be honored across groups.
		 *
		 * This is a complex matter and you are probably reading this because you changed observer and screwed it up.
		 * Don't change the test: go back and get it right. */
		testObserverNotifyOrderWithPriorityAndDifferentGroups: function() {
			var idx = 0,
				filter = observer.getGroupAsWildcardFilter("oscar.mike.foxtrot.golf"),
				ns1 = "oscar.mike.foxtrot.golf",
				ns2 = "oscar.mike.*.golf",
				ns3 = "whisky.tango.foxtrot",
				result = { },
				expected = {
					0: 1,
					1: 6,
					2: 3,
					3: 7,
					4: 0,
					5: 5 };

			function curriedSubscriber(sauce) {
				return function () {
					result[idx++] = sauce;
				};
			}
			try {
				observer.subscribe(curriedSubscriber(0), { group: ns1, priority: Observer.priority.LOW });
				observer.subscribe(curriedSubscriber(1), { group: ns2, priority: Observer.priority.HIGH });
				observer.subscribe(curriedSubscriber(2), { group: ns3, priority: Observer.priority.HIGH });
				observer.subscribe(curriedSubscriber(3), { group: ns2 });
				observer.subscribe(curriedSubscriber(4), { group: ns3 });
				observer.subscribe(curriedSubscriber(5), { group: ns1, priority: Observer.priority.LOW });
				observer.subscribe(curriedSubscriber(6), { group: ns1, priority: Observer.priority.HIGH });
				observer.subscribe(curriedSubscriber(7), { group: ns1, priority: Observer.priority.MED });
				observer.setFilter(filter);
				observer.notify().then(function() {
					assert.deepEqual(result, expected, "YOU HAVE BROKEN OBSERVER: GO AND FIX IT! _DO NOT_ CHANGE THIS TEST.");
				});
			} finally {
				assert.deepEqual(result, expected, "YOU HAVE BROKEN OBSERVER: GO AND FIX IT! _DO NOT_ CHANGE THIS TEST.");
				observer.reset(ns1);
				observer.reset(ns2);
				observer.reset(ns3);
			}
		},
		testObserverNotifySubscriberNotFunction: function() {
			// this is an extension of the subscriber test

			var someObject = {
					prop1: function() {
						aBoolean = true;
					}
				},
				aBoolean = false;

			observer.subscribe(someObject);
			observer.notify();
			assert.isFalse(aBoolean, "Notifying an object subscriber should not trigger the functions of that object.");
		},
		testObserverFilterSimple: function() {
			var wasNotified;

			function subscriber() {
				wasNotified = false;
			}

			function filterSubscriber() {
				wasNotified = true;
			}

			observer.subscribe(filterSubscriber, { group: ns });
			// if filter does nothing then the next subscriber will be the last called.
			observer.subscribe(subscriber, { priority: Observer.priority.LOW });

			observer.setFilter(ns);
			observer.notify();
			assert.isTrue(wasNotified, "wasNotified should not have been set by subscriber not matching filter.");
		},
		testObserverFilterWildcard: function() {

			var wasNotified = 0,
				otherNs = "ui.some.other.namespace",
				wildNs = "ui.*.namespace",
				filterFn = observer.getGroupAsWildcardFilter(otherNs);  // assume observer.getGroupAsWildcardFilter works as expected - tested later

			function subscriber() {
				wasNotified += 1;
			}

			function filterSubscriber() {
				wasNotified += 1;
			}

			function wildFilterSubscriber() {
				wasNotified += 1;
			}

			try {
				observer.subscribe(subscriber);
				observer.subscribe(filterSubscriber, { group: otherNs });
				observer.subscribe(wildFilterSubscriber, { group: wildNs });

				// wild card filter should result in both subscriberTwo and subscriberThree being called by notify
				observer.setFilter(filterFn);
				observer.notify();
			} finally {
				assert.strictEqual(wasNotified, 2, "wasNotified should only be incremented by each subscriber matching the wildcarded group name.");
				observer.reset(otherNs);
				observer.reset(wildNs);
			}
		},
		testObserverFilterFunction: function() {
			var wasNotified = 0,
				localns = "ui.some.namespace",
				otherNs = "ui.some.other.namespace",
				thirdNs = "ui.this.namespace";

			// example
			// instead of internal groups we want to filter for base groups in ui.some.*
			var filterFn = function(group) {
				var result = false,
					segments = group.split(".");
				if (segments[0] === "ui" && segments[1] === "some") {
					result = true;
				}
				return result;
			};


			function subscriber() {
				wasNotified += 1;
			}

			function filterSubscriber() {
				wasNotified += 2;
			}

			function wildFilterSubscriber() {
				wasNotified += 4;
			}


			observer.subscribe(subscriber, { group: localns });
			observer.subscribe(filterSubscriber, { group: otherNs });
			observer.subscribe(wildFilterSubscriber, { group: thirdNs });

			observer.setFilter(filterFn);
			observer.notify();

			assert.strictEqual(wasNotified, 3, "wasNotified should be adjusted only by functions in a group matching that set by the filter function");
			observer.reset(localns);
			observer.reset(otherNs);
			observer.reset(thirdNs);
		},
		testObserverFilterFunctionNotBoolean: function() {
			var wasNotified = 0,
				localns = "ui.some.namespace",
				otherNs = "ui.some.other.namespace",
				thirdNs = "ui.this.namespace",
				// instead of internal groups we want to filter for base groups in ui.some.*
				filterFn = function(group) {
					var result = "no",
						segments = group.split(".");
					if (segments[0] === "ui" && segments[1] === "some") {
						result = "yes";
					}
					return result;
				};

			function subscriber() {
				wasNotified += 1;
			}

			function filterSubscriber() {
				wasNotified += 2;
			}

			function wildFilterSubscriber() {
				wasNotified += 4;
			}

			observer.subscribe(subscriber, { group: localns });
			observer.subscribe(filterSubscriber, { group: otherNs });
			observer.subscribe(wildFilterSubscriber, { group: thirdNs });

			observer.setFilter(filterFn);

			// we expect JavaScript truthy/falsey not strictly boolean.
			observer.notify();

			assert.strictEqual(wasNotified, 7, "wasNotified should be set by everything which does not return a falsey value");
			observer.reset(localns);
			observer.reset(otherNs);
			observer.reset(thirdNs);
		},
		testObserverFilterNoParams: function() {
			var wasNotified = 0;

			function subscriber() {
				wasNotified += 1;
			}

			function filterSubscriber() {
				wasNotified += 2;
			}

			observer.subscribe(subscriber);
			observer.subscribe(filterSubscriber, { group: ns });

			try {
				observer.setFilter();  // try to call observer.setFilter with no filter defined should throw an error
			} catch (e) {
				observer.notify();
			} finally {
				assert.strictEqual(wasNotified, 1, "Call to notify should call subscribers in GLOBAL group since no filter was set");
			}
		},
		testObserverFilterNotStringOrFunction: function() {
			var wasNotified = 0;

			function subscriber() {
				wasNotified += 1;
			}

			function filterSubscriber() {
				wasNotified += 2;
			}

			observer.subscribe(subscriber);
			observer.subscribe(filterSubscriber, { group: ns });

			try {
				observer.setFilter(null);
			} catch (e) {
				observer.notify();
			} finally {
				assert.strictEqual(wasNotified, 1, "Call to notify should call subscribers in GLOBAL group since no filter was set");
				observer.reset(ns);
			}
		},
		testSetFilterNotFalsyStringOrFunctionThrowsError: function() {
			try {
				observer.setFilter({});
				assert.isTrue(false, "should have thrown a TypeError");
			} catch (e) {
				assert.strictEqual("arg must be a String or Function", e.message);
			}
		},
		testGetGroupAsWildcardFilter: function() {
			/* What we are testing here is that foo.*.bar returns true from filter "foo.a.b.c....bar" and
			 * "foo.bar.somethingelse" does not. */
			var localns = "ui.*.name.space",
				otherNs = "ui.*.namespace",
				filter = "ui.long.sample.foo.bar.namespace",
				returnedValue = observer.getGroupAsWildcardFilter(filter);

			assert.isFalse(returnedValue(localns));
			assert.isTrue(returnedValue(otherNs));
		},
		testObserverGetGroupAsWildcardFilterNoFilter: function() {
			var hadError = false;

			// attempting to use a null filter throws an error
			try {
				observer.getGroupAsWildcardFilter(null);
			} catch (error) {
				hadError = true;
			} finally {
				assert.isTrue(hadError, "Calling getGroupAsWildcardFilter without a filter should throw an error.");
			}
		},
		testObserverGetGroupAsWildcardFilterNonsenseFilter: function() {
			var filter = function() {
					return true;
				},
				hadError = false;

			try {
				observer.getGroupAsWildcardFilter(filter);  // nonsense filter, even if of the correct "type", will throw an error
			} catch (error) {
				hadError = true;
			} finally {
				assert.isTrue(hadError, "calling getGroupAsWildcardFilter without a valid filter should throw an error");
			}
		},
		testObserverCallback: function() {
			var someCounter = 1;

			function mySubscriber() {
				return 2;
			}

			function callback(result) {
				someCounter = result;
			}

			observer.subscribe(mySubscriber);
			observer.setCallback(callback);
			observer.notify();
			assert.strictEqual(someCounter, 2, "The return value of the subscriber should be the value passed to callback.");
		},
		testObserverCallbackReturnStopsNotify: function() {
			var notified = null;

			function mySubscriber() {
				notified = true;
				return 2;
			}

			function mySubscriber2() {
				notified = false;
				return 0;
			}

			function callback(result) {
				return !!result;
			}

			observer.subscribe(mySubscriber);
			observer.subscribe(mySubscriber2);
			observer.setCallback(callback);
			observer.notify();
			assert.isTrue(notified, "Callback return true should prevent notify from continuing.");
		},
		testObserverCallbackNoFunction: function() {
			var hadError = false;
			observer.subscribe(function () { });
			try {
				observer.setCallback();  // this call should always result in an error
			} catch (error) {
				hadError = true;
			} finally {
				assert.isTrue(hadError);
			}
		},
		testObserverCallbackNotFunctionStillNotifies: function() {
			var wasNotified,
				callback = { };

			function subscriber() {
				wasNotified = true;
			}

			try {
				observer.subscribe(subscriber);
				observer.setCallback(callback);
				observer.notify();
			} finally {
				assert.isTrue(wasNotified, "setCallback without a valid callback function does not break notify.");
			}
		},
		testObserverCallbackNullStillNotifies: function() {
			var wasNotified;

			function subscriber() {
				wasNotified = true;
			}

			try {
				observer.subscribe(subscriber);
				observer.setCallback(null);  // throws an error
			} catch (e) {
				observer.notify();
			} finally {
				assert.isTrue(wasNotified, "setCallback with a null callback function does not break notify.");
			}
		},
		testObserverCallbackWrongFunction: function() {
			var wasNotified = 0;

			function subscriber() {
				wasNotified = 1;
				return true;
			}

			function callback() {
				/* set up a callback function with no formal params then use arguments to get params actually passed*/
				if (arguments[0] !== undefined) {
					wasNotified += 2;
				}
				if (arguments[1] !== undefined) {
					wasNotified += 4;
				}
				if (arguments[2] !== undefined) {
					wasNotified += 8;
				}
			}

			observer.subscribe(subscriber);

			// we know callback can be any function
			// we know notify() calls the callback function passing three params
			// context, subscriber, result
			observer.setCallback(callback);
			observer.notify();
			assert.isTrue(wasNotified > 1, "wasNotified should be changed if any params were passed to callback.");
		},
		/* Reset tests */
		testObserverReset: function() {
			/* this is a modification of two tests we have already seen. We know from above that wasNotified will change if reset does not work as expected */
			var wasNotified = false;

			function subscriber(arg1, arg2) {
				if (arg1 === "foo" && arg2 === "bar") {
					wasNotified = true;
				}
			}
			observer.subscribe(subscriber);

			observer.reset();
			observer.notify("foo", "bar");
			assert.isFalse(wasNotified);
		},
		testObserverResetWithGroup: function() {
			var wasNotified = false;

			function subscriber() {
				wasNotified = true;
			}
			observer.subscribe(subscriber, { group: ns });

			observer.reset(ns);
			observer.setFilter(ns);
			observer.notify();
			assert.isFalse(wasNotified);
		},
		testObserverResetGlobalNotGroup: function() {
			var wasNotified = 0;

			function subscriber() {
				wasNotified = 1;
			}
			function subscriber2() {
				wasNotified = 2;
			}
			observer.subscribe(subscriber, { group: ns });
			observer.subscribe(subscriber2);

			observer.reset();
			observer.notify();  // no filter so notify global
			observer.setFilter(ns);
			observer.notify();  // group not resert
			assert.strictEqual(wasNotified, 1);
		},
		testObserverResetGroupNotGlobal: function() {
			var wasNotified = 0;

			function subscriber() {
				wasNotified = 1;
			}
			function subscriber2() {
				wasNotified = 2;
			}
			observer.subscribe(subscriber, { group: ns });
			observer.subscribe(subscriber2);

			observer.reset(ns);
			observer.notify();  // no filter so notify global
			observer.setFilter(ns);
			observer.notify();
			assert.strictEqual(wasNotified, 2);
		}
	});

	function getRandomInt(min, max) {
		min = Math.ceil(min);
		max = Math.floor(max);
		return Math.floor(Math.random() * (max - min)) + min;
	}
});
