package com.urbancode.air.plugin.onetest

import org.apache.http.Header;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;

import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.urbancode.util.OneTestUCDConstants;
import com.urbancode.util.TokenUtil;
import com.urbancode.util.OutputPropertyStatus;

import groovy.json.JsonSlurper;
import java.util.concurrent.TimeUnit;

import java.net.UnknownHostException;
import java.text.SimpleDateFormat;
import java.net.URL;

import javax.net.ssl.SSLException;

class OneTestUCDHelper {

	def airPluginTool;
	def props = [];
	def datasets;
	def variables;
	def tags;

	URL url;
	String serverUrl;
	String offlineToken;
	String filepath;
	String repoId;
	String projId;
	String repoName;
	String projName;
	String assetName;
	String type;
	String desktopProjectId;
	String branchName;
	String datasetsString;
	String variablesString;
	String tagsString;
	String[] datasetsList
	String[] variablesList
	String[] tagsList
	String secretsCollectionName;
	String ritEnv;
	String proxyHost;
	String proxyPass;
	String proxyPort;
	String proxyUser;
	String trustStorePath;
	String trustStorePassword;
	String teamSpace;
	String spaceId;

	CloseableHttpClient client;

	TokenUtil util;
	String accessToken;
	String refreshToken;
	JsonObject variableObject;

	public OneTestUCDHelper(def airPluginToolIn) {
		airPluginTool = airPluginToolIn;
		props = airPluginTool.getStepProperties();

		filepath = props['filepath']?.trim();
		teamSpace = props['teamspace']?.trim();
		projName = props['projName']?.trim();
		repoName = props['repoName']?.trim();
		branchName = props['branchName']?.trim();
		datasetsString = props['datasets'];
		variablesString = props['variables'];
		tagsString = props['tags'];
		secretsCollectionName = props['secretsCollectionName']?.trim();
		ritEnv = props['ritEnv']?.trim();
		proxyHost = props['proxyHost']?.trim();
		offlineToken = props['offlineToken']?.trim();
		trustStorePath = props['trustStorePath']?.trim();
		trustStorePassword = props['trustStorePassword']?.trim();

		try {
			url = new URL(props['serverUrl'].trim());
		} catch (MalformedURLException e) {
			println e.getMessage();
			System.exit(1);
		}
		serverUrl = 'https://' + url.getAuthority();
		util = new TokenUtil();
		validateInput();
		assetName = filepath.tokenize('/').last();
		assetName = assetName.take(assetName.lastIndexOf('.'));

		//Default branch to master if null
		if (!branchName) {
			branchName = "master";
		}

		println "Server URL: '${serverUrl}'";

		if (proxyHost) {
			proxyPass = props['proxyPass'];
			proxyPort = props['proxyPort'].trim();
			proxyUser = props['proxyUser'].trim();

			println "[Info] Using Proxy Host ${proxyHost}";
			println "[Info] Using Proxy Port ${proxyPort}";

			if (proxyUser) {
				println "[Info] Using Proxy User ${proxyUser}";
			}
		}
		def tokenResponse = util.getToken(offlineToken, serverUrl, trustStorePath, trustStorePassword);
		accessToken = tokenResponse.access_token;
		refreshToken = tokenResponse.refresh_token;
		client = util.getClient();
	}

	def getProjectIdByName(String projName) {
		String encodedProjName = URLEncoder.encode(projName, 'UTF-8');
		String url = serverUrl + '/rest/projects?archived=false&member=true&name=' + encodedProjName;
		HttpGet get = new HttpGet(url);
		get.addHeader("Content-Type", "application/json");
		get.addHeader("Authorization", "Bearer " + accessToken);
		get.addHeader("spaceId", spaceId);
		def response = client.execute(get);
		def entity = EntityUtils.toString(response.getEntity());
		def slurper = new JsonSlurper();
		def parsedJson = slurper.parseText(entity);
		def projId = parsedJson.data.id;
		if (!projId) {
			validateResponse(response, "Project Name", projName);
		}
		return projId;
	}

	def getRepoIdByName(String repoName, String projId) {
		String repoId = null;
		String url = serverUrl + '/rest/projects/' + projId + '/repositories/';
		url = URLifyII(url);
		HttpGet get = new HttpGet(url);
		get.addHeader("Content-Type", "application/json");
		get.addHeader("Authorization", "Bearer " + accessToken);
		def response = client.execute(get);
		def entity = EntityUtils.toString(response.getEntity());
		def slurper = new JsonSlurper();
		def parsedJson = slurper.parseText(entity);
		def repoArray = parsedJson.content;
		for (def repos : repoArray) {
			if (repos.uri.equals(repoName)) {
				repoId = repos.id;
			}
		}
		if (!repoId) {
			validateResponse(response, "Repository Name", repoName);
		}
		return repoId;
	}

