/*
 * 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.htmlreport;

import org.apiguardian.api.API;
import org.joox.Match;
import org.opentest4j.reporting.events.core.CpuCores;
import org.opentest4j.reporting.events.core.HostName;
import org.opentest4j.reporting.events.core.OperatingSystem;
import org.opentest4j.reporting.events.core.UserName;
import org.opentest4j.reporting.schema.QualifiedName;
import org.opentest4j.reporting.tooling.spi.htmlreport.Contributor;
import org.opentest4j.reporting.tooling.spi.htmlreport.Image;
import org.opentest4j.reporting.tooling.spi.htmlreport.KeyValuePairs;
import org.opentest4j.reporting.tooling.spi.htmlreport.Labels;
import org.opentest4j.reporting.tooling.spi.htmlreport.Paragraph;
import org.opentest4j.reporting.tooling.spi.htmlreport.Section;
import org.opentest4j.reporting.tooling.spi.htmlreport.Subsections;
import org.w3c.dom.Element;

import java.nio.file.Path;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.function.BiConsumer;

import static java.util.stream.Collectors.toList;
import static org.apiguardian.api.API.Status.INTERNAL;
import static org.joox.JOOX.$;
import static org.opentest4j.reporting.tooling.core.util.DomUtils.matches;
import static org.opentest4j.reporting.tooling.core.util.DomUtils.stream;

/**
 * Contributes sections to the HTML report elements in the core schema.
 *
 * @since 0.2.0
 */
@API(status = INTERNAL, since = "0.2.0")
public class CoreContributor implements Contributor {

	/**
	 * Create a new instance.
	 */
	public CoreContributor() {
	}

	@Override
	public List<Section> contributeSectionsForExecution(Context context) {
		var sections = new ArrayList<Section>();
		createInfrastructureSection(context.element()).ifPresent(sections::add);
		return sections;
	}

	@Override
	public List<Section> contributeSectionsForTestNode(Context context) {
		var sections = new ArrayList<Section>();
		createTagsSection(context.element()).ifPresent(sections::add);
		createSourcesSection(context.element()).ifPresent(sections::add);
		createReasonSection(context.element()).ifPresent(sections::add);
		createAttachmentsSection(context).ifPresent(sections::add);
		return sections;
	}

	private static Optional<Section> createInfrastructureSection(Element element) {
		var infrastructure = $(element).child("infrastructure");

		var table = new LinkedHashMap<String, String>();
		addToTable(infrastructure, HostName.ELEMENT, "Hostname", table::put);
		addToTable(infrastructure, UserName.ELEMENT, "Username", table::put);
		addToTable(infrastructure, OperatingSystem.ELEMENT, "Operating system", table::put);
		addToTable(infrastructure, CpuCores.ELEMENT, "CPU cores", table::put);

		if (table.isEmpty()) {
			return Optional.empty();
		}

		var keyValuePairs = KeyValuePairs.builder().content(table).build();
		return Optional.of(Section.builder().title("Infrastructure").addBlock(keyValuePairs).build());
	}

	private static Optional<Section> createTagsSection(Element element) {
		var tags = $(element).child("metadata").child("tags").children("tag").map(e -> e.element().getTextContent());
		if (tags.isEmpty()) {
			return Optional.empty();
		}
		var sortedTags = tags.stream().sorted().collect(toList());
		var labels = Labels.builder().content(sortedTags).build();
		return Optional.of(Section.builder().title("Tags").order(0).addBlock(labels).build());
	}

	private static Optional<Section> createSourcesSection(Element element) {
		var children = $(element).children("sources").children().get();
		if (children.isEmpty()) {
			return Optional.empty();
		}
		var subsections = Subsections.builder();
		children.stream().map(child -> {
			var type = child.getLocalName();
			if (child.getLocalName().endsWith("Source")) {
				type = child.getLocalName().substring(0, child.getLocalName().length() - "Source".length());
			}
			var subsection = Section.builder().title(capitalize(type));

			var attributes = KeyValuePairs.builder();
			stream(child.getAttributes()) //
					.filter(it -> it.getNodeValue() != null && !it.getNodeValue().isEmpty()) //
					.forEach(it -> attributes.putContent(it.getNodeName(), it.getNodeValue()));

			var filePosition = $(child).child("filePosition").first();
			if (filePosition.isNotEmpty()) {
				attributes.putContent("line", filePosition.attr("line"));
				var column = filePosition.attr("column");
				if (column != null) {
					attributes.putContent("column", column);
				}
			}

			subsection.addBlock(attributes.build());
			return subsection.build();
		}).forEach(subsections::addContent);

		return Optional.of(Section.builder().title("Sources").order(10).addBlock(subsections.build()).build());
	}

	private static Optional<Section> createReasonSection(Element element) {
		var reason = $(element).child("result").child("reason").text();
		if (reason == null) {
			return Optional.empty();
		}
		var paragraph = Paragraph.builder().content(reason).build();
		return Optional.of(Section.builder().title("Reason").order(20).addBlock(paragraph).build());
	}

	private static Optional<Section> createAttachmentsSection(Context context) {
		var children = $(context.element()).children("attachments").children().get();
		if (children.isEmpty()) {
			return Optional.empty();
		}

		var subsections = Subsections.builder();
		children.stream().map(child -> {
			var type = child.getLocalName();
			var section = Section.builder().title(capitalize(type));
			if ("file".equals(child.getLocalName())) {
				var attributes = KeyValuePairs.builder();
				attributes.putContent("time", $(child).attr("time"));
				var originalPath = context.sourceXmlFile().getParent().resolve($(child).attr("path")).toAbsolutePath();
				var path = tryRelativize(context.targetHtmlFile().getParent(), originalPath);
				var filename = path.getFileName().toString();
				attributes.putContent("filename", filename);
				attributes.putContent("path", "link:" + path);
				if (filename.endsWith(".png") || filename.endsWith(".jpg") || filename.endsWith(".jpeg")) {
					section.addBlock(Image.builder().content(path.toString()).altText(filename).build());
				}
				section.addBlock(attributes.build());
			}
			else if ("data".equals(child.getLocalName())) {
				var attributes = KeyValuePairs.builder();
				attributes.putContent("time", $(child).attr("time"));
				$(child).children("entry").forEach(entry -> {
					var key = entry.getAttribute("key");
					var value = entry.getTextContent();
					attributes.putContent(key, value);
				});
				section.addBlock(attributes.build());
			}
			return section.build();
		}).forEach(subsections::addContent);

		return Optional.of(Section.builder().title("Attachments").order(30).addBlock(subsections.build()).build());
	}

	private static Path tryRelativize(Path parent, Path path) {
		try {
			return parent.relativize(path);
		}
		catch (IllegalArgumentException e) {
			// This can happen on Windows if the paths are on different drives
			return path;
		}
	}

	static void addToTable(Match march, QualifiedName elementName, String label, BiConsumer<String, String> table) {
		var value = findChild(march, elementName).text();
		if (value != null) {
			table.accept(label, value);
		}
	}

	static Match findChild(Match march, QualifiedName elementName) {
		return march.child(c -> matches(elementName, c.element()));
	}

	private static String capitalize(String value) {
		return value.substring(0, 1).toUpperCase(Locale.ROOT) + value.substring(1);
	}

}
