(function(){
	
	var $ = function(id) {
		return document.getElementById(id);
	};
	
	// create an element
	var $E = function(tag, properties, children) {
//try {
		var el = document.createElement(tag);

		// set any properties
		for (var prop in properties || {}) {
			el[prop] = properties[prop];
		}
		// append a single dom node
		if (children && children.nodeType == 1) {
			el.appendChild(children);
		
		// append an array of dom nodes
		} else if (children && children.constructor === Array) {
			for (var i = 0, len = children.length; i < len; i++) {
        el.appendChild(children[i]);
			}
		// set innerHTML
		} else if (typeof children === 'string' || typeof children === 'number') {
			el.innerHTML = children;
		}
		return el;
//} catch (e) { console.log([tag, properties, children, e]); }			
	};
	
	// build a human-readable string representing the given variable
	var inspect = function(value, level) {
		var result, level = level || 0;
    if (level >= 1) {
      return '';
    }
		// inspect an array
    if (value && value.constructor === Array) {
			// show no more than 5 items
      var newVals = [], max = value.length > 5 ? 5 : value.length;
			// inspect each child
      for (var i = 0, len = max; i < len; i++) {
        newVals.push(inspect(value[i]), ++level);
      }
      result = '[' + newVals.join(',') + (value.length > 5 ? ',...' : '') + ']';
    }
		// inspect a dom element
    else if (value && value.nodeType == 1) {
			// show tagName, id, and class
      result = '<' + value.tagName.toLowerCase() + 
        (value.id ? ' id="' + value.id + '"' : '') +
        (value.className ? ' class="' + value.className + '"' : '') + '>';
    }
		// cast to string
		result = String(value);
		// inspect properties
		/*if (typeof value != 'string' && (/object/i).test(result)) {
			// show up to 5 properties
			var newVals = [], i = 1;
			for (var prop in value) {
				newVals.push(prop + ':' + inspect(value[prop]), ++level);
				if (++i > 5) {
					break;
				}
			}
			result = '{' + newVals.join(',') + (i > 5 ? ',...' : '') + '}';
		} else */if (typeof value == 'string') {
			// surround strings with single quotes
			result = "'" + result + "'";
		}
		// limit to 300 characters and escape HTML
		return result.length > 300 ? escapeHTML(result.substr(0, 300)) + '...' : escapeHTML(result);
	};

  var escapeHTML = function(string) {
		return string
			.replace(/&/g, '&amp;')                                        
    	.replace(/>/g, '&gt;')                                        
    	.replace(/</g, '&lt;')                                         
    	.replace(/"/g, '&quot;');
  };
	
	var saveResult = function(passed, args) {
		var name = this.currentTestName;
		if (passed) {
	  	this.assertions.passed++;
			this.assertionsPassed++;
			if (args && args[2]) { // save description if given
				this.assertions.push(args[2]);
			}
	  } else {
			this.assertions.failed++;
			this.assertionsFailed++;
			this.assertions.push(args);
		}		
	};
	
	var writeContainers = function(div) {			
		this.tbody = $E('tbody', null, $E('tr', null, [
			$E('th', {className: 'name'}, 'Test Name'),
			$E('th', {className: 'passed'}, 'Passed'),
			$E('th', {className: 'failed'}, 'Failed'),
			$E('th', {className: 'messages'}, 'Result Messages')
		]));
		this.summary = $E('span', {id: 'summary'}, 'RUNNING');
		div.appendChild($E('p', {id : 'summary-frame'}, this.summary));
		div.appendChild($E('table', {id: 'unit', cellSpacing: '0'}, this.tbody));
	}

	var writeResultRow = function(name, exception) {
		var assertions = this.assertions;
		var lis = [], nameContent = [], messageContents = [];
		// build name content
		if (assertions.passed > 0) {
			for (var i = 0, len = assertions.length; i < len; i++) {
				if (typeof assertions[i] === 'string' && assertions[i].length) {
			// pass with message!
					lis.push($E('li', null, assertions[i]));
				}
			}
		}
		if (lis.length) {
			nameContent.push($E('h3', {className: 'expandable', onclick: function() { pulp.unit.togglePassed(this) }}, [
				$E('span', null, '&#x25B7;'),
				$E('span', null, ' ' + name)
			]));
			var ul = $E('ul', null, lis);
			ul.style.display = 'none';
			nameContent.push(ul);
		} else {
			nameContent.push($E('h3', null, '&#x25CB; ' + name));
		}
		// build message content
		if (assertions.failed > 0) {
			lis = [];
			for (i = 0, len = assertions.length; i < len; i++) {
				if (typeof assertions[i] != 'string') {
					// failed
					expected = assertions[i][0];
					actual = assertions[i][1];
					description = assertions[i][2];
					lis.push($E('li', null, 
						(description ? description + '; ' : '') + 'EXPECTED: &#8220;' + inspect(expected) + 
						'&#8221; ACTUAL: &#8220;' + inspect(actual) + '&#8221;'
					));
				}
			}
			if (exception) {
				lis.push($E('li', null, '(EXCEPTION) ' + exception.name + ': ' + exception.message + ' (' + exception.line + ')'));
			}
			messageContents.push($E('ul', null, lis));
		}
		var bench = assertions.benchmarks;
		if (bench.length) {
			for (var i = 0, len = bench.length; i < len; i++) {
				messageContents.push($E('p', {className: 'benchmark'}, [
					$E('span', null, 'BENCHMARK: &#8220;' + escapeHTML(bench[i][2]) + '&#8221; over ' + bench[i][1] + 's '),
					$E('input', {onclick: (function(fn, seconds, description) {
						if (typeof fn === 'function') {
							// single benchmark
							var run = runBenchmark;
							var write = writeBenchmarkResult;
						} else {
							// hash of functions to compare
							var run = runBenchmarkCompare;
							var write = writeBenchmarkCompareResult;							
						}
						return function(event) {
							event = event || window.event;
							var btn = event.target || event.srcElement;
							btn.disabled = true;
							btn.value = '...';
							// TODO: force dom refresh
							var result = run(fn, seconds, description);
							write(btn, result);
							btn.disabled = false;
							btn.value = 'Run';								
						};
					})(bench[i][0], bench[i][1], bench[i][2]), value: 'Run', type: 'button'})
				]));
			}
		}
		var info = assertions.info;
		if (info.length) {
			for (var i = 0, len = info.length; i < len; i++) {
				messageContents.push($E('p', {className: 'info'}, 'INFO: ' + escapeHTML(info[i])));
			}
		}		

		this.tbody.appendChild($E('tr', {className: (assertions.failed > 0 ? 'failed' : 'passed')}, [
			$E('td', {className: 'name'}, nameContent),
			$E('td', {className: 'passed'}, assertions.passed),
			$E('td', {className: 'failed'}, assertions.failed),
			$E('td', {className: 'messages'}, messageContents.length ? messageContents : '&nbsp;')
		]));
  };

	var writeBenchmarkResult = function(btn, time) {
		btn.parentNode.appendChild($E('span', null, ' (' + (time.toFixed(2)) + 'ms) ')); 
	};
	
	var writeBenchmarkResult = function(btn, results) {
		var report = ' (', min = false, ms;
		// find quickest
		for (var testName in results) {
			ms = results[testName];
			if (min === false || min < ms) {
				min = ms;
			}
		}
		// find winner
		for (var testName in results) {
			if (min == results[testName]) {
				result += 'winner=' + escapeHTML(testName) + ' at ' + time.toFixed(2) + 'ms; ';
				break;
			}
		}
		
		// show quickness as a percent of min
		var percent;
		for (var testName in results) {
			if (min != results[testName]) {
				percent = results[testName] / min;
				result += escapeHTML(testName) + '=' + percent.toFixed(1) + '%; ';
			}
		}		
		//report = report.slice(0, -2) + ') ';
		btn.parentNode.appendChild($E('span', null, report)); 
	};	
	
	var writeSummary = function() {
		var totalAssertions = this.assertionsPassed + this.assertionsFailed;
		var totalTests = this.testsPassed + this.testsFailed;
		this.summary.className = (this.testsFailed ? 'failed' : 'passed');
		this.summary.innerHTML = 'TESTS: Passed ' + this.testsPassed + '/' + totalTests + 
			'; ASSERTIONS: Passed ' + this.assertionsPassed + '/' + totalAssertions;
	};
		
	var runBenchmark = function(fn, seconds) {
		var begin = new Date().getTime();
		var end = begin + (seconds * 1000);
		var i = 0, start, now;
		while (1) {
			i++;
			fn();
			now = new Date().getTime();
			if (now >= end) {
				break;
			}
		}
		return (now - begin) / i;
	};
	
	var runBenchmarkCompare = function(fns, seconds, description) {
		var results = {};
		for (var testName in fns) {
			results[testName] = runBenchmark(fns[testName], seconds); 
		}
		return results;
	};		
	
	pulp.unit = function(resultTable, test) {
		return new pulp.Unit(resultTable, test);
	};
	
	pulp.extend(pulp.unit, {
		togglePassed: function(h3) {
			var symbol = h3.getElementsByTagName('span')[0];
			var isOpen = symbol.innerHTML == '\u25BC'
			symbol.innerHTML = (isOpen ? '&#x25B7;' : '&#x25BC;');
			h3.parentNode.getElementsByTagName('ul')[0].style.display = (isOpen ? 'none' : '');
		},
		inspect: inspect
	});
	
	pulp.Unit = pulp.Class.create({
		getPrivate: function(varName) {
			return eval(varName);
		},
		initialize: function(tests) {
			this.tests = tests;
			this.running = false;
		},
		run: function(div) {
			var node = $(div || 'results');
			if (!node) {
				throw new Error('pulp.unit: unable to find element with id "' + div + '" to write unit test results');
			}
			if (this.running) {
		  	return;
		  }
			this.running = true;
			
			writeContainers.call(this, node);
			
			this.assertionsPassed = 0;
			this.assertionsFailed = 0;
			this.testsPassed = 0;
			this.testsFailed = 0;
			for (var name in this.tests) {
				if (typeof this.tests[name] === 'function') {			
					this.assertions = [];
					this.assertions.benchmarks = [];
					this.assertions.info = [];
					this.assertions.passed = 0;
					this.assertions.failed = 0;
//				try {
						this.tests[name].call(this);
						writeResultRow.call(this, name);
						if (this.assertions.failed == 0) {
							this.testsPassed++;
						} else {
							this.testsFailed++;
						}
//				} catch(e) {
//					this.testsFailed++;
//					writeResultRow.call(this, name, e);
//throw e;					
//console.log(e);          
//				}
				} else {
					this.startSection(name);
				}					
			}
			writeSummary.call(this);
//console.log(this.benchmarks);
			this.running = false;			
		},
		startSection: function(name) {
			this.tbody.appendChild($E('tr', null, $E('td', {
				colSpan: '4',
				className: 'section'
			}, escapeHTML(name))));
		},
		info: function(text) {
			this.assertions.info.push(text);
		},
		benchmark: function(fn, seconds, description) {			
			this.assertions.benchmarks.push([fn, seconds, description || fn.toString()]);
		},
		assertEqual: function(expected, actual, description) {
			saveResult.call(this, expected == actual, arguments);
		},
		assertNotEqual: function(expected, actual, description) {
			saveResult.call(this, expected != actual, arguments);
		},
		assertIdentical: function(expected, actual, description) {
			saveResult.call(this, expected === actual, arguments);
		},
		assertNotIdentical: function(expected, actual, description) {
			saveResult.call(this, expected !== actual, arguments);
		},
		assert: function(actual, description) {
			saveResult.call(this, !!actual, [true, actual, description]);
		},
		assertUndefined: function(actual, description) {
			saveResult.call(this, actual === undefined, [undefined, actual, description]);
		},		
		assertEnumEqual: function(expected, actual, description) {
			if (expected.length == actual.length) {
				var same = true;
				try {
					for (var i = 0, len = expected.length; i < len; i++) {
						if (expected[i] != actual[i]) {
							same = false;
							break;
						}
					}
				} catch(e) {
					same = false;
				}
			} else {
				var same = false;
			}
			saveResult.call(this, same, [expected, actual, description]);
		},
		assertEnumIdentical: function(expected, actual, description) {
			if (expected.length == actual.length) {
				var same = true;
				try {
					for (var i = 0, len = expected.length; i < len; i++) {
						if (expected[i] !== actual[i]) {
							same = false;
							break;
						}
					}
				} catch(e) {
					same = false;
				}
			} else {
				var same = false;
			}
			saveResult.call(this, same, [expected, actual, description]);
		},		
    assertEnumTestFunction: function(array, testFn, description) {
    	try {
		    var pass = true;
				for (var i = 0, len = array.length; i < len; i++) {
					if (testFn.call(this, array[i], i, array) == false) {
		        pass = false;
		        break;
		      }
		    }
			} catch(e) {
				pass = false;
			}
      saveResult.call(this, pass, [true, pass, description]);
    },
		assertInspectEqual: function(expected, actual, description) {
			saveResult.call(this, inspect(expected) == inspect(actual), arguments);
		},
		assertPropertiesEqual: function(expected, actual, description) {
			var actualClone = pulp.extend({}, actual);
			var same = true;
			for (var prop in expected) {
				if (expected[prop] == actualClone[prop]) {
					delete actualClone[prop];
				} else {
					same = false;
					break;
				}
			}
			if (same) {
				for (var prop in actualClone) {
					same = false;
				}
			}
			saveResult.call(this, same, arguments);
		},
		assertPropertiesMatch: function(expected, actual, description) {
			var same = true;
			for (var prop in expected) {
				if (expected[prop] != actual[prop]) {
					same = false;
					break;
				}
			}
			saveResult.call(this, same, arguments);
		}
	});
	
})();