	def getAssetByName(String repoId, String projId) {
		String assetId;
		String encodedAssetName = URLEncoder.encode(assetName, 'UTF-8');
		String encodedBranchName = URLEncoder.encode(branchName, 'UTF-8');
		String url = serverUrl + '/rest/projects/' + projId + '/assets/?name=' + encodedAssetName + '&revision=' + encodedBranchName;
		url = URLifyII(url);
		HttpGet get = new HttpGet(url);
		get.addHeader("Content-Type", "application/json");
		get.addHeader("Authorization", "Bearer " + accessToken);
		def response = client.execute(get);
		def entity = EntityUtils.toString(response.getEntity());
		def slurper = new JsonSlurper();
		def parsedJson = slurper.parseText(entity);
		def assetArray = parsedJson.content;
		for (def asset : assetArray) {
			if (asset.path.equals(filepath) && asset.repository_id.equals(repoId)) {
				assetId = asset.id;
				type = asset.external_type;
				desktopProjectId = asset.desktop_project_id;
			}
		}
		if (!assetId) {
			validateResponse(response, "Asset Name", filepath);
		}
		return assetId;
	}

	def getTeamSpaceIdByName(String teamSpace) {
		String encodedteamSpace = URLEncoder.encode(teamSpace, 'UTF-8');
		String url = serverUrl + "/rest/spaces?search=" + encodedteamSpace + "&member=true";
		url = URLifyII(url);
		HttpGet get = new HttpGet(url);
		get.addHeader("Content-Type", "application/json");
		get.addHeader("Authorization", "Bearer " + accessToken);
		def response = client.execute(get);
		def statusCode = response.getStatusLine().getStatusCode();
		if (statusCode != 200) {
			println "[Error] Request failed with status ${statusCode}.";
			println('Response:\n' + resp.entity?.content?.getText("UTF-8"));
			System.exit(1);
		}
		def entity = EntityUtils.toString(response.getEntity());
		def slurper = new JsonSlurper();
		def parsedJson = slurper.parseText(entity);
		def spaceArray = parsedJson.content;

		boolean spaceFound = false;
		for (def space : parsedJson) {
			if (teamSpace.equals(space.displayName)) {
				spaceId = space.id;
				spaceFound = true;
			}
		}
		if (!spaceFound) {
			println "You do not have access to the team space " + teamSpace + " or the team space was not found in the server. Please check the Team Space field in the task.";
			System.exit(1);
		}
	}

