/*jshint node:true */
/*global describe, it, before, after, beforeEach, afterEach */

"use strict";

var mongoose = require("mongoose"),
	assert = require("assert"),
	yarm = require("../index.js"),
	common = require("./common"),

	request = common.request,
	callbackTests = common.callbackTests,
	allMethods = common.allMethods,
	composeTests = common.composeTests;



function assertJSON(json) {
	var data;
	try {
		data = JSON.parse(json);
	} catch(e) {
		assert.strictEqual(json, "[valid json]");
	}

	return data;
}

function assertReturnedDoc(body, doc) {
	var returned = assertJSON(body);

	// Remove properties added by mongoose
	delete returned.__v;
	delete returned._id;

	if (returned.docArray) {
		returned.docArray.forEach(function(subdoc) {
			delete subdoc._id;
		});
	}

	assert.deepEqual(returned, doc);
}


function assertEmpty(body) {
	if (body && body.length > 0) {
		assert.strictEqual(body, "[empty body]");
	}
}

function assertCreated(res, body) {
	assert.strictEqual(res.statusCode, 201);
	assert.strictEqual(body, "Created");
}


/*!
 * Test data
 */


var testSchema = new mongoose.Schema({
	field1: String,
	field2: String,
	subDoc: {
		field: String
	},
	docArray: [{
		field: String,
		sub: {
			field: String
		}
	}],
	array: [String],
	"url encoded/property": String
});

testSchema.virtual("description").get(function() {
	return "Document " + this.field1 + " with " + this.docArray.length + " sub-documents";
});

var TestModel = mongoose.model("test", testSchema);

/* Empty docArrays are not mandatory, but mongoose adds them anyway so
   having them here makes comparison easier */
var testData = [
	{ field1: "foo", docArray: [], array: [] },
	{ field1: "bar", field2: "baz", docArray: [], array: [] },
	{ field1: "sub", subDoc: { field: "foo" }, docArray: [], array: ["foo", "bar"] },
	{ field1: "arr", docArray: [{ field: "foo" }, { field: "bar" }, { field: "baz", sub: { field: "sub" } }], array: [] },
	{ field1: "urldecode", docArray: [], array: [], "url encoded/property": "foo" }
];



/*!
 * Test helpers
 */


/* Resource definition helpers */
function mongooseResource(name, model) {
	yarm.remove(name);
	return yarm.mongoose(name, model);
}

function aggregateResource(name, model, pipeline, options) {
	yarm.remove(name);
	return yarm.aggregate(name, model, pipeline, options);
}


/* Collection result checking helper */
function assertCollection(res, body, field1values) {
	var data = assertJSON(body);

	// Basic response check
	assert.strictEqual(res.statusCode, 200);
	assert.strictEqual(typeof data, "object");
	assert.strictEqual(data._count, field1values.length);
	assert(Array.isArray(data._items));
	assert.strictEqual(data._items.length, field1values.length);

	// Find expected objects in testData
	var expected = {};
	testData.forEach(function(doc) {
		if (field1values.indexOf(doc.field1) !== -1) {
			expected[doc.field1] = doc;
		}
	});

	// Check that all items are expected
	data._items.forEach(function(doc) {
		assert(doc.field1 in expected);

		// Cleanup fields before comparing
		delete doc.__v;
		delete doc._href;
		if (doc.subDoc) {
			delete doc.subDoc._href;
		}
		doc.docArray.forEach(function(subdoc) {
			delete subdoc._href;
			if (subdoc.sub) {
				delete subdoc.sub._href;
			}
		});

		assert.deepEqual(doc, expected[doc.field1]);
	});
}


