package com.mockobjects.dynamic;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

import junit.framework.AssertionFailedError;

import com.mockobjects.Verifiable;
import com.mockobjects.constraint.Constraint;

public class Mock implements InvocationHandler, Verifiable {
	private String name;
	private Object proxy;
	private CallFactory callFactory;
	private CallableAddable callSequence;

	public Mock(CallFactory callFactory, CallableAddable callableAddable, Class mockedClass, String name) {
		this.name = name;
		this.callFactory = callFactory;
		this.callSequence = callableAddable;
		this.proxy = Proxy.newProxyInstance(getClass().getClassLoader(), new Class[] { mockedClass }, this);
	}

	public Mock(Class mockedClass, String nonDefaultName) {
		this(new DefaultCallFactory(), new CallBag(), mockedClass, nonDefaultName);
	}

	public Mock(Class mockedClass) {
		this(mockedClass, mockNameFromClass(mockedClass));
	}

    public void reset() {
        this.callSequence.reset();
    }
    
	public static String mockNameFromClass(Class c) {
		return "mock" + className(c);
	}
	
	public static String className(Class c) {
		String name = c.getName();
		int dotIndex = name.lastIndexOf('.');

		if (dotIndex >= 0) {
			return name.substring(dotIndex + 1);
		} else {
			return name;
		}
	}

	private ConstraintMatcher createConstraintMatcher(Object constraintArg) {
		// Can't overload this method as callee had an Object parameter, and java
		// doesn't do a secondary dispatch on the true underlying type
		
		if (constraintArg instanceof Constraint[]) {
			// to support possible legacy usage of new Contraint[] {...}
			return new FullConstraintMatcher((Constraint[])constraintArg);
		} else if (constraintArg instanceof Constraint) {
			// to support usage of C.lt(5) type constraints 
			return C.args((Constraint)constraintArg);
		} else {
			// normal usage of the overloaded expect/match object parameter
			return C.args(C.eq(constraintArg));
		}
	}
	
	public String getMockName() {
		return this.name;
	}
	
	public String toString() {
		return this.name;
	}

	public Object proxy() {
		return this.proxy;
	}

	public Object invoke(Object proxy, Method method, Object[] args)
		throws Throwable {
		try {
			if (isCheckingEqualityOnProxy(method, args)) {
				return new Boolean(args[0] == this.proxy);
			} else if (isMockNameGetter(method, args)) {
				return this.getMockName();
			} else {
				return callSequence.call(this, method.getName(), (args == null ? new Object[0] : args));
			}
		} catch (AssertionFailedError ex) {
			throw new AssertionFailedError(name + ": " + ex.getMessage());
		}
	}

	private boolean isCheckingEqualityOnProxy(Method method, Object[] args) {
		return (method.getName().equals("equals")) && (args.length == 1) && (Proxy.isProxyClass(args[0].getClass()));
	}
	
	private boolean isMockNameGetter(Method method, Object[] args) {
		return (method.getName().equals("getMockName")) && (args.length == 0);
	}

	public void verify() {
		try {
			callSequence.verify();
		} catch (AssertionFailedError ex) {
			throw new AssertionFailedError(name + ": " + ex.getMessage());
		}
	}

	public void expect(String methodName) {
		expect(methodName, C.NO_ARGS);
	}
	
	public void expect(String methodName, Object singleEqualArg) {
		expect(methodName, createConstraintMatcher(singleEqualArg));
	}
		
	public void expect(String methodName, ConstraintMatcher args) {
		callSequence.addExpect(callFactory.createCallExpectation(callFactory.createCallSignature(methodName, args, callFactory.createVoidStub())));
	}
	
	public void expectAndReturn(String methodName, Object result) {
		this.expectAndReturn(methodName, C.NO_ARGS, result);
	}
	
	public void expectAndReturn(String methodName, boolean result) {
		this.expectAndReturn(methodName, new Boolean(result));
	}
	
	public void expectAndReturn(String methodName, int result) {
		this.expectAndReturn(methodName, new Integer(result));
	}

	public void expectAndReturn(String methodName, Object singleEqualArg, Object result) {
		this.expectAndReturn(methodName, createConstraintMatcher(singleEqualArg), result);
	}
	
	public void expectAndReturn(String methodName, Object singleEqualArg, boolean result) {
		this.expectAndReturn(methodName, singleEqualArg, new Boolean(result));
	}
	
	public void expectAndReturn(String methodName, Object singleEqualArg, int result) {
		this.expectAndReturn(methodName, singleEqualArg, new Integer(result));
	}
	
	public void expectAndReturn(String methodName, ConstraintMatcher args, Object result) {
		callSequence.addExpect(callFactory.createCallExpectation(callFactory.createCallSignature(methodName, args, callFactory.createReturnStub(result))));
	}
	
