package com.quantum.listeners;

import static com.qmetry.qaf.automation.core.ConfigurationManager.getBundle;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import org.apache.commons.collections4.ListUtils;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.apache.commons.lang.text.StrSubstitutor;
import org.apache.commons.lang3.ArrayUtils;
import org.openqa.selenium.WebDriver;
import org.testng.IInvokedMethod;
import org.testng.ITestContext;
import org.testng.ITestListener;
import org.testng.ITestResult;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.stream.JsonReader;
import com.perfecto.reportium.WebDriverProvider;
import com.perfecto.reportium.client.ReportiumClient;
import com.perfecto.reportium.client.ReportiumClientFactory;
import com.perfecto.reportium.model.CustomField;
import com.perfecto.reportium.model.Job;
import com.perfecto.reportium.model.PerfectoExecutionContext;
import com.perfecto.reportium.model.PerfectoExecutionContext.PerfectoExecutionContextBuilder;
import com.perfecto.reportium.model.Project;
import com.perfecto.reportium.test.TestContext;
import com.perfecto.reportium.test.TestContext.Builder;
import com.perfecto.reportium.test.result.TestResult;
import com.perfecto.reportium.test.result.TestResultFactory;
import com.perfecto.reportium.testng.ReportiumTestNgListener;
import com.qmetry.qaf.automation.core.CheckpointResultBean;
import com.qmetry.qaf.automation.core.ConfigurationManager;
import com.qmetry.qaf.automation.core.MessageTypes;
import com.qmetry.qaf.automation.core.TestBaseProvider;
import com.qmetry.qaf.automation.keys.ApplicationProperties;
import com.qmetry.qaf.automation.step.QAFTestStepListener;
import com.qmetry.qaf.automation.step.StepExecutionTracker;
import com.qmetry.qaf.automation.step.TestStep;
import com.qmetry.qaf.automation.step.client.TestNGScenario;
import com.qmetry.qaf.automation.step.client.text.BDDDefinitionHelper.ParamType;
import com.qmetry.qaf.automation.ui.WebDriverTestCase;
import com.quantum.utils.ConsoleUtils;
import com.quantum.utils.DeviceUtils;
import com.quantum.utils.DriverUtils;
import com.quantum.utils.ReportUtils;

import cucumber.runtime.RuntimeOptions;
import cucumber.runtime.RuntimeOptionsFactory;

class Messages {
	List<String> StackTraceErrors;
	List<String> CustomFields;
	List<String> Tags;
	String CustomError;
	String JsonFile;

	public String getJsonFile() {
		return this.JsonFile;
	}

	public void setJsonFile(String jsonFile) {
		this.JsonFile = jsonFile;
	}

	public String getCustomError() {
		return this.CustomError;
	}

	public void setCustomError(String customError) {
		this.CustomError = customError;
	}

	public List<String> getStackTraceErrors() {
		return this.StackTraceErrors;
	}

	public void setStackTraceErrors(List<String> error) {
		this.StackTraceErrors = error;
	}

	public List<String> getCustomFields() {
		return this.CustomFields;
	}

	public void setCustomFields(List<String> customFields) {
		this.CustomFields = customFields;
	}

	public List<String> getTags() {
		return this.Tags;
	}

	public void setTags(List<String> tags) {
		this.Tags = tags;
	}
}

public class QuantumReportiumListener extends ReportiumTestNgListener implements QAFTestStepListener, ITestListener {

	public static final String PERFECTO_REPORT_CLIENT = "perfecto.report.client";

	public static ReportiumClient getReportClient() {
		return (ReportiumClient) getBundle().getObject(PERFECTO_REPORT_CLIENT);
	}

