/*******************************************************************************
 * Copyright (c) 2011, 2013 Anton Gorenkov and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     Anton Gorenkov - initial API and implementation
 *     Marc-Andre Laperle (Ericsson)
 *******************************************************************************/
package org.eclipse.cdt.testsrunner.internal.boost;

import java.text.MessageFormat;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Stack;

import org.eclipse.cdt.testsrunner.model.ITestItem;
import org.eclipse.cdt.testsrunner.model.ITestItem.Status;
import org.eclipse.cdt.testsrunner.model.ITestMessage;
import org.eclipse.cdt.testsrunner.model.ITestModelUpdater;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;

/**
 * Parses the Boost.Test XML log and notifies the Tests Runner Core about how
 * the testing process is going.
 */
public class BoostXmlLogHandler extends DefaultHandler {

	// Boost.Test XML log tags
	private static final String XML_NODE_TEST_LOG = "TestLog"; //$NON-NLS-1$
	private static final String XML_NODE_TEST_SUITE = "TestSuite"; //$NON-NLS-1$
	private static final String XML_NODE_TEST_CASE = "TestCase"; //$NON-NLS-1$
	private static final String XML_NODE_TESTING_TIME = "TestingTime"; //$NON-NLS-1$
	private static final String XML_NODE_LAST_CHECKPOINT = "LastCheckpoint"; //$NON-NLS-1$

	// Boost.Test XML log message levels representation
	private static final String XML_NODE_INFO = "Info"; //$NON-NLS-1$
	private static final String XML_NODE_MESSAGE = "Message"; //$NON-NLS-1$
	private static final String XML_NODE_WARNING = "Warning"; //$NON-NLS-1$
	private static final String XML_NODE_ERROR = "Error"; //$NON-NLS-1$
	private static final String XML_NODE_FATAL_ERROR = "FatalError"; //$NON-NLS-1$
	private static final String XML_NODE_EXCEPTION = "Exception"; //$NON-NLS-1$

	// Boost.Test XML log attributes
	private static final String XML_ATTR_TEST_SUITE_NAME = "name"; //$NON-NLS-1$
	private static final String XML_ATTR_TEST_CASE_NAME = "name"; //$NON-NLS-1$
	private static final String XML_ATTR_MESSAGE_FILE = "file"; //$NON-NLS-1$
	private static final String XML_ATTR_MESSAGE_LINE = "line"; //$NON-NLS-1$

	/** Maps the string message level representation to the Tests Runner internal enum code. */
	private static final Map<String, ITestMessage.Level> STRING_TO_MESSAGE_LEVEL;
	static {
		Map<String, ITestMessage.Level> aMap = new HashMap<>();
		aMap.put(XML_NODE_INFO, ITestMessage.Level.Info);
		aMap.put(XML_NODE_MESSAGE, ITestMessage.Level.Message);
		aMap.put(XML_NODE_WARNING, ITestMessage.Level.Warning);
		aMap.put(XML_NODE_ERROR, ITestMessage.Level.Error);
		aMap.put(XML_NODE_FATAL_ERROR, ITestMessage.Level.FatalError);
		// NOTE: Exception node is processed separately
		STRING_TO_MESSAGE_LEVEL = Collections.unmodifiableMap(aMap);
	}

	/** The default file name for test message location. */
	private static final String DEFAULT_LOCATION_FILE = null;

	/** The default line number for test message location. */
	private static final int DEFAULT_LOCATION_LINE = -1;

	/** The interface to notify the Tests Runner Core */
	private ITestModelUpdater modelUpdater;

	/** Stores the text between XML tags. */
	private Stack<StringBuilder> elementDataStack = new Stack<>();

	/** File name for current test message location. */
	private String fileName;

	/** Line number for current test message location. */
	private int lineNumber;

	/** Current test case status. */
	private ITestItem.Status testStatus;

	/**
	 * Keep track of the last test case name so that we can handle
	 * parameterized test cases which have the same name
	 */
	private String lastTestCaseName = ""; //$NON-NLS-1$
	private static final int SAME_TEST_CASE_NAME_COUNT_START = 2;
	private int sameTestCaseNameCount = SAME_TEST_CASE_NAME_COUNT_START;

	BoostXmlLogHandler(ITestModelUpdater modelUpdater) {
		this.modelUpdater = modelUpdater;
	}

