/*
 *  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.security.scan;

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

import javax.swing.JComponent;

import com.eviware.soapui.SoapUI;
import com.eviware.soapui.config.SecurityScanConfig;
import com.eviware.soapui.config.StrategyTypeConfig;
import com.eviware.soapui.config.TestAssertionConfig;
import com.eviware.soapui.impl.wsdl.AbstractWsdlModelItem;
import com.eviware.soapui.impl.wsdl.WsdlSubmitContext;
import com.eviware.soapui.impl.wsdl.support.assertions.AssertableConfig;
import com.eviware.soapui.impl.wsdl.support.assertions.AssertionsSupport;
import com.eviware.soapui.impl.wsdl.teststeps.TestRequest;
import com.eviware.soapui.impl.wsdl.teststeps.WsdlMessageAssertion;
import com.eviware.soapui.impl.wsdl.teststeps.WsdlTestRequestStep;
import com.eviware.soapui.impl.wsdl.teststeps.WsdlTestStep;
import com.eviware.soapui.impl.wsdl.teststeps.assertions.TestAssertionRegistry.AssertableType;
import com.eviware.soapui.model.ModelItem;
import com.eviware.soapui.model.iface.Interface;
import com.eviware.soapui.model.iface.MessageExchange;
import com.eviware.soapui.model.iface.SubmitContext;
import com.eviware.soapui.model.security.SecurityScan;
import com.eviware.soapui.model.testsuite.Assertable;
import com.eviware.soapui.model.testsuite.AssertionError;
import com.eviware.soapui.model.testsuite.AssertionsListener;
import com.eviware.soapui.model.testsuite.ResponseAssertion;
import com.eviware.soapui.model.testsuite.SamplerTestStep;
import com.eviware.soapui.model.testsuite.TestAssertion;
import com.eviware.soapui.model.testsuite.TestStep;
import com.eviware.soapui.security.ExecutionStrategyHolder;
import com.eviware.soapui.security.Securable;
import com.eviware.soapui.security.SecurityTest;
import com.eviware.soapui.security.SecurityTestRunContext;
import com.eviware.soapui.security.SecurityTestRunner;
import com.eviware.soapui.security.SecurityTestRunnerImpl;
import com.eviware.soapui.security.result.SecurityScanRequestResult;
import com.eviware.soapui.security.result.SecurityScanResult;
import com.eviware.soapui.security.result.SecurityResult.ResultStatus;
import com.eviware.soapui.security.support.FailedSecurityMessageExchange;
import com.eviware.soapui.security.support.SecurityTestRunListener;

/**
 * 
 * Implementation that is common for all security scans. Support for security
 * workflow.
 * 
 * @author robert
 * 
 */
