/*
 *  soapUI, copyright (C) 2004-2012 smartbear.com 
 *
 *  soapUI is free software; you can redistribute it and/or modify it under the 
 *  terms of version 2.1 of the GNU Lesser General Public License as published by 
 *  the Free Software Foundation.
 *
 *  soapUI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 
 *  even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 
 *  See the GNU Lesser General Public License for more details at gnu.org.
 */

package com.eviware.soapui.tools;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import org.apache.commons.cli.CommandLine;

import com.eviware.soapui.SoapUI;
import com.eviware.soapui.impl.wsdl.WsdlProject;
import com.eviware.soapui.impl.wsdl.loadtest.WsdlLoadTest;
import com.eviware.soapui.impl.wsdl.loadtest.data.actions.ExportLoadTestLogAction;
import com.eviware.soapui.impl.wsdl.loadtest.data.actions.ExportStatisticsAction;
import com.eviware.soapui.impl.wsdl.loadtest.log.LoadTestLog;
import com.eviware.soapui.impl.wsdl.loadtest.log.LoadTestLogEntry;
import com.eviware.soapui.model.project.ProjectFactoryRegistry;
import com.eviware.soapui.model.testsuite.LoadTestRunContext;
import com.eviware.soapui.model.testsuite.LoadTestRunListener;
import com.eviware.soapui.model.testsuite.LoadTestRunner;
import com.eviware.soapui.model.testsuite.TestCase;
import com.eviware.soapui.model.testsuite.TestCaseRunContext;
import com.eviware.soapui.model.testsuite.TestCaseRunner;
import com.eviware.soapui.model.testsuite.TestRunner.Status;
import com.eviware.soapui.model.testsuite.TestStep;
import com.eviware.soapui.model.testsuite.TestStepResult;
import com.eviware.soapui.model.testsuite.TestSuite;
import com.eviware.soapui.settings.UISettings;
import com.eviware.soapui.support.SoapUIException;
import com.eviware.soapui.support.StringUtils;

/**
 * Standalone test-runner used from maven-plugin, can also be used from
 * command-line (see xdocs) or directly from other classes.
 * <p>
 * For standalone usage, set the project file (with setProjectFile) and other
 * desired properties before calling run
 * </p>
 * 
 * @author Ole.Matzura
 */

public class SoapUILoadTestRunner extends AbstractSoapUITestRunner implements LoadTestRunListener
{
	private String testSuite;
	private String testCase;
	private String loadTest;
	private boolean printReport;
	private List<LoadTestRunner> failedTests = new ArrayList<LoadTestRunner>();
	private int testCaseCount;
	private int loadTestCount;
	private int limit = -1;
	private long threadCount = -1;
	private boolean saveAfterRun;

	public static String TITLE = "soapUI " + SoapUI.SOAPUI_VERSION + " LoadTest Runner";

	/**
	 * Runs the loadtests in the specified soapUI project file, see soapUI xdocs
	 * for details.
	 * 
	 * @param args
	 * @throws Exception
	 */

	public static void main( String[] args )
	{
		System.exit( new SoapUILoadTestRunner().runFromCommandLine( args ) );
	}