	@Override
	public void startElement(String namespaceURI, String localName, String qName, Attributes attrs)
			throws SAXException {

		elementDataStack.push(new StringBuilder());
		if (qName == XML_NODE_TEST_SUITE) {
			String testSuiteName = attrs.getValue(XML_ATTR_TEST_SUITE_NAME);
			modelUpdater.enterTestSuite(testSuiteName);

		} else if (qName == XML_NODE_TEST_CASE) {
			String testCaseName = attrs.getValue(XML_ATTR_TEST_CASE_NAME);

			if (lastTestCaseName.equals(testCaseName)) {
				testCaseName += " (" + sameTestCaseNameCount + ")"; //$NON-NLS-1$ //$NON-NLS-2$
				++sameTestCaseNameCount;
			} else {
				lastTestCaseName = testCaseName;
				sameTestCaseNameCount = SAME_TEST_CASE_NAME_COUNT_START;
			}

			modelUpdater.enterTestCase(testCaseName);
			testStatus = Status.Passed;

		} else if (STRING_TO_MESSAGE_LEVEL.containsKey(qName) || qName == XML_NODE_LAST_CHECKPOINT) {
			fileName = attrs.getValue(XML_ATTR_MESSAGE_FILE);
			String lineNumberStr = attrs.getValue(XML_ATTR_MESSAGE_LINE);
			lineNumber = lineNumberStr != null ? Integer.parseInt(lineNumberStr.trim()) : DEFAULT_LOCATION_LINE;

		} else if (qName == XML_NODE_EXCEPTION) {
			fileName = DEFAULT_LOCATION_FILE;
			lineNumber = DEFAULT_LOCATION_LINE;

		} else if (qName == XML_NODE_TESTING_TIME) {

		} else if (qName == XML_NODE_TEST_LOG) {
			/* just skip, do nothing */

		} else {
			logAndThrowErrorForElement(qName);
		}
	}

	/**
	 * Common routing: notifies the Tests Runner core about new test message
	 * and resets the internal state.
	 *
	 * @param level test message level
	 */
	private void addCurrentMessage(ITestMessage.Level level) {
		modelUpdater.addTestMessage(fileName, lineNumber, level, elementDataStack.peek().toString());
		fileName = DEFAULT_LOCATION_FILE;
		lineNumber = DEFAULT_LOCATION_LINE;
		if (level == ITestMessage.Level.Error || level == ITestMessage.Level.FatalError) {
			if (testStatus != ITestItem.Status.Aborted) {
				testStatus = ITestItem.Status.Failed;
			}

		} else if (level == ITestMessage.Level.Exception) {
			testStatus = ITestItem.Status.Aborted;
		}
	}

	@Override
	public void endElement(String namespaceURI, String localName, String qName) throws SAXException {

		if (qName == XML_NODE_TEST_SUITE) {
			modelUpdater.exitTestSuite();

		} else if (qName == XML_NODE_TEST_CASE) {
			modelUpdater.setTestStatus(testStatus);
			modelUpdater.exitTestCase();

		} else if (qName == XML_NODE_TESTING_TIME) {
			modelUpdater.setTestingTime(Integer.parseInt(elementDataStack.peek().toString().trim()) / 1000);

		} else if (STRING_TO_MESSAGE_LEVEL.containsKey(qName)) {
			addCurrentMessage(STRING_TO_MESSAGE_LEVEL.get(qName));

		} else if (qName == XML_NODE_EXCEPTION) {
			if (fileName != DEFAULT_LOCATION_FILE && !fileName.isEmpty() && lineNumber >= 0) {
				elementDataStack.peek().append(BoostTestsRunnerMessages.BoostXmlLogHandler_exception_suffix);
			}
			addCurrentMessage(ITestMessage.Level.Exception);

		} else if (qName == XML_NODE_TEST_LOG || qName == XML_NODE_LAST_CHECKPOINT) {
			/* just skip, do nothing */

		} else {
			logAndThrowErrorForElement(qName);
		}
		elementDataStack.pop();
	}

	@Override
	public void characters(char[] ch, int start, int length) {
		StringBuilder builder = elementDataStack.peek();
		for (int i = start; i < start + length; i++) {
			builder.append(ch[i]);
		}
	}

	/**
	 * Throws the testing exception for the specified XML tag.
	 *
	 * @param tagName XML tag name
	 * @throws SAXException the exception that will be thrown
	 */
	private void logAndThrowErrorForElement(String tagName) throws SAXException {
		logAndThrowError(MessageFormat.format(BoostTestsRunnerMessages.BoostXmlLogHandler_wrong_tag_name, tagName));
	}

	/**
	 * Throws the testing exception with the specified message.
	 *
	 * @param message the reason
	 * @throws SAXException the exception that will be thrown
	 */
	private void logAndThrowError(String message) throws SAXException {
		SAXException e = new SAXException(message);
		BoostTestsRunnerPlugin.log(e);
		throw e;
	}

	@Override
	public void warning(SAXParseException ex) throws SAXException {
		BoostTestsRunnerPlugin.log(ex);
	}

	@Override
	public void error(SAXParseException ex) throws SAXException {
		BoostTestsRunnerPlugin.log(ex);
		throw ex;
	}

	@Override
	public void fatalError(SAXParseException ex) throws SAXException {
		BoostTestsRunnerPlugin.log(ex);
		throw ex;
	}

}
