/*
 * Copyright 2015-2023 the original author or authors.
 *
 * All rights reserved. This program and the accompanying materials are
 * made available under the terms of the Eclipse Public License v2.0 which
 * accompanies this distribution and is available at
 *
 * https://www.eclipse.org/legal/epl-v20.html
 */

package org.junit.platform.reporting.open.xml;

import static org.apiguardian.api.API.Status.EXPERIMENTAL;
import static org.junit.platform.reporting.open.xml.JUnitFactory.legacyReportingName;
import static org.junit.platform.reporting.open.xml.JUnitFactory.type;
import static org.junit.platform.reporting.open.xml.JUnitFactory.uniqueId;
import static org.opentest4j.reporting.events.core.CoreFactory.attachments;
import static org.opentest4j.reporting.events.core.CoreFactory.cpuCores;
import static org.opentest4j.reporting.events.core.CoreFactory.data;
import static org.opentest4j.reporting.events.core.CoreFactory.directorySource;
import static org.opentest4j.reporting.events.core.CoreFactory.fileSource;
import static org.opentest4j.reporting.events.core.CoreFactory.hostName;
import static org.opentest4j.reporting.events.core.CoreFactory.infrastructure;
import static org.opentest4j.reporting.events.core.CoreFactory.metadata;
import static org.opentest4j.reporting.events.core.CoreFactory.operatingSystem;
import static org.opentest4j.reporting.events.core.CoreFactory.reason;
import static org.opentest4j.reporting.events.core.CoreFactory.result;
import static org.opentest4j.reporting.events.core.CoreFactory.sources;
import static org.opentest4j.reporting.events.core.CoreFactory.tag;
import static org.opentest4j.reporting.events.core.CoreFactory.tags;
import static org.opentest4j.reporting.events.core.CoreFactory.uriSource;
import static org.opentest4j.reporting.events.core.CoreFactory.userName;
import static org.opentest4j.reporting.events.java.JavaFactory.classSource;
import static org.opentest4j.reporting.events.java.JavaFactory.classpathResourceSource;
import static org.opentest4j.reporting.events.java.JavaFactory.fileEncoding;
import static org.opentest4j.reporting.events.java.JavaFactory.heapSize;
import static org.opentest4j.reporting.events.java.JavaFactory.javaVersion;
import static org.opentest4j.reporting.events.java.JavaFactory.methodSource;
import static org.opentest4j.reporting.events.java.JavaFactory.packageSource;
import static org.opentest4j.reporting.events.java.JavaFactory.throwable;
import static org.opentest4j.reporting.events.root.RootFactory.finished;
import static org.opentest4j.reporting.events.root.RootFactory.reported;
import static org.opentest4j.reporting.events.root.RootFactory.started;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.file.Path;
import java.time.Instant;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

import org.apiguardian.api.API;
import org.junit.platform.commons.JUnitException;
import org.junit.platform.commons.util.StringUtils;
import org.junit.platform.engine.ConfigurationParameters;
import org.junit.platform.engine.TestExecutionResult;
import org.junit.platform.engine.TestSource;
import org.junit.platform.engine.UniqueId;
import org.junit.platform.engine.reporting.ReportEntry;
import org.junit.platform.engine.support.descriptor.ClassSource;
import org.junit.platform.engine.support.descriptor.ClasspathResourceSource;
import org.junit.platform.engine.support.descriptor.CompositeTestSource;
import org.junit.platform.engine.support.descriptor.DirectorySource;
import org.junit.platform.engine.support.descriptor.FileSource;
import org.junit.platform.engine.support.descriptor.MethodSource;
import org.junit.platform.engine.support.descriptor.PackageSource;
import org.junit.platform.engine.support.descriptor.UriSource;
import org.junit.platform.launcher.TestExecutionListener;
import org.junit.platform.launcher.TestIdentifier;
import org.junit.platform.launcher.TestPlan;
import org.junit.platform.launcher.listeners.OutputDir;
import org.opentest4j.reporting.events.api.DocumentWriter;
import org.opentest4j.reporting.events.api.NamespaceRegistry;
import org.opentest4j.reporting.events.core.Result;
import org.opentest4j.reporting.events.core.Sources;
import org.opentest4j.reporting.events.root.Events;
import org.opentest4j.reporting.schema.Namespace;