	def startJob() {
		getTeamSpaceIdByName(teamSpace);
		projId = getProjectIdByName(projName)[0];
		repoId = getRepoIdByName(repoName, projId);
		String assetId = getAssetByName(repoId, projId);
		if (type.equals("APISUITE") || type.equals("APISTUB") || type.equals("APITEST")) {
			if (!ritEnv) {
				println("Relative environment is mandatory attribute in order to run API Test");
				System.exit(1);
			} else {
				validateEnvironment(ritEnv);
			}
		}

		String location = null;
		HttpPost startJobMethod = new HttpPost(serverUrl + '/rest/projects/' + projId + '/executions/');
		startJobMethod.addHeader("Accept", "application/json");
		startJobMethod.addHeader("Content-Type", "application/json");
		startJobMethod.addHeader("Authorization", "Bearer " + accessToken);
		String methodBody = "";

		if (!type.equals("APISUITE") && !type.equals("APISTUB") && !type.equals("APITEST")) {
			ritEnv = null;
		}

		methodBody =
				'{"testAsset": {' +
				'"assetId": "' + assetId + '",' +
				'"revision": "' + branchName + '"' +
				'},' +
				'"offlineToken": "' + refreshToken + '",' +
				'"environment": "' + ritEnv + '"}'

		JsonObject reqObject = new JsonParser().parse(methodBody).getAsJsonObject();
		if (datasetsString) {
			def datasetList = datasetsString.split(";");
			JsonArray datasetArray = new JsonArray();
			for(def datasets : datasetList) {
				def datasetsArray = datasets.split(":");
				if(datasetsArray.length != 2) {
					throw new Exception(
					"Please enter Dataset value in format -- SourceDataset:SwapDataset");
				} else if (datasetsArray[0] == null || datasetsArray[0].trim().isEmpty()) {
					throw new Exception(
					"Source Dataset is not given for Swapdataset");
				} else if (datasetsArray[1] == null || datasetsArray[1].trim().isEmpty()) {
					throw new Exception(
					"SwapDataset is not given for Source Dataset");
				}
				def srcId = getsrcDatasetId(projId, assetId, branchName, datasetsArray[0]);
				def repId = getReplaceDatasetId(projId, srcId, branchName, datasetsArray);

				JsonObject sourceObj = new JsonObject();
				JsonObject assetObj = new JsonObject();
				JsonObject replaceObj = new JsonObject();

				assetObj.addProperty("assetId", srcId);
				sourceObj.add("source", assetObj);
				replaceObj.addProperty("datasetId", repId);
				sourceObj.add("replacement", replaceObj);

				datasetArray.add(sourceObj);
			}

			reqObject.add("dataSources", datasetArray);
		}

		if (variablesString) {
			JsonObject variableObject = new JsonObject();
			if (variablesString) {
				String[] variablesList = variablesString.split(";");
				for (variable in variablesList) {
					variableObject.addProperty(variable.tokenize("=")[0], variable.tokenize("=")[1]);
				}
				reqObject.add("variables", variableObject);
			}
		}

		def tags = [];
		if (tagsString) {
			tagsList = tagsString?.split(',')*.trim();
			tags = tagsList.collect { it };
			Gson gson = new GsonBuilder().create();
			JsonElement jsonElement = gson.toJsonTree(tags);
			reqObject.add("tags", jsonElement);
		}

		if (secretsCollectionName) {
			def secretId = getSecretId(projId,secretsCollectionName);
			reqObject.addProperty("secretsCollection", secretId);
		}

		StringEntity postEntity = new StringEntity(reqObject.toString());
		startJobMethod.setEntity(postEntity);
		def resp = client.execute(startJobMethod);
		def statusCode = resp.getStatusLine().getStatusCode();
		if (statusCode != 201) {
			println "[Error] Request failed with status ${statusCode}. Exiting Failure.";
			println('Response:\n' + resp.entity?.content?.getText("UTF-8"));
			System.exit(1);
		}

		def entity = EntityUtils.toString(resp.getEntity());
		def slurper = new JsonSlurper();
		def parsedJson = slurper.parseText(entity);
		def map = entity.properties;
		def execId = parsedJson.id;
		def resultId = parsedJson.result.id;
		String status = parsedJson.status;
		String prevStatus = status;
		println("Execution ID: " + execId + " " + "Results ID: " + resultId + "\n" + "Execution Status: " + status);

		if (!(status.equals("COMPLETE") || status.equals("COMPLETE_WITH_ERROR") ||
				status.equals("STOPPED_BY_USER") || status.equals("STOPPED_AUTOMATICALLY") ||
				status.equals("INCOMPLETE") || status.equals("CANCELED") || status.equals("LAUNCH_FAILED"))) {

			String statusURL = serverUrl + "/rest/projects/" + projId + "/executions/" + execId;

			while (!(status.equals("COMPLETE") || status.equals("COMPLETE_WITH_ERROR") ||
					status.equals("STOPPED_BY_USER") || status.equals("STOPPED_AUTOMATICALLY") ||
					status.equals("INCOMPLETE") || status.equals("CANCELED") || status.equals("LAUNCH_FAILED"))) {
				TimeUnit.SECONDS.sleep(10);
				HttpGet get = new HttpGet(statusURL);
				get.addHeader("Content-Type", "application/json");
				get.addHeader("Authorization", "Bearer " + accessToken);
				CloseableHttpResponse response = client.execute(get);
				def httpStatus = response.getStatusLine().getStatusCode();
				if (httpStatus == 401) {
					def tokenResponse = util.getToken(offlineToken, serverUrl, trustStorePath, trustStorePassword);
					accessToken = tokenResponse.access_token;
					refreshToken = tokenResponse.refresh_token;
					client = util.getClient();
				}
				def resEntity = EntityUtils.toString(response.getEntity());
				if(!resEntity.isEmpty()) {
					def resEntitySlurper = new JsonSlurper();
					def parsedRes = resEntitySlurper.parseText(resEntity);
					status = parsedRes.status;
				}
				//The output step property is set to Success for APISTUB once it starts as the APISTUB keeps running until explicitly stopped.
				if (type.equals("APISTUB") && status.equals("RUNNING")) {
					println("Execution Status: " + status);
					setOutputProperty(OutputPropertyStatus.SUCCESS);
					break;
				}
				if (status != null && !prevStatus.equals(status)) {
					println("Execution Status: " + status);
				}
				prevStatus = status;
			}

			if (!type.equals("APISTUB")) {
				getVerdict(projId, resultId, status);
			}
		} else {
			if (!type.equals("APISTUB")) {
				getVerdict(projId, resultId, status);
			}
		}

		if (statusCode != 201) {
			println "[Error] Request failed with status ${statusCode}. Exiting Failure.";
			println('Response:\n' + resp.entity?.content?.getText("UTF-8"));
			System.exit(1);
		}
	}

