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

import com.eviware.soapui.SoapUI;
import com.eviware.soapui.SoapUISystemProperties;
import com.eviware.soapui.config.*;
import com.eviware.soapui.config.TestOnDemandLocationsRequestDocumentConfig.TestOnDemandLocationsRequest;
import com.eviware.soapui.config.TestOnDemandLocationsRequestDocumentConfig.TestOnDemandLocationsRequest.Request;
import com.eviware.soapui.config.TestOnDemandUploadRequestDocumentConfig.TestOnDemandUploadRequest;
import com.eviware.soapui.impl.wsdl.WsdlProject;
import com.eviware.soapui.impl.wsdl.submit.transports.http.support.methods.ExtendedPostMethod;
import com.eviware.soapui.impl.wsdl.support.CompressionSupport;
import com.eviware.soapui.impl.wsdl.support.http.HttpClientSupport;
import com.eviware.soapui.impl.wsdl.testcase.WsdlTestCase;
import com.eviware.soapui.settings.SSLSettings;
import com.eviware.soapui.support.xml.XmlUtils;
import com.google.common.base.Strings;
import com.google.common.io.ByteStreams;
import edu.umd.cs.findbugs.annotations.NonNull;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.StringEntity;
import org.apache.log4j.Logger;
import org.apache.xmlbeans.XmlOptions;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathFactory;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/**
 * @author Erik R. Yverling
 * 
 *         Calls the AlertSite API for running Test On Demand.
 */

public class TestOnDemandCaller
{
	// FIXME Should these be in a configuration file instead?

	private final static String DEFAULT_PROTOCOL = "https";
	private final static String PROTOCOL_DELIMITER = "://";
	private final static String PROD_HOST = "www.alertsite.com";
	private static final String LOCATIONS_PATH = "/restapi/v2/devices/list/locations";
	private static final String UPLOAD_PATH = "/restapi/v2/devices/upload/testondemand";
	private final static String TEST_ON_DEMAND_DOMAIN = getDomain();
	private static final String LOCATIONS_URI = getProtocol() + PROTOCOL_DELIMITER + TEST_ON_DEMAND_DOMAIN
			+ LOCATIONS_PATH;
	private static final String UPLOAD_URI = getProtocol() + PROTOCOL_DELIMITER + TEST_ON_DEMAND_DOMAIN + UPLOAD_PATH;

	private static final String REDIRECT_URL_XPATH_EXPRESSION = "//RedirectURL";
	private static final String LOCATION_XPATH_EXPRESSION = "//Location";
	private static final String LOCATION_CODE_XPATH_EXPRESSION = "LocCode";
	private static final String LOCATION_NAME_XPATH_EXPRESSION = "LocName";
	private static final String LOCATION_SERVER_IP_ADDRESSES_XPATH_EXPRESSION = "LocIPs";

	private static final String API_VERSION = "2";
	private static final String APPLICATION_ZIP = "application/zip";
	private static final String BASE64 = "base64";
	private static final String USER_AGENT = "soapUI-" + SoapUI.SOAPUI_VERSION;

	private static final String LOCATIONS_NAME = "ListLocations";
	private static final String LOCATIONS_PARAMETER = "server_attrib=ITEST";

	private static final String UPLOAD_NAME = "TestOnDemand";
	private static final String UPLOAD_PARAMETER_LOCATION_PREFIX = "test_location=";

	private static final String SERVER_IP_ADDRESSES_DELIMETER = ",";

	protected static final String COULD_NOT_SAVE_TEMPORARY_PROJECT_MESSAGE = "Could not save temporary project file before sending TestCase";

	private final XPath xpath = XPathFactory.newInstance().newXPath();

	private static final Logger log = Logger.getLogger( TestOnDemandCaller.class );

	@NonNull
	public List<Location> getLocations() throws Exception
	{
		Document responseDocument = makeCall( LOCATIONS_URI, generateLocationsRequestXML() );
		NodeList locationNodes = ( NodeList )xpath.evaluate( LOCATION_XPATH_EXPRESSION, responseDocument,
				XPathConstants.NODESET );

		List<Location> locations = new ArrayList<Location>();
		for( int i = 0; i < locationNodes.getLength(); i++ )
		{
			Node locationNode = locationNodes.item( i );
			String name = ( String )xpath.evaluate( LOCATION_NAME_XPATH_EXPRESSION, locationNode, XPathConstants.STRING );
			String code = ( String )xpath.evaluate( LOCATION_CODE_XPATH_EXPRESSION, locationNode, XPathConstants.STRING );
			String unformattedServerIPAddresses = ( String )xpath.evaluate( LOCATION_SERVER_IP_ADDRESSES_XPATH_EXPRESSION,
					locationNode, XPathConstants.STRING );

			String[] serverIPAddresses = new String[0];
			if( !unformattedServerIPAddresses.isEmpty() )
			{
				serverIPAddresses = unformattedServerIPAddresses.split( SERVER_IP_ADDRESSES_DELIMETER );
			}

			locations.add( new Location( code, name, serverIPAddresses ) );
		}

		return locations;
	}