	public Messages parseFailureJsonFile(String actualMessage) {
		String jsonStr = null;
		String failureConfigLoc = ConfigurationManager.getBundle().getString("failureReasonConfig", "src/main/resources/failureReasons.json");
		
		File failureConfigFile = new File(failureConfigLoc);
		
		if (!failureConfigFile.exists()) {
			System.out.println("Ignoring Failure Reasons because JSON file was not found in path: " + failureConfigLoc);
			return null;
		}
		
		
		GsonBuilder gsonBuilder = new GsonBuilder();
		gsonBuilder.setLenient();
		Gson gson = gsonBuilder.create();
		JsonReader reader = null;

		try {
			reader = new JsonReader(new FileReader(failureConfigLoc));
		} catch (FileNotFoundException e) {
			// TODO Auto-generated catch block
			System.out.println("Problem parsing Failure Reason JSON file: " + failureConfigLoc);
			e.printStackTrace();
		}
		Messages[] response = gson.fromJson(reader, Messages[].class);
		
		for (Messages messages : response) {
			if (messages.getStackTraceErrors() == null) {
				System.out.println("Failure Reason JSON file has wrong formmat, please read here https://developers.perfectomobile.com/pages/viewpage.action?pageId=31103917: " + failureConfigLoc);
				return null;
			
			}
				
			for (String error : ListUtils.emptyIfNull(messages.getStackTraceErrors()))	{
				if (actualMessage.contains(error)) {
					messages.setJsonFile(failureConfigLoc);
					return messages;
			}
		}
//			if (messages.getStackTraceErrors().toString().contains(actualMessage)) {
//				messages.setJsonFile(failureConfigLoc);
//				return messages;
//			}

		}

		return null;
	}

	@Override
	@SuppressWarnings("unchecked")
	public void onStart(ITestContext context) {
		if (getBundle().getString("remote.server", "").contains("perfecto")) {

			List<String> stepListeners = getBundle().getList(ApplicationProperties.TESTSTEP_LISTENERS.key);
			if (!stepListeners.contains(this.getClass().getName())) {
				stepListeners.add(this.getClass().getName());
				getBundle().setProperty(ApplicationProperties.TESTSTEP_LISTENERS.key, stepListeners);
			}

			if (getBundle().getBoolean("perfecto.default.driver.listener", true)) {
				List<String> driverListeners = getBundle()
						.getList(ApplicationProperties.WEBDRIVER_COMMAND_LISTENERS.key);
				if (!driverListeners.contains(PerfectoDriverListener.class.getName())) {
					driverListeners.add(PerfectoDriverListener.class.getName());
					getBundle().setProperty(ApplicationProperties.WEBDRIVER_COMMAND_LISTENERS.key, driverListeners);
				}
			}
		}
	}

	@Override
	public void onTestStart(ITestResult testResult) {
		if (getBundle().getString("remote.server", "").contains("perfecto")) {

			// get custom fields "%name-value" from groups
			// compile actual groups
			String[] groups = testResult.getMethod().getGroups();
			ArrayList<String> groupsFinal = new ArrayList<String>();

			ArrayList<CustomField> cfc = new ArrayList<CustomField>();
			for (String string : groups) {
				if (string.startsWith(getBundle().getString("custom.field.identifier", "%"))) {
					try {
						cfc.add(new CustomField(
								string.split(getBundle().getString("custom.field.delimiter", "-"))[0].substring(1),
								string.split(getBundle().getString("custom.field.delimiter", "-"))[1]));
					} catch (Exception ex) {
						throw new NullPointerException(
								"Custom field key/value pair not delimited properly.  Example of proper default usage: %Developer-Jeremy.  Check application properties custom.field.delimiter and custom.field.identifier for custom values that may have been set.");
					}
				} else {
					groupsFinal.add(string);
				}
			}
			
			if(ConfigurationManager.getBundle().getString("custom.field") != null) {
				String customFieldValue = ConfigurationManager.getBundle().getString("custom.field");
				String[] customFieldPairs = customFieldValue.split(",");
				for(String customFieldPair : customFieldPairs) {
					try {
					cfc.add(new CustomField(
							customFieldPair.split(getBundle().getString("custom.field.delimiter", "-"))[0],
							customFieldPair.split(getBundle().getString("custom.field.delimiter", "-"))[1]));
					} catch (Exception ex) {
						new NullPointerException(
								"Custom field key/value pair not delimited properly.  Example of proper default usage: %Developer-Jeremy.  Check application properties custom.field.delimiter and custom.field.identifier for custom values that may have been set.").printStackTrace();
					}
				}
			}

			Builder testContext = new TestContext.Builder();
			if (groupsFinal.size() > 0) {
				testContext.withTestExecutionTags(groupsFinal.toString().replace('[', ' ').replace(']',' ').split(","));
			}

			if (cfc.size() > 0) {
				testContext.withCustomFields(cfc);
			}

			createReportiumClient(testResult).testStart(
					testResult.getMethod().getMethodName() + getDataDrivenText(testResult), testContext.build());

			if (testResult.getParameters().length > 0 && getBundle().getBoolean("addFullDataToReport", false)) {
				logStepStart("Test Data used");
				ReportUtils.reportComment(testResult.getParameters()[0].toString());
				logStepEnd();
			}
		}

		Map<Object, Object> dataPasser = new HashMap<Object, Object>();
		ConfigurationManager.getBundle().addProperty("dataPasser" + Thread.currentThread(), dataPasser);
	}

