/*******************************************************************************
 * Copyright (c) 2011, 2012 Anton Gorenkov
 *
 * 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
 *******************************************************************************/
package org.eclipse.cdt.testsrunner.internal.model;

import java.io.InputStream;
import java.text.DateFormat;
import java.text.MessageFormat;
import java.util.Date;
import java.util.EnumMap;
import java.util.Map;

import org.eclipse.cdt.testsrunner.internal.TestsRunnerPlugin;
import org.eclipse.cdt.testsrunner.internal.launcher.TestsRunnerProviderInfo;
import org.eclipse.cdt.testsrunner.launcher.ITestsRunnerProvider;
import org.eclipse.cdt.testsrunner.model.IModelVisitor;
import org.eclipse.cdt.testsrunner.model.ITestCase;
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.ITestModelAccessor;
import org.eclipse.cdt.testsrunner.model.ITestSuite;
import org.eclipse.cdt.testsrunner.model.ITestingSession;
import org.eclipse.cdt.testsrunner.model.ITestingSessionListener;
import org.eclipse.cdt.testsrunner.model.TestingException;
import org.eclipse.debug.core.DebugException;
import org.eclipse.debug.core.ILaunch;

/**
 * Stores the information about tests running.
 */
public class TestingSession implements ITestingSession {

	/** Launch object the is connected to the tests running. */
	private ILaunch launch;

	/** Information about used Tests Runner provider plug-in. */
	private TestsRunnerProviderInfo testsRunnerProviderInfo;

	/** Main interface to Tests Runner provider plug-in. */
	private ITestsRunnerProvider testsRunnerProvider;

	/**
	 * Test Model manager that is used to fill and update the model for the
	 * session.
	 */
	private TestModelManager modelManager;

	/**
	 * Total tests counter. It is -1 by default, that means that total tests
	 * count is not available.
	 *
	 * @see getTotalCounter()
	 */
	private int totalCounter = -1;

	/** Already finished tests counter. */
	private int currentCounter = 0;

	/**
	 * Test counters map by test status. They are used to quickly provide simple
	 * statistics without model scanning.
	 *
	 */
	private Map<ITestItem.Status, Integer> statusCounters = new EnumMap<>(ITestItem.Status.class);

	/**
	 * The flag stores whether the testing session contains errors at the
	 * moment.
	 *
	 * @see hasErrors()
	 */
	private boolean hasErrors = false;

	/**
	 * The flag stores whether the testing session was stopped by user.
	 *
	 * @see wasStopped()
	 */
	private boolean wasStopped = false;

	/**
	 * The flag stores whether the testing session has been finished (with or
	 * without errors).
	 */
	private boolean finished = false;

	/** Stores current status of the testing session. */
	private String statusMessage = ModelMessages.TestingSession_starting_status;

	/** Stores the time when the testing session was created. */
	private long startTime;

	/**
	 * Counts the number of the test cases in tests hierarchy.
	 */
	private class TestCasesCounter implements IModelVisitor {

		public int result = 0;

		@Override
		public void visit(ITestCase testCase) {
			++result;
		}

		@Override
		public void visit(ITestSuite testSuite) {
		}

		@Override
		public void visit(ITestMessage testMessage) {
		}

		@Override
		public void leave(ITestSuite testSuite) {
		}

		@Override
		public void leave(ITestCase testCase) {
		}

		@Override
		public void leave(ITestMessage testMessage) {
		}
	}

