/*
 * Copyright 2012-2018 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
 *
 *      http://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 io.spring.initializr.test.generator;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.LinkedHashMap;
import java.util.Map;

import io.spring.initializr.generator.ProjectRequest;
import io.spring.initializr.metadata.BillOfMaterials;
import io.spring.initializr.metadata.Dependency;
import io.spring.initializr.metadata.InitializrConfiguration.Env.Maven.ParentPom;
import io.spring.initializr.metadata.Repository;
import org.custommonkey.xmlunit.SimpleNamespaceContext;
import org.custommonkey.xmlunit.XMLUnit;
import org.custommonkey.xmlunit.XpathEngine;
import org.custommonkey.xmlunit.exceptions.XpathException;
import org.junit.Assert;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import static org.assertj.core.api.Assertions.assertThat;

/**
 * XPath assertions that are specific to a standard Maven POM.
 *
 * @author Stephane Nicoll
 */
public class PomAssert {

	private final XpathEngine eng;

	private final Document doc;

	private final ParentPom parentPom;

	private final Map<String, String> properties = new LinkedHashMap<>();

	private final Map<String, Dependency> dependencies = new LinkedHashMap<>();

	private final Map<String, BillOfMaterials> boms = new LinkedHashMap<>();

	private final Map<String, Repository> repositories = new LinkedHashMap<>();

	public PomAssert(String content) {
		this.eng = XMLUnit.newXpathEngine();
		Map<String, String> context = new LinkedHashMap<>();
		context.put("pom", "http://maven.apache.org/POM/4.0.0");
		SimpleNamespaceContext namespaceContext = new SimpleNamespaceContext(context);
		this.eng.setNamespaceContext(namespaceContext);
		try {
			this.doc = XMLUnit.buildControlDocument(content);
		}
		catch (Exception e) {
			throw new IllegalArgumentException("Cannot parse XML", e);
		}
		this.parentPom = parseParent();
		parseProperties();
		parseDependencies();
		parseBoms();
		parseRepositories();
	}

	/**
	 * Validate that this generated pom validates against its request.
	 * @param request the source request
	 * @return a POM assert
	 */
	public PomAssert validateProjectRequest(ProjectRequest request) {
		return hasGroupId(request.getGroupId()).hasArtifactId(request.getArtifactId())
				.hasVersion(request.getVersion()).hasPackaging(request.getPackaging())
				.hasName(request.getName()).hasDescription(request.getDescription())
				.hasJavaVersion(request.getJavaVersion());
	}

	public PomAssert hasGroupId(String groupId) {
		try {
			assertThat(this.eng.evaluate(createRootNodeXPath("groupId"), this.doc))
					.isEqualTo(groupId);
		}
		catch (XpathException e) {
			throw new IllegalStateException("Cannot find path", e);
		}
		return this;
	}

	public PomAssert hasArtifactId(String artifactId) {
		try {
			assertThat(this.eng.evaluate(createRootNodeXPath("artifactId"), this.doc))
					.isEqualTo(artifactId);
		}
		catch (XpathException e) {
			throw new IllegalStateException("Cannot find path", e);
		}
		return this;
	}

	public PomAssert hasVersion(String version) {
		try {
			assertThat(this.eng.evaluate(createRootNodeXPath("version"), this.doc))
					.isEqualTo(version);
		}
		catch (XpathException e) {
			throw new IllegalStateException("Cannot find path", e);
		}
		return this;
	}

	public PomAssert hasPackaging(String packaging) {
		try {
			assertThat(this.eng.evaluate(createRootNodeXPath("packaging"), this.doc))
					.isEqualTo(packaging);
		}
		catch (XpathException e) {
			throw new IllegalStateException("Cannot find path", e);
		}
		return this;
	}

	public PomAssert hasName(String name) {
		try {
			assertThat(this.eng.evaluate(createRootNodeXPath("name"), this.doc))
					.isEqualTo(name);
		}
		catch (XpathException e) {
			throw new IllegalStateException("Cannot find path", e);
		}
		return this;
	}