	@Override
	public void beforeInvocation(IInvokedMethod method, ITestResult testResult) {
		if (method.isTestMethod()) {
			// Before execution of test method
			ConsoleUtils.surroundWithSquare("TEST STARTED: " + getTestName(testResult)
					+ (testResult.getParameters().length > 0 ? " [" + testResult.getParameters()[0] + "]" : ""));

		}
	}

	@Override
	public void beforExecute(StepExecutionTracker stepExecutionTracker) {
		String stepDescription = getProcessStepDescription(stepExecutionTracker.getStep());
		String msg = "BEGIN STEP: " + stepDescription;
		ConsoleUtils.logInfoBlocks(msg, ConsoleUtils.lower_block + " ", 10);
		logStepStart(stepDescription);
	}

	@Override
	public void afterExecute(StepExecutionTracker stepExecutionTracker) {
		//logStepEnd();
		String msg = "END STEP: " + stepExecutionTracker.getStep().getDescription();
		ConsoleUtils.logInfoBlocks(msg, ConsoleUtils.upper_block + " ", 10);
	}

	@Override
	public void onFailure(StepExecutionTracker stepExecutionTracker) {

	}

	@Override
	public void onTestSuccess(ITestResult testResult) {
		ReportiumClient client = getReportClient();
		if (null != client) {
			client.testStop(TestResultFactory.createSuccess());
			logTestEnd(testResult);
		}

		tearIt(testResult);
	}

	@Override
	public void onTestFailure(ITestResult testResult) {
		ReportiumClient client = getReportClient();
		if (null != client) {

			String failMsg = "";
			List<CheckpointResultBean> checkpointsList = TestBaseProvider.instance().get().getCheckPointResults();
			for (CheckpointResultBean result : checkpointsList) {
				if (result.getType().equals(MessageTypes.TestStepFail.toString())) {
					failMsg += "Step:" + result.getMessage() + " failed" + "\n";
					// List<CheckpointResultBean> subList = result.getSubCheckPoints();
					// for (CheckpointResultBean sub : subList) {
					// if (sub.getType().equals(MessageTypes.Fail.toString())){
					// failMsg += sub.getMessage() + "\n";
					// }
					// }
				}
			}

			if (testResult.getThrowable() == null) {
				client.testStop(TestResultFactory.createFailure(failMsg.isEmpty() ? "An error occurred" : failMsg,
						new Exception(
								"There was some validation failure in the scenario which did not provide any throwable object.")));
			} else {
				ExceptionUtils.getStackTrace(testResult.getThrowable());
				String actualExceptionMessage = testResult.getThrowable().toString();
				Messages message = parseFailureJsonFile(actualExceptionMessage);

				if (message != null) {
					String customError = message.getCustomError();
					List<String> customFields = ListUtils.emptyIfNull(message.getCustomFields());
					List<String> tags = ListUtils.emptyIfNull(message.getTags());
					String fileLoc = message.getJsonFile();

					ArrayList<CustomField> cfc = new ArrayList<CustomField>();

					for (String customField : customFields) {
						try {
							cfc.add(new CustomField(
									customField.split(getBundle().getString("custom.field.delimiter", "-"))[0],
									customField.split(getBundle().getString("custom.field.delimiter", "-"))[1]));
						} catch (Exception ex) {
							throw new NullPointerException(
									"Custom field key/value pair not delimited properly in failure reason json file: "
											+ fileLoc
											+ ".  Example of proper default usage: Developer-Jeremy.  Check application properties custom.field.delimiter for custom values that may have been set.");
						}
					}

					ArrayList<String> tagsFinal = new ArrayList<String>();
					for (String tag : tags) {
						tagsFinal.add(tag);
					}

					Builder testContext = new TestContext.Builder();

					if (cfc.size() > 0) {
						testContext.withCustomFields(cfc);
					}

					if (tagsFinal.size() > 0) {
						testContext.withTestExecutionTags(tagsFinal);
					}

					TestResult reportiumResult = TestResultFactory.createFailure(
							failMsg.isEmpty() ? "An error occurred" : failMsg, testResult.getThrowable(), customError);
					client.testStop(reportiumResult, testContext.build());
				} else {
					client.testStop(TestResultFactory.createFailure(failMsg.isEmpty() ? "An error occurred" : failMsg,
							testResult.getThrowable()));
				}
			}

			logTestEnd(testResult);

			tearIt(testResult);
		}
	}

