package eu.dnetlib.contract.runner;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.cli.BasicParser;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.OptionBuilder;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.log4j.Logger;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.util.StringUtils;

import eu.dnetlib.contract.ctx.GlobalContractContext;
import eu.dnetlib.contract.ctx.GlobalContractContextPlaceholder;
import eu.dnetlib.contract.event.ExceptionContractEvent;
import eu.dnetlib.contract.event.IContractEvent;
import eu.dnetlib.contract.runner.report.CSVReportGenerator;
import eu.dnetlib.contract.runner.report.ComplexReportGenerator;
import eu.dnetlib.contract.runner.report.IReportGenerator;
import eu.dnetlib.contract.runner.report.ReportGeneratorHelper;
import eu.dnetlib.contract.runner.report.StdoutReportGenerator;
import eu.dnetlib.contract.runner.report.XMLReportGenerator;
import eu.dnetlib.contract.utils.SpringUtils;


/**
 * Contract test runner main class.
 * Uses spring configuration and runs contract tests using invokers on spring beans.
 * @author mhorst
 *
 */
public class ContractTestRunner {
	
	public static final String ASPECT_TEST_ENTRY_POINTCUT = "aspect.test.entry.pointcut";
	public static final String ASPECT_TEST_RESULT_POINTCUT = "aspect.test.result.pointcut";
	public static final String ASPECT_TEST_EXC_POINTCUT = "aspect.test.exc.pointcut";
	public static final String OMIT_ALL_POINTCUT = "execution (** xxx())";
	
	
	protected static final Logger log = Logger.getLogger(ContractTestRunner.class);
	
	/**
	 * ContractTestRunner configuration.
	 */
	private ContractTestRunnerConfiguration contractTestRunnerConfiguration;
	
	/**
	 * Currents application spring context.
	 */
	private ApplicationContext appCtx;
	
	/**
	 * Contract test runner context.
	 */
	private ContractTestRunnerContext contractTestRunnerContext;
	
	/**
	 * GlobalContractContext placeholder.
	 */
	private GlobalContractContextPlaceholder placeholder;