	public PomAssert hasDescription(String description) {
		try {
			assertThat(this.eng.evaluate(createRootNodeXPath("description"), this.doc))
					.isEqualTo(description);
		}
		catch (XpathException e) {
			throw new IllegalStateException("Cannot find path", e);
		}
		return this;
	}

	public PomAssert hasJavaVersion(String javaVersion) {
		try {
			assertThat(
					this.eng.evaluate(createPropertyNodeXpath("java.version"), this.doc))
							.isEqualTo(javaVersion);
		}
		catch (XpathException e) {
			throw new IllegalStateException("Cannot find path", e);
		}
		return this;
	}

	public PomAssert hasProperty(String name, String value) {
		assertThat(this.properties).containsKeys(name);
		assertThat(this.properties).containsEntry(name, value);
		return this;
	}

	public PomAssert hasNoProperty(String name) {
		assertThat(this.properties).doesNotContainKeys(name);
		return this;
	}

	public PomAssert hasDependenciesCount(int count) {
		assertThat(this.dependencies).hasSize(count);
		return this;
	}

	public PomAssert hasSpringBootStarterTomcat() {
		return hasDependency(
				Dependency.withId("tomcat", "provided").asSpringBootStarter("tomcat"));
	}

	public PomAssert hasSpringBootStarterTest() {
		return hasDependency(
				Dependency.withId("test", "test").asSpringBootStarter("test"));
	}

	public PomAssert hasSpringBootStarterDependency(String dependency) {
		return hasDependency("org.springframework.boot",
				"spring-boot-starter-" + dependency);
	}

	public PomAssert hasSpringBootStarterRootDependency() {
		return hasDependency("org.springframework.boot", "spring-boot-starter");
	}

	public PomAssert hasDependency(String groupId, String artifactId) {
		return hasDependency(groupId, artifactId, null);
	}

	public PomAssert hasDependency(String groupId, String artifactId, String version) {
		return hasDependency(Dependency.create(groupId, artifactId, version, "compile"));
	}

	public PomAssert hasParent(String groupId, String artifactId, String version) {
		assertThat(this.parentPom.getGroupId()).isEqualTo(groupId);
		assertThat(this.parentPom.getArtifactId()).isEqualTo(artifactId);
		assertThat(this.parentPom.getVersion()).isEqualTo(version);
		return this;
	}

	public PomAssert hasSpringBootParent(String version) {
		return hasParent("org.springframework.boot", "spring-boot-starter-parent",
				version);
	}

	public PomAssert hasDependency(Dependency expected) {
		String id = generateDependencyId(expected.getGroupId(), expected.getArtifactId());
		assertThat(this.dependencies).containsKey(id);
		Dependency dependency = this.dependencies.get(id);
		if (expected.getVersion() != null) {
			assertThat(dependency.getVersion()).isEqualTo(expected.getVersion());
		}
		if (expected.getScope() != null) {
			assertThat(dependency.getScope()).isEqualTo(expected.getScope());
		}
		if (expected.getType() != null) {
			assertThat(dependency.getType()).isEqualTo(expected.getType());
		}
		return this;
	}

	public PomAssert hasBom(String groupId, String artifactId, String version) {
		String id = generateBomId(groupId, artifactId);
		assertThat(this.boms).containsKey(id);
		BillOfMaterials bom = this.boms.get(id);
		assertThat(bom.getVersion()).isEqualTo(version);
		return this;
	}

	public PomAssert hasBomsCount(int count) {
		assertThat(this.boms).hasSize(count);
		return this;
	}

	public PomAssert hasNoRepository() {
		try {
			Assert.assertEquals(0, this.eng
					.getMatchingNodes(createRootNodeXPath("repositories"), this.doc)
					.getLength());
		}
		catch (XpathException e) {
			throw new IllegalStateException("Cannot find path", e);
		}
		return this;
	}

	public PomAssert hasSnapshotRepository() {
		hasRepository("spring-snapshots", "Spring Snapshots",
				"https://repo.spring.io/snapshot", true);
		hasPluginRepository("spring-snapshots");
		return this;
	}

