( function () {
	var MTH = {};

	MTH.enterFullscreenMock = function () {
		this.first().addClass( 'jq-fullscreened' ).data( 'isFullscreened', true );

		$( document ).trigger( $.Event( 'jq-fullscreen-change', { element: this, fullscreen: true } ) );
	};

	MTH.exitFullscreenMock = function () {
		this.first().removeClass( 'jq-fullscreened' ).data( 'isFullscreened', false );

		$( document ).trigger( $.Event( 'jq-fullscreen-change', { element: this, fullscreen: false } ) );
	};

	/**
	 * Returns the exception thrown by callback, or undefined if no exception was thrown.
	 *
	 * @param {Function} callback
	 * @return {Error}
	 */
	MTH.getException = function ( callback ) {
		var ex;
		try {
			callback();
		} catch ( e ) {
			ex = e;
		}
		return ex;
	};

	/**
	 * Creates an mw.storage-like object.
	 *
	 * @param {Object} storage localStorage stub with getItem, setItem, removeItem methods
	 * @return {mw.SafeStorage} Local storage-like object
	 */
	MTH.createLocalStorage = function ( storage ) {
		return new ( Object.getPrototypeOf( mw.storage ) ).constructor( storage );
	};

	/**
	 * Returns an mw.storage that mimicks lack of localStorage support.
	 *
	 * @return {mw.SafeStorage} Local storage-like object
	 */
	MTH.getUnsupportedLocalStorage = function () {
		return this.createLocalStorage( undefined );
	};

	/**
	 * Returns an mw.storage that mimicks localStorage being disabled in browser.
	 *
	 * @return {mw.SafeStorage} Local storage-like object
	 */
	MTH.getDisabledLocalStorage = function () {
		var e = function () {
			throw new Error( 'Error' );
		};

		return this.createLocalStorage( {
			getItem: e,
			setItem: e,
			removeItem: e
		} );
	};

	/**
	 * Returns a fake local storage which is not saved between reloads.
	 *
	 * @param {Object} [initialData]
	 * @return {mw.SafeStorage} Local storage-like object
	 */
	MTH.getFakeLocalStorage = function ( initialData ) {
		var bag = new mw.Map();
		bag.set( initialData );

		return this.createLocalStorage( {
			getItem: function ( key ) { return bag.get( key ); },
			setItem: function ( key, value ) { bag.set( key, value ); },
			removeItem: function ( key ) { bag.set( key, null ); }
		} );
	};

	/**
	 * Returns a viewer object with all the appropriate placeholder functions.
	 *
	 * @return {mw.mmv.MultimediaViewer}
	 */
	MTH.getMultimediaViewer = function () {
		return new mw.mmv.MultimediaViewer( {
			imageQueryParameter: function () {},
			language: function () {},
			recordVirtualViewBeaconURI: function () {},
			extensions: function () {
				return { jpg: 'default' };
			}
		} );
	};

	MTH.asyncPromises = [];

	/**
	 * Given a method/function that returns a promise, this'll return a function
	 * that just wraps the original & returns the original result, but also
	 * executes an assert.async() right before it's called, and resolves that
	 * async after that promise has completed.
	 *
	 * Example usage: given a method `bootstrap.openImage` that returns a
	 * promise, just call it like this to wrap this functionality around it:
	 * `bootstrap.openImage = asyncMethod( bootstrap.openImage, bootstrap );`
	 *
	 * Now, every time some part of the code calls this function, it'll just
	 * execute as it normally would, but your tests won't finish until these
	 * functions (and any .then tacked on to them) have completed.
	 *
	 * This method will make sure your tests don't end prematurely (before the
	 * promises have been resolved), but that's it. If you need to run
	 * additional code after all promises have resolved, you can call the
	 * complementary `waitForAsync`, which will return a promise that doesn't
	 * resolve until all of these promises have.
	 *
	 * @param {Object} object
	 * @param {string} method
	 * @param {QUnit.assert} [assert]
	 * @return {Function}
	 */
	MTH.asyncMethod = function ( object, method, assert ) {
		return function () {
			// apply arguments to original promise
			var promise = object[ method ].apply( object, arguments ),
				done;

			this.asyncPromises.push( promise );

			if ( assert ) {
				done = assert.async();
				// use setTimeout to ensure `done` is not the first callback handler
				// to execute (possibly ending the test's wait right before
				// the result of the promise is executed)
				setTimeout( promise.then.bind( null, done, done ) );
			}

			return promise;
		}.bind( this );
	};

	/**
	 * Returns a promise that will not resolve until all of the promises that
	 * were created in functions upon which `asyncMethod` was called have
	 * resolved.
	 *
	 * @return {$.Promise}
	 */
	MTH.waitForAsync = function () {
		var deferred = $.Deferred();

		// it's possible that, before this function call, some code was executed
		// that triggers async code that will eventually end up `asyncPromises`
		// in order to give that code a chance to run, we'll add another promise
		// to the array, that will only resolve at the end of the current call
		// stack (using setTimeout)
		this.asyncPromises.push( deferred.promise() );
		setTimeout( deferred.resolve );

		return QUnit.whenPromisesComplete.apply( null, this.asyncPromises ).then(
			function () {
				this.asyncPromises = [];
			}.bind( this )
		);
	};

	mw.mmv.testHelpers = MTH;
}() );