	/**
	 * Constructor parametrized with Spring configuration file location.
	 * @param contractTestRunnerContext
	 * @throws ContractTestRunnerException
	 */
	@SuppressWarnings("unchecked")
	public ContractTestRunner(ContractTestRunnerContext contractTestRunnerContext) 
		throws ContractTestRunnerException {
		this.contractTestRunnerContext = contractTestRunnerContext;
		if (contractTestRunnerContext==null ||
				contractTestRunnerContext.getSpringConfigLocation()==null) {
			log.error("got null springConfigLocation! Cannot inject spring beans.");
//			setting as a plug
			placeholder = new GlobalContractContextPlaceholder();
		} else {
			appCtx = SpringUtils.getSpringContext(contractTestRunnerContext.getSpringConfigLocation());
			if (appCtx instanceof AbstractApplicationContext) {
				log.debug("registering shutdown hook...");
				((AbstractApplicationContext)appCtx).registerShutdownHook();
			} else {
				log.warn("couldn't register shutdown hook");
			}
			
//			retrieving ContractTestRunnerConfiguration:
			Map<String, ContractTestRunnerConfiguration> contractTestRunnerBeans = appCtx.getBeansOfType(
					ContractTestRunnerConfiguration.class);
			if (contractTestRunnerBeans.isEmpty()) {
				throw new ContractTestRunnerException("no bean of type " +
						ContractTestRunnerConfiguration.class.getCanonicalName() + 
						" could be found!");
			} else if (contractTestRunnerBeans.keySet().size()>1) {
				throw new ContractTestRunnerException("more than one bean of type " +
						ContractTestRunnerConfiguration.class.getCanonicalName() + 
						" was found! Single bean is required!");
			} else {
				log.debug("retrieving bean " + contractTestRunnerBeans.keySet().iterator().next() + 
						" for type " + ContractTestRunnerConfiguration.class.getCanonicalName());
				contractTestRunnerConfiguration = contractTestRunnerBeans.values().iterator().next();
			}

//			retrieving GlobalContractContext:
			Map<String, GlobalContractContext> globalContractContextBeans = appCtx.getBeansOfType(
					GlobalContractContext.class);
			if (globalContractContextBeans.isEmpty()) {
				throw new ContractTestRunnerException("no bean of type " +
						GlobalContractContext.class.getCanonicalName() + 
						" could be found!");
			} else if (globalContractContextBeans.keySet().size()>1) {
				throw new ContractTestRunnerException("more than one bean of type " +
						GlobalContractContext.class.getCanonicalName() + 
						" was found! Single bean is required!");
			} else {
				log.debug("retrieving bean " + globalContractContextBeans.keySet().iterator().next() + 
						" for type " + GlobalContractContext.class.getCanonicalName());
				contractTestRunnerContext.setGlobalContractContext(
						globalContractContextBeans.values().iterator().next());
			}
			
//			injecting placeholder for resolving context values
			Map<String, GlobalContractContextPlaceholder> placeholderBeans = appCtx.getBeansOfType(
					GlobalContractContextPlaceholder.class);
			if (placeholderBeans.isEmpty()) {
				throw new ContractTestRunnerException("no bean of type " +
						GlobalContractContextPlaceholder.class.getCanonicalName() + 
						" could be found!");
			} else if (placeholderBeans.keySet().size()>1) {
				throw new ContractTestRunnerException("more than one bean of type " +
						GlobalContractContextPlaceholder.class.getCanonicalName() + 
						" was found! Single bean is required!");
			} else {
				log.debug("retrieving bean " + placeholderBeans.keySet().iterator().next() + 
						" for type " + GlobalContractContextPlaceholder.class.getCanonicalName());
				placeholder = placeholderBeans.values().iterator().next();
			}
			
//			initializing result context
			if (contractTestRunnerConfiguration.getExecutionDataList()!=null) {
				contractTestRunnerContext.getGlobalContractContext().getResultContext().setExecutionDataResultEntries(
						new InvokerDataResultEntry[contractTestRunnerConfiguration.getExecutionDataList().size()]);
			}
			if (contractTestRunnerConfiguration.getBeforeDataList()!=null) {
				contractTestRunnerContext.getGlobalContractContext().getResultContext().setBeforeDataResultEntries(
						new InvokerDataResultEntry[contractTestRunnerConfiguration.getBeforeDataList().size()]);
			}
			if (contractTestRunnerConfiguration.getAfterDataList()!=null) {
				contractTestRunnerContext.getGlobalContractContext().getResultContext().setAfterDataResultEntries(
						new InvokerDataResultEntry[contractTestRunnerConfiguration.getAfterDataList().size()]);
			}
		}
	}
	
