var buster = typeof window !== 'undefined' ? window.buster : require('buster');
var assert = buster.assert;
var refute = buster.refute;
var fail = buster.referee.fail;

var when = require('../when');

var sentinel = {};
var other = {};

function fakeResolved(val) {
	return {
		then: function(callback) {
			return fakeResolved(callback ? callback(val) : val);
		}
	};
}

function fakeRejected(reason) {
	return {
		then: function(callback, errback) {
			return errback ? fakeResolved(errback(reason)) : fakeRejected(reason);
		}
	};
}

buster.testCase('when.defer', {

	'resolve': {
		'should fulfill with an immediate value': function(done) {
			var d = when.defer();

			d.promise.then(
				function(val) {
					assert.same(val, sentinel);
				},
				fail
			).ensure(done);

			d.resolve(sentinel);
		},

		'should fulfill with fulfilled promised': function(done) {
			var d = when.defer();

			d.promise.then(
				function(val) {
					assert.same(val, sentinel);
				},
				fail
			).ensure(done);

			d.resolve(fakeResolved(sentinel));
		},

		'should reject with rejected promise': function(done) {
			var d = when.defer();

			d.promise.then(
				fail,
				function(val) {
					assert.same(val, sentinel);
				}
			).ensure(done);

			d.resolve(fakeRejected(sentinel));
		},

		'should invoke newly added callback when already resolved': function(done) {
			var d = when.defer();

			d.resolve(sentinel);

			d.promise.then(
				function(val) {
					assert.same(val, sentinel);
					done();
				},
				fail
			).ensure(done);
		}
	},

	'reject': {
		'should reject with an immediate value': function(done) {
			var d = when.defer();

			d.promise.then(
				fail,
				function(val) {
					assert.same(val, sentinel);
				}
			).ensure(done);

			d.reject(sentinel);
		},

		'should reject with fulfilled promised': function(done) {
			var d, expected;

			d = when.defer();
			expected = fakeResolved(sentinel);

			d.promise.then(
				fail,
				function(val) {
					assert.same(val, expected);
				}
			).ensure(done);

			d.reject(expected);
		},

		'should reject with rejected promise': function(done) {
			var d, expected;

			d = when.defer();
			expected = fakeRejected(sentinel);

			d.promise.then(
				fail,
				function(val) {
					assert.same(val, expected);
				}
			).ensure(done);

			d.reject(expected);
		},

		'should invoke newly added errback when already rejected': function(done) {
			var d = when.defer();

			d.reject(sentinel);

			d.promise.then(
				fail,
				function (val) {
					assert.equals(val, sentinel);
				}
			).ensure(done);
		}
	},

	'notify': {

		'should notify of progress updates': function(done) {
			var d = when.defer();

			d.promise.then(
				fail,
				fail,
				function(val) {
					assert.same(val, sentinel);
					done();
				}
			);

			d.notify(sentinel);
		},

		'should propagate progress to downstream promises': function(done) {
			var d = when.defer();

			d.promise
			.then(fail, fail,
				function(update) {
					return update;
				}
			)
			.then(fail, fail,
				function(update) {
					assert.same(update, sentinel);
					done();
				}
			);

			d.notify(sentinel);
		},

		'should propagate transformed progress to downstream promises': function(done) {
			var d = when.defer();

			d.promise
			.then(fail, fail,
				function() {
					return sentinel;
				}
			)
			.then(fail, fail,
				function(update) {
					assert.same(update, sentinel);
					done();
				}
			);

			d.notify(other);
		},

		'should propagate caught exception value as progress': function(done) {
			var d = when.defer();

			d.promise
			.then(fail, fail,
				function() {
					throw sentinel;
				}
			)
			.then(fail, fail,
				function(update) {
					assert.same(update, sentinel);
					done();
				}
			);

			d.notify(other);
		},

		'should forward progress events when intermediary callback (tied to a resolved promise) returns a promise': function(done) {
			var d, d2;

			d = when.defer();
			d2 = when.defer();

			// resolve d BEFORE calling attaching progress handler
			d.resolve();

			d.promise.then(
				function() {
					return when.promise(function(resolve, reject, notify) {
						setTimeout(function() {
							notify(sentinel);
						}, 0);
					});
				}
			).then(null, null,
				function onProgress(update) {
					assert.same(update, sentinel);
					done();
				}
			);
		},

		'should forward progress events when intermediary callback (tied to an unresovled promise) returns a promise': function(done) {
			var d = when.defer();

			d.promise.then(
				function() {
					return when.promise(function(resolve, reject, notify) {
						setTimeout(function() {
							notify(sentinel);
						}, 0);
					});
				}
			).then(null, null,
				function onProgress(update) {
					assert.same(update, sentinel);
					done();
				}
			);

			// resolve d AFTER calling attaching progress handler
			d.resolve();
		},

		'should forward progress when resolved with another promise': function(done) {
			var d, d2;

			d = when.defer();
			d2 = when.defer();

			d.promise
			.then(fail, fail,
				function() {
					return sentinel;
				}
			)
			.then(fail, fail,
				function(update) {
					assert.same(update, sentinel);
					done();
				}
			);

			d.resolve(d2.promise);

			d2.notify();
		},

		'should allow resolve after progress': function(done) {
			var d = when.defer();

			var progressed = false;
			d.promise.then(
				function() {
					assert(progressed);
					done();
				},
				fail,
				function() {
					progressed = true;
				}
			);

			d.notify();
			d.resolve();
		},

		'should allow reject after progress': function(done) {
			var d = when.defer();

			var progressed = false;
			d.promise.then(
				fail,
				function() {
					assert(progressed);
					done();
				},
				function() {
					progressed = true;
				}
			);

			d.notify();
			d.reject();
		},

		'should be indistinguishable after resolution': function() {
			var d, before, after;

			d = when.defer();

			before = d.notify(sentinel);
			d.resolve();
			after = d.notify(sentinel);

			assert.same(before, after);
		}
	},

	'should return silently on progress when already resolved': function() {
		var d = when.defer();
		d.resolve();

		refute.defined(d.notify());
	},

	'should return silently on progress when already rejected': function() {
		var d = when.defer();
		d.reject();

		refute.defined(d.notify());
	}

});