	private void tearIt(ITestResult testResult) {
		if ((testResult.getTestContext().getCurrentXmlTest().getParallel().toString().equalsIgnoreCase("methods")
				& testResult.getTestClass().getName().toLowerCase().contains("scenario"))
				|| ConfigurationManager.getBundle().getString("global.datadriven.parallel", "false")
						.equalsIgnoreCase("true")
				|| testResult.getTestContext().getCurrentXmlTest().getXmlClasses().get(0).getName()
						.contains("com.qmetry.qaf.automation.step.client.excel.ExcelTestFactory")
				|| testResult.getTestContext().getCurrentXmlTest().getXmlClasses().get(0).getName()
						.contains("com.qmetry.qaf.automation.step.client.csv.KwdTestFactory")

		) {
			Object testInstance = testResult.getInstance();
			((WebDriverTestCase) testInstance).getTestBase().tearDown();
		}
	}

	@Override
	public void onTestSkipped(ITestResult result) {
		ReportiumClient client = getReportClient();
		if (null != client) {
			// By default all the skipped tests will be failed, if you want
			if (ConfigurationManager.getBundle().getString("skippedTests", "fail").equalsIgnoreCase("pass")) {
				client.testStop(TestResultFactory.createSuccess());
			} else {
				String failureMessage = result.getThrowable().getMessage();
				failureMessage = (failureMessage.isEmpty() || failureMessage == null) ? "This test was skipped"
						: result.getThrowable().getMessage();
				client.testStop(TestResultFactory.createFailure(failureMessage, result.getThrowable()));
			}
			logTestEnd(result);
		}
	}

	@Override
	public void onTestFailedButWithinSuccessPercentage(ITestResult result) {

	}

	@Override
	public void onFinish(ITestContext context) {
	}

	public static void logTestStep(String message) {
		try {
			getReportClient().stepStart(message);
		} catch (Exception e) {
			// ignore...
		}
	}

	public static void logStepStart(String message) {
		try {
			getReportClient().stepStart(message);
		} catch (Exception e) {
			// ignore...
		}
	}

	public static void logStepEnd() {
		try {
			getReportClient().stepEnd();
		} catch (Exception e) {
			// ignore...
		}
	}

	public static void logAssert(String message, boolean status) {
		try {
			getReportClient().reportiumAssert(message, status);
		} catch (Exception e) {
			// ignore...
		}
	}