	/**
	 * Runs contract test.
	 * @return result object of method invokation
	 * @throws ContractTestRunnerException
	 */
	private Object[] runInternal() throws ContractTestRunnerException {
		validateContractTestRunnerConfiguration(this.contractTestRunnerConfiguration);
//		execute initial operations
		runBefore(this.contractTestRunnerConfiguration, this.contractTestRunnerContext, 
				this.placeholder);
		try {
//			enable contract verification
			if (this.contractTestRunnerContext!=null && this.contractTestRunnerContext.getGlobalContractContext()!=null) {
				this.contractTestRunnerContext.getGlobalContractContext().setPerformCheckPointValidation(true);
			} else {
				log.error("unable to set performCheckPointValidation: null context object!");
			}
			return performTest(this.contractTestRunnerConfiguration, 
					this.contractTestRunnerContext, 
					this.placeholder);
		} finally {
//			disable contract verification
			if (contractTestRunnerContext!=null && contractTestRunnerContext.getGlobalContractContext()!=null) {
				contractTestRunnerContext.getGlobalContractContext().setPerformCheckPointValidation(false);
			} else {
				log.error("unable to set performCheckPointValidation: null context object!");
			}
//			execute cleanup operations
			runAfter(this.contractTestRunnerConfiguration, this.contractTestRunnerContext, 
					this.placeholder);
		}
	}
	/**
	 * Executes method under test. 
	 * Supports multiple InvokerData elements and returns array of results.
	 * @param conf
	 * @param ctx
	 * @param placeholder
	 * @throws ContractTestRunnerException 
	 * 
	 */
	protected Object[] performTest(ContractTestRunnerConfiguration conf, ContractTestRunnerContext ctx, 
			GlobalContractContextPlaceholder placeholder) throws ContractTestRunnerException {
		if (conf.getExecutionRunnable()!=null) {
//			runnable mode
			conf.getExecutionRunnable().run(ctx.getGlobalContractContext());
//			working in runnable mode always return null
			return null;
		} else {
//			invoker mode
			int index = 0;
			Object[] results = new Object[conf.getExecutionDataList().size()];
			for (InvokerData executionData : conf.getExecutionDataList()) {
				Method method = getApplicableMethod(executionData);
				if (method!=null) {
					try {
						log.info("running <tested> method: "+method.getDeclaringClass() + 
								"#" + method.getName() + "()");
						Object resultObject = method.invoke(executionData.getTarget(), 
								placeholder.resolve(executionData.getArgs()));
						if (ctx!=null && ctx.getGlobalContractContext()!=null) {
							ctx.getGlobalContractContext().getResultContext().getExecutionDataResultEntries()[index] = 
								new InvokerDataResultEntry(executionData, resultObject);
						} else {
							log.error("unable to store execution data result entry: null context object!");
						}
						results[index] = resultObject;
						log.info("<tested> method: "+method.getDeclaringClass() + 
								"#" + method.getName() + "()" + 
								" execution finished");
					} catch (IllegalArgumentException e) {
						throw new ContractTestRunnerException("Couldn't invoke method: "+
								executionData.getMethodName()+
								" on object type: " + executionData.getTarget().getClass().getName(),e);
					} catch (IllegalAccessException e) {
						throw new ContractTestRunnerException("Couldn't invoke method: "+
								executionData.getMethodName()+
								" on object type: " + executionData.getTarget().getClass().getName(),e);
					} catch (InvocationTargetException e) {
						throw new ContractTestRunnerException("Couldn't invoke method: "+
								executionData.getMethodName()+
								" on object type: " + executionData.getTarget().getClass().getName(),e);
					}
				} else {
					throw new ContractTestRunnerException("Couldn't find Method object for object type: " +
							executionData.getTarget().getClass().getName() + 
							" ,method name: "+executionData.getMethodName()+
							" and params: ["+getParamTypesCSV(executionData)+"]");
				}
				index++;
			}
			return results;
		}
	}

	/**
	 * Performs all initial operations.
	 * @param conf
	 * @param ctx
	 * @param placeholder
	 * @throws ContractTestRunnerException
	 */
	protected void runBefore(ContractTestRunnerConfiguration conf, ContractTestRunnerContext ctx, 
			GlobalContractContextPlaceholder placeholder) throws ContractTestRunnerException {
		if (conf.getBeforeRunnable()!=null) {
			log.info("Executing <before> runnable class: " + 
					conf.getBeforeRunnable().getClass().getName());
			conf.getBeforeRunnable().run(ctx!=null?ctx.getGlobalContractContext():null);
			log.info("<before> runnable class: " + 
					conf.getBeforeRunnable().getClass().getName() +
					" execution finished");
		}
		if (conf.getBeforeDataList()!=null) {
			int index = 0;
			for (InvokerData beforeData : conf.getBeforeDataList()) {
				validateInvokerData(beforeData);
				Method beforeMethod = getApplicableMethod(beforeData);
				if (beforeMethod!=null) {
					try {
					log.info("running <before> method: "+beforeMethod.getDeclaringClass() + 
							"#" + beforeMethod.getName() + "()");
					Object resultObject = beforeMethod.invoke(beforeData.getTarget(), placeholder.resolve(beforeData.getArgs()));
					if (ctx!=null && ctx.getGlobalContractContext()!=null) {
						ctx.getGlobalContractContext().getResultContext().
							getBeforeDataResultEntries()[index] = new InvokerDataResultEntry(beforeData, resultObject);
					} else {
						log.error("unable to store <before> data result entry: null context object!");
					}
					log.info("<before> method: "+beforeMethod.getDeclaringClass() + 
							"#" + beforeMethod.getName() + "()" + 
							" execution finished");
					} catch (IllegalArgumentException e) {
						throw new ContractTestRunnerException("Couldn't invoke method: "+
								beforeData.getMethodName()+
								" on object type: " + beforeData.getTarget().getClass().getName(),e);
					} catch (IllegalAccessException e) {
						throw new ContractTestRunnerException("Couldn't invoke method: "+
								beforeData.getMethodName()+
								" on object type: " + beforeData.getTarget().getClass().getName(),e);
					} catch (InvocationTargetException e) {
						throw new ContractTestRunnerException("Couldn't invoke method: "+
								beforeData.getMethodName()+
								" on object type: " + beforeData.getTarget().getClass().getName(),e);
					}
				} else {
					throw new ContractTestRunnerException("Couldn't find Method for object type: " +
							beforeData.getTarget().getClass().getName() + 
							" ,method name: "+beforeData.getMethodName()+
							" and params: ["+getParamTypesCSV(beforeData)+"]");
				}
				index++;
			}
		}
	}
	