	public PomAssert hasRepository(String id, String name, String url,
			Boolean snapshotsEnabled) {
		assertThat(this.repositories).containsKeys(id);
		Repository repository = this.repositories.get(id);
		if (name != null) {
			assertThat(repository.getName()).isEqualTo(name);
		}
		if (url != null) {
			try {
				assertThat(repository.getUrl()).isEqualTo(new URL(url));
			}
			catch (MalformedURLException e) {
				throw new IllegalArgumentException("Cannot parse URL", e);
			}
		}
		if (snapshotsEnabled) {
			assertThat(repository.isSnapshotsEnabled()).isEqualTo(snapshotsEnabled);
		}
		return this;
	}

	public PomAssert hasRepositoriesCount(int count) {
		assertThat(this.repositories).hasSize(count);
		return this;
	}

	private PomAssert hasPluginRepository(String name) {
		NodeList nodes;
		try {
			nodes = this.eng.getMatchingNodes(
					createRootNodeXPath("pluginRepositories/pom:pluginRepository/pom:id"),
					this.doc);
		}
		catch (XpathException e) {
			throw new IllegalStateException("Cannot find path", e);
		}
		for (int i = 0; i < nodes.getLength(); i++) {
			if (name.equals(nodes.item(i).getTextContent())) {
				return this;
			}
		}
		throw new IllegalArgumentException("No plugin repository found with id " + name);
	}

	public static String createPropertyNodeXpath(String propertyName) {
		return createRootNodeXPath("properties/pom:" + propertyName);
	}

	public static String createRootNodeXPath(String node) {
		return "/pom:project/pom:" + node;
	}

	private ParentPom parseParent() {
		ParentPom parent = new ParentPom();
		try {
			parent.setGroupId(this.eng.evaluate(createRootNodeXPath("parent/pom:groupId"),
					this.doc));
			parent.setArtifactId(this.eng
					.evaluate(createRootNodeXPath("parent/pom:artifactId"), this.doc));
			parent.setVersion(this.eng.evaluate(createRootNodeXPath("parent/pom:version"),
					this.doc));
			return parent;
		}
		catch (XpathException e) {
			throw new IllegalStateException("Cannot find path", e);
		}
	}

	private void parseProperties() {
		NodeList nodes;
		try {
			nodes = this.eng.getMatchingNodes(createRootNodeXPath("properties/*"),
					this.doc);
		}
		catch (XpathException e) {
			throw new IllegalStateException("Cannot find path", e);
		}
		for (int i = 0; i < nodes.getLength(); i++) {
			Node item = nodes.item(i);
			if (item instanceof Element) {
				Element element = (Element) item;
				this.properties.put(element.getTagName(), element.getTextContent());
			}
		}
	}

	private void parseDependencies() {
		NodeList nodes;
		try {
			nodes = this.eng.getMatchingNodes(
					createRootNodeXPath("dependencies/pom:dependency"), this.doc);
		}
		catch (XpathException e) {
			throw new IllegalStateException("Cannot find path", e);
		}
		for (int i = 0; i < nodes.getLength(); i++) {
			Node item = nodes.item(i);
			if (item instanceof Element) {
				Dependency dependency = new Dependency();
				Element element = (Element) item;
				NodeList groupId = element.getElementsByTagName("groupId");
				if (groupId.getLength() > 0) {
					dependency.setGroupId(groupId.item(0).getTextContent());
				}
				NodeList artifactId = element.getElementsByTagName("artifactId");
				if (artifactId.getLength() > 0) {
					dependency.setArtifactId(artifactId.item(0).getTextContent());
				}
				NodeList version = element.getElementsByTagName("version");
				if (version.getLength() > 0) {
					dependency.setVersion(version.item(0).getTextContent());
				}
				NodeList scope = element.getElementsByTagName("scope");
				if (scope.getLength() > 0) {
					dependency.setScope(scope.item(0).getTextContent());
				}
				NodeList type = element.getElementsByTagName("type");
				if (type.getLength() > 0) {
					dependency.setType(type.item(0).getTextContent());
				}
				String id = dependency.generateId();
				assertThat(this.dependencies).doesNotContainKeys(id);
				this.dependencies.put(id, dependency);
			}
		}
	}