public abstract class AbstractSecurityScan extends AbstractWsdlModelItem<SecurityScanConfig> implements
		ResponseAssertion, SecurityScan
{
	private SecurityScanResult securityScanResult;
	private SecurityScanRequestResult securityScanRequestResult;
	private TestStep testStep;
	protected AssertionsSupport assertionsSupport;

	private AssertionStatus currentStatus;
	private ExecutionStrategyHolder executionStrategy;
	private TestStep originalTestStepClone;
	private PropertyChangeSupport pcs = new PropertyChangeSupport( this );
	private boolean skipFurtherRunning;

	public AbstractSecurityScan( TestStep testStep, SecurityScanConfig config, ModelItem parent, String icon )
	{
		super( config, parent, icon );
		if( config == null )
		{
			config = SecurityScanConfig.Factory.newInstance();
			setConfig( config );
		}

		this.testStep = testStep;

		if( config.getExecutionStrategy() == null )
		{
			config.addNewExecutionStrategy();
			config.getExecutionStrategy().setStrategy( StrategyTypeConfig.ONE_BY_ONE );
			config.getExecutionStrategy().setDelay( 100 );
		}
		else if( config.getExecutionStrategy().getStrategy() == null )
		{
			config.getExecutionStrategy().setStrategy( StrategyTypeConfig.ONE_BY_ONE );
			config.getExecutionStrategy().setDelay( 100 );
		}

		/*
		 * if security scan have no strategy, set its value to
		 * StrategyTypeConfig.NO_STRATEGY.
		 */
		setExecutionStrategy( new ExecutionStrategyHolder( config.getExecutionStrategy() ) );

		if( config.getCheckedParameters() == null )
			config.addNewCheckedParameters();

		initAssertions();

		setApplyForFailedTestStep( config.getApplyForFailedStep() );
		if( !config.isSetDisabled() )
			setDisabled( false );
	}

	@Override
	public void copyConfig( SecurityScanConfig config )
	{
		super.setConfig( config );
		getConfig().setType( config.getType() );
		getConfig().setName( config.getName() );
		getConfig().setConfig( config.getConfig() );
		getConfig().setTestStep( config.getTestStep() );

		TestAssertionConfig[] assertions = config.getAssertionList().toArray( new TestAssertionConfig[0] );
		getConfig().setAssertionArray( assertions );
		initAssertions();

		getConfig().setExecutionStrategy( config.getExecutionStrategy() );
		setExecutionStrategy( new ExecutionStrategyHolder( config.getExecutionStrategy() ) );
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.eviware.soapui.security.scan.SecurityScan#updateSecurityConfig(com
	 * .eviware.soapui.config.SecurityScanConfig)
	 */
	public void updateSecurityConfig( SecurityScanConfig config )
	{
		setConfig( config );

		assertionsSupport.refresh();

		if( executionStrategy != null && config.getExecutionStrategy() != null )
		{
			executionStrategy.updateConfig( config.getExecutionStrategy() );
		}
	}

	protected void initAssertions()
	{
		assertionsSupport = new AssertionsSupport( this, new AssertableConfig()
		{
			public TestAssertionConfig addNewAssertion()
			{
				return getConfig().addNewAssertion();
			}

			public List<TestAssertionConfig> getAssertionList()
			{
				return getConfig().getAssertionList();
			}

			public void removeAssertion( int ix )
			{
				getConfig().removeAssertion( ix );
			}

			public TestAssertionConfig insertAssertion( TestAssertionConfig source, int ix )
			{
				TestAssertionConfig conf = getConfig().insertNewAssertion( ix );
				conf.set( source );
				return conf;
			}
		} );
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.eviware.soapui.security.scan.SecurityScan#run(com.eviware.soapui
	 * .model.testsuite.TestStep,
	 * com.eviware.soapui.security.SecurityTestRunContext,
	 * com.eviware.soapui.security.SecurityTestRunner)
	 */
	public SecurityScanResult run( TestStep testStep, SecurityTestRunContext context,
			SecurityTestRunner securityTestRunner )
	{
		securityScanResult = new SecurityScanResult( this );
		SecurityTestRunListener[] securityTestListeners = ( ( SecurityTest )getParent() ).getSecurityTestRunListeners();

		PropertyChangeNotifier notifier = new PropertyChangeNotifier();
		boolean noMutations = true;
		while( hasNext( testStep, context ) )
		{
			noMutations = false;
			if( ( ( SecurityTestRunnerImpl )securityTestRunner ).isCanceled() )
			{
				securityScanResult.setStatus( ResultStatus.CANCELED );
				clear();
				return securityScanResult;
			}
			securityScanRequestResult = new SecurityScanRequestResult( this );
			securityScanRequestResult.startTimer();
			originalTestStepClone = ( ( SecurityTestRunnerImpl )securityTestRunner )
					.cloneForSecurityScan( ( WsdlTestStep )this.testStep );
			execute( securityTestRunner, originalTestStepClone, context );
			notifier.notifyChange();
			securityScanRequestResult.stopTimer();
			assertResponse( getSecurityScanRequestResult().getMessageExchange(), context );
			// add to summary result
			securityScanResult.addSecurityRequestResult( getSecurityScanRequestResult() );
			for( int i = 0; i < securityTestListeners.length; i++ )
			{
				if( Arrays.asList( ( ( SecurityTest )getParent() ).getSecurityTestRunListeners() ).contains(
						securityTestListeners[i] ) )
					securityTestListeners[i].afterSecurityScanRequest( ( SecurityTestRunnerImpl )securityTestRunner,
							context, securityScanRequestResult );
			}

			try
			{
				Thread.sleep( getExecutionStrategy().getDelay() );
			}
			catch( InterruptedException e )
			{
				SoapUI.logError( e, "Security Scan Request Delay Interrupted!" );
			}
		}

		if( noMutations )
		{
			securityScanResult.setStatus( ResultStatus.SKIPPED );
		}
		return securityScanResult;
	}

	protected void clear()
	{

	}

	/**
	 * should be implemented in every particular scan it executes one request,
	 * modified by securityScan if necessary and internally adds messages for
	 * logging to SecurityScanRequestResult
	 */
	abstract protected void execute( SecurityTestRunner runner, TestStep testStep, SecurityTestRunContext context );

	/**
	 * checks if specific SecurityScan still has modifications left
	 * 
	 * @param testStep2
	 * @param context
	 */
	abstract protected boolean hasNext( TestStep testStep2, SecurityTestRunContext context );

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.eviware.soapui.security.scan.SecurityScan#isConfigurable()
	 */

	public boolean isConfigurable()
	{
		return true;
	}

	/**
	 * Overide if SecurityScan have Optional component
	 */
	@Override
	public JComponent getComponent()
	{
		return null;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.eviware.soapui.security.scan.SecurityScan#getType()
	 */
	public abstract String getType();

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.eviware.soapui.security.scan.SecurityScan#getTestStep()
	 */
	public TestStep getTestStep()
	{
		return testStep;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.eviware.soapui.security.scan.SecurityScan#setTestStep(com.eviware
	 * .soapui.model.testsuite.TestStep)
	 */
	public void setTestStep( TestStep step )
	{
		testStep = step;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.eviware.soapui.security.scan.SecurityScan#isDisabled()
	 */
	public boolean isDisabled()
	{
		return getConfig().getDisabled();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.eviware.soapui.security.scan.SecurityScan#setDisabled(boolean)
	 */
	public void setDisabled( boolean disabled )
	{
		boolean oldValue = isDisabled();
		getConfig().setDisabled( disabled );
		pcs.firePropertyChange( "disabled", oldValue, disabled );
	}

	public static boolean isSecurable( TestStep testStep )
	{
		if( testStep != null && testStep instanceof Securable )
		{
			return true;
		}
		else
		{
			return false;
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.eviware.soapui.security.scan.SecurityScan#getExecutionStrategy()
	 */
	public ExecutionStrategyHolder getExecutionStrategy()
	{
		return this.executionStrategy;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.eviware.soapui.security.scan.SecurityScan#setExecutionStrategy(com
	 * .eviware.soapui.security.ExecutionStrategyHolder)
	 */
	public void setExecutionStrategy( ExecutionStrategyHolder executionStrategy )
	{
		ExecutionStrategyHolder oldValue = getExecutionStrategy();
		this.executionStrategy = executionStrategy;
		pcs.firePropertyChange( "executionStrategy", oldValue, executionStrategy );
	}

	protected TestRequest getOriginalResult( SecurityTestRunnerImpl securityRunner, TestStep testStep )
	{
		testStep.run( securityRunner, securityRunner.getRunContext() );

		return getRequest( testStep );
	}

	protected TestRequest getRequest( TestStep testStep )
	{
		if( testStep instanceof SamplerTestStep )
		{
			return ( ( SamplerTestStep )testStep ).getTestRequest();
		}
		return null;
	}

	private class PropertyChangeNotifier
	{
		private ResultStatus oldStatus;

		public PropertyChangeNotifier()
		{
			oldStatus = getSecurityStatus();
		}

		public void notifyChange()
		{
			ResultStatus newStatus = getSecurityStatus();

			if( oldStatus != newStatus )
				notifyPropertyChanged( STATUS_PROPERTY, oldStatus, newStatus );

			oldStatus = newStatus;
		}
	}

	@Override
	public TestAssertion addAssertion( String label )
	{
		PropertyChangeNotifier notifier = new PropertyChangeNotifier();
		try
		{
			WsdlMessageAssertion assertion = assertionsSupport.addWsdlAssertion( label );
			if( assertion == null )
				return null;

			if( getAssertableContent() != null )
			{
				assertRequests( assertion );
				assertResponses( assertion );
				notifier.notifyChange();
			}

			return assertion;
		}

		catch( Exception e )
		{
			SoapUI.logError( e );
			return null;
		}
	}

	/**
	 * @param assertion
	 *           run all responses against this assertion
	 */
	private void assertResponses( WsdlMessageAssertion assertion )
	{
		if( securityScanResult != null )
			for( SecurityScanRequestResult result : securityScanResult.getSecurityRequestResultList() )
			{
				if( result.getMessageExchange() == null )
					return;

				assertion.assertResponse( result.getMessageExchange(), new WsdlSubmitContext( testStep ) );
			}
	}

	/**
	 * @param assertion
	 *           run all request against this assertion
	 */
	private void assertRequests( WsdlMessageAssertion assertion )
	{
		if( securityScanResult != null )
			for( SecurityScanRequestResult result : securityScanResult.getSecurityRequestResultList() )
			{
				if( result.getMessageExchange() == null )
					return;

				assertion.assertRequest( result.getMessageExchange(), new WsdlSubmitContext( testStep ) );
			}
	}

	@Override
	public void removeAssertion( TestAssertion assertion )
	{
		PropertyChangeNotifier notifier = new PropertyChangeNotifier();

		try
		{
			assertionsSupport.removeAssertion( ( WsdlMessageAssertion )assertion );

		}
		finally
		{
			( ( WsdlMessageAssertion )assertion ).release();
			notifier.notifyChange();
		}
	}

	@Override
	public TestAssertion moveAssertion( int ix, int offset )
	{
		WsdlMessageAssertion assertion = getAssertionAt( ix );
		PropertyChangeNotifier notifier = new PropertyChangeNotifier();

		try
		{
			return assertionsSupport.moveAssertion( ix, offset );
		}
		finally
		{
			( ( WsdlMessageAssertion )assertion ).release();
			notifier.notifyChange();
		}
	}

	@Override
	public WsdlMessageAssertion getAssertionAt( int c )
	{
		return assertionsSupport.getAssertionAt( c );
	}

	@Override
	public void addAssertionsListener( AssertionsListener listener )
	{
		assertionsSupport.addAssertionsListener( listener );
	}

	@Override
	public void removeAssertionsListener( AssertionsListener listener )
	{
		assertionsSupport.removeAssertionsListener( listener );
	}

	@Override
	public int getAssertionCount()
	{
		return assertionsSupport.getAssertionCount();
	}

	@Override
	public AssertionStatus getAssertionStatus()
	{
		int cnt = getAssertionCount();
		if( cnt == 0 )
			return currentStatus;

		if( securityScanResult != null && securityScanResult.getStatus() == ResultStatus.OK )
			currentStatus = AssertionStatus.VALID;
		else
			currentStatus = AssertionStatus.FAILED;

		return currentStatus;
	}

	public ResultStatus getSecurityStatus()
	{
		return securityScanResult != null ? securityScanResult.getStatus() : ResultStatus.UNKNOWN;
	}

	@Override
	public String getAssertableContent()
	{
		if( testStep instanceof Assertable )
			return ( ( Assertable )testStep ).getAssertableContent();

		return null;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.eviware.soapui.model.testsuite.Assertable#getAssertableType()
	 * 
	 * Decided to go with assertions on request and response so we can implement
	 * "men in the middle" attacks using monitor.
	 */
	@Override
	public AssertableType getAssertableType()
	{
		return AssertableType.BOTH;
	}

	@Override
	public TestAssertion getAssertionByName( String name )
	{
		return assertionsSupport.getAssertionByName( name );
	}

	@Override
	public List<TestAssertion> getAssertionList()
	{
		return new ArrayList<TestAssertion>( assertionsSupport.getAssertionList() );
	}

	@Override
	public Map<String, TestAssertion> getAssertions()
	{
		return assertionsSupport.getAssertions();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.eviware.soapui.security.scan.SecurityScan#getAssertionsSupport()
	 */
	public AssertionsSupport getAssertionsSupport()
	{
		return assertionsSupport;
	}

	@Override
	public TestAssertion cloneAssertion( TestAssertion source, String name )
	{
		return assertionsSupport.cloneAssertion( source, name );
	}

	@Override
	public String getDefaultAssertableContent()
	{
		if( testStep instanceof Assertable )
			return ( ( Assertable )testStep ).getDefaultAssertableContent();

		return null;
	}

	@Override
	public Interface getInterface()
	{
		if( testStep instanceof WsdlTestRequestStep )
			return ( ( WsdlTestRequestStep )testStep ).getInterface();

		return null;
	}

	@Override
	public ModelItem getModelItem()
	{
		return this;
	}

	public AssertionStatus assertResponse( MessageExchange messageExchange, SubmitContext context )
	{
		AssertionStatus finalResult = null;

		try
		{
			PropertyChangeNotifier notifier = new PropertyChangeNotifier();

			if( messageExchange != null )
			{
				context.setProperty( SECURITY_SCAN_REQUEST_RESULT, getSecurityScanRequestResult() );

				for( WsdlMessageAssertion assertion : assertionsSupport.getAssertionList() )
				{
					AssertionStatus currentResult = assertion.assertResponse( messageExchange, context );
					updateMessages( currentResult, assertion );

					if( finalResult == null || finalResult != AssertionStatus.FAILED )
					{
						finalResult = currentResult;
					}
				}

				setStatus( finalResult );

				notifier.notifyChange();
			}
		}
		catch( Exception e )
		{
			e.printStackTrace();
		}
		return finalResult;
	}

	/**
	 * Sets SecurityScanStatus based on the status of all assertions added
	 * 
	 * @param result
	 * @param assertion
	 */
	private void setStatus( AssertionStatus result )
	{
		if( result == AssertionStatus.FAILED )
		{
			getSecurityScanRequestResult().setStatus( ResultStatus.FAILED );
		}
		else if( result == AssertionStatus.VALID )
		{
			getSecurityScanRequestResult().setStatus( ResultStatus.OK );

		}
		else if( result == AssertionStatus.UNKNOWN )
		{
			getSecurityScanRequestResult().setStatus( ResultStatus.UNKNOWN );
		}
	}

	private void updateMessages( AssertionStatus result, WsdlMessageAssertion assertion )
	{
		if( result == AssertionStatus.FAILED )
		{
			for( AssertionError error : assertion.getErrors() )
				getSecurityScanRequestResult().addMessage( error.getMessage() );
		}
	}

	// name used in configuration panel
	/*
	 * (non-Javadoc)
	 * 
	 * @see com.eviware.soapui.security.scan.SecurityScan#getConfigName()
	 */
	public abstract String getConfigName();

	// description usd in configuration panel
	/*
	 * (non-Javadoc)
	 * 
	 * @see com.eviware.soapui.security.scan.SecurityScan#getConfigDescription()
	 */
	public abstract String getConfigDescription();

	// help url used for configuration panel ( help for this scan )
	/*
	 * (non-Javadoc)
	 * 
	 * @see com.eviware.soapui.security.scan.SecurityScan#getHelpURL()
	 */
	public abstract String getHelpURL();

	protected void setSecurityScanRequestResult( SecurityScanRequestResult securityScanRequestResult )
	{
		this.securityScanRequestResult = securityScanRequestResult;
	}

	protected SecurityScanRequestResult getSecurityScanRequestResult()
	{
		return securityScanRequestResult;
	}

	/**
	 * Overide if SecurityScan needs advanced settings
	 */
	@Override
	public JComponent getAdvancedSettingsPanel()
	{
		return null;
	}

	@Override
	public SecurityScanResult getSecurityScanResult()
	{
		return securityScanResult;
	}

	/**
	 * @param message
	 * @param testStep
	 */
	protected void reportSecurityScanException( String message )
	{
		getSecurityScanRequestResult().setMessageExchange( new FailedSecurityMessageExchange() );
		getSecurityScanRequestResult().setStatus( ResultStatus.FAILED );
		getSecurityScanRequestResult().addMessage( message );
	}

	@Override
	public void addWsdlAssertion( String assertionLabel )
	{
		assertionsSupport.addWsdlAssertion( assertionLabel );
	}

	@Override
	public boolean isApplyForFailedStep()
	{
		return getConfig().getApplyForFailedStep();
	}

	@Override
	public void setApplyForFailedTestStep( boolean apply )
	{
		getConfig().setApplyForFailedStep( apply );
	}

	@Override
	public boolean isRunOnlyOnce()
	{
		return getConfig().getRunOnlyOnce();
	}

	@Override
	public void setRunOnlyOnce( boolean runOnlyOnce )
	{
		getConfig().setRunOnlyOnce( runOnlyOnce );
	}

	public void release()
	{
		if( assertionsSupport != null )
		{
			assertionsSupport.release();
		}

		if( securityScanResult != null )
		{
			securityScanResult.release();
		}

		if( securityScanRequestResult != null )
		{
			securityScanRequestResult.release();
		}

	}

	public void addPropertyChangeListener( PropertyChangeListener listener )
	{
		pcs.addPropertyChangeListener( listener );
	}

	public void removePropertyChangeListener( PropertyChangeListener listener )
	{
		pcs.removePropertyChangeListener( listener );
	}

	@Override
	public boolean isSkipFurtherRunning()
	{
		return skipFurtherRunning;
	}

	@Override
	public void setSkipFurtherRunning( boolean skipFurtherRunning )
	{
		this.skipFurtherRunning = skipFurtherRunning;
	}

}