	/**
	 * Performs all cleanup operations.
	 * @param conf
	 * @param ctx
	 * @param placeholder
	 * @throws ContractTestRunnerException
	 */
	protected void runAfter(ContractTestRunnerConfiguration conf, ContractTestRunnerContext ctx, 
			GlobalContractContextPlaceholder placeholder) throws ContractTestRunnerException {
		if (conf.getAfterRunnable()!=null) {
			log.info("Executing <after> runnable class: " + 
					conf.getAfterRunnable().getClass().getName());
			conf.getAfterRunnable().run(ctx!=null?ctx.getGlobalContractContext():null);
			log.info("<after> runnable class: " + 
					contractTestRunnerConfiguration.getAfterRunnable().getClass().getName() +
					" execution finished");
		}
		if (conf.getAfterDataList()!=null) {
			int index = 0;
			for (InvokerData afterData : conf.getAfterDataList()) {
				validateInvokerData(afterData);
				Method afterMethod = getApplicableMethod(afterData);
				if (afterMethod!=null) {
					try {
						log.info("running <after> method: "+afterMethod.getDeclaringClass() + 
								"#" + afterMethod.getName() + "()");
						Object resultObject = afterMethod.invoke(afterData.getTarget(), placeholder.resolve(afterData.getArgs()));
						if (ctx!=null && ctx.getGlobalContractContext()!=null) {
							ctx.getGlobalContractContext().getResultContext().
								getAfterDataResultEntries()[index] = new InvokerDataResultEntry(afterData, resultObject);
						} else {
							log.info("unable to store <after> data result entry: null context object!");
						}
						log.info("<after> method: "+afterMethod.getDeclaringClass() + 
								"#" + afterMethod.getName() + "()" + 
								" execution finished");
					} catch (IllegalArgumentException e) {
						throw new ContractTestRunnerException("Couldn't invoke method: "+
								afterData.getMethodName()+
								" on object type: " + afterData.getTarget().getClass().getName(),e);
					} catch (IllegalAccessException e) {
						throw new ContractTestRunnerException("Couldn't invoke method: "+
								afterData.getMethodName()+
								" on object type: " + afterData.getTarget().getClass().getName(),e);
					} catch (InvocationTargetException e) {
						throw new ContractTestRunnerException("Couldn't invoke method: "+
								afterData.getMethodName()+
								" on object type: " + afterData.getTarget().getClass().getName(),e);
					}
				} else {
					throw new ContractTestRunnerException("Couldn't find Method object for object type: " +
							afterData.getTarget().getClass().getName() + 
							" ,method name: "+afterData.getMethodName()+
							" and params: ["+getParamTypesCSV(afterData)+"]");
				}
				index++;
			}
		}
	}
	
