rio.Spec = {
	executeSpec: function(spec, stdout, env, context, specPage, doEval) {
		try {
			var stubManager = new env.rio.StubManager();
			var mockManager = new env.rio.MockManager();
			
			var stub = function(obj, method) {
				return new env.rio.Stub(obj, method);
			};
			var stubs = [
				stub(env, "specPage").withValue(specPage),
				stub(env, "describe").withValue(
					env.rio.Spec.describe.curry(env.rio.Spec, stdout, [], [], context, env.specPage, stubManager, mockManager)
				),
				stub(env, "stub").withValue(stubManager.stub.bind(stubManager)),
				stub(env, "pending").withValue("PENDING"),
				stub(env, "fail").withValue(
					function(msg) {
						throw msg;
					}
				),
				stub(env, "insertComponent").withValue(
					env.specPage.addComponent.bind(env.specPage)
				),
				stub(env.Function.prototype, "active").withValue(
					function() {
						this._activeSpec = true;
						return this;
					}
				),
				stub(Function.prototype, "active").withValue(
					function() {
						this._activeSpec = true;
						return this;
					}
				),
				stub(env, "shouldBeDefined").withValue(
					function(val) {
						rio.Assertions.shouldNot(val == undefined, "expected '" + val + "' to be defined.");
					}
				),
				stub(env, "shouldBeUndefined").withValue(
					function(val) {
						rio.Assertions.should(val == undefined, "expected '" + val + "' to be undefined.");
					}
				),
				stub(env, "shouldEqual").withValue(
					function(val1, val2) {
						rio.Assertions.should(val1 == val2, "expected '" + val1 + "' to equal '" + val2 + "'");
					}
				),
				stub(env.rio, "mockManager").withValue(mockManager),
				stub(rio, "mockManager").withValue(mockManager)
			];

			var instrumentModel = function(model) {
				stubs.push(stub(model, "_idCache").withValue({}));
				stubs.push(stub(model, "_identityCache").withValue({}));
				stubs.push(stub(model, "_collectionEntities").withValue({}));
				stubs.push(stub(model, "_transaction").withValue([]));
				stubs.push(stub(model, "_transactionQueued").withValue(false));
				stubs.push(stub(model, "__transactionInProgress").withValue(false));
				stubs.push(stub(model, "prepareTransaction").withValue(function() {
					this.executeTransaction(rio.Undo.isProcessingUndo(), rio.Undo.isProcessingRedo());
				}.bind(model)));
				
				var newInitialize = model.prototype.initialize.wrap(function(proceed) {
					this.__example = true;
					proceed.apply(this, $A(arguments).slice(1));
				});
				stubs.push(stub(model.prototype, "initialize").withValue(newInitialize));
				
				var newExample = model.example.wrap(function(proceed, exampleName) {
					var json = this._examples[exampleName];
					if (json && this.id) {
						var id = json.id;
						var fromCache = this.getFromCache(this.id(id));
						if (fromCache) {
							return fromCache;
						}
					}
					return proceed.apply(this, $A(arguments).slice(1));
				});
				stubs.push(stub(model, "example").withValue(newExample))
			};
			for (modelName in env.rio.models) {
				instrumentModel(env.rio.models[modelName]);
			}
			stubs.push(stub(env, "instrumentModel").withValue(instrumentModel));
			
			var newRequest = env.Ajax.Request.prototype.request.wrap(function(proceed, url) {
				var resourceUrl = url.match(/^(\/[^\/]*).*$/)[1];
				var model = Object.values(env.rio.models).detect(function(m) {
					return m.url && (m.url() == resourceUrl);
				});
				
				if (model) {
					if (this.options.method == "delete") {
						this.options.onSuccess({});
						return;
					}
					if (this.options.method == "put") {
						return;
					}

					var id = url.match(/^\/[^\/]*\/(.*)$/);
					
					var jsonFromParams = function(json) {
						if (env.rio.environment.includeRootInJson) {
							var newJSON = {};
							newJSON[model.NAME.underscore()] = json;
							json = newJSON;
						}
						return json;
					};
					
					var json;
					if (id) {
						if (!id[1].isNumeric()) {
							return;
						}
						id = id[1];
						var foundName = Object.keys(model._examples).detect(function(exampleName) {
							var exampleJson = model._examples[exampleName];
							return exampleJson.id == id;
						});
						if (!foundName) {
							(this.options.onFailure || Prototype.emptyFunction)();
							return;
						}
						json = jsonFromParams(model.exampleParams(foundName));
					} else if (this.options.method == "get") {
						var parameters = this.options.parameters.conditions.evalJSON();
						
						json = Object.keys(model._examples).select(function(exampleName) {
							var exampleJson = model.exampleParams(exampleName);
							
							return Object.keys(parameters).all(function(parameter) {
								var val = parameters[parameter];
								var jsonVal = exampleJson[parameter.underscore()] || exampleJson[parameter.camelize()];
								if (val && val.not) {
									return jsonVal != val.not;
								} else {
									return jsonVal == val;
								}
							});
							
						}).map(function(exampleName) {
							var exampleJson = model.exampleParams(exampleName);
							var exampleId = exampleJson.id;

							return jsonFromParams(exampleJson);
						});
					} else {
						/*
							This is a very limited handling of 'POST' requests.

							-It doesn't handle transactions
							-It has no tests
						*/
						var newId = Object.values(model._identityCache).min() - 1;
						newId = isNaN(newId) ? -1 : newId;
						json = jsonFromParams({ id: newId });
					}
					this.options.onSuccess({ responseJSON: json, status: 200 });
				} else {
					return proceed(url);
				}
			});
			stubs.push(stub(env.Ajax.Request.prototype, "request").withValue(newRequest));
			
			stubs.push(stub(env, "preloadFixtures").withValue(function() {
				var callbackQueue = [];
				var requestStubMethod = env.Ajax.Request.prototype.request.wrap(function(proceed) {
					var oldOnSuccess = this.options.onSuccess;
					this.options.onSuccess = function() {
						var successArgs = arguments;
						callbackQueue.push(function() {
							oldOnSuccess.apply(this, successArgs);
						});
					};
					return proceed.apply(this, $A(arguments).slice(1));
				});
				var requestStub = stub(Ajax.Request.prototype, "request").withValue(requestStubMethod);
				for (modelName in env.rio.models) {
					var model = env.rio.models[modelName];
					Object.keys(model._examples).each(function(exampleName) {
						model.example(exampleName);
					});
				}
				callbackQueue.each(function(callback) { callback(); });
				requestStub.release();
			}));
			
			stubs.push(stub(env, "render").withValue(function(page) {
				env.rio.app.clearPage();
				env.Element.body().insert(page.html());
				page.render();
				env.rio.app.setCurrentPage(page);
			}));
			
			doEval(spec);
		} catch(e) {
			stdout(e, "warningLogItem");
			// Ignore spec parsing errors. 
		} finally {
			stubs.invoke("release");
		}
	},
	
	describe: function(that, stdout, befores, afters, context, specPage, stubManager, mockManager, cls, content) {
		try {
			var findActiveSpec = function(desc) {
				var activeSpec;
				Object.keys(desc).each(function(k) {
					var v = desc[k];
					if (v == "PENDING") { return; }
					var vActiveSpec = (v._activeSpec && k) || (!Object.isFunction(v) && findActiveSpec(v));
					if (vActiveSpec) {
						activeSpec = vActiveSpec;
					}
				});
				return activeSpec;
			};
			var activeSpec = findActiveSpec(content);
			if (activeSpec) {
				
				var removeNonActiveSpecs = function(desc) {
					Object.keys(desc).each(function(k) {
						var v = desc[k];
						if (v == "PENDING") {
							delete desc[k];
						} else if (Object.isFunction(v)) {
							if (!v._activeSpec && !(k == "beforeEach" || k == "afterEach")) {
								delete desc[k];
							}
						} else {
							removeNonActiveSpecs(v);
						}
					});
				};
				
				removeNonActiveSpecs(content);
			}
		} catch(e) {
			stdout(e, "warningLogItem");
		}
		
		befores = befores.clone();
		if (content.beforeEach) {
			befores.push(content.beforeEach);
		}
		afters = afters.clone();
		if (content.afterEach) {
			afters.unshift(content.afterEach);
		}
		
		if (content.builder) {
			var extraSpecs = content.builder();
			for (extraSpecName in extraSpecs) {
				content[extraSpecName] = extraSpecs[extraSpecName];
			}
		}
		
		Object.keys(content).without("beforeEach").without("afterEach").without("builder").each(function(k) {
			var specName = cls.toString() + " " + k;
			arg = content[k];
			
			
			if (Object.isFunction(content[k]) || content[k].constructor == Function) {
				var specRun = new Object();
				
				(function() {
					try {
						// =============================================================
						// = BEGIN TEST OVERRIDES TO ACCOUNT FOR DEFER NON-DETERMINISM =
						// =============================================================
						stub(rio.Undo, "_doAfterAction").andDo(function() { rio.Undo.afterAction() });
						// =============================================================
						// = END TEST OVERRIDES TO ACCOUNT FOR DEFER NON-DETERMINISM   =
						// =============================================================
						context.examples++;
						
						for (modelName in rio.models) {
							var model = rio.models[modelName];
							model._idCache = {};
							model._identityCache = {};
							model._collectionEntities = {};
							model._transaction = [];
							model._transactionQueued = false;
							model.__transactionInProgress = false;
						}
						
						// preloadFixtures();
						befores.each(function(beforeEach) { beforeEach.bind(this)() }.bind(this));
						content[k].bind(this)();
						mockManager.verifyAll();
						stdout("- " + specName, "passed");
					} catch(e) {
						context.failures++;
						stdout("- " + specName + "\n" + " - (" + (e.message || e) + ") " + (e.detail || ""), "failed");
					} finally {
						try {
							for (var i=0; i<afters.length; i++) {
								var afterEach = afters[i];
								afterEach.bind(this)();
							}
						} finally {
							try {
								mockManager.releaseAll();
								stubManager.releaseAll();
							} catch(e) {
								stdout("Failed to release mocks and stubs", "warningLogItem");
							} finally {
								specPage.reset();
							}
						}
					}
				}.bind(specRun))();
			} else if (Object.isString(content[k]) && content[k] == "PENDING") {
				stdout("- " + specName, "pending");
				context.examples++;
				context.pendings++;
			} else {
				that.describe(that, stdout, befores, afters, context, specPage, stubManager, mockManager, specName, content[k]);
			}
		});
	},

	toString: function() { return "Spec"; }
};