	public void expectAndReturn(String methodName, ConstraintMatcher args, boolean result) {
		this.expectAndReturn(methodName, args, new Boolean(result));	
	}
	
	public void expectAndReturn(String methodName, ConstraintMatcher args, int result) {
		this.expectAndReturn(methodName, args, new Integer(result));	
	}

	public void expectAndThrow(String methodName, Throwable exception) {
		this.expectAndThrow(methodName, C.NO_ARGS, exception);
	}
	
	public void expectAndThrow(String methodName, Object singleEqualArg, Throwable exception) {
		this.expectAndThrow(methodName, createConstraintMatcher(singleEqualArg), exception);
	}
	
	public void expectAndThrow(String methodName, ConstraintMatcher args, Throwable exception) {
		callSequence.addExpect(callFactory.createCallExpectation(callFactory.createCallSignature(methodName, args, callFactory.createThrowStub(exception))));
	}
	
	public void matchAndReturn(String methodName, Object result) {
		this.matchAndReturn(methodName, C.NO_ARGS, result);
	}
	
	public void matchAndReturn(String methodName, boolean result) {
		this.matchAndReturn(methodName, new Boolean(result));
	}
	
	public void matchAndReturn(String methodName, int result) {
		this.matchAndReturn(methodName, new Integer(result));
	}
	
	public void matchAndReturn(String methodName, Object singleEqualArg, Object result) {
		this.matchAndReturn(methodName, createConstraintMatcher(singleEqualArg), result);
	}
	
	public void matchAndReturn(String methodName, boolean singleEqualArg, Object result) {
		this.matchAndReturn(methodName, new Boolean(singleEqualArg), result);
	}
	
	public void matchAndReturn(String methodName, int singleEqualArg, Object result) {
		this.matchAndReturn(methodName, new Integer(singleEqualArg), result);
	}
	
	public void matchAndReturn(String methodName, Object singleEqualArg, boolean result) {
		this.matchAndReturn(methodName, singleEqualArg, new Boolean(result));
	}
	
	public void matchAndReturn(String methodName, Object singleEqualArg, int result) {
		this.matchAndReturn(methodName, singleEqualArg, new Integer(result));
	}
	
	public void matchAndReturn(String methodName, ConstraintMatcher args, Object result) {
		callSequence.addMatch(callFactory.createCallSignature(methodName, args, callFactory.createReturnStub(result)));
	}
	
	public void matchAndReturn(String methodName, ConstraintMatcher args, boolean result) {
		this.matchAndReturn(methodName, args, new Boolean(result));
	}

	public void matchAndReturn(String methodName, ConstraintMatcher args, int result) {
		this.matchAndReturn(methodName, args, new Integer(result));
	}
	
	public void matchAndThrow(String methodName, Throwable throwable) {
		this.matchAndThrow(methodName, C.NO_ARGS, throwable);
	}
	
	public void matchAndThrow(String methodName, Object singleEqualArg, Throwable throwable) {
		this.matchAndThrow(methodName, createConstraintMatcher(singleEqualArg), throwable);
	}
	
	public void matchAndThrow(String methodName, boolean singleEqualArg, Throwable throwable) {
		this.matchAndThrow(methodName,new Boolean(singleEqualArg), throwable);
	}
	
	public void matchAndThrow(String methodName, int singleEqualArg, Throwable throwable) {
		this.matchAndThrow(methodName,new Integer(singleEqualArg), throwable);
	}
	
	public void matchAndThrow(String methodName, ConstraintMatcher args, Throwable throwable) {
		callSequence.addMatch(callFactory.createCallSignature(methodName, args, callFactory.createThrowStub(throwable)));
	}
		
	/** @deprecated @see OrderedMock
	*/
	public void expect(String methodName, CallSequence deprecated) {
		throw new AssertionFailedError("method is deprecated! Use: new OrderedMock() instead...");
	}
	
	/** @deprecated @see OrderedMock
	*/
	public void expectAndReturn(String methodName, CallSequence deprecated, Object result) {
		throw new AssertionFailedError("method is deprecated! Use: new OrderedMock() instead...");
	}
	
	/** @deprecated @see OrderedMock
	*/
	public void expectAndThrow(String methodName, CallSequence deprecated, Throwable throwable) {
		throw new AssertionFailedError("method is deprecated! Use: new OrderedMock() instead...");
	}
	
	/** @deprecated @see expect
	 */
	public void expectVoid(String methodName, ConstraintMatcher args) {
		this.expect(methodName, args);
	}
	
	/** @deprecated @see expect
	 */
	public void expectVoid(String methodName, Object equalArg) {
		this.expect(methodName,equalArg);
	}
	
	/** @deprecated @see expect
	*/
	public void expectVoid(String methodName) {
		this.expect(methodName);
	}
	
	/** @deprecated Not required, as if methodName is called, you will get a an exception
	*/
	public void expectNotCalled(String methodName) {
	}
}