	private void logTestEnd(ITestResult testResult) {
		String endText = "TEST " + (testResult.isSuccess() ? "PASSED" : "FAILED") + ": ";
		addReportLink(testResult, getReportClient().getReportUrl());

		ConsoleUtils.logWarningBlocks(
				"REPORTIUM URL: " + getReportClient().getReportUrl().replace("[", "%5B").replace("]", "%5D"));
		ConsoleUtils.surroundWithSquare(endText + getTestName(testResult)
				+ (testResult.getParameters().length > 0 ? " [" + testResult.getParameters()[0] + "]" : ""));

	}

	@Override
	protected String getTestName(ITestResult result) {

		return result.getTestName() == null ? result.getMethod().getMethodName() : result.getTestName();
	}

	/**
	 * Creates client and set into configuration for later use during test execution
	 * using {@link #getReportiumClient()}.
	 *
	 * param testResult
	 * 
	 * @return newly created {@link ReportiumClient} object
	 */
	@Override
	protected ReportiumClient createReportiumClient(ITestResult testResult) {
		ReportiumClient reportiumClient = new ReportiumClientFactory().createLoggerClient();

		String suiteName = testResult.getTestContext().getSuite().getName();
		String prjName = getBundle().getString("project.name", suiteName);
		String prjVer = getBundle().getString("project.ver", "1.0");
		String xmlTestName = testResult.getTestContext().getName();
		String allTags = xmlTestName + "," + suiteName
				+ (System.getProperty("reportium-tags") == null ? "" : "," + System.getProperty("reportium-tags"));

		Object testInstance = testResult.getInstance();

		HashMap<String, WebDriver> driverList = new HashMap<String, WebDriver>();
		String driverNameList = ConfigurationManager.getBundle().getString("driverNameList", "");

		if (driverNameList.isEmpty()) {
			WebDriver driver = null;
			if (testInstance instanceof WebDriverTestCase)
				driver = ((WebDriverTestCase) testInstance).getDriver();
			else if (testInstance instanceof WebDriverProvider)
				driver = ((WebDriverProvider) testInstance).getWebDriver();
			driverList.put("Default Driver", driver);
		} else {
			for (String driverName : driverNameList.split(",")) {
				System.out.println("Adding driver with name - " + driverName);
				DriverUtils.switchToDriver(driverName);
				driverList.put(driverName, DeviceUtils.getQAFDriver());
			}
		}

		PerfectoExecutionContextBuilder perfectoExecutionContextBuilder = new PerfectoExecutionContext.PerfectoExecutionContextBuilder()
				.withProject(new Project(prjName, prjVer)).withContextTags(allTags.split(","))
				.withJob(new Job(getBundle().getString("JOB_NAME", System.getProperty("reportium-job-name")),
						getBundle().getInt("BUILD_NUMBER",
								System.getProperty("reportium-job-number") == null ? 0
										: Integer.parseInt(System.getProperty("reportium-job-number"))))
												.withBranch(System.getProperty("reportium-job-branch")));
		for (String driverName : driverList.keySet()) {
			
			perfectoExecutionContextBuilder.withWebDriver(driverList.get(driverName), driverName);
		}

		reportiumClient = new ReportiumClientFactory()
				.createPerfectoReportiumClient(perfectoExecutionContextBuilder.build());

		getBundle().setProperty(PERFECTO_REPORT_CLIENT, reportiumClient);

		return reportiumClient;
	}

	@Override
	protected String[] getTags(ITestResult testResult) {

		RuntimeOptions cucumberOptions = getCucumberOptions(testResult);
		List<String> optionsList = cucumberOptions.getFilters().stream().map(object -> Objects.toString(object, null))
				.collect(Collectors.toList());
		optionsList.addAll(cucumberOptions.getFeaturePaths());
		optionsList.addAll(cucumberOptions.getGlue());

		return ArrayUtils.addAll(super.getTags(testResult), optionsList.toArray(new String[optionsList.size()]));
	}