	/**
	 * Runs contract test and generates report using {@link IReportGenerator} module.
	 * @param generator report generator module
	 * @return result object of method invokation.
	 * @throws ContractTestRunnerException
	 */
	public Object[] run(IReportGenerator generator) throws ContractTestRunnerException {
		try {
			return runInternal();
		} finally {
			log.info("generating report...");
			List<ContractTestRunnerContext> contexts = new ArrayList<ContractTestRunnerContext>(1);
			contexts.add(this.getContractTestRunnerContext());
    		generator.generateReport(contexts);
			if (this.appCtx!=null && 
					(this.appCtx instanceof AbstractApplicationContext)) {
				((AbstractApplicationContext)appCtx).close();
			}
		}
	}
	
	/**
	 * Runs contract test.
	 * @return result object of method invokation.
	 * @throws ContractTestRunnerException
	 */
	protected Object[] run() throws ContractTestRunnerException {
		try {
			return runInternal();
		} finally {
			if (this.appCtx!=null && 
					(this.appCtx instanceof AbstractApplicationContext)) {
				((AbstractApplicationContext)appCtx).close();
			}
		}
	}
	
	/**
	 * Validates {@link ContractTestRunnerConfiguration} given as parameter.
	 * @param conf ContractTestRunner configuration
	 * @throws ContractTestRunnerException
	 */
	protected static void validateContractTestRunnerConfiguration(ContractTestRunnerConfiguration conf) 
		throws ContractTestRunnerException {
		if (conf==null) {
			throw new ContractTestRunnerException("invalid configuration: null configuration object!");
		}
		if (conf.getExecutionRunnable()!=null) {
//			ok, using IRunnable to run tested method
		} else if (conf.getExecutionDataList()!=null && 
				!conf.getExecutionDataList().isEmpty()) {
			for (InvokerData currentInvokerData : conf.getExecutionDataList()) {
				validateInvokerData(currentInvokerData);
			}
		} else {
			throw new ContractTestRunnerException("invalid configuration: " +
					"neither execution runnable nor execution data found!");
		}
	}
	
	/**
	 * Validates {@link InvokerData} object.
	 * @param invokerData
	 * @throws ContractTestRunnerException
	 */
	protected static void validateInvokerData(InvokerData invokerData) throws ContractTestRunnerException {
		if (invokerData.getTarget()==null) {
			throw new ContractTestRunnerException("invalid configuration: null target object!");
		}
		if (invokerData.getMethodName()==null || invokerData.getMethodName().length()==0) {
			throw new ContractTestRunnerException("invalid configuration: no method name!");
		}
		if (invokerData.getParameterTypes()!=null) {
			if(invokerData.getArgs()==null) {
				throw new ContractTestRunnerException("invalid configuration: " +
						"no args provided while parameterTypes are defined!");
			} else if (invokerData.getArgs().length!=invokerData.getParameterTypes().length) {
				throw new ContractTestRunnerException("invalid configuration: " +
						"number of args provided is not equal to the " +
						"number of declared parameterTypes!");
			}
		}
	}
	
	/**
	 * Returns applicable method to the {@link InvokerData} object given as parameter.
	 * @param invokerData InvokerData configuration
	 * @return applicable method to the InvokerData object given as parameter
	 * @throws ContractTestRunnerException
	 */
	public static Method getApplicableMethod(InvokerData invokerData) throws ContractTestRunnerException {
		if (validateAgainstParameterTypes(invokerData)) {
			try {
				return invokerData.getTarget().getClass().getMethod(
						invokerData.getMethodName(), 
						invokerData.getParameterTypes());
			} catch (SecurityException e) {
				throw new ContractTestRunnerException("Couldn't get Method object for object type: " +
						invokerData.getTarget().getClass().getName() + " and method name: "+
						invokerData.getMethodName()+"!",e);
			} catch (NoSuchMethodException e) {
				throw new ContractTestRunnerException("Couldn't get Method object for object type: " +
						invokerData.getTarget().getClass().getName() + " and method name: "+
						invokerData.getMethodName()+"!",e);
			}
		} else {
			Method[] methods = invokerData.getTarget().getClass().getMethods();
			for (Method method : methods) {
				if (method.getName().equals(invokerData.getMethodName())) {
//					got proper method name, checking parameters:
					Class<?>[] parameterTypes = method.getParameterTypes();
					if (parameterTypes.length==invokerData.getArgs().length) {
						boolean invalidParam = false;
						for (int i = 0; i < parameterTypes.length; i++) {
//							if (!parameterTypes[i].isInstance(invokerData.getArgs()[i])) {
							if (!isAcceptableParam(parameterTypes[i], invokerData.getArgs()[i])) {
								invalidParam = true;
								break;
							}
						}
						if (!invalidParam) {
							return method;
						}
					}
				}
			}
			return null;
		}
	}
	