	// FIXME We should do some performance testing of large soapUI project files.
	@NonNull
	public String sendTestCase( @NonNull WsdlTestCase testCase, @NonNull Location location ) throws Exception
	{

		final ExtendedPostMethod post = new ExtendedPostMethod();
		post.setURI( new URI( UPLOAD_URI ) );

		String locationCode = location.getCode();

		String encodedTestSuiteName = getBase64EncodedString( testCase.getTestSuite().getName().getBytes() );
		String encodedTestCaseName = getBase64EncodedString( testCase.getName().getBytes() );

		File tempProjectFile = saveTemporaryProject( testCase.getTestSuite().getProject() );
		byte[] projectFileData = getBytes( tempProjectFile.getAbsolutePath() );
		byte[] zipedProjectFileData = zipBytes( testCase.getTestSuite().getProject().getName(), projectFileData );
		String encodedZipedProjectFile = getBase64EncodedString( zipedProjectFileData );

		String projectPassword = testCase.getTestSuite().getProject().getShadowPassword();
		String encodedProjectPassword = getBase64EncodedString( Strings.nullToEmpty( projectPassword ).getBytes() );

		String keystoreFilePath = SoapUI.getSettings().getString( SSLSettings.KEYSTORE, "" );
		byte[] keystoreFileData = getBytes( keystoreFilePath );
		String encodedKeystoreFile = getBase64EncodedString( keystoreFileData );

		String encodedKeystorePassword = getBase64EncodedString( SoapUI.getSettings()
				.getString( SSLSettings.KEYSTORE_PASSWORD, "" ).getBytes() );

		String requestContent = generateUploadRequestXML( locationCode, encodedTestSuiteName, encodedTestCaseName,
				encodedZipedProjectFile, encodedProjectPassword, encodedKeystoreFile, encodedKeystorePassword );

		byte[] compressedRequestContent = CompressionSupport.compress( CompressionSupport.ALG_GZIP,
				requestContent.getBytes() );
		post.setEntity( new ByteArrayEntity( compressedRequestContent ) );

		Document responseDocument = makeCall( UPLOAD_URI, requestContent );
		String redirectURL = ( String )xpath.evaluate( REDIRECT_URL_XPATH_EXPRESSION, responseDocument,
				XPathConstants.STRING );

		if( Strings.isNullOrEmpty( redirectURL ) )
		{
			throw new RuntimeException( "The RedirectURL element is missing in the response message" );
		}
		return redirectURL;
	}

	// FIXME Add to utility class and make DependencyValidator.saveProject() use this
	protected File saveTemporaryProject( WsdlProject project )
	{
		File tempFile = null;
		try
		{
			tempFile = File.createTempFile( "project-temp-", ".xml", null );
			project.saveIn( tempFile );
		}
		catch( IOException e )
		{
			SoapUI.logError( e, COULD_NOT_SAVE_TEMPORARY_PROJECT_MESSAGE );
		}
		return tempFile;
	}

	private Document makeCall( String uri, String requestContent ) throws Exception
	{
		final ExtendedPostMethod post = new ExtendedPostMethod();
		post.setURI( new URI( uri ) );

		post.setEntity(new StringEntity(requestContent));

		// FIXME Should we remove the logging printouts before release? The upload request maybe would be to large?

		log.debug("Sending request to " + uri);
		log.debug( requestContent );

		HttpClientSupport.execute(post);

		byte[] responseBody = post.getResponseBody();

		log.debug( "Got response from " + uri );
		log.debug( new String( responseBody ) );

		String reponseBodyAsString = new String( responseBody );
		return XmlUtils.parseXml(reponseBodyAsString);
	}

	private String generateLocationsRequestXML()
	{
		TestOnDemandLocationsRequest locationsRequest = TestOnDemandLocationsRequestDocumentConfig.Factory.newInstance()
				.addNewTestOnDemandLocationsRequest();

		Request request = locationsRequest.addNewRequest();
		request.setApiVersion( API_VERSION );

		TestOnDemandHeaderConfig header = request.addNewHeader();
		header.setUserAgent( USER_AGENT );

		TestOnDemandCommandConfig command = request.addNewBody().addNewCommand();
		command.setName( LOCATIONS_NAME );
		command.setParameters( LOCATIONS_PARAMETER );
		return locationsRequest.xmlText( getXmlOptionsWithoutNamespaces() );
	}