	def getVerdict(String projId, def resultId, String status) {
		HttpGet get = new HttpGet(serverUrl + "/rest/projects/" + projId + "/results/" + resultId);
		get.addHeader("Content-Type", "application/json");
		get.addHeader("Authorization", "Bearer " + accessToken);

		def response = client.execute(get);
		def entity = EntityUtils.toString(response.getEntity());
		def slurper = new JsonSlurper();
		def parsedJson = slurper.parseText(entity);
		def verdict = parsedJson.verdict;
		println("\nTest Result: " +verdict);

		if(verdict == null || verdict.equals("FAIL") || verdict.equals("ERROR")) {
			setOutputProperty(OutputPropertyStatus.FAILURE);
		}
		else {
			setOutputProperty(OutputPropertyStatus.SUCCESS);
		}

		if (!(status.equals("CANCELED") || status.equals("LAUNCH_FAILED"))) {
			def reportArray = parsedJson.reports;
			if (reportArray.size() > 0) {
				println("\nReports information:");
				for (def jsonObj : reportArray) {
					String reportName = jsonObj.name;
					String href = jsonObj.href;
					println(reportName + " : " + new URI(serverUrl).resolve(href));
				}
			}
		}
	}

	/*
	 * The output properties are set using the instance of AirPluginTool and 
	 * the properties are then loaded to output file defined in AirPluginTool instance 
	 * which is picked up by the server
	 */
	def setOutputProperty(OutputPropertyStatus status) {
		airPluginTool.setOutputProperty("Status", status.toString());
		airPluginTool.storeOutputProperties();
	}

	def validateInput() {
		boolean invalid = false;
		String errorMsg;
		if (!url) {
			invalid = true;
			errorMsg = "HCL DevOps Test Hub URL can not be blank";
		} else if (!filepath) {
			invalid = true;
			errorMsg = "filepath can not be blank";
		} else if (!repoName) {
			invalid = true;
			errorMsg = "Repository Name can not be blank";
		} else if (!projName) {
			invalid = true;
			errorMsg = "Project Name can not be blank";
		}

		if (!teamSpace) {
			invalid = true;
			errorMsg = "Team Space name can not be blank";
		} else if (trustStorePath) {
			File f = new File(trustStorePath);
			if (!f.isFile()) {
				invalid = true;
				errorMsg = "The path given for the property Custom Trust Store Path: " + trustStorePath + " does not exist";
			}
		}

		if (!invalid) {
			try {
				client = util.buildClient(trustStorePath, trustStorePassword);
				HttpGet get = new HttpGet(serverUrl);
				client.execute(get);
			} catch (UnknownHostException e) {
				invalid = true;
				errorMsg = "Could not establish connection to the server " + serverUrl + ".Please check the server URL and server connectivity. Error: " + e.getMessage();
			}
			catch (SSLException e) {
				invalid = true;
				errorMsg = "Could not establish secure connection to the server " + serverUrl + ". Please validate the SSL certificate of the server or import the CA certificate or SSL certificate of the server to your trust store and provide the trust store path to the property Custom Trust Store Path. Error: " + e.getMessage();
			}
			catch (Exception e) {
				invalid = true;
				errorMsg = "Could not establish connection to the server " + serverUrl + ". Error: " + e.getMessage();
			}
		}

		if (invalid) {
			println("***************  " + errorMsg + "   ********************");
			System.exit(1);
		}
	}

	def String URLifyII(String text) {
		int startIndex = 0;
		int endIndex = text.length() - 1;
		StringBuilder urlifiedText = new StringBuilder();

		// Find first non-space character
		while (text.charAt(startIndex) == ' ' && startIndex < endIndex) {
			startIndex++;
		}

		// Find last non-space character
		while (text.charAt(endIndex) == ' ' && endIndex >= startIndex) {
			endIndex--;
		}

		// Repeat text, and replace spaces with %20
		for (int i = startIndex; i <= endIndex; i++) {
			if (text.charAt(i) != ' ') {
				urlifiedText.append(text.charAt(i));
			} else {
				urlifiedText.append("%20");
			}
		}
		return urlifiedText.toString();
	}