	protected boolean processCommandLine( CommandLine cmd )
	{
		String message = "";

		if( cmd.hasOption( "e" ) )
			setEndpoint( cmd.getOptionValue( "e" ) );

		if( cmd.hasOption( "s" ) )
		{
			String testSuite = getCommandLineOptionSubstSpace( cmd, "s" );
			setTestSuite( testSuite );
		}

		if( cmd.hasOption( "c" ) )
		{
			String testCase = cmd.getOptionValue( "c" );
			setTestCase( testCase );
		}

		if( cmd.hasOption( "l" ) )
			setLoadTest( cmd.getOptionValue( "l" ) );

		if( cmd.hasOption( "u" ) )
			setUsername( cmd.getOptionValue( "u" ) );

		if( cmd.hasOption( "p" ) )
			setPassword( cmd.getOptionValue( "p" ) );

		if( cmd.hasOption( "w" ) )
			setWssPasswordType( cmd.getOptionValue( "w" ) );

		if( cmd.hasOption( "d" ) )
			setDomain( cmd.getOptionValue( "d" ) );

		if( cmd.hasOption( "h" ) )
			setHost( cmd.getOptionValue( "h" ) );

		if( cmd.hasOption( "m" ) )
			setLimit( Integer.parseInt( cmd.getOptionValue( "m" ) ) );

		if( cmd.hasOption( "n" ) )
			setThreadCount( Integer.parseInt( cmd.getOptionValue( "n" ) ) );

		if( cmd.hasOption( "f" ) )
			setOutputFolder( getCommandLineOptionSubstSpace( cmd, "f" ) );

		if( cmd.hasOption( "t" ) )
			setSettingsFile( getCommandLineOptionSubstSpace( cmd, "t" ) );

		setPrintReport( cmd.hasOption( "r" ) );
		setSaveAfterRun( cmd.hasOption( "S" ) );

		if( cmd.hasOption( "x" ) )
		{
			setProjectPassword( cmd.getOptionValue( "x" ) );
		}

		if( cmd.hasOption( "v" ) )
		{
			setSoapUISettingsPassword( cmd.getOptionValue( "v" ) );
		}

		if( cmd.hasOption( "D" ) )
		{
			setSystemProperties( cmd.getOptionValues( "D" ) );
		}

		if( cmd.hasOption( "G" ) )
		{
			setGlobalProperties( cmd.getOptionValues( "G" ) );
		}

		if( cmd.hasOption( "P" ) )
		{
			setProjectProperties( cmd.getOptionValues( "P" ) );
		}

		if( message.length() > 0 )
		{
			log.error( message );
			return false;
		}

		return true;
	}

	public void setLimit( int limit )
	{
		this.limit = limit;
	}

	public void setThreadCount( long threadCount )
	{
		this.threadCount = threadCount;
	}

	protected SoapUIOptions initCommandLineOptions()
	{
		SoapUIOptions options = new SoapUIOptions( "loadtestrunner" );
		options.addOption( "e", true, "Sets the endpoint" );
		options.addOption( "s", true, "Sets the testsuite" );
		options.addOption( "c", true, "Sets the testcase" );
		options.addOption( "l", true, "Sets the loadtest" );
		options.addOption( "u", true, "Sets the username" );
		options.addOption( "p", true, "Sets the password" );
		options.addOption( "w", true, "Sets the WSS password type, either 'Text' or 'Digest'" );
		options.addOption( "d", true, "Sets the domain" );
		options.addOption( "h", true, "Sets the host" );
		options.addOption( "m", true, "Overrides the LoadTest Limit" );
		options.addOption( "n", true, "Overrides the LoadTest ThreadCount" );
		options.addOption( "r", false, "Exports statistics and testlogs for each LoadTest run" );
		options.addOption( "f", true, "Sets the output folder to export to" );
		options.addOption( "t", true, "Sets the soapui-settings.xml file to use" );
		options.addOption( "x", true, "Sets project password for decryption if project is encrypted" );
		options.addOption( "v", true, "Sets password for soapui-settings.xml file" );
		options.addOption( "D", true, "Sets system property with name=value" );
		options.addOption( "G", true, "Sets global property with name=value" );
		options.addOption( "P", true, "Sets or overrides project property with name=value" );
		options.addOption( "S", false, "Saves the project after running the tests" );

		return options;
	}

	public SoapUILoadTestRunner()
	{
		this( TITLE );
	}

	public SoapUILoadTestRunner( String title )
	{
		super( title );
	}

	public void setLoadTest( String loadTest )
	{
		this.loadTest = loadTest;
	}

	public void setPrintReport( boolean printReport )
	{
		this.printReport = printReport;
	}

	public void setSaveAfterRun( boolean saveAfterRun )
	{
		this.saveAfterRun = saveAfterRun;
	}

	/**
	 * Runs the testcases as configured with setXXX methods
	 * 
	 * @throws Exception
	 *            thrown if any tests fail
	 */