	/**
	 * The constructor.
	 *
	 * @param launch connected launch object
	 * @param testsRunnerProviderInfo the information about the tests runner
	 * @param previousSession is used to determine total tests count & for tests
	 * hierarchy reusing if it is considered as similar
	 */
	public TestingSession(ILaunch launch, TestsRunnerProviderInfo testsRunnerProviderInfo,
			TestingSession previousSession) {
		this.launch = launch;
		this.testsRunnerProviderInfo = testsRunnerProviderInfo;
		this.testsRunnerProvider = testsRunnerProviderInfo.instantiateTestsRunnerProvider();
		this.startTime = System.currentTimeMillis();
		// Calculate approximate tests count by the previous similar testing session (if available)
		if (previousSession != null) {
			TestCasesCounter testCasesCounter = new TestCasesCounter();
			previousSession.getModelAccessor().getRootSuite().visit(testCasesCounter);
			totalCounter = testCasesCounter.result;
		}
		ITestSuite rootTestSuite = previousSession != null ? previousSession.getModelAccessor().getRootSuite() : null;
		this.modelManager = new TestModelManager(rootTestSuite,
				testsRunnerProviderInfo.isAllowedTestingTimeMeasurement());
		this.modelManager.addChangesListener(new ITestingSessionListener() {

			@Override
			public void testingStarted() {
			}

			@Override
			public void testingFinished() {
				// This is necessary if totalCounter was -1 (tests count was unknown)
				// or if tests count was estimated not accurately
				totalCounter = currentCounter;
			}

			@Override
			public void exitTestSuite(ITestSuite testSuite) {
			}

			@Override
			public void exitTestCase(ITestCase testCase) {
				// Update testing session info (counters, flags)
				Status testStatus = testCase.getStatus();
				statusCounters.put(testStatus, getCount(testStatus) + 1);
				++currentCounter;
				if (testStatus.isError())
					hasErrors = true;
			}

			@Override
			public void enterTestSuite(ITestSuite testSuite) {
			}

			@Override
			public void enterTestCase(ITestCase testCase) {
			}

			@Override
			public void childrenUpdate(ITestSuite parent) {
			}
		});
	}

	/**
	 * Starts the processing of the test module output.
	 *
	 * @param inputStream test module output stream
	 */
	public void run(InputStream inputStream) {
		modelManager.testingStarted();
		try {
			testsRunnerProvider.run(modelManager, inputStream);
			// If testing session was stopped, the status is set in stop()
			if (!wasStopped()) {
				double testingTime = getModelAccessor().getRootSuite().getTestingTime();
				statusMessage = MessageFormat.format(ModelMessages.TestingSession_finished_status,
						testingTime / 1000.0);
			}
		} catch (TestingException e) {
			// If testing session was stopped, the status is set in stop()
			if (!wasStopped()) {
				statusMessage = e.getLocalizedMessage();
				hasErrors = true;
			}
		}
		finished = true;
		modelManager.testingFinished();
	}

	@Override
	public int getCurrentCounter() {
		return currentCounter;
	}

	@Override
	public int getTotalCounter() {
		return totalCounter;
	}

	@Override
	public int getCount(ITestItem.Status status) {
		Integer counterValue = statusCounters.get(status);
		return (counterValue == null) ? 0 : counterValue;
	}

	@Override
	public boolean hasErrors() {
		return hasErrors;
	}

	@Override
	public boolean wasStopped() {
		return wasStopped;
	}

	@Override
	public boolean isFinished() {
		return finished;
	}

	@Override
	public ITestModelAccessor getModelAccessor() {
		return modelManager;
	}

	@Override
	public ILaunch getLaunch() {
		return launch;
	}

	@Override
	public TestsRunnerProviderInfo getTestsRunnerProviderInfo() {
		return testsRunnerProviderInfo;
	}

	@Override
	public String getStatusMessage() {
		return statusMessage;
	}

	@Override
	public String getName() {
		String launchConfName = launch.getLaunchConfiguration().getName();
		String startTimeStr = DateFormat.getDateTimeInstance().format(new Date(startTime));
		return MessageFormat.format(ModelMessages.TestingSession_name_format, launchConfName, startTimeStr);
	}

	@Override
	public void stop() {
		if (!launch.isTerminated() && launch.canTerminate()) {
			try {
				launch.terminate();
				wasStopped = true;
				statusMessage = ModelMessages.TestingSession_stopped_status;
			} catch (DebugException e) {
				TestsRunnerPlugin.log(e);
			}
		}
	}

}