	def validateResponse(def response, String missingAttr, String attrVal) {
		def errorMsg = '';
		if (missingAttr.equals("Asset Name")) {
			errorMsg = OneTestUCDConstants.RESPONSE_ERROR_FILEPATH_ASSET.replace("<varName>", attrVal);
		} else {
			errorMsg = OneTestUCDConstants.RESPONSE_ERROR.replace("<missingVar>", missingAttr).replace("<varName>", attrVal);
		}
		println(errorMsg);
		System.exit(1);
	}

	def validateEnvironment(def ritEnv) {
		String encodedBranchName = URLEncoder.encode(branchName, 'UTF-8')
		HttpGet get = new HttpGet(serverUrl + "/rest/projects/" + projId
				+ "/assets/?assetTypes=environment&revision="
				+ encodedBranchName + "&desktopProjectId=" + desktopProjectId);
		get.addHeader("Content-Type", "application/json");
		get.addHeader("Authorization", "Bearer " + accessToken);

		def response = client.execute(get);
		def entity = EntityUtils.toString(response.getEntity());
		def slurper = new JsonSlurper();
		def parsedJson = slurper.parseText(entity);
		def total = parsedJson.totalElements;
		def retrievedEnvName;
		def gotEnv = false;
		if (total > 0) {
			for (def i = 0; i < total; i++) {
				retrievedEnvName = parsedJson.content[i].name;
				if (ritEnv == retrievedEnvName) {
					gotEnv = true;
				}
			}
			if (gotEnv == false) {
				println("The test environment " + ritEnv + " is not valid for the test.");
				System.exit(1);
			}
		}
	}

	def getsrcDatasetId(String projId, String assetId, String branch, String srcDataSet) {
		String srcdataSetId = null;
		//Rest url is rest/projects/$projectId/assets/$assetId/$revision/dependencies/?assetTypes=dataset
		String url = serverUrl + '/rest/projects/' + projId + '/assets/' + assetId + '/' + branch + '/dependencies/?assetTypes=dataset';
		url = URLifyII(url);
		HttpGet get = new HttpGet(url);
		get.addHeader("Content-Type", "application/json");
		get.addHeader("Authorization", "Bearer " + accessToken);

		def response = client.execute(get);
		def entity = EntityUtils.toString(response.getEntity());
		def slurper = new JsonSlurper();
		def parsedJson = slurper.parseText(entity);
		def dataArray = parsedJson.content;

		for (def data : dataArray) {
			if (data.path.equals(srcDataSet)) {
				srcdataSetId = data.id;
			}
		}
		if (!srcdataSetId) {
			throw new Exception("No Data set configured for the Asset");
		}
		return srcdataSetId;
	}

	def getReplaceDatasetId(String projId, String srcDatasetId, String branch, String[] repDataSet) {
		String repdataSetId = null;
		String url = serverUrl + '/rest/projects/' + projId + '/datasets/?branch=' + branch + '&assetId=' + srcDatasetId + '&findSwaps=true';
		url = URLifyII(url);
		HttpGet get = new HttpGet(url);
		get.addHeader("Content-Type", "application/json");
		get.addHeader("Authorization", "Bearer " + accessToken);

		def response = client.execute(get);
		def entity = EntityUtils.toString(response.getEntity());
		def slurper = new JsonSlurper();
		def parsedJson = slurper.parseText(entity);
		def dataArray = parsedJson.data;

		for (def data : dataArray) {
			if (data.displayPath.equals(repDataSet[1])) {
				repdataSetId = data.datasetId;
			}
		}
		if (!repdataSetId) {
			throw new Exception("No SwapDataset ("+ repDataSet[1] +") is configured for the Source Dataset("+ repDataSet[0]+")");
		}
		return repdataSetId;
	}

	def getSecretId(String projId, String secretName) {
		String secretId = null;

		String url = serverUrl + '/rest/projects/' + projId + '/secrets/?type=ENVIRONMENT';
		url = URLifyII(url);
		HttpGet get = new HttpGet(url);
		get.addHeader("Content-Type", "application/json");
		get.addHeader("Authorization", "Bearer " + accessToken);

		def response = client.execute(get);
		def entity = EntityUtils.toString(response.getEntity());
		def slurper = new JsonSlurper();
		def parsedJson = slurper.parseText(entity);
		def dataArray = parsedJson.data;

		for (def data : dataArray) {
			if (data.name.equals(secretName)) {
				secretId = data.id;
			}
		}
		if (!secretId) {
			println "No Secret configured on the Server.";
		}
		return secretId;
	}
}