rio.Assertions = {
	shouldEqual: function(val) {
		this.should(val == this, "expected '" + val + "', got '" + this + "'");
	},

	shouldNotEqual: function(val) {
		this.should(val != this, "expected not '" + val + "', got '" + this + "'");
	},
	
	shouldBeTrue: function() {
		var truthy = this.valueOf() ? true : false;
		this.should(truthy, "expected: 'true', got '" + this + "'");
	},

	shouldNotBeTrue: function() {
		var truthy = this.valueOf() ? true : false;
		this.shouldNot(truthy, "expected: 'false', got '" + this + "'");
	},
	
	shouldBeFalse: function() {
		this.shouldNotBeTrue();
	},
	
	should: function(result, msg) {
		if (!result) {
			throw msg;
		};
	},
	
	shouldNot: function(result, msg) {
		if (result) {
			throw msg;
		};
	}
};

Object.extend(rio.Id.prototype, rio.Assertions);
Object.extend(rio.Binding, rio.Assertions);

Object.extend(String.prototype, rio.Assertions);
Object.extend(Boolean.prototype, rio.Assertions);
Object.extend(Number.prototype, rio.Assertions);
Object.extend(Function.prototype, rio.Assertions);
Object.extend(Array.prototype, rio.Assertions);
Object.extend(Date.prototype, rio.Assertions);