/* DocumentArray collection result checking helper */
function assertDocArrayCollection(res, body, fieldvalues) {
	var docArray = testData[3].docArray;

	var data = assertJSON(body);

	// Basic response check
	assert.strictEqual(res.statusCode, 200);
	assert.strictEqual(typeof data, "object");
	assert.strictEqual(data._count, fieldvalues.length);
	assert(Array.isArray(data._items));
	assert.strictEqual(data._items.length, fieldvalues.length);


	// Find expected objects in testData
	var expected = {};
	docArray.forEach(function(doc) {
		if (fieldvalues.indexOf(doc.field) !== -1) {
			expected[doc.field] = doc;
		}
	});

	// Check that all items are expected
	data._items.forEach(function(doc) {
		assert(doc.field in expected);

		// Cleanup fields before comparing
		delete doc.__v;
		delete doc._href;

		assert.deepEqual(doc, expected[doc.field]);
	});
}


/*!
 * Test definitions
 */


describe("Mongoose resources", function() {
	// Connect once before all tests
	before(function(done) {
		mongoose.connect("mongodb://localhost/yarmTest", function(err) {
			done(err);
		});
	});


	// Drop database and disconnect once after all tests
	after(function(done) {
		mongoose.connection.db.dropDatabase(function(err) {
			if (err) {
				done(err);
			} else {
				mongoose.disconnect(function(err) {
					done(err);
				});
			}
		});
	});


	// Create data before each test
	beforeEach(function(done) {
		var copy = [];

		function saveNext() {
			var data = testData.shift();

			if (!data) {
				testData = copy;
				done();
				return;
			}

			copy.push(data);

			var doc = new TestModel(data);
			doc.save(function(err) {
				if (err) {
					done(err);
				} else {
					TestModel.findOne({ field1: doc.field1 }, function(err, found) {
						if (err) {
							done(err);
						} else if (!found) {
							done(new Error("Previousy saved document was not found again"));
						} else {
							/* Grab saved _ids */
							data._id = found._id.toString();

							if (data.docArray) {
								data.docArray.forEach(function(sub, index) {
									sub._id = found.docArray[index]._id.toString();
								});
							}

							saveNext();
						}
					});
				}
			});
		}

		saveNext();
	});


	// Drop data after each test
	afterEach(function(done) {
		TestModel.remove(function(err) {
			done(err);
		});
	});


	describe("Model resources", function() {
		describe("Model collections", function() {
			it("should GET collections", function(done) {
				mongooseResource("test", TestModel);

				request.get("/test", function(res, body) {
					assertCollection(res, body, ["foo", "bar", "sub", "arr", "urldecode"]);
					done();
				});
			});

			function queryTest(query, expected, done) {
				mongooseResource("test", TestModel);

				request.get("/test?query=" + encodeURIComponent(query), function(res, body) {
					assertCollection(res, body, expected);
					done();
				});
			}

			it(
				"should compare fields when query has field:value",
				queryTest.bind(null, "field1:foo", ["foo"])
			);

			it(
				"should regex-compare fields when query has field:/regexp/",
				queryTest.bind(null, "field1:/a/", ["bar", "arr"])
			);

			it(
				"should regex-compare with flags",
				queryTest.bind(null, "field1:/A/i", ["bar", "arr"])
			);

			it(
				"should negate comparisons with field!value",
				queryTest.bind(null, "field1!foo", ["bar", "arr", "sub", "urldecode"])
			);

			it(
				"should negate regex-comparisons with field!/regexp/",
				queryTest.bind(null, "field1!/a/", ["foo", "sub", "urldecode"])
			);

			it(
				"should allow queries on sub-fields",
				queryTest.bind(null, "subDoc.field:foo", ["sub"])
			);

			it(
				"should allow queries with AND operators",
				queryTest.bind(null, "field1:/a/ AND field2:baz", ["bar"])
			);

			it(
				"should allow queries with OR operators",
				queryTest.bind(null, "field1:/o/ OR field2:/a/", ["foo", "bar", "urldecode"])
			);

			it(
				"should allow queries with both AND and OR operators",
				queryTest.bind(null, "field1:/o/ OR field1:/a/ AND field2:/a/", ["foo", "bar", "urldecode"])
			);

			it("should POST new documents to collections", function(done) {
				mongooseResource("test", TestModel);
				var doc = {
					field1: "add",
					field2: "hello",
					subDoc: {
						field: "world"
					},
					docArray: [
						{ field: "a" },
						{ field: "b" }
					],
					array: []
				};

				request.post("/test", doc, function(res, body) {
					assertCreated(res, body);

					// Check addition to mongoose collection first
					TestModel.findOne({ field1: "add" }, function(err, item) {
						assert.ifError(err);
						assert(item);

						done();
					});
				});
			});

			it("should return POSTed documents when postResponse is true", function(done) {
				mongooseResource("test", TestModel).set("postResponse", true);
				var doc = {
					field1: "add",
					field2: "hello",
					subDoc: {
						field: "world"
					},
					docArray: [
						{ field: "a" },
						{ field: "b" }
					],
					array: [ "x", "y" ]
				};

				request.post("/test", doc, function(res, body) {
					assert.strictEqual(res.statusCode, 200);

					assertReturnedDoc(body, doc);
					done();
				});
			});

			it("should allow sorting collections", function(done) {
				mongooseResource("test", TestModel);

				request.get("/test?sort=field1", function(res, body) {
					var data = assertJSON(body);
					var docs = data._items;

					assert.strictEqual(docs[0].field1, "arr");
					assert.strictEqual(docs[1].field1, "bar");
					assert.strictEqual(docs[2].field1, "foo");
					assert.strictEqual(docs[3].field1, "sub");
					done();
				});
			});

			it("should allow setting the default sort with an option", function(done) {
				mongooseResource("test", TestModel)
					.set("sort", { field1: "asc" });

				request.get("/test", function(res, body) {
					var data = assertJSON(body);
					var docs = data._items;

					assert.strictEqual(docs[0].field1, "arr");
					assert.strictEqual(docs[1].field1, "bar");
					assert.strictEqual(docs[2].field1, "foo");
					assert.strictEqual(docs[3].field1, "sub");
					done();
				});
			});
		});

		describe("Documents", function() {
			it(
				"should GET documents in collection",
				composeTests(testData.map(function(item) {
					return function(done) {
						mongooseResource("test", TestModel);

						request.get("/test/" + item._id, function(res, body) {
							var doc = assertJSON(body);

							assert.strictEqual(res.statusCode, 200);
							assert.strictEqual(typeof doc, "object");

							/* Remove additional properties before comparing */
							delete doc.__v;
							delete doc._href;
							if (doc.docArray) {
								doc.docArray.forEach(function(sub) {
									delete sub._href;
								});
							}

							assert.deepEqual(doc, item);

							done();
						});
					};
				}))
			);

			it(
				"should allow setting mongoose toObject options",
				composeTests(testData.map(function(item) {
					return function(done) {
						mongooseResource("test", TestModel)
							.set("toObject", { virtuals: true });

						request.get("/test/" + item._id, function(res, body) {
							var doc = assertJSON(body);

							assert.strictEqual(res.statusCode, 200);
							assert.strictEqual(typeof doc, "object");

							/* Remove additional properties before comparing */
							delete doc.__v;
							delete doc._href;
							if (doc.docArray) {
								doc.docArray.forEach(function(sub) {
									delete sub._href;
								});
							}

							assert.strictEqual(
								doc.description,
								"Document " + item.field1 + " with " + item.docArray.length + " sub-documents"
							);

							done();
						});
					};
				}))
			);

			it("should 404 on nonexistent documents", function(done) {
				mongooseResource("test", TestModel);

				request.get("/test/nonexistent", function(res, body) {
					assert.strictEqual(res.statusCode, 404);
					assert.strictEqual(body, "Not found");

					done();
				});
			});

			it(
				"should allow specifying an alternate primary key",
				composeTests(testData.map(function(item) {
					return function(done) {
						mongooseResource("test", TestModel)
							.set("key", "field1");

						request.get("/test/" + item.field1, function(res, body) {
							var doc = assertJSON(body);

							assert.strictEqual(res.statusCode, 200);
							assert.strictEqual(typeof doc, "object");

							/* Remove additional properties before comparing */
							delete doc.__v;
							delete doc._href;
							if (doc.docArray) {
								doc.docArray.forEach(function(sub) {
									delete sub._href;
								});
							}

							assert.deepEqual(doc, item);

							done();
						});
					};
				}))
			);

			it("should DELETE documents", function(done) {
				var item = testData[0];
				mongooseResource("test", TestModel);

				request.del("/test/" + item._id, function(res, body) {
					assertEmpty(body);
					assert.strictEqual(res.statusCode, 204);

					TestModel.find({ _id: item._id }, function(err, items) {
						assert.ifError(err);
						assert.strictEqual(items.length, 0);
						done();
					});
				});
			});

			it("should PUT documents", function(done) {
				var item = testData[0];
				mongooseResource("test", TestModel);

				request.put("/test/" + item._id, { field2: "bar" }, function(res, body) {
					assertEmpty(body);
					assert.strictEqual(res.statusCode, 204);

					TestModel.findById(item._id, function(err, doc) {
						assert.ifError(err);
						assert.strictEqual(doc.field1, "foo");
						assert.strictEqual(doc.field2, "bar");
						done();
					});
				});
			});
		});

		describe("Document fields", function() {
			it(
				"should GET fields from documents",
				composeTests(testData.map(function(item) {
					return function(done) {
						mongooseResource("test", TestModel);

						request.get("/test/" + item._id + "/field1", function(res, body) {
							assert.strictEqual(res.statusCode, 200);
							assert.strictEqual(body, item.field1);

							done();
						});
					};
				}))
			);

			it("should URLdecode field names", function(done) {
				mongooseResource("test", TestModel);

				request.get("/test/" + testData[4]._id + "/url%20encoded%2Fproperty", function(res, body) {
					assert.strictEqual(res.statusCode, 200);
					assert.strictEqual(body, "foo");

					done();
				});
			});

			it("should 404 on nonexistent document fields", function(done) {
				mongooseResource("test", TestModel);

				request.get("/test/" + testData[0]._id + "/nonexistent", function(res, body) {
					assert.strictEqual(res.statusCode, 404);
					assert.strictEqual(body, "Not found");

					done();
				});
			});

			it(
				"should DELETE field values in documents",
				composeTests(testData.map(function(item) {
					return function(done) {
						mongooseResource("test", TestModel);

						request.del("/test/" + item._id + "/field1", function(res, body) {
							assertEmpty(body);
							assert.strictEqual(res.statusCode, 204);

							TestModel.findById(item._id, function(err, doc) {
								assert.strictEqual(doc.field1, undefined);
								done();
							});
						});
					};
				}))
			);

			it(
				"should PUT field values in documents",
				composeTests(testData.map(function(item) {
					return function(done) {
						mongooseResource("test", TestModel);

						request.put("/test/" + item._id + "/field1", { _value: "newValue" }, function(res, body) {
							assertEmpty(body);
							assert.strictEqual(res.statusCode, 204);

							TestModel.findById(item._id, function(err, doc) {
								assert.ifError(err);
								assert.strictEqual(doc.field1, "newValue");
								done();
							});
						});
					};
				}))
			);

			it(
				"should POST to array fields in documents",
				composeTests(testData.map(function(item) {
					return function(done) {
						mongooseResource("test", TestModel);

						request.post("/test/" + item._id + "/array", { _value: "baz" }, function(res, body) {
							assert.strictEqual(body, "Created");
							assert.strictEqual(res.statusCode, 201);

							TestModel.findById(item._id, function(err, doc) {
								assert.ifError(err);
								assert.notStrictEqual(doc.array.indexOf("baz"), -1);
								done();
							});
						});
					};
				}))
			);

			it("should DELETE array field items in documents", function(done) {
				mongooseResource("test", TestModel);

				var item = testData.filter(function(item) {
					return item.field1 === "sub";
				})[0];

				request.del("/test/" + item._id + "/array/0", function(res, body) {
					assertEmpty(body);
					assert.strictEqual(res.statusCode, 204);

					TestModel.findById(item._id, function(err, doc) {
						assert.ifError(err);
						assert.strictEqual(doc.array.indexOf("foo"), -1);
						done();
					});
				});
			});
		});

		describe("Subdocuments", function() {
			it("should GET subdocuments", function(done) {
				var item = testData[2];
				mongooseResource("test", TestModel);

				request.get("/test/" + item._id + "/subDoc", function(res, body) {
					var doc = assertJSON(body);

					assert.strictEqual(res.statusCode, 200);
					assert.strictEqual(typeof doc, "object");

					delete doc._href;
					assert.deepEqual(doc, item.subDoc);

					done();
				});
			});

			it("should DELETE subdocuments", function(done) {
				var item = testData[2];
				mongooseResource("test", TestModel);

				request.del("/test/" + item._id + "/subDoc", function(res, body) {
					assertEmpty(body);
					assert.strictEqual(res.statusCode, 204);

					TestModel.findById(item._id, function(err, doc) {
						assert.ifError(err);

						// Mongoose limitation: subDoc is still present but empty
						assert.strictEqual(doc.subDoc.field, undefined);
						done();
					});
				});
			});

			it("should PUT subdocuments", function(done) {
				var item = testData[2];
				mongooseResource("test", TestModel);

				request.put("/test/" + item._id + "/subDoc", { _value: { field: "bar" } }, function(res, body) {
					assertEmpty(body);
					assert.strictEqual(res.statusCode, 204);

					TestModel.findById(item._id, function(err, doc) {
						assert.ifError(err);
						assert.strictEqual(doc.subDoc.field, "bar");

						done();
					});
				});
			});
		});

		describe("Subdocument fields", function() {
			it("should GET fields in subdocuments", function(done) {
				var item = testData[2];
				mongooseResource("test", TestModel);

				request.get("/test/" + item._id + "/subDoc/field", function(res, body) {
					assert.strictEqual(res.statusCode, 200);
					assert.deepEqual(body, item.subDoc.field);

					done();
				});
			});

			it("should DELETE fields in subdocuments", function(done) {
				var item = testData[2];
				mongooseResource("test", TestModel);

				request.del("/test/" + item._id + "/subDoc/field", function(res, body) {
					assertEmpty(body);
					assert.strictEqual(res.statusCode, 204);

					TestModel.findById(item._id, function(err, doc) {
						assert.ifError(err);
						assert.strictEqual(doc.subDoc.field, undefined);

						done();
					});
				});
			});


			it("should PUT field values in subdocuments", function(done) {
				var item = testData[2];
				mongooseResource("test", TestModel);

				request.put("/test/" + item._id + "/subDoc/field", { _value: "newValue" }, function(res, body) {
					assertEmpty(body);
					assert.strictEqual(res.statusCode, 204);

					TestModel.findById(item._id, function(err, doc) {
						assert.ifError(err);
						assert.strictEqual(doc.subDoc.field, "newValue");
						done();
					});
				});
			});
		});

		describe("DocumentArray collections", function() {
			it("should GET DocumentArrays as collections", function(done) {
				var item = testData[3];
				mongooseResource("test", TestModel);

				request.get("/test/" + item._id + "/docArray", function(res, body) {
					var data = assertJSON(body);

					assert.strictEqual(res.statusCode, 200);
					assert.strictEqual(typeof data, "object");
					assert.strictEqual(data._count, item.docArray.length);
					assert(Array.isArray(data._items));
					assert.strictEqual(data._items.length, item.docArray.length);

					item.docArray.forEach(function(doc) {
						var found = false;

						data._items.forEach(function(rdoc) {
							if (rdoc.field1 === doc.field1) {
								found = true;
							}
						});

						assert(found);
					});

					done();
				});
			});

			function queryTest(query, expected, done) {
				var item = testData[3];
				mongooseResource("test", TestModel);

				request.get("/test/" + item._id + "/docArray?query=" + encodeURIComponent(query), function(res, body) {
					assertDocArrayCollection(res, body, expected);
					done();
				});
			}

			it(
				"should compare fields when query has field:value",
				queryTest.bind(null, "field:foo", ["foo"])
			);

			it(
				"should regex-compare fields when query has field:/regexp/",
				queryTest.bind(null, "field:/a/", ["bar", "baz"])
			);

			it(
				"should regex-compare with flags",
				queryTest.bind(null, "field:/A/i", ["bar", "baz"])
			);

			it(
				"should negate comparisons when query has field!value",
				queryTest.bind(null, "field!foo", ["bar", "baz"])
			);

			it(
				"should negate regex-comparisons when query has field!/regexp/",
				queryTest.bind(null, "field!/a/", ["foo"])
			);

			it(
				"should allow queries on sub-fields",
				queryTest.bind(null, "sub.field:sub", ["baz"])
			);

			it(
				"should allow queries with AND operators",
				queryTest.bind(null, "field:/a/ AND field:/^b/", ["bar", "baz"])
			);

			it(
				"should allow queries with OR operators",
				queryTest.bind(null, "field:/o/ OR field:/a/", ["foo", "bar", "baz"])
			);

			it(
				"should allow queries with both AND and OR operators",
				queryTest.bind(null, "field:/o/ OR field:/z/ AND field:/^b/", ["foo", "baz"])
			);

			it("should POST new documents to DocumentArray collections", function(done) {
				var item = testData[3];
				mongooseResource("test", TestModel);

				request.post("/test/" + item._id + "/docArray", { field: "bang" }, function(res, body) {
					assertCreated(res, body);
					assert.strictEqual(res.statusCode, 201);

					TestModel.findById(item._id, function(err, doc) {
						assert.ifError(err);

						var subs = doc.docArray.filter(function(sub) {
							return sub.field === "bang";
						});

						assert.strictEqual(subs.length, 1);
						done();
					});
				});
			});

			it("should return POSTed documents when postResponse is true", function(done) {
				var item = testData[3];
				mongooseResource("test", TestModel).set("postResponse", true);

				request.post("/test/" + item._id + "/docArray", { field: "bang" }, function(res, body) {
					assert.strictEqual(res.statusCode, 200);
					assertReturnedDoc(body, { field: "bang" });

					done();
				});
			});

			it("should POST new documents to DocumentArrays at specified index", function(done) {
				var item = testData[3];
				mongooseResource("test", TestModel);

				request.post("/test/" + item._id + "/docArray?index=1", { field: "bang" }, function(res, body) {
					assertCreated(res, body);
					assert.strictEqual(res.statusCode, 201);

					TestModel.findById(item._id, function(err, doc) {
						assert.ifError(err);

						var subs = doc.docArray.filter(function(sub) {
							return sub.field === "bang";
						});

						assert.strictEqual(subs.length, 1);
						assert.strictEqual(doc.docArray.indexOf(subs[0]), 1);
						done();
					});
				});
			});
		});

		describe("DocumentArray documents", function() {
			it("should GET documents in DocumentArrays",
				composeTests(testData[3].docArray.map(function(item) {
					return function(done) {
						mongooseResource("test", TestModel);

						request.get("/test/" + testData[3]._id + "/docArray/" + item._id, function(res, body) {
							var doc = assertJSON(body);

							assert.strictEqual(res.statusCode, 200);
							assert.strictEqual(typeof doc, "object");

							/* Remove additional properties before comparing */
							delete doc.__v;
							delete doc._href;

							assert.deepEqual(doc, item);

							done();
						});
					};
				}))
			);

			it(
				"should allow specifying an alternate primary key on collection paths",
				composeTests(testData[3].docArray.map(function(item) {
					return function(done) {
						mongooseResource("test", TestModel)
							.sub(":id/docArray")
							.set("subkeys", "field");

						request.get("/test/" + testData[3]._id + "/docArray/" + item.field, function(res, body) {
							var doc = assertJSON(body);

							assert.strictEqual(res.statusCode, 200);
							assert.strictEqual(typeof doc, "object");

							/* Remove additional properties before comparing */
							delete doc.__v;
							delete doc._href;

							assert.deepEqual(doc, item);

							done();
						});
					};
				}))
			);

			it("should DELETE documents in DocumentArray collections", function(done) {
				var item = testData[3],
					sub = item.docArray[0];

				mongooseResource("test", TestModel);

				request.del("/test/" + item._id + "/docArray/" + sub._id, function(res, body) {
					assertEmpty(body);
					assert.strictEqual(res.statusCode, 204);

					TestModel.findById(item._id, function(err, doc) {
						assert.ifError(err);
						assert.strictEqual(doc.docArray.id(sub._id), null);

						done();
					});
				});
			});

			it("should PUT documents in DocumentArray collections", function(done) {
				var item = testData[3],
					sub = item.docArray[0];

				mongooseResource("test", TestModel);

				request.put("/test/" + item._id + "/docArray/" + sub._id, { field: "bang" }, function(res, body) {
					assertEmpty(body);
					assert.strictEqual(res.statusCode, 204);

					TestModel.findById(item._id, function(err, doc) {
						assert.ifError(err);
						assert.strictEqual(doc.docArray.id(sub._id).field, "bang");

						done();
					});
				});
			});
		});

		describe("DocumentArray document fields", function() {
			it("should GET fields in documents in DocumentArrays",
				composeTests(testData[3].docArray.map(function(item, index) {
					return function(done) {
						mongooseResource("test", TestModel);

						request.get("/test/" + testData[3]._id + "/docArray/" + item._id + "/field", function(res, body) {
							assert.strictEqual(res.statusCode, 200);
							assert.strictEqual(body, testData[3].docArray[index].field);

							done();
						});
					};
				}))
			);

			it("should DELETE fields in documents in DocumentArrays", function(done) {
				var item = testData[3],
					sub = item.docArray[0];

				request.del("/test/" + item._id + "/docArray/" + sub._id + "/field", function(res, body) {
					assertEmpty(body);
					assert.strictEqual(res.statusCode, 204);

					TestModel.findById(item._id, function(err, doc) {
						assert.ifError(err);
						assert.strictEqual(doc.docArray.id(sub._id).field, undefined);

						done();
					});
				});
			});

			it("should PUT fields in documents in DocumentArrays", function(done) {
				var item = testData[3],
					sub = item.docArray[0];

				request.put("/test/" + item._id + "/docArray/" + sub._id + "/field", { _value: "bang" }, function(res, body) {
					assertEmpty(body);
					assert.strictEqual(res.statusCode, 204);

					TestModel.findById(item._id, function(err, doc) {
						assert.ifError(err);
						assert.strictEqual(doc.docArray.id(sub._id).field, "bang");

						done();
					});
				});
			});
		});

		describe("Overrides", function() {
			it("should allow overriding documents", function(done) {
				mongooseResource("test", TestModel)
					.sub('override/property')
					.get(function(req, cb) {
						cb(null, "hello, world!");
					});

				request.get("/test/override/property", function(res, body) {
					assert.strictEqual(res.statusCode, 200);
					assert.strictEqual(body, "hello, world!");
					done();
				});
			});

			it("should allow overriding document properties", function(done) {
				var item = testData[0];
				mongooseResource("test", TestModel)
					.sub(':id/helloworld')
					.get(function(req, cb) {
						cb(null, "hello, world!");
					});

				request.get("/test/" + item._id + "/helloworld", function(res, body) {
					assert.strictEqual(res.statusCode, 200);
					assert.strictEqual(body, "hello, world!");
					done();
				});
			});
		});
	});

	describe("Aggregate resources", function() {
		var aggregatePipeline = [
				{ $project: {
					field1: 1,
					docArray: 1
				} },
				{ $unwind: "$docArray" },
				{ $project: {
					_id: "$docArray.field",
					parent: "$field1"
				} },
				{ $sort: {
					_id: -1
				} }
			];

		function aggregateTest(query, expected, done) {
			var uri = "/test";
			aggregateResource("test", TestModel, aggregatePipeline);

			if (query) {
				uri += "?query=" + encodeURIComponent(query);
			}

			request.get(uri, function(res, body) {
				assert.strictEqual(res.statusCode, 200);

				var data = assertJSON(body);
				assert.strictEqual(typeof data, "object");
				assert.strictEqual(data._count, expected.length);
				assert(Array.isArray(data._items));
				assert.strictEqual(data._items.length, expected.length);
				assert.deepEqual(
					data._items.map(function(item) {
						return item._id;
					}),
					expected
				);

				done();
			});
		}

		it(
			"should GET aggregates as collections",
			aggregateTest.bind(null, "", [ "foo", "baz", "bar" ])
		);

		it(
			"should compare fields when query has field:value",
			aggregateTest.bind(null, "_id:foo", ["foo"])
		);

		it(
			"should regex-compare fields when query has field:/regexp/",
			aggregateTest.bind(null, "_id:/a/", ["baz", "bar"])
		);

		it(
			"should regex-compare with flags",
			aggregateTest.bind(null, "_id:/A/i", ["baz", "bar"])
		);

		it(
			"should negate comparisons when query has field!value",
			aggregateTest.bind(null, "_id!foo", ["baz", "bar"])
		);

		it(
			"should negate regex-comparisons when query has field!/regexp/",
			aggregateTest.bind(null, "_id!/a/", ["foo"])
		);

		it(
			"should allow queries with AND operators",
			aggregateTest.bind(null, "_id:/a/ AND parent:arr", ["baz", "bar"])
		);

		it(
			"should allow queries with OR operators",
			aggregateTest.bind(null, "_id:/a/ OR _id:/o/", ["foo", "baz", "bar"])
		);

		it(
			"should allow queries with both AND and OR operators",
			aggregateTest.bind(null, "parent:/r/ OR _id:/a/ AND _id:/b/", ["foo", "baz", "bar"])
		);

		it("should GET agregated documents with their projected _id", function(done) {
			aggregateResource("test", TestModel, aggregatePipeline);

			request.get("/test/bar", function(res, body) {
				assert.strictEqual(res.statusCode, 200);
				var doc = assertJSON(body);
				assert.strictEqual(typeof doc, "object");
				assert.strictEqual(doc._id, "bar");
				assert.strictEqual(doc.parent, "arr");

				done();
			});
		});

		it("should 404 on nonexistent aggregated documents", function(done) {
			aggregateResource("test", TestModel, aggregatePipeline);

			request.get("/test/nope", function(res, body) {
				assert.strictEqual(res.statusCode, 404);
				assert.strictEqual(body, "Not found");

				done();
			});
		});

		it("should allow defining custom subresources", function(done) {
			aggregateResource("test", TestModel, aggregatePipeline)
				.sub(":id/foo")
				.get(function(req, cb) {
					cb(null, "I'm foo inside " + req.mongoose.item._id);
				});

			request.get("/test/bar/foo", function(res, body) {
				assert.strictEqual(res.statusCode, 200);
				assert.strictEqual(body, "I'm foo inside bar");
				done();
			});
		});
	});
});