	private RuntimeOptions getCucumberOptions(ITestResult testResult) {
		try {
			return new RuntimeOptionsFactory(Class.forName(testResult.getTestClass().getName())).create();
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
		return null;
	}

	private void addReportLink(ITestResult result, String url) {
		((TestNGScenario) result.getMethod()).getMetaData().put("Perfecto-report",
				"<a href=\"" + url + "\" target=\"_blank\">view</a>");
	}

	@SuppressWarnings("rawtypes")
	private String getDataDrivenText(ITestResult testResult) {

		String result = "";
		if (testResult.getParameters().length > 0) {

			Map map = (Map) testResult.getParameters()[0];
			if (map.containsKey("recDescription")) {
				result = " [" + map.get("recDescription") + "]";
			} else if (map.containsKey("recId")) {
				result = " [" + map.get("recId") + "]";
			}
		}
		return result;
	}

	public static List<String> getArgNames(String def) {
		// Pattern p = Pattern.compile("[$][{](.*?)}");
		// Pattern p = Pattern.compile("\"(.*?)[$][{](.*?)}\"");
		// String allChars = "[a-zA-Z0-9!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>\\/?]";
		Pattern p = Pattern.compile(
				"\\\"([a-zA-Z0-9!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>\\/? ]*)[$][{](([a-zA-Z0-9!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>\\/? ]*))}([a-zA-Z0-9!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>\\/? ]*)\\\"");

		Matcher matcher = p.matcher(def);
		List<String> args = new ArrayList<String>();
		while (matcher.find()) {
			String paramName = matcher.group();
			String finalParamNam = paramName.substring(1, paramName.length() - 2);
			args.add(finalParamNam.replace("${", "{"));
		}
		return args;
	}

	@SuppressWarnings("unchecked")
	private String getProcessStepDescription(TestStep step) {
		// process parameters in step;

		String description = step.getDescription();

		// if (step instanceof CustomStep) {

		Object[] actualArgs = step.getActualArgs();
		String def = step.getDescription();

		if ((actualArgs != null) && (actualArgs.length > 0)) {
			Map<String, Object> paramMap = new HashMap<>();
			paramMap.putAll(step.getStepExecutionTracker().getContext());
			List<String> paramNames = getArgNames(def);

			System.out.println(paramNames);

			if ((paramNames != null) && (!paramNames.isEmpty())) {

				for (int i = 0; i < paramNames.size(); i++) {
					String paramName = paramNames.get(i).trim();
					// remove starting { and ending } from parameter name
					paramName = paramName.substring(1, paramName.length() - 1).split(":", 2)[0];

					// in case of data driven test args[0] should not be overriden
					// with steps args[0]
					if ((actualArgs[i] instanceof String)) {

						String pstr = (String) actualArgs[i];

						if (pstr.startsWith("${") && pstr.endsWith("}")) {
							String pname = pstr.substring(2, pstr.length() - 1);
							actualArgs[i] = paramMap.containsKey(pstr) ? paramMap.get(pstr)
									: paramMap.containsKey(pname) ? paramMap.get(pname)
											: getBundle().containsKey(pstr) ? getBundle().getObject(pstr)
													: getBundle().getObject(pname);
						} else if (pstr.indexOf("$") >= 0) {
							pstr = getBundle().getSubstitutor().replace(pstr);
							actualArgs[i] = StrSubstitutor.replace(pstr, paramMap);
						}
						// continue;
						ParamType ptype = ParamType.getType(pstr);
						if (ptype.equals(ParamType.MAP)) {
							Map<String, Object> kv = new Gson().fromJson(pstr, Map.class);
							paramMap.put(paramName, kv);
							for (String key : kv.keySet()) {
								paramMap.put(paramName + "." + key, kv.get(key));
							}
						} else if (ptype.equals(ParamType.LIST)) {
							List<Object> lst = new Gson().fromJson(pstr, List.class);
							paramMap.put(paramName, lst);
							for (int li = 0; li < lst.size(); li++) {
								paramMap.put(paramName + "[" + li + "]", lst.get(li));
							}
						}
					}

					paramMap.put("${args[" + i + "]}", actualArgs[i]);
					paramMap.put("args[" + i + "]", actualArgs[i]);
					paramMap.put(paramName, actualArgs[i]);

				}

				description = StrSubstitutor.replace(description, paramMap);

			}
		}
		return description;
	}
}