/*
 * Copyright 2012-2017 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 org.springframework.boot.cli.compiler.grape;

import java.io.File;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import groovy.lang.GroovyClassLoader;
import org.eclipse.aether.DefaultRepositorySystemSession;
import org.eclipse.aether.repository.Authentication;
import org.eclipse.aether.repository.RemoteRepository;
import org.junit.Test;

import org.springframework.boot.cli.compiler.dependencies.SpringBootDependenciesDependencyManagement;
import org.springframework.test.util.ReflectionTestUtils;

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

/**
 * Tests for {@link AetherGrapeEngine}.
 *
 * @author Andy Wilkinson
 */
public class AetherGrapeEngineTests {

	private final GroovyClassLoader groovyClassLoader = new GroovyClassLoader();

	private final RepositoryConfiguration springMilestones = new RepositoryConfiguration(
			"spring-milestones", URI.create("https://repo.spring.io/milestone"), false);

	private AetherGrapeEngine createGrapeEngine(
			RepositoryConfiguration... additionalRepositories) {
		List<RepositoryConfiguration> repositoryConfigurations = new ArrayList<>();
		repositoryConfigurations.add(new RepositoryConfiguration("central",
				URI.create("http://repo1.maven.org/maven2"), false));
		repositoryConfigurations.addAll(Arrays.asList(additionalRepositories));
		DependencyResolutionContext dependencyResolutionContext = new DependencyResolutionContext();
		dependencyResolutionContext.addDependencyManagement(
				new SpringBootDependenciesDependencyManagement());
		return AetherGrapeEngineFactory.create(this.groovyClassLoader,
				repositoryConfigurations, dependencyResolutionContext, false);
	}

	@Test
	public void dependencyResolution() {
		Map<String, Object> args = new HashMap<>();
		createGrapeEngine(this.springMilestones).grab(args,
				createDependency("org.springframework", "spring-jdbc", null));
		assertThat(this.groovyClassLoader.getURLs()).hasSize(5);
	}

	@Test
	public void proxySelector() {
		doWithCustomUserHome(new Runnable() {

			@Override
			public void run() {
				AetherGrapeEngine grapeEngine = createGrapeEngine();

				DefaultRepositorySystemSession session = (DefaultRepositorySystemSession) ReflectionTestUtils
						.getField(grapeEngine, "session");

				assertThat(session.getProxySelector() instanceof CompositeProxySelector)
						.isTrue();
			}

		});
	}

	@Test
	public void repositoryMirrors() {
		doWithCustomUserHome(new Runnable() {

			@SuppressWarnings("unchecked")
			@Override
			public void run() {
				AetherGrapeEngine grapeEngine = createGrapeEngine();

				List<RemoteRepository> repositories = (List<RemoteRepository>) ReflectionTestUtils
						.getField(grapeEngine, "repositories");
				assertThat(repositories).hasSize(1);
				assertThat(repositories.get(0).getId()).isEqualTo("central-mirror");
			}
		});
	}

	@Test
	public void repositoryAuthentication() {
		doWithCustomUserHome(new Runnable() {

			@SuppressWarnings("unchecked")
			@Override
			public void run() {
				AetherGrapeEngine grapeEngine = createGrapeEngine();

				List<RemoteRepository> repositories = (List<RemoteRepository>) ReflectionTestUtils
						.getField(grapeEngine, "repositories");
				assertThat(repositories).hasSize(1);
				Authentication authentication = repositories.get(0).getAuthentication();
				assertThat(authentication).isNotNull();
			}
		});
	}

	@Test
	public void dependencyResolutionWithExclusions() {
		Map<String, Object> args = new HashMap<>();
		args.put("excludes",
				Arrays.asList(createExclusion("org.springframework", "spring-core")));

		createGrapeEngine(this.springMilestones).grab(args,
				createDependency("org.springframework", "spring-jdbc", "3.2.4.RELEASE"),
				createDependency("org.springframework", "spring-beans", "3.2.4.RELEASE"));

		assertThat(this.groovyClassLoader.getURLs().length).isEqualTo(3);
	}

	@Test
	public void nonTransitiveDependencyResolution() {
		Map<String, Object> args = new HashMap<>();

		createGrapeEngine().grab(args, createDependency("org.springframework",
				"spring-jdbc", "3.2.4.RELEASE", false));

		assertThat(this.groovyClassLoader.getURLs().length).isEqualTo(1);
	}