	private String generateUploadRequestXML( String locationCode, String encodedTestSuiteName,
			String encodedTestCaseName, String encodedZipedProjectFile, String encodedProjectPassword,
			String encodedKeystoreFile, String encodedKeystorePassword )
	{
		TestOnDemandUploadRequest uploadRequestConfig = TestOnDemandUploadRequestDocumentConfig.Factory.newInstance()
				.addNewTestOnDemandUploadRequest();

		com.eviware.soapui.config.TestOnDemandUploadRequestDocumentConfig.TestOnDemandUploadRequest.Request requestConfig = uploadRequestConfig
				.addNewRequest();
		requestConfig.setApiVersion( API_VERSION );

		TestOnDemandHeaderConfig headerConfig = requestConfig.addNewHeader();
		headerConfig.setUserAgent( USER_AGENT );

		TestOnDemandUploadBodyConfig bodyConfig = requestConfig.addNewBody();

		TestOnDemandCommandConfig commandConfig = bodyConfig.addNewCommand();
		commandConfig.setName( UPLOAD_NAME );
		commandConfig.setParameters( UPLOAD_PARAMETER_LOCATION_PREFIX + locationCode );

		TestOnDemandTxnConfig txnConfig = bodyConfig.addNewTxn();

		TestOnDemandTestSuiteConfig testSuiteConfig = txnConfig.addNewTestSuite();
		testSuiteConfig.setEnctype( BASE64 );
		testSuiteConfig.setStringValue( encodedTestSuiteName );

		TestOnDemandTestCaseConfig testCaseConfig = txnConfig.addNewTestCase();
		testCaseConfig.setEnctype( BASE64 );
		testCaseConfig.setStringValue( encodedTestCaseName );

		TestOnDemandContentConfig contentConfig = txnConfig.addNewContent();
		contentConfig.setEnctype( BASE64 );
		contentConfig.setType( APPLICATION_ZIP );
		contentConfig.setStringValue( encodedZipedProjectFile );

		TestOnDemandProjectPasswordConfig projectPasswordConfig = txnConfig.addNewPassword();
		projectPasswordConfig.setEnctype( BASE64 );
		projectPasswordConfig.setStringValue( encodedProjectPassword );

		TestOnDemandKeystoreConfig keystoreConfig = bodyConfig.addNewKeystore();

		TestOnDemandFileConfig fileConfig = keystoreConfig.addNewFile();
		fileConfig.setEnctype( BASE64 );
		fileConfig.setStringValue( encodedKeystoreFile );

		TestOnDemandKeystorePasswordConfig keystorePasswordConfig = keystoreConfig.addNewPassword();
		keystorePasswordConfig.setEnctype( BASE64 );
		keystorePasswordConfig.setStringValue( encodedKeystorePassword );

		return uploadRequestConfig.xmlText( getXmlOptionsWithoutNamespaces() );
	}

	private XmlOptions getXmlOptionsWithoutNamespaces()
	{
		XmlOptions options = new XmlOptions();
		options.setUseDefaultNamespace();
		Map<String, String> namespaces = new HashMap<String, String>();
		namespaces.put( "", "http://eviware.com/soapui/config" );
		options.setSaveImplicitNamespaces( namespaces );
		return options;
	}

	private static byte[] getBytes( String filePath ) throws IOException
	{
		byte[] byteArray = new byte[0];
		if( !Strings.isNullOrEmpty( filePath ) )
		{
			File file = new File( filePath );
			FileInputStream inputStream = null;
			try
			{
				inputStream = new FileInputStream( file );
				byteArray = ByteStreams.toByteArray( inputStream );
			}
			finally
			{
				IOUtils.closeQuietly( inputStream );
			}
		}
		return byteArray;
	}

	private static byte[] zipBytes( String filename, byte[] dataToBeZiped ) throws IOException
	{
		ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
		ZipOutputStream zipedOutputStream = new ZipOutputStream( outputStream );
		ZipEntry entry = new ZipEntry( filename );
		entry.setSize( dataToBeZiped.length );
		try
		{
			zipedOutputStream.putNextEntry( entry );
			zipedOutputStream.write( dataToBeZiped );
		}
		finally
		{
			zipedOutputStream.closeEntry();
			zipedOutputStream.close();
		}
		return outputStream.toByteArray();
	}

	private static String getDomain()
	{
		String customEndpoint = System.getProperty( SoapUISystemProperties.TEST_ON_DEMAND_HOST );

		if( customEndpoint == null )
		{
			return PROD_HOST;
		}
		else
		{
			return customEndpoint;
		}
	}

	private static String getProtocol()
	{
		return System.getProperty( SoapUISystemProperties.TEST_ON_DEMAND_PROTOCOL, DEFAULT_PROTOCOL );
	}

	private static String getBase64EncodedString(byte [] bytesToEncode) {
		return new String(Base64.encodeBase64(bytesToEncode));
	}
}