	/**
	 * Checks whether object given as parameter is an instance of clazz.
	 * This method was introduced to handle primitive types properly.
	 * When object is null and clazz is not primitive it is considered
	 * as an acceptable parameter.
	 * @param clazz not null {@link Class}
	 * @param obj object being tested
	 * @return true in an object is instance of clazz
	 */
	protected static boolean isAcceptableParam(Class<?> clazz, Object obj) {
		if (obj!=null) {
			Class<?> boxedClass = PrimitivesAutoboxer.box(clazz);
			return boxedClass.isInstance(obj);
		} else {
			if (clazz.isPrimitive()) {
//				null is not allowed for primitives
				return false;
			} else {
				return true;
			}
		}
	}
	
	/**
	 * Returns param types for given {@link InvokerData}.
	 * Mainly used for logging purposes.
	 * @param invokerData
	 * @return array of param type names
	 */
	protected String getParamTypesCSV(InvokerData invokerData) {
		if (invokerData!=null) {
			if (invokerData.getParameterTypes()!=null) {
				String[] result = new String[invokerData.getParameterTypes().length];
				for (int i=0; i< result.length; i++) {
					result[i] = invokerData.getParameterTypes()[i].getName(); 
				}
				return StringUtils.arrayToDelimitedString(result, "; ");
			} else if (invokerData.getArgs()!=null &&
					invokerData.getArgs().length>0) {
				String[] result = new String[invokerData.getArgs().length];
				for (int i=0; i< result.length; i++) {
//					watch out for null
					if (invokerData.getArgs()[i]!=null) {
						result[i] = invokerData.getArgs()[i].getClass().getName();
					}
				}
				return StringUtils.arrayToDelimitedString(result, "; ");
			} 
		} 
		return "";
	}
	
	/**
	 * Checks whether {@link InvokerData#getParameterTypes()} should be used
	 * to invoke proper method.
	 * @param invokerData InvokerData configuration
	 * @return true if parameterTypes should be used to determine proper method
	 */
	private static boolean validateAgainstParameterTypes(InvokerData invokerData) {
		if (invokerData.getParameterTypes()!=null) {
			return true;
		} else {
			if (invokerData.getArgs()==null) {
				return true;
			} else {
				return false;
			}
		}
	}

	/**
	 * Returns ContractTestRunner configuration.
	 * @return ContractTestRunner configuration
	 */
	public ContractTestRunnerConfiguration getContractTestRunnerConfiguration() {
		return contractTestRunnerConfiguration;
	}

	/**
	 * Sets ContractTestRunner configuration.
	 * @param contractTestRunnerConfiguration
	 */
	public void setContractTestRunnerConfiguration(
			ContractTestRunnerConfiguration contractTestRunnerConfiguration) {
		this.contractTestRunnerConfiguration = contractTestRunnerConfiguration;
	}
	
	/**
	 * Sets default system values for contract definitions.
	 */
	protected static void setDefaultSystemValues() {
//		all properties can be used by PropertyPlaceholderConfigurer as default
		setSystemValue(ASPECT_TEST_ENTRY_POINTCUT, OMIT_ALL_POINTCUT);
		setSystemValue(ASPECT_TEST_RESULT_POINTCUT, OMIT_ALL_POINTCUT);
		setSystemValue(ASPECT_TEST_EXC_POINTCUT, OMIT_ALL_POINTCUT);
	}
	
	protected static void setSystemValue(String prop, String value) {
		log.debug("setting default system property " + prop + " to value: " + value);
		System.setProperty(prop, value);
	}
	