	@Test
	public void dependencyResolutionWithCustomClassLoader() {
		Map<String, Object> args = new HashMap<>();
		GroovyClassLoader customClassLoader = new GroovyClassLoader();
		args.put("classLoader", customClassLoader);

		createGrapeEngine(this.springMilestones).grab(args,
				createDependency("org.springframework", "spring-jdbc", null));

		assertThat(this.groovyClassLoader.getURLs().length).isEqualTo(0);
		assertThat(customClassLoader.getURLs().length).isEqualTo(5);
	}

	@Test
	public void resolutionWithCustomResolver() {
		Map<String, Object> args = new HashMap<>();
		AetherGrapeEngine grapeEngine = this.createGrapeEngine();
		grapeEngine
				.addResolver(createResolver("restlet.org", "http://maven.restlet.org"));
		grapeEngine.grab(args, createDependency("org.restlet", "org.restlet", "1.1.6"));
		assertThat(this.groovyClassLoader.getURLs().length).isEqualTo(1);
	}

	@Test(expected = IllegalArgumentException.class)
	public void differingTypeAndExt() {
		Map<String, Object> dependency = createDependency("org.grails",
				"grails-dependencies", "2.4.0");
		dependency.put("type", "foo");
		dependency.put("ext", "bar");
		createGrapeEngine().grab(Collections.emptyMap(), dependency);
	}

	@Test
	public void pomDependencyResolutionViaType() {
		Map<String, Object> args = new HashMap<>();
		Map<String, Object> dependency = createDependency("org.springframework",
				"spring-framework-bom", "4.0.5.RELEASE");
		dependency.put("type", "pom");
		createGrapeEngine().grab(args, dependency);
		URL[] urls = this.groovyClassLoader.getURLs();
		assertThat(urls.length).isEqualTo(1);
		assertThat(urls[0].toExternalForm().endsWith(".pom")).isTrue();
	}

	@Test
	public void pomDependencyResolutionViaExt() {
		Map<String, Object> args = new HashMap<>();
		Map<String, Object> dependency = createDependency("org.springframework",
				"spring-framework-bom", "4.0.5.RELEASE");
		dependency.put("ext", "pom");
		createGrapeEngine().grab(args, dependency);
		URL[] urls = this.groovyClassLoader.getURLs();
		assertThat(urls.length).isEqualTo(1);
		assertThat(urls[0].toExternalForm().endsWith(".pom")).isTrue();
	}

	@Test
	public void resolutionWithClassifier() {
		Map<String, Object> args = new HashMap<>();

		Map<String, Object> dependency = createDependency("org.springframework",
				"spring-jdbc", "3.2.4.RELEASE", false);
		dependency.put("classifier", "sources");
		createGrapeEngine().grab(args, dependency);

		URL[] urls = this.groovyClassLoader.getURLs();
		assertThat(urls.length).isEqualTo(1);
		assertThat(urls[0].toExternalForm().endsWith("-sources.jar")).isTrue();
	}

	private Map<String, Object> createDependency(String group, String module,
			String version) {
		Map<String, Object> dependency = new HashMap<>();
		dependency.put("group", group);
		dependency.put("module", module);
		dependency.put("version", version);
		return dependency;
	}

	private Map<String, Object> createDependency(String group, String module,
			String version, boolean transitive) {
		Map<String, Object> dependency = createDependency(group, module, version);
		dependency.put("transitive", transitive);
		return dependency;
	}

	private Map<String, Object> createResolver(String name, String url) {
		Map<String, Object> resolver = new HashMap<>();
		resolver.put("name", name);
		resolver.put("root", url);
		return resolver;
	}

	private Map<String, Object> createExclusion(String group, String module) {
		Map<String, Object> exclusion = new HashMap<>();
		exclusion.put("group", group);
		exclusion.put("module", module);
		return exclusion;
	}

	private void doWithCustomUserHome(Runnable action) {
		doWithSystemProperty("user.home",
				new File("src/test/resources").getAbsolutePath(), action);
	}

	private void doWithSystemProperty(String key, String value, Runnable action) {
		String previousValue = setOrClearSystemProperty(key, value);
		try {
			action.run();
		}
		finally {
			setOrClearSystemProperty(key, previousValue);
		}
	}

	private String setOrClearSystemProperty(String key, String value) {
		if (value != null) {
			return System.setProperty(key, value);
		}
		return System.clearProperty(key);
	}

}
