/*
 * Copyright 2021-2024 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.opentest4j.reporting.tooling.core.converter;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.opentest4j.reporting.events.api.DocumentWriter;
import org.opentest4j.reporting.events.api.NamespaceRegistry;
import org.opentest4j.reporting.events.root.Events;
import org.opentest4j.reporting.schema.Namespace;
import org.opentest4j.reporting.tooling.core.util.DomUtils;
import org.opentest4j.reporting.tooling.core.util.DomUtils.NamespaceVersion;
import org.xmlunit.assertj3.XmlAssert;
import org.xmlunit.builder.Input;
import org.xmlunit.util.Convert;

import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Map;
import java.util.function.Consumer;

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.infrastructure;
import static org.opentest4j.reporting.events.core.CoreFactory.metadata;
import static org.opentest4j.reporting.events.core.CoreFactory.result;
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.Result.Status.SUCCESSFUL;
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;

class DefaultConverterTests {

	static final NamespaceRegistry NAMESPACE_REGISTRY = NamespaceRegistry.builder(Namespace.REPORTING_CORE) //
			.add("e", Namespace.REPORTING_EVENTS) //
			.add("java", Namespace.REPORTING_JAVA) //
			.build();

	static final NamespaceVersion CURRENT_VERSION = DomUtils.parseNamespace(
		Namespace.REPORTING_HIERARCHY.getUri()).orElseThrow().getVersion();

	Path sourceFile;
	Path targetFile;

	@BeforeEach
	void setUp(@TempDir Path tempDir) {
		sourceFile = tempDir.resolve("events.xml");
		targetFile = tempDir.resolve("hierarchy.xml");
	}

	@Test
	void convertsInfrastructureSection() throws Exception {
		writeXml(sourceFile,
			writer -> writer.append(infrastructure(), infrastructure -> infrastructure.append(cpuCores(42))));

		new DefaultConverter().convert(sourceFile, targetFile);

		assertAll(targetFile, CURRENT_VERSION, it -> it.valueByXPath("//*/c:infrastructure/c:cpuCores").isEqualTo(42));
	}

	@Test
	void convertsStartedAndFinishedEvents() throws Exception {
		var duration = Duration.ofMillis(42);
		var startTime = Instant.now().minus(duration);
		writeXml(sourceFile, writer -> writer //
				.append(started("1", startTime, "container"), started -> started //
						.append(metadata(), metadata -> metadata //
								.append(tags(), tags -> tags //
										.append(tag("a"))))) //
				.append(started("2", startTime.plus(duration), "test"), started -> started.withParentId("1")) //
				.append(finished("2", startTime.plus(duration.multipliedBy(2)))) //
				.append(finished("1", startTime.plus(duration.multipliedBy(3))), finished -> finished //
						.append(result(SUCCESSFUL))));

		new DefaultConverter().convert(sourceFile, targetFile);

		assertAll(targetFile, CURRENT_VERSION, //
			it -> it.nodesByXPath("//*/h:root") //
					.hasSize(1) //
					.haveAttribute("name", "container") //
					.haveAttribute("start", startTime.toString()) //
					.haveAttribute("duration", duration.multipliedBy(3).toString()), //
			it -> it.valueByXPath("//*/h:root/c:metadata/c:tags/c:tag") //
					.isEqualTo("a"), //
			it -> it.valueByXPath("//*/h:root/c:result/@status") //
					.isEqualTo("SUCCESSFUL"), //
			it -> it.nodesByXPath("//*/h:root/h:child") //
					.hasSize(1) //
					.haveAttribute("name", "test") //
					.haveAttribute("start", startTime.plus(duration).toString()) //
					.haveAttribute("duration", duration.toString()) //
		);
	}

	@Test
	void mergesReportEntries() throws Exception {
		writeXml(sourceFile, writer -> writer.append(started("1", Instant.now(), "test"), started -> started //
				.append(attachments(), attachments -> attachments //
						.append(data(LocalDateTime.now()), data -> data.addEntry("started", "1")))) //
				.append(reported("1", Instant.now()), reported -> reported //
						.append(attachments(), attachments -> attachments //
								.append(data(LocalDateTime.now()), data -> data.addEntry("reported", "2")))) //
				.append(finished("1", Instant.now()), finished -> finished //
						.append(attachments(), attachments -> attachments //
								.append(data(LocalDateTime.now()), data -> data.addEntry("finished", "3")))));

		new DefaultConverter().convert(sourceFile, targetFile);

		assertAll(targetFile, CURRENT_VERSION, //
			it -> it.nodesByXPath("//*/h:root/c:attachments").hasSize(1), //
			it -> it.nodesByXPath("//*/h:root/c:attachments/c:data").hasSize(3), //
			it -> it.valueByXPath("(//*/h:root/c:attachments/c:data/c:entry/@key)[1]").isEqualTo("started"), //
			it -> it.valueByXPath("(//*/h:root/c:attachments/c:data/c:entry/text())[1]").isEqualTo("1"), //
			it -> it.valueByXPath("(//*/h:root/c:attachments/c:data/c:entry/@key)[2]").isEqualTo("reported"), //
			it -> it.valueByXPath("(//*/h:root/c:attachments/c:data/c:entry/text())[2]").isEqualTo("2"), //
			it -> it.valueByXPath("(//*/h:root/c:attachments/c:data/c:entry/@key)[3]").isEqualTo("finished"), //
			it -> it.valueByXPath("(//*/h:root/c:attachments/c:data/c:entry/text())[3]").isEqualTo("3") //
		);
	}

	@Test
	void convertsOldVersions() throws Exception {
		Files.writeString(sourceFile,
			"""
					<e:events xmlns="https://schemas.opentest4j.org/reporting/core/0.1.0" xmlns:e="https://schemas.opentest4j.org/reporting/events/0.1.0">
					  <infrastructure>
					    <cpuCores>42</cpuCores>
					  </infrastructure>
					</e:events>
					""");

		new DefaultConverter().convert(sourceFile, targetFile);

		assertAll(targetFile, NamespaceVersion.parse("0.1.0"), //
			it -> it.nodesByXPath("//h:execution").hasSize(1), //
			it -> it.valueByXPath("//*/c:infrastructure/c:cpuCores").isEqualTo(42));
	}

	private void writeXml(Path eventsXmlFile, Consumer<DocumentWriter<Events>> action) throws Exception {
		try (var writer = Events.createDocumentWriter(NAMESPACE_REGISTRY, eventsXmlFile)) {
			action.accept(writer);
		}
	}

	@SafeVarargs
	private static void assertAll(Path targetFile, NamespaceVersion version, Consumer<XmlAssert>... checks) {
		var document = Convert.toDocument(Input.from(targetFile).build());
		var xmlAssert = XmlAssert.assertThat(document).withNamespaceContext(Map.of( //
			"h", toNamespaceUri(Namespace.REPORTING_HIERARCHY, version), //
			"c", toNamespaceUri(Namespace.REPORTING_CORE, version) //
		));
		Assertions.assertAll(targetFile.toUri().toString(),
			Arrays.stream(checks).map(it -> () -> it.accept(xmlAssert)));
	}

	private static String toNamespaceUri(Namespace namespace, NamespaceVersion version) {
		return DomUtils.parseNamespace(namespace.getUri()).orElseThrow().withVersion(version).toNamespace().getUri();
	}

}