	/**
	 * Contract test runner main method.
	 * Takes multiple spring configurations as parameters.
	 * @param args
	 */
	public static void main(String[] args) {
		try {
			BasicParser parser = new BasicParser();
			Options opts = prepareCLIOptions();
	        CommandLine cl = parser.parse(opts, args);
	        if (cl.hasOption('h')) {
	            HelpFormatter f = new HelpFormatter();
	            f.printHelp(ContractTestRunner.class.getName(), opts);
	            return;
	        } else {
	        	String[] filePaths = getContractPaths(cl);
	        	if (filePaths==null || filePaths.length==0) {
	    			log.error("No spring configuration provided! " +
	    					"At least one spring configuration file path expected!");
	    			return;
	    		}
//	        	
	        	setDefaultSystemValues();
	        	boolean exceptionOccured = false;
	    		List<ContractTestRunnerContext> contexts = new ArrayList<ContractTestRunnerContext>(filePaths.length);
	    		for (int i=0; i<filePaths.length; i++) {
	    			ContractTestRunnerContext contractTestRunnerContext = new ContractTestRunnerContext(filePaths[i]);
	    			contexts.add(contractTestRunnerContext);
	    			try {
	    				log.info("Running contract test for: "+
	    						contractTestRunnerContext.getSpringConfigLocation() +"...");
	    				ContractTestRunner runner = new ContractTestRunner(contractTestRunnerContext);
	    				runner.run();
	    				log.info("Contract test finisned for "+
	    						contractTestRunnerContext.getSpringConfigLocation());
	    			} catch (ContractTestRunnerException e) {
	    				if (isExceptionExpected(e, 
	    						contractTestRunnerContext.getGlobalContractContext())) {
	    					log.info("got expected exception", e);
	    				} else {
	    					log.error("Unexpected ContractTestRunnerException occured when running contract test on " + 
		    						contractTestRunnerContext.getSpringConfigLocation(), e);
		    				contractTestRunnerContext.getGlobalContractContext().setFailure(e);
		    				exceptionOccured = true;
	    				}
	    			} catch (Exception e) {
    					log.error("Unexpected exception occured when running contract test on " + 
	    						contractTestRunnerContext.getSpringConfigLocation(), e);
	    				if (contractTestRunnerContext.getGlobalContractContext()!=null) {
	    					contractTestRunnerContext.getGlobalContractContext().setFailure(e);
	    				} else {
	    					log.warn("global contract context not initialized, " +
	    							"cannot set failure to attached in report!");
	    				}
	    				exceptionOccured = true;
	    			}
	    		}
	        	log.info("generating report...");
	    		IReportGenerator generator = getReportGenerator(cl.getOptionValue('r'));
	    		boolean allSucceeded = generator.generateReport(contexts);
	    		log.info("shutting down ContractTestRunner...");
	    		if (!allSucceeded || exceptionOccured) {
	    			log.warn("some contracts did not succeeded!");
	    			System.exit(-1);
	    		}
	        }
		} catch (ParseException e) {
			log.error("Exception occured when parsing entry parameters!", e);
			System.exit(-1);
		}
	}
	
	/**
	 * Checks whether given exception was expected. Returns true if last validated
	 * checkpoint was ExceptionCheckPoint and if exception caught is the same one as
	 * parameter of this method.
	 * @param e exception to be checked
	 * @param ctx
	 * @return true if exception was expected
	 */
	protected static boolean isExceptionExpected(ContractTestRunnerException e,
			GlobalContractContext ctx) {
		Exception candidateEx = getAllowedCandidate(e);
		if (candidateEx!=null) {
			Set<IContractEvent> lastEvents = ReportGeneratorHelper.getLastSuccesfulEventsFromChain(
					ReportGeneratorHelper.getAllProcessingPaths(
					ctx.getCursor().getRootEvaluationContext()));
			if (lastEvents.size()>0) {
				for (IContractEvent event : lastEvents) {
					if (event instanceof ExceptionContractEvent) {
						if (candidateEx == ((ExceptionContractEvent)event).getException()) {
							return true;
						}
					}
				}
			}	
		}
		return false;
	}

