/*
	Sphere Javascript Unit Test Module. version 1.1.1

Portions of this software are based on:
	jstest: Simple JavaScript testing library, v1
	Copyright Kragen Sitaker, 2005. Licensed under GNU GPL.

	JSUnit.net 
	Licensed under GNU GPL 2, 2.1 and Mozilla Public License 1.1

	1.0.0 20080621 FBn First version
	1.1.0 20080718 FBn Updated
    1.1.1 20090314 FBn clear logfile before each run
*/

function UnitTest(init){
	// Always do UT = new UnitTest(). If the programmer forgets the 'new', then do it for them.
	if(this instanceof UnitTest == false) {
		return new UnitTest(init);
	};

	var current_test = null; // Holds the name of the current test
	var current_result = null; // Holds the latest assert* result
	var suit_result = null; // Holds the cumulative result of a current_test

	// Functions that are 'required' are detected/collected using .LoadScript(<script>, true);
	this.totalRequiredFunctions = 0;
	// How many of the required functions have passed?
	this.totalgoodRequiredFunctions = 0;
	// How many functions have passed?
	this.totalgoodfunctions= 0;
	// How many functions have failed?
	this.totalbadfunctions= 0;
	// How many tests have failed?
	this.totalfailed = 0;
	// How many tests have passed?
	this.totalsucceeded = 0;

	// Default logfilename. To redefine, use one of the methods below
	//   UT = new UnitTest( { logfile: 'mylogname.log' })
	//   UT = new UnitTest(); UT.logfile = 'mylogname.log';
	this.logfile = "unittest.log";
	this.deletelog = true; // Just show us the latest run
	this.setDeleteLog= function(bool){ this.deletelog=bool; return this; }

	this.onErrorsOnly = false;
	this.LogOnErrorsOnly = function(bool){ this.onErrorsOnly = bool; return this; }

	var ALLOWED= {}; // functions/variables allowed to exist in global scope. Use .allowTaint() to set.
	var REQUIREDTESTS = {}; // holds all required functions

	var SPHERE = {}; // Holds all Sphere Functions
	var CHECK = {}; // Holds functions that check all internal Sphere functions

	var TRANSFER = {}; // Variables to transfer from one test to another.

	this.transfer = function(name, value){
		if(value)
			TRANSFER[name||"_"] = this.CopyObj(value);
		else
			return TRANSFER[name||"_"];
	}

	this.CopyObj = function(what){
		var O;
		if(typeof what=='object') {
			O = new Object();
			var i;
			for(i in what){
				if(typeof what[i]=='object')
					O[i]=new CopyObj(what[i]);
				else
					O[i]=what[i];
			}
		}else
			O=what;
		return O;
	}

	this.Abort = function(str, level){

		var _GetStack = "";
		try {
				toedeledokie();
		} catch (e) {
			_GetStack = e.stack;
		}
		_GetStack = _GetStack.split('\n')[level||1]
		Abort(str+"\nError generated at:\n"+ _GetStack);
	}


	// For one single run of a test, dont complain about a variable/function being defined ini global name space.
	// The function is however, allowed to exist
	this.allowTaint = function(thing){ ALLOWED[thing] = true; return this; }

	this.write = function(testname,msg){ this.loghandle.write(testname+":"+msg); return this; }
	this.SCRIPTDATA = {};
	this.LoadScript = function(f,dontskip){
		var str = "";
		if(!f)
			this.Abort('LoadScript(): empty filename!', 2);
		if(this.SCRIPTDATA[f])
			str = this.SCRIPTDATA[f];
		else {
			var raw = OpenRawFile("../scripts/" + f);
			var bytearray;
			while( (bytearray = raw.read(256)) && (bytearray.length>0)){
				str += CreateStringFromByteArray(bytearray);
			}
			raw.close();

			// Now get rid of comments...
			eval( 'function plzTidy(){' + str +'}' );
			str = plzTidy.toString(); //we stringify the function, it will autoformat our function
			str = str.replace(/function plzTidy\(\) \{/,'').replace(/\}\s*$/,'').replace(/([\r\n]+)    /g,"$1");
			this.SCRIPTDATA[f] = str;
		}

		if(!dontskip) return str;

		if(str.match(/function/)){
			var A_exp = str.match(/function\s+([\w\d\$]+)\s*\(|([\w\d\$]+)\s*=\s*function\s*\(/g);
			while( exp = A_exp.shift() ){
				exp = exp.replace(/\s*=\s*function\s*\(/, "");
				exp = exp.replace(/function\s*/, "").replace(/\s*\(/,"");
				if(REQUIREDTESTS[exp]===undefined){
					REQUIREDTESTS[exp] = false;
					this.totalRequiredFunctions++;
				}
			}
		}
		return str;
	}

	this.RequireScript = function(file,parent){
		if(!parent) return this.LoadScript(file);
		F = Function( "eval(" + this.LoadScript(file) + ")" );
		return F.call(parent);
	}

	this.PrepareScript = function(file){
		return this.LoadScript(file,true);
	}

	this.checkglobals = false; // Careful, causes coredumps!
	{
		var FUNCTIONS = this.LoadScript('ss_functions.table', true);
		var A_exp = FUNCTIONS.match(/SS_FUNCTION\((\w+),/g);
		FUNCTIONS = "";
		var exp;
		while( exp = A_exp.shift() ){
			exp = exp.replace(",","").replace("SS_FUNCTION(", "");
			// Skip some functions that do not exist in Sphere v1.13
			if( (GetVersion() == '1.13') && (exp == "SetLayerAlpha" || exp == "GetLayerAlpha") ) continue;
			if( (GetVersion() == '1.5') && ( exp == "GetJoystickX" 
			|| exp == "GetJoystickY"
			|| exp == "SetLayerAlpha" 
			|| exp == "GetLayerAlpha" 
			)) continue;

				
			// Here we hold the references to the REAL sphere functions:
			SPHERE[exp] = Function("return "+exp); SPHERE[exp] = SPHERE[exp]();
			// This checks and restores the REAL sphere functions:
			CHECK[exp] = Function("SPHERE", 
				"if ("+exp+".toString().match(/native code/)=='native code')return true; "+exp+"=SPHERE['"+exp+"']; return false;")
		}
	};

	var UTDEBUG = false;
	var dtxt = ""; // Debug text
	this.setDEBUG = function(bool){ UTDEBUG=bool; return this; }
	this.getDEBUG = function(bool){ return UTDEBUG; }

	/**
	*  Executes an external file. Unlike EvaluateScript, it will not load _before_ all else, wreaking scope havoc.
	*/
	this.readfile = function(file){
		var F = OpenRawFile(file) || Abort('Could not read file "'+file+'"');
		
		var S = CreateStringFromByteArray(F.read(F.getSize()));
		F.close();
		return S;
	}

	this.runtests = function(suite) {
		suit_result = true;
		var scope = new Object();
		for (current_test in suite) {
			var raised_exception = false;
			current_result = null;
			if(REQUIREDTESTS[current_test]!==undefined)
				REQUIREDTESTS[current_test] = true;
			// stuff inside catch will become global, so have to create outerloop with UTDEBUG
			if(UTDEBUG){
				try {
					//scope.test = eval ( suite[current_test].toSource());
					//scope.test.call(scope,[this]);
					suite[current_test](scope);
				} catch (e) {
					var i;
					for (i in e) raised_exception += i + ' = ' + e[i] +"\n"; // To dump all stack, intimidating!
				}
			}else{
				try {
					suite[current_test](scope);
				} catch (e) {
					raised_exception = e.name + ": " + e.message;
				}
			}
			if(UTDEBUG) this.writeResult('DEBUG', 'UTDEBUG', false, dtxt + raised_exception)
			var tainted = 0; // false
			var ftest;
			if(this.checkglobals){
				var func;
				for(func in SPHERE){
					if(ALLOWED[func]) continue;
					if (!CHECK[func](SPHERE)){
						this.writeResult( "TAINTED!", current_test, false, "Define '"+func+"' function as local!\n");
						++tainted;
					};
					GarbageCollect();
				}
			}
			var obj;
			for(obj in this.that) { 
				if (obj=='game' || obj=='UnitTest' ) continue;
				if(ALLOWED[obj]) continue;
				// if(obj in SPHERE) continue;
				this.writeResult( "Lingering Global Variable", false, false, obj + ' has been made global! ( use delete('+obj+'); )');
				this.writeResult( "Auto-Deleting Variable", false, false, obj + '='+eval(obj));
				eval("delete("+obj+");");
			}

			if (raised_exception) {
				this.writeResult( "FATAL", current_test, false, raised_exception);
				++this.totalbadfunctions;
			} else if (current_result == null) {
				this.writeResult( "no assertions", current_test, false, "Define some tests!")
				++this.totalbadfunctions;
				if(REQUIREDTESTS[current_test] !== undefined)
					REQUIREDTESTS[current_test] = false;
			} else if (suit_result && !tainted) {
				this.writeResult( "passed", false, true)
				++this.totalgoodfunctions;
			} else if (tainted){
				this.writeResult( "taintcheck", false, false, tainted + ' function(s) are tainted')
				++this.totalbadfunctions;
			} else if(!current_result){
				this.writeResult( "NOTE", current_test, false, "No good assertion results, rewrite either the test or the code! "+suit_result);
				++this.totalbadfunctions;
			}
		}
		ALLOWED = {};
		return this;
	}

	this.showResults = function(){

		var completedRequiredTests = true;
		var test;
		for(test in REQUIREDTESTS){
			if(!REQUIREDTESTS[test]){
				completedRequiredTests = false;
				this.writeResult( "missing test", test, false, "Create Test for '"+test+"'!" );
			}else{
				this.totalgoodRequiredFunctions++;
			}
		}

		this.write("TOTAL goodfunctions", this.totalgoodfunctions);
		this.write("TOTAL badfunctions", this.totalbadfunctions);
		this.write("TOTAL totalRequiredFunctions", this.totalRequiredFunctions);
		this.write("TOTAL totalgoodRequiredFunctions", this.totalgoodRequiredFunctions);
		this.write("TOTAL coverage", Math.round(this.totalgoodRequiredFunctions/this.totalRequiredFunctions*100)+"%" );
		this.write("TOTAL succeeded", this.totalsucceeded);
		this.write("TOTAL fails", this.totalfailed);

		return this.totalfailed;
	};

	this.writeResult = function(testtype,testname,result, errmsg){
		if(!testname) testname ="";
		if(testname) testname = "(" + testname + ")";
		if(result){
			this.totalsucceeded++;
			if(!this.onErrorsOnly)
				this.write(current_test+testname,testtype+" OK");
		}else{
			this.totalfailed++;
			this.write("*"+current_test+testname,testtype+" FAIL: "+errmsg);
		}
		return this;
	}

	function listPropertiesAndFunctions(obj) {
		var rv = [];
		var yy;
		for (yy in obj) rv.push(yy);
		return rv;
	}

	function listProperties(obj) {
		var rv = [];
		var yy;
		for (yy in obj)
			if(TypeOf(obj[yy]) != 'function')
				rv.push(yy);
		return rv;
	}

	function make_set(array) {
		var rv = {};
		var uu;
		for(uu = 0; uu < array.length; ++uu)
			rv[array[uu]] = 1;
		return rv;
	}    

	function all_in_set(set, items) {
		var k;
		for(k = 0; k < items.length; ++k)
			if (!set[items[k]]) return false;
		return true;
	}


	function equal_array(a, b) {
		if (UTDEBUG) dtxt = 'equal_array: length'
		if(a.length != b.length) return false;
		var k;
		for(k = 0; k < a.length; ++k){
			if (UTDEBUG) dtxt = 'equal_array: index[k]:' + a[k] + "===" + b[k];
			if(!equal(a[k],b[k])) return false;
		}
		return true;
	}

	function equal_surface(a, b) {
		if (UTDEBUG) dtxt = 'equal_surface: width/height';
		if(a.width != b.width) return false;
		if(a.height != b.height) return false;
		var i,j;
		for(i = 0; i < a.width; ++i)
		for(j = 0; j < a.height; ++j)
			if(!equal(a.getPixel(i,j),b.getPixel(i,j))) return false;
		return true;
	}

	function equal_objectX(a, b, properties, functions) {
		if (UTDEBUG) dtxt = 'equal_objectX: constructor';
		if (a.constructor != b.constructor) return false;
		if(properties !== undefined){
			if (UTDEBUG) dtxt = 'equal_objectX: properties';
			if(properties.length == -1)
				properties = listProperties(a);
			var p;
			for(var k = 0; k < properties.length; ++k) {
				p = properties[k];
				if (UTDEBUG) dtxt = 'equal_object: '+p;
				if(!equal(a[p], b[p])) return false;
					return true;
			}
		}

		for(var k = 0; k < functions.length; ++k) {
			if (UTDEBUG) dtxt = 'equal_objectX: testing function '+functions[k];
			var ra = eval('a.'+functions[k]+"()");
			var rb = eval('b.'+functions[k]+"()");
			if (UTDEBUG) dtxt = 'equal_objectX: testing function '+functions[k]+": ("+ra+")===("+rb+")";
			if(!equal(ra, rb)) return false;
		}
		return true;
	}

	function equal_object(a, b) {
		if (UTDEBUG) dtxt = 'equal_object: constructor';
		if (a.constructor != b.constructor) return false;
		if (UTDEBUG) dtxt = 'equal_object: type';
		if (TypeOf(a) != TypeOf(b)) return false;
		if (UTDEBUG) dtxt = 'equal_object: properties';
		var anames = listPropertiesAndFunctions(a);
		var bnames = listPropertiesAndFunctions(b);
		if (!all_in_set(make_set(anames), bnames)) return false;
		if (!all_in_set(make_set(bnames), anames)) return false;
		var xx,name;
		for (xx in anames) {
			name = anames[xx];
			if (UTDEBUG) dtxt = 'equal_object: '+name;
			if (!equal(a[name], b[name])) return false;
		}
		return true;
	}

	function equal(a, b) {

		if (a === null || a === undefined || a.constructor == Number || a.constructor == String || a.constructor == Function) 
			return (a === b);
		else if (a.constructor == Array && b.constructor == Array)
			return equal_array(a, b);
		else if (TypeOf(a) != TypeOf(b))
			return false;

		switch( TypeOf(a) ) {
			case 'byte_array':
				return equal_array(a, b);
			case 'color':
				return equal_objectX(a, b, ['red','green','blue'], ['toJSON'] );
			case 'sfxr':
				return equal_objectX(a, b, undefined, ["getMasterVolume", "getSoundVolume", "getBitrate", "getSampleRate", "getWaveType", "getBaseFrequency", "getMinFrequency", "getFrequencySlide", "getFrequencySlideDelta", "getSquareDuty", "getSquareDutySweep", "getVibratoDepth", "getVibratoSpeed", "getVibratoDelay", "getAttack", "getSustain", "getDecay", "getRelease", "getFilter", "getLowPassFilterCutoff", "getLowPassFilterCutoffSweep", "getFilterResonance", "getHighPassFilterCutoff", "getHighPassFilterCutoffSweep", "getPhaserOffset", "getPhaserOffsetSweep", "getRepeatSpeed", "getArpeggio", "getArpeggioSpeed" ] );
			case 'colormatrix':
				return equal_objectX(a, b, ["rn","rr","rg","rb","gn","gr","gg","gb","bn","br","bg","bb"], ['toJSON'] );
			case 'image':
				return equal_objectX(a, b, ['width','height'], ['createSurface'] );
			case 'surface':
				return equal_surface(a, b);
			default:
				return equal_object(a, b);
		}
			
	}

	function ToSource(A){
		if(A === null) return 'null';
		if(A === undefined) return 'undefined';
		return A.toSource();
	}

	this.assertEquals = function(A, B, name, label){
		 this.writeResult(label||'assertEquals', name, current_result = equal(A,B), ToSource(A) + "!=" + ToSource(B) );
		 suit_result = current_result && suit_result;
	};

	this.assertNotEquals = function(A, B, name, label){
		 this.writeResult(label||'assertNotEquals', name, current_result = !equal(A,B), ToSource(B) + "=?=" + ToSource(B) );
		 suit_result = current_result && suit_result;
	}

	this.assert = function(A, name, label){
		 this.writeResult(label||'assert', name, current_result = A, ToSource(A) );
		 suit_result = current_result && suit_result;
	}

	this.assertTrue = function(A, name){ return this.assertEquals(A, true, name, 'assertTrue'); }
	this.assertFalse= function(A, name){ return this.assertEquals(A, false, name, 'assertFalse'); }
	this.assertUndefined = function(A, name){ return this.assertEquals(A, undefined, name, 'assertUndefined'); }
	this.assertNotUndefined = function(A, name){ return this.assertNotEquals(A, undefined, name, 'assertNotUndefined'); }
	this.assertNull = function(A, name){ return this.assertEquals(A, null, name, 'assertNull'); }
	this.assertNotNull = function(A, name){ return this.assertNotEquals(A, null, name, 'assertNotNull'); }
	this.assertNot = function(A, name){ return this.assertEquals(!!A, false, name, 'assertNot'); }
	// this.assertNot = function(A, name){ this.writeResult('assertNot', name, !A, ToSource(A) ); } // This also works

	function TypeOf(v){
		if(typeof v !='object')return(typeof v);
		if(v==null)return null;
		var A=v.constructor.toString().match(/function (\w+)/);
		if(A && A[1]!="Object") return A[1];
		A=v.toString().match(/\[object (\w+)\]/);
		if(A) return A[1];
		return typeof(v);
	}

	this.TypeOf = function(v){
		return TypeOf(v);
	}

	this.assertType = function(A, T, name, type){
		if(!type) type = 'assertType';
		this.writeResult(type, name, current_result = this.TypeOf(A) == T, this.TypeOf(A)+"!="+T );
		suit_result = current_result && suit_result;
	}

	this.assertEqualType = function(A, T, name){ return this.assertEquals(this.TypeOf(A), this.TypeOf(T), name, 'assertEqualType') };
	this.assertNotEqualType = function(A, T, name){ return this.assertNotEquals(this.TypeOf(A), this.TypeOf(T), name, 'assertNotEqualType') };
	this.assertNumber = function(A, name){ return this.assertType(A, 'number', name, 'assertNumber');} 
	this.assertInteger = function(A, name){ return (this.assertNumber(A,name) & this.assertEquals(A, Math.floor(A), name, 'assertInteger') ); }
	this.assertString = function(A, name){ return this.assertType(A, 'string', name, 'assertString');} 
	this.assertBoolean = function(A, name){ return this.assertType(A, 'boolean', name, 'assertBoolean');} 

	// misleading, we should also check all properties are there too (it now only checks typeof). TODO
	this.assertSfxr      = function(A, name){ return this.assertEquals(this.TypeOf(A), 'sfxr', name); };
	this.assertByteArray = function(A, name){ return this.assertEquals(this.TypeOf(A), 'byte_array', name); };
	this.assertColor     = function(A, name){ return this.assertEquals(this.TypeOf(A), 'color', name); };

	this.assertAprox     = function(A,B,delta,name){ return this.assert((A>=B-delta)&&(A<=B+delta), 'assertAprox'); };

	// Running .setup() with a function parameter will set it and without parameters it will be executed.
	this.SETUP_F = function(){};
	this.setup = function(f){
		if(f)
			this.SETUP_F = f; 
		else
			this.SETUP_F(); 
		return this;
	}

	// same goes for .teardown() and they are chainable.
	this.TEARDOWN_F = function(){};
	this.teardown = function(f){
		if(f)
			this.TEARDOWN_F = f;
		else
			this.TEARDOWN_F();
		return this;
	};

	this.that = {};

	this.setThat = function(that){ this.that = that; return this; };

	var action;
	for (action in init){
		if( (typeof this[action] == 'function') && (typeof init[action] == 'object') )
			this[action].apply(this, init[action]);
		else
			this[action] = init[action];
	}

	if(this.deletelog)
		RemoveFile("logs/"+this.logfile);
	this.loghandle = OpenLog(this.logfile);

	return true;
}
