/*! DexterJS - v0.5.4 -  * https://github.com/leobalter/DexterJS
 * Copyright (c) 2014 Leonardo Balter; Licensed MIT, GPL */
(function() {
var Dexter = {
		stored: []
	},
	timerArray = [],
	restore, actions,
	originalTimeout = setTimeout,
	originalInterval = setInterval,
	originalClearTimeout = clearTimeout,
	originalClearInterval = clearInterval;

restore = function() {
	this._seenObj[ this._seenMethod ] = this._oldCall;
	this.isActive = false;
};

function setDexterObjs( scope, obj, method ) {
	scope._oldCall = obj[ method ];
	scope._seenObj = obj;
	scope._seenMethod = method;
}

actions = {
	'spy': function( that, args ) {
		// call order issues
		var returned = that._oldCall.apply( this, args );

		if ( typeof ( that.callback ) === 'function' ) {
			that.callback.apply( this, args );
		}
		// calls the original method
		return returned;
	},
	'fake': function( that, args ) {
		if ( typeof ( that.callback ) === 'function' ) {
			return that.callback.apply( this, args );
		}
	}
};

function DexterObj( action, obj, method, callback ) {
	var that = this;
	this.called = 0;
	this.isActive = true;

	if ( typeof ( method ) !== 'string' ) {
		throw 'Dexter should receive method name as a String';
	}

	if ( !obj || typeof ( obj[ method ] ) !== 'function' ) {
		throw 'Dexter should receive a valid object and method combination in arguments. Ex.: window & "alert".';
	}

	if ( typeof ( callback ) === 'function' ) {
		this.callback = callback;
	}

	setDexterObjs( this, obj, method );

	obj[ method ] = function() {
		var args = [].slice.apply( arguments );

		that.called = that.called + 1;

		return actions[ action ].call( this, that, args );
	};
}

function createDexterObj( name ) {
	return function( obj, method, callback ) {
		var newObj = new DexterObj( name, obj, method, callback );
		Dexter.stored.push( newObj );

		return newObj;
	};
}

function restoreAll() {
	while ( Dexter.stored.length ) {
		Dexter.stored.pop().restore();
	}

	return Dexter.stored.length === 0;
}

DexterObj.prototype = {
	restore: restore
};

/* Timer */
function Timer() {
	if ( this instanceof Timer ) {
		/* jshint -W020 */
		setTimeout = fakeSetTimeout;
		setInterval = fakeSetInterval;
		clearTimeout = fakeClearTimer;
		clearInterval = fakeClearTimer;
	} else {
		return new Timer();
	}
}

function fakeSetTimeout( callback, timer ) {
	timerArray.push({
		cb: callback,
		time: timer,
		type: 'timeout'
	});
	return timerArray.length;
}

function fakeSetInterval( callback, timer ) {
	timerArray.push({
		cb: callback,
		time: timer,
		type: 'interval',
		originalTime: timer
	});
	return timerArray.length;
}

function fakeClearTimer( timeoutIndex ) {
	timerArray.splice( timeoutIndex - 1, 1 );
}

Timer.prototype = {

	tick: function( n ) {
		var index = 0,
			thisTimer;

		for (; index < timerArray.length; index++ ) {
			thisTimer = timerArray[ index ];

			timerArray[ index ].time = thisTimer.time - n;

			if ( thisTimer.type === 'timeout' ) {
				Timer.prototype.tickTimeout( index );
			}

			if ( thisTimer.type === 'interval' ) {
				Timer.prototype.tickInterval( n, index );
			}
		}

	},

	tickTimeout: function( index ) {
		var thisTimer = timerArray[ index ];

		if ( thisTimer.time <= 0 ) {
			Timer.prototype.executeCallback( index );
			timerArray.splice( index - 1, 1 );
		}
	},

	tickInterval: function( n, index ) {
		var thisTimer = timerArray[ index ],
			howManyTimesWillRun = Math.floor(( n / thisTimer.originalTime )) / 1;

		if ( thisTimer.time <= 0 ) {
			timerArray[ index ].time = ( thisTimer.originalTime - thisTimer.time );
		}

		while ( howManyTimesWillRun ) {
			Timer.prototype.executeCallback( index );
			howManyTimesWillRun--;
		}
	},

	executeCallback: function( index ) {
		var thisTimer = timerArray[ index ];

		if ( typeof thisTimer.cb === 'string' ) {
			/* jshint evil:true */
			eval( thisTimer.cb );
		} else {
			thisTimer.cb();
		}
	},

	restore: function() {
		/* jshint -W020 */
		setTimeout = originalTimeout;
		setInterval = originalInterval;
		clearInterval = originalClearInterval;
		clearTimeout = originalClearTimeout;
		this.resetTimers();
	},

	resetTimers: function() {
		timerArray = [];
	}
};

Dexter.spy = createDexterObj( 'spy' );
Dexter.fake = createDexterObj( 'fake' );
Dexter.restore = restoreAll;
Dexter.timer = Timer;

if ( typeof module !== 'undefined' && module.exports ) {

	// For CommonJS environments, export everything
	module.exports = Dexter;
} else if ( typeof define === 'function' && define.amd ) {

	// amd Enviroments, client and server side
	define( 'dexter', [], function() {
		return Dexter;
	});
} else if ( typeof window !== 'undefined' ) {

	// Old school
	window.Dexter = Dexter;
}

})();