	/**
	 * Returns allowed exception candidate.
	 * Null is returned when candidate not found.
	 * @param e
	 * @return allowed exception candidate
	 */
	protected static Exception getAllowedCandidate(ContractTestRunnerException e) {
		if (e.getCause()!=null && (e.getCause() instanceof InvocationTargetException)) {
			return (e.getCause().getCause() instanceof Exception)?
					(Exception)e.getCause().getCause():null;
		} else {
			return null;
		}
	}
	
	
	
	/**
	 * Returns array of contract file locations.
	 * @param cl CommandLine
	 * @return array of contract file locations
	 */
	private static String[] getContractPaths(CommandLine cl) {
		if (cl==null)
			return null;
		
    	String contractFilePath = cl.getOptionValue('f');
    	if (contractFilePath!=null) {
    		try {
				BufferedReader reader = new BufferedReader(new FileReader(new File(contractFilePath)));
	    		List<String> filePaths = new ArrayList<String>();
	    		String line = null;
	    		while ((line = reader.readLine()) != null) {
	    			String trimmed = line.trim();
//	    			support for commenting out contract files
	    			if (trimmed.length()>0 && !trimmed.startsWith("#")) {
	    				filePaths.add(trimmed);
	    			}
	    		}
	    		return filePaths.toArray(new String[filePaths.size()]);
    		} catch (IOException e) {
    			log.error("Exception occured when reading contract list from file: "+contractFilePath, e);
    			return null;
			}
    	} else {
    		return cl.getOptionValues('l');
    	}
	}
	
	/**
	 * Returns proper {@link IReportGenerator} implementation according to the reportGenName.
	 * @param reportGenName
	 * @return proper IReportGenerator implementation according to the reportGenName
	 */
	private static IReportGenerator getReportGenerator(String reportGenName) {
		if (reportGenName==null || 
				reportGenName.equalsIgnoreCase("complex")) {
			log.info("using <complex> report generator module");
			List<IReportGenerator> generators = new ArrayList<IReportGenerator>();
			generators.add(new StdoutReportGenerator());
			generators.add(new XMLReportGenerator());
			return new ComplexReportGenerator(generators);
		} else if (reportGenName.equalsIgnoreCase("stdout")) {
			log.info("using <stdout> report generator module");
			return new StdoutReportGenerator();
		} else if (reportGenName.equalsIgnoreCase("csv")) {
			log.info("using <CSV> report generator module");
			return new CSVReportGenerator();
		} else if (reportGenName.equalsIgnoreCase("xml")) {
			log.info("using <XML> report generator module");
			return new XMLReportGenerator();
		} else {
			log.info("using default <stdout> report generator module");
			return new StdoutReportGenerator();
		}
	}
	
	/**
	 * Prepares Command Line Interface options.
	 * @return Options
	 */
	@SuppressWarnings("static-access")
	private static Options prepareCLIOptions() {
		Options opts = new Options();
		Option helpOption = new Option("h", "help", false,
				"prints help for contract test runner module");
		opts.addOption(helpOption);
		
		Option contractListOption = OptionBuilder.withArgName("files")
			.withValueSeparator()
			.withLongOpt("contractList")
			.hasArgs()
			.withDescription("list of contract spring definition files")
			.create("l");
		opts.addOption(contractListOption);
		
		Option contractFileOption = OptionBuilder.withArgName("file")
		.withLongOpt("contractFile")
		.hasArg()
		.withDescription("single file containing list of contract configurations file paths, " +
				"one configuration per line")
		.create("f");
		opts.addOption(contractFileOption);
		
		Option reportGenOption = OptionBuilder.withArgName("module")
		.withLongOpt("reportGen")
		.hasArg()
		.withDescription("report generator module, one of: stdout [default], csv, xml")
		.create("r");
		opts.addOption(reportGenOption);
		return opts;
	}
	
	/**
	 * Returns contract test runner context.
	 * @return contract test runner context
	 */
	public ContractTestRunnerContext getContractTestRunnerContext() {
		return contractTestRunnerContext;
	}
	
}