	private void parseBoms() {
		NodeList nodes;
		try {
			nodes = this.eng.getMatchingNodes(
					createRootNodeXPath(
							"dependencyManagement/pom:dependencies/pom:dependency"),
					this.doc);
		}
		catch (XpathException e) {
			throw new IllegalStateException("Cannot find path", e);
		}
		for (int i = 0; i < nodes.getLength(); i++) {
			Node item = nodes.item(i);
			if (item instanceof Element) {
				Element element = (Element) item;
				NodeList type = element.getElementsByTagName("type");
				NodeList scope = element.getElementsByTagName("scope");
				if (isBom(type, scope)) {
					BillOfMaterials bom = new BillOfMaterials();
					NodeList groupId = element.getElementsByTagName("groupId");
					if (groupId.getLength() > 0) {
						bom.setGroupId(groupId.item(0).getTextContent());
					}
					NodeList artifactId = element.getElementsByTagName("artifactId");
					if (artifactId.getLength() > 0) {
						bom.setArtifactId(artifactId.item(0).getTextContent());
					}
					NodeList version = element.getElementsByTagName("version");
					if (version.getLength() > 0) {
						bom.setVersion(version.item(0).getTextContent());
					}
					String id = generateBomId(bom.getGroupId(), bom.getArtifactId());
					assertThat(this.boms).doesNotContainKeys(id);
					this.boms.put(id, bom);
				}
			}
		}
	}

	private void parseRepositories() {
		NodeList nodes;
		try {
			nodes = this.eng.getMatchingNodes(
					createRootNodeXPath("repositories/pom:repository"), this.doc);
		}
		catch (XpathException e) {
			throw new IllegalStateException("Cannot find path", e);

		}
		for (int i = 0; i < nodes.getLength(); i++) {
			Node item = nodes.item(i);
			if (item instanceof Element) {
				Repository repository = new Repository();
				Element element = (Element) item;
				NodeList type = element.getElementsByTagName("id");
				String id = type.item(0).getTextContent();
				NodeList name = element.getElementsByTagName("name");
				if (name.getLength() > 0) {
					repository.setName(name.item(0).getTextContent());
				}
				NodeList url = element.getElementsByTagName("url");
				if (url.getLength() > 0) {
					try {
						repository.setUrl(new URL(url.item(0).getTextContent()));
					}
					catch (MalformedURLException | DOMException e) {
						throw new IllegalStateException("Cannot parse URL", e);
					}
				}
				NodeList snapshots = element.getElementsByTagName("snapshots");
				if (snapshots.getLength() > 0) {
					Element snapshotsElement = (Element) snapshots.item(0);
					NodeList snapshotsEnabled = snapshotsElement
							.getElementsByTagName("enabled");
					if (snapshotsEnabled.getLength() > 0) {
						repository.setSnapshotsEnabled(
								"true".equals(snapshotsEnabled.item(0).getTextContent()));
					}
				}
				assertThat(this.repositories).doesNotContainKeys(id);
				this.repositories.put(id, repository);
			}
		}
	}

	private static boolean isBom(NodeList type, NodeList scope) {
		if (type.getLength() == 0 || scope.getLength() == 0) {
			return false;
		}
		String typeValue = type.item(0).getTextContent();
		String scopeValue = scope.item(0).getTextContent();
		return "pom".equals(typeValue) && "import".equals(scopeValue);
	}

	private static String generateBomId(String groupId, String artifactId) {
		return groupId + ":" + artifactId;
	}

	private static String generateDependencyId(String groupId, String artifactId) {
		Dependency dependency = new Dependency();
		dependency.setGroupId(groupId);
		dependency.setArtifactId(artifactId);
		return dependency.generateId();
	}

}