/**
 * Open Test Reporting events XML generating test execution listener.
 *
 * @since 1.9
 */
@API(status = EXPERIMENTAL, since = "1.9")
public class OpenTestReportGeneratingListener implements TestExecutionListener {

	static final String ENABLED_PROPERTY_NAME = "junit.platform.reporting.open.xml.enabled";
	static final String OUTPUT_DIR_PROPERTY_NAME = "junit.platform.reporting.output.dir";

	private final AtomicInteger idCounter = new AtomicInteger();
	private final Map<UniqueId, String> inProgressIds = new ConcurrentHashMap<>();
	private DocumentWriter<Events> eventsFileWriter = DocumentWriter.noop();

	public OpenTestReportGeneratingListener() {
	}

	@Override
	public void testPlanExecutionStarted(TestPlan testPlan) {
		ConfigurationParameters config = testPlan.getConfigurationParameters();
		if (isEnabled(config)) {
			NamespaceRegistry namespaceRegistry = NamespaceRegistry.builder(Namespace.REPORTING_CORE) //
					.add("e", Namespace.REPORTING_EVENTS) //
					.add("java", Namespace.REPORTING_JAVA) //
					.add("junit", JUnitFactory.NAMESPACE,
						"https://junit.org/junit5/schemas/open-test-reporting/junit-1.9.xsd") //
					.build();
			Path eventsXml = OutputDir.create(config.get(OUTPUT_DIR_PROPERTY_NAME)) //
					.createFile("junit-platform-events", "xml");
			try {
				eventsFileWriter = Events.createDocumentWriter(namespaceRegistry, eventsXml);
				reportInfrastructure();
			}
			catch (Exception e) {
				throw new JUnitException("Failed to initialize XML events file: " + eventsXml, e);
			}
		}
	}

	private Boolean isEnabled(ConfigurationParameters config) {
		return config.getBoolean(ENABLED_PROPERTY_NAME).orElse(false);
	}

	private void reportInfrastructure() {
		eventsFileWriter.append(infrastructure(), infrastructure -> {
			try {
				String hostName = InetAddress.getLocalHost().getHostName();
				infrastructure.append(hostName(hostName));
			}
			catch (UnknownHostException ignored) {
			}
			infrastructure //
					.append(userName(System.getProperty("user.name"))) //
					.append(operatingSystem(System.getProperty("os.name"))) //
					.append(cpuCores(Runtime.getRuntime().availableProcessors())) //
					.append(javaVersion(System.getProperty("java.version"))) //
					.append(fileEncoding(System.getProperty("file.encoding"))) //
					.append(heapSize(), heapSize -> heapSize.withMax(Runtime.getRuntime().maxMemory()));
		});
	}

	@Override
	public void testPlanExecutionFinished(TestPlan testPlan) {
		try {
			eventsFileWriter.close();
		}
		catch (IOException e) {
			throw new UncheckedIOException("Failed to close XML events file", e);
		}
		finally {
			eventsFileWriter = DocumentWriter.noop();
		}
	}

	@Override
	public void executionSkipped(TestIdentifier testIdentifier, String reason) {
		String id = String.valueOf(idCounter.incrementAndGet());
		reportStarted(testIdentifier, id);
		eventsFileWriter.append(finished(id, Instant.now()), //
			finished -> finished.append(result(Result.Status.SKIPPED), result -> {
				if (StringUtils.isNotBlank(reason)) {
					result.append(reason(reason));
				}
			}));
	}

	@Override
	public void executionStarted(TestIdentifier testIdentifier) {
		String id = String.valueOf(idCounter.incrementAndGet());
		inProgressIds.put(testIdentifier.getUniqueIdObject(), id);
		reportStarted(testIdentifier, id);
	}