(function( globalObj ) {
var Dexter,
	statusCodes, unsafeHeaders, fakeXHRObj, CreateFakeXHR,
	ajaxObjs = {};

/***
 * checks for XHR existence
 * returns => XHR fn name || false
 ***/
ajaxObjs.xhr = (function() {
	var xhr;
	try {
		xhr = new XMLHttpRequest();
		return XMLHttpRequest;
	} catch ( e ) {
		return false;
	}
}());

if ( typeof module !== 'undefined' && module.exports ) {

	// For CommonJS environments, export everything
	module.exports = function() {
		return {};
	};
	return false;
} else if ( ajaxObjs.xhr ) {
	if ( typeof define === 'function' && define.amd ) {

		// amd Enviroments, client and server side
		define( 'fakeXHR', [], function() {
			return new CreateFakeXHR();
		});
	} else {
		Dexter = window.Dexter;
		Dexter.fakeXHR = function() {
			return new CreateFakeXHR();
		};
	}
}

// Status code and their respective texts
statusCodes = {
	100: 'Continue',
	101: 'Switching Protocols',
	200: 'OK',
	201: 'Created',
	202: 'Accepted',
	203: 'Non-Authoritative Information',
	204: 'No Content',
	205: 'Reset Content',
	206: 'Partial Content',
	300: 'Multiple Choice',
	301: 'Moved Permanently',
	302: 'Found',
	303: 'See Other',
	304: 'Not Modified',
	305: 'Use Proxy',
	307: 'Temporary Redirect',
	400: 'Bad Request',
	401: 'Unauthorized',
	402: 'Payment Required',
	403: 'Forbidden',
	404: 'Not Found',
	405: 'Method Not Allowed',
	406: 'Not Acceptable',
	407: 'Proxy Authentication Required',
	408: 'Request Timeout',
	409: 'Conflict',
	410: 'Gone',
	411: 'Length Required',
	412: 'Precondition Failed',
	413: 'Request Entity Too Large',
	414: 'Request-URI Too Long',
	415: 'Unsupported Media Type',
	416: 'Requested Range Not Satisfiable',
	417: 'Expectation Failed',
	422: 'Unprocessable Entity',
	500: 'Internal Server Error',
	501: 'Not Implemented',
	502: 'Bad Gateway',
	503: 'Service Unavailable',
	504: 'Gateway Timeout',
	505: 'HTTP Version Not Supported'
};

// Some headers should be avoided
unsafeHeaders = [
	'Accept-Charset',
	'Accept-Encoding',
	'Connection',
	'Content-Length',
	'Cookie',
	'Cookie2',
	'Content-Transfer-Encoding',
	'Date',
	'Expect',
	'Host',
	'Keep-Alive',
	'Referer',
	'TE',
	'Trailer',
	'Transfer-Encoding',
	'Upgrade',
	'User-Agent',
	'Via'
];

/***
 * verifyState helps verifying XHR readyState in cases when that should be
 * 1 (Opened) and sendFlag can´t be true. (not yet sent request)
 ***/
function verifyState( state, sendFlag ) {
	if ( state !== 1 || sendFlag ) {
		throw new Error( 'INVALID_STATE_ERR' );
	}
}

/***
 * string to XML parser. Got this from SinonJS that got the same from JSpec
 ***/
function parseXML( text ) {
	var xmlDoc,
		parser;

	if ( typeof globalObj.DOMParser !== 'undefined' ) {
		parser = new globalObj.DOMParser();
		xmlDoc = parser.parseFromString( text, 'text/xml' );
	} else {
		xmlDoc = new ActiveXObject( 'Microsoft.XMLDOM' );
		xmlDoc.async = 'false';
		xmlDoc.loadXML( text );
	}

	return xmlDoc;
}

fakeXHRObj = {
	// Status constants
	UNSENT: 0,
	OPENED: 1,
	HEADERS_RECEIVED: 2,
	LOADING: 3,
	DONE: 4,
	// event handlers
	onabort: null,
	onerror: null,
	onload: null,
	onloadend: null,
	onloadstart: null,
	onprogress: null,
	onreadystatechange: null,
	ontimeout: null,
	// readyState always start by 0
	readyState: 0,
	// other properties
	response: '',
	responseText: '',
	responseType: '',
	responseXML: null,
	withCredentials: false,
	// status code
	status: 0,
	// status text relative to the status code
	statusText: '',
	// timeout to be set, starts by 0
	timeout: 0,
	/***
	 * fake .abort
	 ***/
	abort: function() {
		// reseting properties
		this.aborted = true;
		this.errorFlag = true;
		this.method = null;
		this.url = null;
		this.async = undefined;
		this.username = null;
		this.password = null;
		this.responseText = null;
		this.responseXML = null;
		this.requestHeaders = {};
		this.sendFlag = false;
		// triggering readystatechange
		if ( this.readyState > this.UNSENT && this.sendFlag ) {
			this.__DexterStateChange( this.DONE );
		} else {
			this.__DexterStateChange( this.UNSENT );
		}

	},
	/***
	 * fake .getResponseHeader
	 ***/
	getResponseHeader: function( key ) {
		var headerName,
			responseHeaders = this.responseHeaders;
		// no return before receiving headers
		if ( this.readyState < this.HEADERS_RECEIVED ) {
			return null;
		}

		// no return for Set-Cookie2
		if ( /^Set-Cookie2?$/i.test( key ) ) {
			return null;
		}

		// we manage key finding to different letter cases
		key = key.toLowerCase();

		for ( headerName in responseHeaders ) {
			if ( responseHeaders.hasOwnProperty( headerName ) ) {
				// do we have that key?
				if ( headerName.toLowerCase() === key ) {
					// se we return its value
					return responseHeaders[ headerName ];
				}
			}
		}

		// no success, return null
		return null;
	},
	/***
	 * fake .open
	 ***/
	open: function( method, url, async, username, password ) {
		// method and url aren´t optional
		if ( typeof ( method ) === 'undefined' || typeof ( url ) === 'undefined' ) {
			throw new Error( 'Not enough arguments' );
		}
		// setting properties
		this.method = method;
		this.url = url;

		// async default is true, so if async is undefined it is set to true,
		// otherwise async get its boolean value
		this.async = ( typeof ( async ) === 'undefined' ? true : !!async );

		this.username = username;
		this.password = password;
		// cleaning these properties
		this.responseText = null;
		this.responseXML = null;
		this.requestHeaders = {};
		this.sendFlag = false;

		// triggering readystatechange with Opened status
		this.__DexterStateChange( this.OPENED );
	},
	/***
	 * fake .send
	 ***/
	send: function( data ) {
		var reqHeaders;
		// readyState verification (xhr should be already opened)
		verifyState( this.readyState, this.sendFlag );

		if ( !/^(get|head)$/i.test( this.method ) ) {
			if ( this.requestHeaders[ 'Content-Type' ] ) {
				reqHeaders = this.requestHeaders[ 'Content-Type' ].split( ';' );
				this.requestHeaders[ 'Content-Type' ] = reqHeaders[ 0 ] + ';charset=utf-8';
			} else {
				this.requestHeaders[ 'Content-Type' ] = 'text/plain;charset=utf-8';
			}
			this.requestBody = data;
		}

		// setting properties
		this.errorFlag = false;
		this.sendFlag = true; // this.async;

		// trigger readystatechange with Opened status
		this.__DexterStateChange( this.OPENED );

		// hummm if think I won´t need this, omg, where´s the specification
		if ( typeof ( this.onSend ) === 'function' ) {
			this.onSend( this );
		}
	},
	/***
	 * fake .setRequestHeader
	 ***/
	setRequestHeader: function( key, value ) {
		// readyState verification (xhr should be already opened)
		verifyState( this.readyState, this.sendFlag );

		// key shouldn´t be one of the unsafeHeaders neither start with Sec- or
		// Proxy-

		if ( ( unsafeHeaders.indexOf( key ) >= 0 ) || /^(Sec-|Proxy-)/.test( key ) ) {
			throw new Error( 'Refused to set unsafe header "' + key + '"' );
		}

		if ( this.requestHeaders[ key ] ) {
			// if we already have this key set, we concatenate values
			this.requestHeaders[ key ] += ',' + value;
		} else {
			// or we just set key and value
			this.requestHeaders[ key ] = value;
		}
	},
	/***
	 * fake getAllResponseHeaders
	 ***/
	getAllResponseHeaders: function() {
		var headers = '',
			header;

		if ( this.readyState < this.HEADERS_RECEIVED ) {
			return '';
		}

		for ( header in this.responseHeaders ) {
			if ( this.responseHeaders.hasOwnProperty( header ) && !/^Set-Cookie2?$/i.test( header ) ) {
				headers += header + ': ' + this.responseHeaders[ header ] + '\r\n';
			}
		}

		return headers;
	},
	/***
	 * __DexterSetResponseHeaders set xhr response headers to make arrangements
	 * before completing the fake ajax request
	 ***/
	__DexterSetResponseHeaders: function( headers ) {
		var header;
		// reseting response headers
		this.responseHeaders = {};

		for ( header in headers ) {
			if ( headers.hasOwnProperty( header ) ) {
				this.responseHeaders[ header ] = headers[ header ];
			}
		}

		// async requests should trigger readystatechange event
		if ( this.async ) {
			this.__DexterStateChange( this.HEADERS_RECEIVED );
		}
	},
	/***
	 * __DexterXHR indicates this is a Dexter faked XHR
	 ***/
	__DexterXHR: true,
	/***
	 * __DexterStateChange handles events on readyState changes
	 ***/
	__DexterStateChange: function( state ) {
		var ev;
		this.readyState = state;

		if ( typeof this.onreadystatechange === 'function' ) {
			try {
				// dumb event creation. "new Event" just fire errors in webkit engines
				ev = document.createEvent( 'Event' );
				ev.initEvent( 'readystatechange', false, false );
			} catch (e) {
				// dammit, IE7!
				// TODO: this would break anyone´s code?
				ev = {
					type: 'readystatechange'
				};
			}

			// the event goes inside an Array
			this.onreadystatechange.call( this, [ ev ]);
		}
	},
	/***
	 * __DexterSetResponseBody builds the response text.
	 ***/
	__DexterSetResponseBody: function( body ) {
		var chunkSize = this.chunkSize || 10,
			index = 0,
			type;

		this.responseText = '';

		if ( this.async ) {
			while ( index <= body.length ) {
				this.__DexterStateChange( this.LOADING );
				this.responseText += body.substring( index, ( index += chunkSize ) );
			}
		} else {
			this.responseText = body;
		}

		type = this.getResponseHeader( 'Content-Type' ) || '';

		if ( body && ( /(text\/xml)|(application\/xml)|(\+xml)/.test( type )) ) {
			try {
				this.responseXML = parseXML( body );
			} catch ( e ) {}
		}
	},
	/***
	 * __DexterRespond is the call to complete a ajax request
	 * @params {
	 *   body : string with responseText (Default: '')
	 *   headers : responseHeaders (Default: {})
	 *   status : Number status code (Default: 200)
	 * }
	 ***/
	__DexterRespond: function( params ) {
		var body = params.body || '',
			headers = params.headers || {},
			DONE = this.DONE;

		// this should be verified to prevent recalling method
		if ( this.readyState === DONE ) {
			throw new Error( 'Request already done' );
		}

		this.__DexterSetResponseHeaders( headers );
		this.status = params.status || 200;
		this.statusText = statusCodes[ this.status ];

		this.__DexterSetResponseBody( body );

		// triggers the readystatechange if is an async request
		if ( this.async ) {
			this.__DexterStateChange( DONE );
		} else {
			// not being async, just set readyState value
			this.readyState = DONE;
		}
	} /***
* not implemented yet XHR functions
* upload:                   function() {},
* getInterface:             function() {},
* overrideMimeType:         function() {},
* sendAsBinary:             function() {}
***/
};

/***
 * CreateFakeXHR builds the fakeXHR object
 * this is a constructor and should be called with 'new'
 ***/
CreateFakeXHR = function() {
	var FakeRequest, fakeObj,
		DexterXHR = this;

	/***
	 * requests will contain all requests made on the fakeXHR object´s lifecycle
	 * doing so, they can be monitored via Dexter.fakeXHR´s instance
	 ***/
	this.requests = [];
	this.doneRequests = [];

	/***
	 * this is the fake request function to be applied to XMLHttpRequest
	 ***/
	FakeRequest = function() {
		// creating a reference of xhr object in Dexter.fakeXHR() object
		DexterXHR.requests.push( this );

		// we set a timeStamp on __DexterRef to identify requests
		this.__DexterRef = Date.now();

		return this;
	};

	fakeObj = function() {
		FakeRequest.call( this );
	};

	// we import the fake XHR prototype to both methods
	fakeObj.prototype = fakeXHRObj;

	globalObj.XMLHttpRequest = fakeObj;
};

/***
 * this is the Dexter.fakeXHR prototype, those method will be seen directly on
 * its returned object. Not on the XHR itself.
 ***/
CreateFakeXHR.prototype = {
	/***
	 * interface to export xhr.__DexterRespond and set this.doneRequests
	 ***/
	respond: function( params, index ) {
		var xhr;

		params = params || {};
		if ( index ) {
			// if index number is set return that indexed element
			xhr = this.requests.splice( index, 1 )[ 0 ];
		} else {
			// else it gets the first request in line
			xhr = this.requests.shift();
		}

		xhr.__DexterRespond( params );

		// selected xhr will be seen on doneRequests collection
		this.doneRequests.push( xhr );
	},
	/***
	 * uses a Dexter.spy on xhr send requests
	 ***/
	spy: function( callback ) {
		var spy = Dexter.spy( fakeXHRObj, 'send', callback );
		// this.__spy will be used on .restore();
		this.__spy = spy;
		return spy;
	},
	/***
	 * restore the XHR objects to their original states, defaking them
	 * this won´t affect already created fake ajax requests.
	 ***/
	restore: function() {
		if ( this.__spy ) {
			this.__spy.restore();
		}
		globalObj.XMLHttpRequest = ajaxObjs.xhr;
	}
};

// Get a reference to the global object, like window in browsers
}( (function() {
	return this;
})() ));