Object.extend(String.prototype, {
	shouldStartWith: function(val) {
		this.should(this.startsWith(val), "expected '" + this + "' to start with '" + val + "'")
	},

	shouldMatch: function(val) {
		this.should(this.match(val), "expected '" + this + "' to match '" + val + "'");
	}
});

Object.extend(Function.prototype, {
	shouldBeCalled: function() {
		var count = 0;
		var wrapped = this.wrap(function(proceed) {
			count++;
			return proceed.apply(this, $A(arguments).slice(1));
		});
		wrapped.expectation = function() {
			return count > 0;
		};
		var expectation = function() {
			return wrapped.expectation();
		};

		var original = this;
		expectation.message = "\n\n" + original.toString() + "\n\n# should be called";
		wrapped.times = function(times) {
			wrapped.expectation = function() {
				return count == times;
			};
			expectation.message = "\n\n" + original.toString() + "\n\n# should be called " + times + " times.";
			return wrapped;
		};
		wrapped.once = wrapped.times.curry(1);
		rio.mockManager.expect(expectation);
		return wrapped;
	},

	shouldNotBeCalled: function() {
		var wrapped = this.wrap(function(proceed) {
			wrapped._called = true;
			proceed.apply(this, $A(arguments).slice(1));
		});
		var expectation = function() {
			return !wrapped._called;
		};
		expectation.message = "\n\n" + this.toString() + "\n\n# should not be called";
		rio.mockManager.expect(expectation);
		return wrapped;
	}
});