	public boolean runRunner() throws Exception
	{
		if( SoapUI.getSettings().getBoolean( UISettings.DONT_DISABLE_GROOVY_LOG ) )
		{
			initGroovyLog();
		}

		String projectFile = getProjectFile();

		// WsdlProject project = new WsdlProject( projectFile,
		// getProjectPassword() );
		WsdlProject project = ( WsdlProject )ProjectFactoryRegistry.getProjectFactory( "wsdl" ).createNew( projectFile,
				getProjectPassword() );

		if( project.isDisabled() )
			throw new Exception( "Failed to load soapUI project file [" + projectFile + "]" );

		initProjectProperties( project );

		int suiteCount = 0;

		if( testSuite != null && project.getTestSuiteByName( testSuite ) == null )
			throw new Exception( "Missing TestSuite named [" + testSuite + "]" );

		for( int c = 0; c < project.getTestSuiteCount(); c++ )
		{
			if( testSuite == null || project.getTestSuiteAt( c ).getName().equalsIgnoreCase( testSuite ) )
			{
				runSuite( project.getTestSuiteAt( c ) );
				suiteCount++ ;
			}
		}

		if( suiteCount == 0 )
		{
			log.warn( "No test-suites matched argument [" + testSuite + "]" );
		}
		else if( testCaseCount == 0 )
		{
			log.warn( "No test-cases matched argument [" + testCase + "]" );
		}
		else if( loadTestCount == 0 )
		{
			log.warn( "No load-tests matched argument [" + loadTest + "]" );
		}
		else
		{
			if( saveAfterRun && !project.isRemote() )
			{
				try
				{
					project.save();
				}
				catch( Throwable t )
				{
					log.error( "Failed to save project", t );
				}
			}

			if( !failedTests.isEmpty() )
			{
				log.info( failedTests.size() + " load tests failed:" );
				for( LoadTestRunner loadTestRunner : failedTests )
				{
					log.info( loadTestRunner.getLoadTest().getName() + ": " + loadTestRunner.getReason() );
				}

				throw new SoapUIException( "LoadTests failed" );
			}
		}

		return true;
	}

	/**
	 * Run tests in the specified TestSuite
	 * 
	 * @param suite
	 *           the TestSuite to run
	 */

	public void runSuite( TestSuite suite )
	{
		if( testCase != null && suite.getTestCaseByName( testCase ) == null )
		{
			return;
		}

		long start = System.currentTimeMillis();
		for( int c = 0; c < suite.getTestCaseCount(); c++ )
		{
			String name = suite.getTestCaseAt( c ).getName();
			if( testCase == null || name.equalsIgnoreCase( testCase ) )
			{
				runTestCase( suite.getTestCaseAt( c ) );
				testCaseCount++ ;
			}
			else
				log.info( "Skipping testcase [" + name + "], filter is [" + testCase + "]" );
		}
		log.info( "soapUI suite [" + suite.getName() + "] finished in " + ( System.currentTimeMillis() - start ) + "ms" );
	}

	/**
	 * Runs the specified TestCase
	 * 
	 * @param testCase
	 *           the testcase to run
	 */

	private void runTestCase( TestCase testCase )
	{
		if( loadTest != null && testCase.getLoadTestByName( loadTest ) == null )
		{
			return;
		}

		for( int c = 0; c < testCase.getLoadTestCount(); c++ )
		{
			String name = testCase.getLoadTestAt( c ).getName();
			if( loadTest == null || loadTest.equalsIgnoreCase( name ) )
			{
				runWsdlLoadTest( ( WsdlLoadTest )testCase.getLoadTestAt( c ) );
				loadTestCount++ ;
			}
		}
	}

	/**
	 * Runs the specified LoadTest
	 * 
	 * @param loadTest
	 *           the loadTest to run
	 */

	protected void runWsdlLoadTest( WsdlLoadTest loadTest )
	{
		try
		{
			log.info( "Running LoadTest [" + loadTest.getName() + "]" );
			if( limit >= 0 )
			{
				log.info( "Overriding limit [" + loadTest.getTestLimit() + "] with specified [" + limit + "]" );
				loadTest.setTestLimit( limit );
			}

			if( threadCount >= 0 )
			{
				log.info( "Overriding threadCount [" + loadTest.getThreadCount() + "] with specified [" + threadCount + "]" );
				loadTest.setThreadCount( threadCount );
			}

			loadTest.addLoadTestRunListener( this );
			LoadTestRunner runner = loadTest.run();

			// wait for test to finish
			while( !runner.hasStopped() )
			{
				if( runner.getStatus() == Status.RUNNING )
				{
					log.info( "LoadTest [" + loadTest.getName() + "] progress: " + runner.getProgress() + ", "
							+ runner.getRunningThreadCount() );
				}
				Thread.sleep( 1000 );
			}

			log.info( "LoadTest [" + loadTest.getName() + "] finished with status " + runner.getStatus().toString() );

			if( printReport )
			{
				log.info( "Exporting log and statistics for LoadTest [" + loadTest.getName() + "]" );

				loadTest.getStatisticsModel().finish();

				exportLog( loadTest );
				exportStatistics( loadTest );
			}
		}
		catch( Exception e )
		{
			SoapUI.logError( e );
			log.error( e );
		}
	}