	private void reportStarted(TestIdentifier testIdentifier, String id) {
		eventsFileWriter.append(started(id, Instant.now(), testIdentifier.getDisplayName()), started -> {
			testIdentifier.getParentIdObject().ifPresent(parentId -> started.withParentId(inProgressIds.get(parentId)));
			started.append(metadata(), metadata -> {
				if (!testIdentifier.getTags().isEmpty()) {
					metadata.append(tags(), tags -> //
					testIdentifier.getTags().forEach(tag -> tags.append(tag(tag.getName()))));
				}
				metadata.append(uniqueId(testIdentifier.getUniqueId())) //
						.append(legacyReportingName(testIdentifier.getLegacyReportingName())) //
						.append(type(testIdentifier.getType()));
			});
			testIdentifier.getSource().ifPresent(
				source -> started.append(sources(), sources -> addTestSource(source, sources)));
		});
	}

	private void addTestSource(TestSource source, Sources sources) {
		if (source instanceof CompositeTestSource) {
			((CompositeTestSource) source).getSources().forEach(it -> addTestSource(it, sources));
		}
		else if (source instanceof ClassSource) {
			ClassSource classSource = (ClassSource) source;
			sources.append(classSource(classSource.getClassName()), //
				element -> classSource.getPosition().ifPresent(
					filePosition -> element.addFilePosition(filePosition.getLine(), filePosition.getColumn())));
		}
		else if (source instanceof MethodSource) {
			MethodSource methodSource = (MethodSource) source;
			sources.append(methodSource(methodSource.getClassName(), methodSource.getMethodName()), element -> {
				String methodParameterTypes = methodSource.getMethodParameterTypes();
				if (methodParameterTypes != null) {
					element.withMethodParameterTypes(methodParameterTypes);
				}
			});
		}
		else if (source instanceof ClasspathResourceSource) {
			ClasspathResourceSource classpathResourceSource = (ClasspathResourceSource) source;
			sources.append(classpathResourceSource(classpathResourceSource.getClasspathResourceName()), //
				element -> classpathResourceSource.getPosition().ifPresent(
					filePosition -> element.addFilePosition(filePosition.getLine(), filePosition.getColumn())));
		}
		else if (source instanceof PackageSource) {
			sources.append(packageSource(((PackageSource) source).getPackageName()));
		}
		else if (source instanceof FileSource) {
			FileSource fileSource = (FileSource) source;
			sources.append(fileSource(fileSource.getFile()), //
				element -> fileSource.getPosition().ifPresent(
					filePosition -> element.addFilePosition(filePosition.getLine(), filePosition.getColumn())));
		}
		else if (source instanceof DirectorySource) {
			sources.append(directorySource(((DirectorySource) source).getFile()));
		}
		else if (source instanceof UriSource) {
			sources.append(uriSource(((UriSource) source).getUri()));
		}
	}

	@Override
	public void reportingEntryPublished(TestIdentifier testIdentifier, ReportEntry entry) {
		String id = inProgressIds.get(testIdentifier.getUniqueIdObject());
		eventsFileWriter.append(reported(id, Instant.now()), //
			reported -> reported.append(attachments(), attachments -> attachments.append(data(), data -> {
				data.withTime(entry.getTimestamp());
				entry.getKeyValuePairs().forEach(data::addEntry);
			})));
	}

	@Override
	public void executionFinished(TestIdentifier testIdentifier, TestExecutionResult testExecutionResult) {
		String id = inProgressIds.remove(testIdentifier.getUniqueIdObject());
		eventsFileWriter.append(finished(id, Instant.now()), //
			finished -> finished.append(result(convertStatus(testExecutionResult.getStatus())), //
				result -> testExecutionResult.getThrowable() //
						.ifPresent(throwable -> result.append(throwable(throwable)))));
	}

	private Result.Status convertStatus(TestExecutionResult.Status status) {
		switch (status) {
			case FAILED:
				return Result.Status.FAILED;
			case SUCCESSFUL:
				return Result.Status.SUCCESSFUL;
			case ABORTED:
				return Result.Status.ABORTED;
		}
		throw new JUnitException("Unhandled status: " + status);
	}

}
