text: /**
   * Mock4JS 0.2
   * http://mock4js.sourceforge.net/
   */
  
  Mock4JS = {
  	_mocksToVerify: [],
  	_convertToConstraint: function(constraintOrValue) {
  		if(constraintOrValue.argumentMatches) {
  			return constraintOrValue; // it's already an ArgumentMatcher
  		} else {
  			return new MatchExactly(constraintOrValue);	// default to eq(...)
  		}
  	},
  	addMockSupport: function(object) {
  		// mock creation
  		object.mock = function(mockedType) {
  			if(!mockedType) {
  				throw new Mock4JSException("Cannot create mock: type to mock cannot be found or is null");
  			}
  			var newMock = new Mock(mockedType);
  			Mock4JS._mocksToVerify.push(newMock);
  			return newMock;
  		}
  
  		// syntactic sugar for expects()
  		object.once = function() {
  			return new CallCounter(1);
  		}
  		object.never = function() {
  			return new CallCounter(0);
  		}
  		object.exactly = function(expectedCallCount) {
  			return new CallCounter(expectedCallCount);
  		}
  		object.atLeastOnce = function() {
  			return new InvokeAtLeastOnce();
  		}
  		
  		// syntactic sugar for argument expectations
  		object.ANYTHING = new MatchAnything();
  		object.NOT_NULL = new MatchAnythingBut(new MatchExactly(null));
  		object.NOT_UNDEFINED = new MatchAnythingBut(new MatchExactly(undefined));
  		object.eq = function(expectedValue) {
  			return new MatchExactly(expectedValue);
  		}
  		object.not = function(valueNotExpected) {
  			var argConstraint = Mock4JS._convertToConstraint(valueNotExpected);
  			return new MatchAnythingBut(argConstraint);
  		}
  		object.and = function() {
  			var constraints = [];
  			for(var i=0; i<arguments.length; i++) {
  				constraints[i] = Mock4JS._convertToConstraint(arguments[i]);
  			}
  			return new MatchAllOf(constraints);
  		}
  		object.or = function() {
  			var constraints = [];
  			for(var i=0; i<arguments.length; i++) {
  				constraints[i] = Mock4JS._convertToConstraint(arguments[i]);
  			}
  			return new MatchAnyOf(constraints);
  		}
  		object.stringContains = function(substring) {
  			return new MatchStringContaining(substring);
  		}
  		
  		// syntactic sugar for will()
  		object.returnValue = function(value) {
  			return new ReturnValueAction(value);
  		}
  		object.throwException = function(exception) {
  			return new ThrowExceptionAction(exception);
  		}
  	},
  	clearMocksToVerify: function() {
  		Mock4JS._mocksToVerify = [];
  	},
  	verifyAllMocks: function() {
  		for(var i=0; i<Mock4JS._mocksToVerify.length; i++) {
  			Mock4JS._mocksToVerify[i].verify();
  		}
  	}
  }
  
  Mock4JSUtil = {
  	hasFunction: function(obj, methodName) {
  		return typeof obj == 'object' && typeof obj[methodName] == 'function';
  	},
  	join: function(list) {
  		var result = "";
  		for(var i=0; i<list.length; i++) {
  			var item = list[i];
  			if(Mock4JSUtil.hasFunction(item, "describe")) {
  				result += item.describe();
  			}
  			else if(typeof list[i] == 'string') {
  				result += "\""+list[i]+"\"";
  			} else {
  				result += list[i];
  			}
  			
  			if(i<list.length-1) result += ", ";
  		}
  		return result;
  	}	
  }
  
  Mock4JSException = function(message) {
  	this.message = message;
  }
  
  Mock4JSException.prototype = {
  	toString: function() {
  		return this.message;
  	}
  }
  
  /**
   * Assert function that makes use of the constraint methods
   */ 
  assertThat = function(expected, argumentMatcher) {
  	if(!argumentMatcher.argumentMatches(expected)) {
  		throw new Mock4JSException("Expected '"+expected+"' to be "+argumentMatcher.describe());
  	}
  }
  
  /**
   * CallCounter
   */
  function CallCounter(expectedCount) {
  	this._expectedCallCount = expectedCount;
  	this._actualCallCount = 0;
  }
  
  CallCounter.prototype = {
  	addActualCall: function() {
  		this._actualCallCount++;
  		if(this._actualCallCount > this._expectedCallCount) {
  			throw new Mock4JSException("unexpected invocation");
  		}
  	},
  	
  	verify: function() {
  		if(this._actualCallCount < this._expectedCallCount) {
  			throw new Mock4JSException("expected method was not invoked the expected number of times");
  		}
  	},
  	
  	describe: function() {
  		if(this._expectedCallCount == 0) {
  			return "not expected";
  		} else if(this._expectedCallCount == 1) {
  			var msg = "expected once";
  			if(this._actualCallCount >= 1) {
  				msg += " and has been invoked";
  			}
  			return msg;
  		} else {
  			var msg = "expected "+this._expectedCallCount+" times";
  			if(this._actualCallCount > 0) {
  				msg += ", invoked "+this._actualCallCount + " times";
  			}
  			return msg;
  		}
  	}
  }
  
  function InvokeAtLeastOnce() {
  	this._hasBeenInvoked = false;
  }
  
  InvokeAtLeastOnce.prototype = {
  	addActualCall: function() {
  		this._hasBeenInvoked = true;
  	},
  	
  	verify: function() {
  		if(this._hasBeenInvoked === false) {
  			throw new Mock4JSException(describe());
  		}
  	},
  	
  	describe: function() {
  		var desc = "expected at least once";
  		if(this._hasBeenInvoked) desc+=" and has been invoked";
  		return desc;
  	}
  }
  
  /**
   * ArgumentMatchers
   */
  
  function MatchExactly(expectedValue) {
  	this._expectedValue = expectedValue;
  }
  
  MatchExactly.prototype = {
  	argumentMatches: function(actualArgument) {
  		if(this._expectedValue instanceof Array) {
  			if(!(actualArgument instanceof Array)) return false;
  			if(this._expectedValue.length != actualArgument.length) return false;
  			for(var i=0; i<this._expectedValue.length; i++) {
  				if(this._expectedValue[i] != actualArgument[i]) return false;
  			}
  			return true;
  		} else {
  			return this._expectedValue == actualArgument;
  		}
  	},
  	describe: function() {
  		if(typeof this._expectedValue == "string") {
  			return "eq(\""+this._expectedValue+"\")";
  		} else {
  			return "eq("+this._expectedValue+")";
  		}
  	}
  }
  
  function MatchAnything() {
  }
  
  MatchAnything.prototype = {
  	argumentMatches: function(actualArgument) {
  		return true;
  	},
  	describe: function() {
  		return "ANYTHING";
  	}
  }
  
  function MatchAnythingBut(matcherToNotMatch) {
  	this._matcherToNotMatch = matcherToNotMatch;
  }
  
  MatchAnythingBut.prototype = {
  	argumentMatches: function(actualArgument) {
  		return !this._matcherToNotMatch.argumentMatches(actualArgument);
  	},
  	describe: function() {
  		return "not("+this._matcherToNotMatch.describe()+")";
  	}
  }
  
  function MatchAllOf(constraints) {
  	this._constraints = constraints;
  }
  
  
  MatchAllOf.prototype = {
  	argumentMatches: function(actualArgument) {
  		for(var i=0; i<this._constraints.length; i++) {
  			var constraint = this._constraints[i];
  			if(!constraint.argumentMatches(actualArgument)) return false;
  		}
  		return true;
  	},
  	describe: function() {
  		return "and("+Mock4JSUtil.join(this._constraints)+")";
  	}
  }
  
  function MatchAnyOf(constraints) {
  	this._constraints = constraints;
  }
  
  MatchAnyOf.prototype = {
  	argumentMatches: function(actualArgument) {
  		for(var i=0; i<this._constraints.length; i++) {
  			var constraint = this._constraints[i];
  			if(constraint.argumentMatches(actualArgument)) return true;
  		}
  		return false;
  	},
  	describe: function() {
  		return "or("+Mock4JSUtil.join(this._constraints)+")";
  	}
  }
  
  
  function MatchStringContaining(stringToLookFor) {
  	this._stringToLookFor = stringToLookFor;
  }
  
  MatchStringContaining.prototype = {
  	argumentMatches: function(actualArgument) {
  		if(typeof actualArgument != 'string') throw new Mock4JSException("stringContains() must be given a string, actually got a "+(typeof actualArgument));
  		return (actualArgument.indexOf(this._stringToLookFor) != -1);
  	},
  	describe: function() {
  		return "a string containing \""+this._stringToLookFor+"\"";
  	}
  }
  
  
  /**
   * StubInvocation
   */
  function StubInvocation(expectedMethodName, expectedArgs, actionSequence) {
  	this._expectedMethodName = expectedMethodName;
  	this._expectedArgs = expectedArgs;
  	this._actionSequence = actionSequence;
  }
  
  StubInvocation.prototype = {
  	matches: function(invokedMethodName, invokedMethodArgs) {
  		if (invokedMethodName != this._expectedMethodName) {
  			return false;
  		}
  		
  		if (invokedMethodArgs.length != this._expectedArgs.length) {
  			return false;
  		}
  		
  		for(var i=0; i<invokedMethodArgs.length; i++) {
  			var expectedArg = this._expectedArgs[i];
  			var invokedArg = invokedMethodArgs[i];
  			if(!expectedArg.argumentMatches(invokedArg)) {
  				return false;
  			}
  		}
  		
  		return true;
  	},
  	
  	invoked: function() {
  		try {
  			return this._actionSequence.invokeNextAction();
  		} catch(e) {
  			if(e instanceof Mock4JSException) {
  				throw new Mock4JSException(this.describeInvocationNameAndArgs()+" - "+e.message);
  			} else {
  				throw e;
  			}
  		}
  	},
  	
  	will: function() {
  		this._actionSequence.addAll.apply(this._actionSequence, arguments);
  	},
  	
  	describeInvocationNameAndArgs: function() {
  		return this._expectedMethodName+"("+Mock4JSUtil.join(this._expectedArgs)+")";
  	},
  	
  	describe: function() {
  		return "stub: "+this.describeInvocationNameAndArgs();
  	},
  	
  	verify: function() {
  	}
  }
  
  /**
   * ExpectedInvocation
   */
  function ExpectedInvocation(expectedMethodName, expectedArgs, expectedCallCounter) {
  	this._stubInvocation = new StubInvocation(expectedMethodName, expectedArgs, new ActionSequence());
  	this._expectedCallCounter = expectedCallCounter;
  }
  
  ExpectedInvocation.prototype = {
  	matches: function(invokedMethodName, invokedMethodArgs) {
  		try {
  			return this._stubInvocation.matches(invokedMethodName, invokedMethodArgs);
  		} catch(e) {
  			throw new Mock4JSException("method "+this._stubInvocation.describeInvocationNameAndArgs()+": "+e.message);
  		}
  	},
  	
  	invoked: function() {
  		try {
  			this._expectedCallCounter.addActualCall();
  		} catch(e) {
  			throw new Mock4JSException(e.message+": "+this._stubInvocation.describeInvocationNameAndArgs());
  		}
  		return this._stubInvocation.invoked();
  	},
  	
  	will: function() {
  		this._stubInvocation.will.apply(this._stubInvocation, arguments);
  	},
  	
  	describe: function() {
  		return this._expectedCallCounter.describe()+": "+this._stubInvocation.describeInvocationNameAndArgs();
  	},
  	
  	verify: function() {
  		try {
  			this._expectedCallCounter.verify();
  		} catch(e) {
  			throw new Mock4JSException(e.message+": "+this._stubInvocation.describeInvocationNameAndArgs());
  		}
  	}
  }
  
  /**
   * MethodActions
   */
  function ReturnValueAction(valueToReturn) {
  	this._valueToReturn = valueToReturn;
  }
  
  ReturnValueAction.prototype = {
  	invoke: function() {
  		return this._valueToReturn;
  	},
  	describe: function() {
  		return "returns "+this._valueToReturn;
  	}
  }
  
  function ThrowExceptionAction(exceptionToThrow) {
  	this._exceptionToThrow = exceptionToThrow;
  }
  
  ThrowExceptionAction.prototype = {
  	invoke: function() {
  		throw this._exceptionToThrow;
  	},
  	describe: function() {
  		return "throws "+this._exceptionToThrow;
  	}
  }
  
  function ActionSequence() {
  	this._ACTIONS_NOT_SETUP = "_ACTIONS_NOT_SETUP";
  	this._actionSequence = this._ACTIONS_NOT_SETUP;
  	this._indexOfNextAction = 0;
  }
  
  ActionSequence.prototype = {
  	invokeNextAction: function() {
  		if(this._actionSequence === this._ACTIONS_NOT_SETUP) {
  			return;
  		} else {
  			if(this._indexOfNextAction >= this._actionSequence.length) {
  				throw new Mock4JSException("no more values to return");
  			} else {
  				var action = this._actionSequence[this._indexOfNextAction];
  				this._indexOfNextAction++;
  				return action.invoke();
  			}
  		}
  	},
  	
  	addAll: function() {
  		this._actionSequence = [];
  		for(var i=0; i<arguments.length; i++) {
  			if(typeof arguments[i] != 'object' && arguments[i].invoke === undefined) {
  				throw new Error("cannot add a method action that does not have an invoke() method");
  			}
  			this._actionSequence.push(arguments[i]);
  		}
  	}
  }
  
  function StubActionSequence() {
  	this._ACTIONS_NOT_SETUP = "_ACTIONS_NOT_SETUP";
  	this._actionSequence = this._ACTIONS_NOT_SETUP;
  	this._indexOfNextAction = 0;
  } 
  
  StubActionSequence.prototype = {
  	invokeNextAction: function() {
  		if(this._actionSequence === this._ACTIONS_NOT_SETUP) {
  			return;
  		} else if(this._actionSequence.length == 1) {
  			// if there is only one method action, keep doing that on every invocation
  			return this._actionSequence[0].invoke();
  		} else {
  			if(this._indexOfNextAction >= this._actionSequence.length) {
  				throw new Mock4JSException("no more values to return");
  			} else {
  				var action = this._actionSequence[this._indexOfNextAction];
  				this._indexOfNextAction++;
  				return action.invoke();
  			}
  		}
  	},
  	
  	addAll: function() {
  		this._actionSequence = [];
  		for(var i=0; i<arguments.length; i++) {
  			if(typeof arguments[i] != 'object' && arguments[i].invoke === undefined) {
  				throw new Error("cannot add a method action that does not have an invoke() method");
  			}
  			this._actionSequence.push(arguments[i]);
  		}
  	}
  }
  
   
  /**
   * Mock
   */
  function Mock(mockedType) {
  	if(mockedType === undefined || mockedType.prototype === undefined) {
  		throw new Mock4JSException("Unable to create Mock: must create Mock using a class not prototype, eg. 'new Mock(TypeToMock)' or using the convenience method 'mock(TypeToMock)'");
  	}
  	this._mockedType = mockedType.prototype;
  	this._expectedCallCount;
  	this._isRecordingExpectations = false;
  	this._expectedInvocations = [];
  
  	// setup proxy
  	var IntermediateClass = new Function();
  	IntermediateClass.prototype = mockedType.prototype;
  	var ChildClass = new Function();
  	ChildClass.prototype = new IntermediateClass();
  	this._proxy = new ChildClass();
  	this._proxy.mock = this;
  	
  	for(property in mockedType.prototype) {
  		if(this._isPublicMethod(mockedType.prototype, property)) {
  			var publicMethodName = property;
  			this._proxy[publicMethodName] = this._createMockedMethod(publicMethodName);
  			this[publicMethodName] = this._createExpectationRecordingMethod(publicMethodName);
  		}
  	}
  }
  
  Mock.prototype = {
  	
  	proxy: function() {
  		return this._proxy;
  	},
  	
  	expects: function(expectedCallCount) {
  		this._expectedCallCount = expectedCallCount;
  		this._isRecordingExpectations = true;
  		this._isRecordingStubs = false;
  		return this;
  	},
  	
  	stubs: function() {
  		this._isRecordingExpectations = false;
  		this._isRecordingStubs = true;
  		return this;
  	},
  	
  	verify: function() {
  		for(var i=0; i<this._expectedInvocations.length; i++) {
  			var expectedInvocation = this._expectedInvocations[i];
  			try {
  				expectedInvocation.verify();
  			} catch(e) {
  				var failMsg = e.message+this._describeMockSetup();
  				throw new Mock4JSException(failMsg);
  			}
  		}
  	},
  	
  	_isPublicMethod: function(mockedType, property) {
  		try {
  			var isMethod = typeof(mockedType[property]) == 'function';
  			var isPublic = property.charAt(0) != "_"; 
  			return isMethod && isPublic;
  		} catch(e) {
  			return false;
  		}
  	},
  
  	_createExpectationRecordingMethod: function(methodName) {
  		return function() {
  			// ensure all arguments are instances of ArgumentMatcher
  			var expectedArgs = [];
  			for(var i=0; i<arguments.length; i++) {
  				if(arguments[i] !== null && arguments[i] !== undefined && arguments[i].argumentMatches) {
  					expectedArgs[i] = arguments[i];
  				} else {
  					expectedArgs[i] = new MatchExactly(arguments[i]);
  				}
  			}
  			
  			// create stub or expected invocation
  			var expectedInvocation;
  			if(this._isRecordingExpectations) {
  				expectedInvocation = new ExpectedInvocation(methodName, expectedArgs, this._expectedCallCount);
  			} else {
  				expectedInvocation = new StubInvocation(methodName, expectedArgs, new StubActionSequence());
  			}
  			
  			this._expectedInvocations.push(expectedInvocation);
  			
  			this._isRecordingExpectations = false;
  			this._isRecordingStubs = false;
  			return expectedInvocation;
  		}
  	},
  	
  	_createMockedMethod: function(methodName) {
  		return function() {
  			// go through expectation list backwards to ensure later expectations override earlier ones
  			for(var i=this.mock._expectedInvocations.length-1; i>=0; i--) {
  				var expectedInvocation = this.mock._expectedInvocations[i];
  				if(expectedInvocation.matches(methodName, arguments)) {
  					try {
  						return expectedInvocation.invoked();
  					} catch(e) {
  						if(e instanceof Mock4JSException) {
  							throw new Mock4JSException(e.message+this.mock._describeMockSetup());
  						} else {
  							// the user setup the mock to throw a specific error, so don't modify the message
  							throw e;
  						}
  					}
  				}
  			}
  			var failMsg = "unexpected invocation: "+methodName+"("+Mock4JSUtil.join(arguments)+")"+this.mock._describeMockSetup();
  			throw new Mock4JSException(failMsg);
  		};
  	},
  	
  	_describeMockSetup: function() {
  		var msg = "\nAllowed:";
  		for(var i=0; i<this._expectedInvocations.length; i++) {
  			var expectedInvocation = this._expectedInvocations[i];
  			msg += "\n" + expectedInvocation.describe();
  		}
  		return msg;
  	}
  }
  