	private void exportStatistics( WsdlLoadTest loadTest ) throws IOException
	{
		ExportStatisticsAction exportStatisticsAction = new ExportStatisticsAction( loadTest.getStatisticsModel() );
		String statisticsFileName = StringUtils.createFileName( loadTest.getName(), '_' ) + "-statistics.txt";
		if( getOutputFolder() != null )
		{
			ensureOutputFolder( loadTest );
			statisticsFileName = getAbsoluteOutputFolder( loadTest ) + File.separator + statisticsFileName;
		}

		int cnt = exportStatisticsAction.exportToFile( new File( statisticsFileName ) );
		log.info( "Exported " + cnt + " statistics to [" + statisticsFileName + "]" );
	}

	private void exportLog( WsdlLoadTest loadTest ) throws IOException
	{
		// export log first
		LoadTestLog loadTestLog = loadTest.getLoadTestLog();
		ExportLoadTestLogAction exportLoadTestLogAction = new ExportLoadTestLogAction( loadTestLog, null );
		String logFileName = StringUtils.createFileName( loadTest.getName(), '_' ) + "-log.txt";
		if( getOutputFolder() != null )
		{
			ensureOutputFolder( loadTest );
			logFileName = getAbsoluteOutputFolder( loadTest ) + File.separator + logFileName;
		}

		int cnt = exportLoadTestLogAction.exportToFile( new File( logFileName ) );
		log.info( "Exported " + cnt + " log items to [" + logFileName + "]" );

		int errorCnt = 0;
		for( int c = 0; c < loadTestLog.getSize(); c++ )
		{
			LoadTestLogEntry entry = ( LoadTestLogEntry )loadTestLog.getElementAt( c );

			if( entry != null && entry.isError() )
			{
				String entryFileName = StringUtils.createFileName( loadTest.getName(), '_' ) + "-error-" + errorCnt++
						+ "-entry.txt";
				if( getOutputFolder() != null )
				{
					ensureOutputFolder( loadTest );
					entryFileName = getAbsoluteOutputFolder( loadTest ) + File.separator + entryFileName;
				}

				try
				{
					entry.exportToFile( entryFileName );
				}
				catch( Exception e )
				{
					SoapUI.logError( e );
				}
			}
		}
		log.info( "Exported " + errorCnt + " error results" );
	}

	/**
	 * Sets the testcase to run
	 * 
	 * @param testCase
	 *           the testcase to run
	 */

	public void setTestCase( String testCase )
	{
		this.testCase = testCase;
	}

	/**
	 * Sets the TestSuite to run. If not set all TestSuites in the specified
	 * project file are run
	 * 
	 * @param testSuite
	 *           the testSuite to run.
	 */

	public void setTestSuite( String testSuite )
	{
		this.testSuite = testSuite;
	}

	public void afterLoadTest( LoadTestRunner loadTestRunner, LoadTestRunContext context )
	{
		if( loadTestRunner.getStatus() == LoadTestRunner.Status.FAILED )
		{
			failedTests.add( loadTestRunner );
		}
	}

	public void afterTestCase( LoadTestRunner loadTestRunner, LoadTestRunContext context, TestCaseRunner testRunner,
			TestCaseRunContext runContext )
	{
	}

	public void afterTestStep( LoadTestRunner loadTestRunner, LoadTestRunContext context, TestCaseRunner testRunner,
			TestCaseRunContext runContext, TestStepResult testStepResult )
	{
		super.afterStep( testRunner, runContext, testStepResult );
	}

	public void beforeLoadTest( LoadTestRunner loadTestRunner, LoadTestRunContext context )
	{
	}

	public void beforeTestCase( LoadTestRunner loadTestRunner, LoadTestRunContext context, TestCaseRunner testRunner,
			TestCaseRunContext runContext )
	{
	}

	public void beforeTestStep( LoadTestRunner loadTestRunner, LoadTestRunContext context, TestCaseRunner testRunner,
			TestCaseRunContext runContext, TestStep testStep )
	{
		super.beforeStep( testRunner, runContext, testStep );
	}

	public void loadTestStarted( LoadTestRunner loadTestRunner, LoadTestRunContext context )
	{
	}

	public void loadTestStopped( LoadTestRunner loadTestRunner, LoadTestRunContext context )
	{
	}
}