Object.extend(Array.prototype, {
	shouldEqual: function(val) {
		var equal = true;
		if (val.size != undefined && val.length == this.length) {
			for (var i = 0; i < this.length; i++) {
				equal = equal && this[i] == val[i];
			}
		} else {
			equal = false;
		}
		this.should(equal, "expected '" + val + "', got '" + this + "'");
	},
	
	shouldInclude: function(val) {
		this.should(this.include(val), "expected " + this + " to include " + val);
	},
	
	shouldNotInclude: function(val) {
		this.shouldNot(this.include(val), "expected " + this + " not to include " + val);
	},
	
	shouldBeEmpty: function() {
		this.should(this.empty(), "expected " + this + " to be empty");
	},

	shouldNotBeEmpty: function() {
		this.shouldNot(this.empty(), "expected " + this + " to not be empty");
	}
});

Object.extend(Date.prototype, {
	shouldEqual: function(val) {
		this.should(this.getTime() == val.getTime(), "expected " + this + " to equal " + val);
	},

	shouldBeBefore: function(val) {
		this.should(this < val, "expected " + this + " to come before " + val);
	},

	shouldBeAfter: function(val) {
		this.should(this > val, "expected " + this + " to come after " + val);
	},

	shouldBeBetween: function(val, val2) {
		this.should(this > val && this < val2, "expected " + this + " to be between " + val + " and " + val2);
	},

	shouldBeWithinOneSecondOfNow: function(val, val2) {
		this.shouldBeBetween(new Date(new Date().getTime() - 1000), new Date(new Date().getTime() + 1000))
	}
});

rio.Stub = Class.create({
	initialize: function(object, method) {
		this.object = object;
		this.method = method;
		this.oldMethod = this.object[this.method];
		
		this.object[this.method] = function() {};
	},

	release: function() {
		if (this.oldMethod == undefined) {
			delete this.object[this.method];
		} else {
			this.object[this.method] = this.oldMethod;
		}
	},

	andReturn: function(value) {
		this.value = value;
		this.object[this.method] = function() {
			return value;
		}
	},

	andDo: function(func) {
		this.andDo = func;
		this.object[this.method] = function() {
			return func.apply(this, arguments);
		}
	},
	
	withValue: function(value) {
		this.object[this.method] = value;
		return this;
	},

	shouldBeCalled: function() {
		this.andDo(function() {}.shouldBeCalled());
	},

	shouldNotBeCalled: function() {
		this.andDo(function() {}.shouldNotBeCalled());
	}
});

rio.StubManager = Class.create({
	initialize: function() {
		this.stubs = [];
	},

	releaseAll: function() {
		for (var i=0; i<this.stubs.length; i++) {
			var stub = this.stubs[i];
			stub.release();
		}
		this.stubs.clear();
	},

	stub: function(obj, method) {
		var stub = new rio.Stub(obj, method);
		this.stubs.unshift(stub);
		return stub;
	}
});

rio.MockManager = Class.create({
	initialize: function() {
		this.expectations = [];
	},
	expect: function(expectation) {
		this.expectations.push(expectation);
	},
	verifyAll: function() {
		this.expectations.each(function(expectation) {
			if (!expectation()) {
				throw({
					message: "expectation failed",
					detail: expectation.message
				});
			}
		});
	},
	releaseAll: function() {
		this.expectations.clear();
	}
});