/*
 * Copyright 2013-2019 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.springframework.cloud.config.server.environment;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.eclipse.jgit.api.CheckoutCommand;
import org.eclipse.jgit.api.CloneCommand;
import org.eclipse.jgit.api.FetchCommand;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.CheckoutConflictException;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.api.errors.InvalidRefNameException;
import org.eclipse.jgit.api.errors.InvalidRemoteException;
import org.eclipse.jgit.api.errors.RefAlreadyExistsException;
import org.eclipse.jgit.api.errors.RefNotFoundException;
import org.eclipse.jgit.api.errors.TransportException;
import org.eclipse.jgit.junit.MockSystemReader;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.transport.FetchResult;
import org.eclipse.jgit.util.FileUtils;
import org.eclipse.jgit.util.SystemReader;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.config.environment.Environment;
import org.springframework.cloud.config.server.config.ConfigServerProperties;
import org.springframework.cloud.config.server.config.EnvironmentRepositoryConfiguration;
import org.springframework.cloud.config.server.test.ConfigServerTestUtils;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

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

/**
 * @author Dave Syer
 * @author Roy Clarkson
 */
public class JGitEnvironmentRepositoryConcurrencyTests {

	protected Log logger = LogFactory.getLog(getClass());

	private ConfigurableApplicationContext context;

	private File basedir = new File("target/config");

	@BeforeAll
	public static void initClass() {
		// mock Git configuration to make tests independent of local Git configuration
		SystemReader.setInstance(new MockSystemReader());
	}

	@BeforeEach
	public void init() throws Exception {
		if (this.basedir.exists()) {
			FileUtils.delete(this.basedir, FileUtils.RECURSIVE);
		}
		ConfigServerTestUtils.deleteLocalRepo("config-copy");
	}

	@AfterEach
	public void close() {
		if (this.context != null) {
			this.context.close();
		}
	}

	@Test
	public void vanilla() throws Exception {
		String uri = ConfigServerTestUtils.prepareLocalRepo();
		this.context = new SpringApplicationBuilder(TestConfiguration.class).web(WebApplicationType.NONE)
				.properties("spring.cloud.config.server.git.uri:" + uri).run();
		final EnvironmentRepository repository = this.context.getBean(EnvironmentRepository.class);
		ExecutorService threads = Executors.newFixedThreadPool(4);
		List<Future<Boolean>> tasks = new ArrayList<Future<Boolean>>();
		for (int i = 0; i < 30; i++) {
			tasks.add(threads.submit(new Runnable() {
				@Override
				public void run() {
					repository.findOne("bar", "staging", "master");
				}
			}, true));
		}
		for (Future<Boolean> future : tasks) {
			future.get();
		}
		Environment environment = repository.findOne("bar", "staging", "master");
		assertThat(environment.getPropertySources().size()).isEqualTo(2);
		assertThat(environment.getName()).isEqualTo("bar");
		assertThat(environment.getProfiles()).isEqualTo(new String[] { "staging" });
		assertThat(environment.getLabel()).isEqualTo("master");
	}

	/**
	 * Simulates following actions in parallel: - Client tries to obtain configuration
	 * with specified label - Spring Refresh Context Event occurs.
	 * @throws Exception when git related exception happens
	 */
	@Test
	public void concurrentRefreshContextAndGetLabels() throws Exception {
		// Prepare the repo
		final JGitConfigServerTestData testData = JGitConfigServerTestData
				.prepareClonedGitRepository(TestConfiguration.class);
		JGitEnvironmentRepository repository = testData.getRepository();
		repository.setCloneOnStart(true);
		repository.setGitFactory(new DelayedGitFactoryMock());
		repository.setBasedir(testData.getClonedGit().getGitWorkingDirectory());
		repository.setUri(testData.getServerGit().getGitWorkingDirectory().getAbsolutePath().replace("file://", ""));

		final AtomicInteger errorCount = new AtomicInteger();

		// Prepare two threads to do the parallel work
		Thread client = new Thread(new Runnable() {
			@Override
			public void run() {
				JGitEnvironmentRepositoryConcurrencyTests.this.logger.info("client start.");
				try {
					Environment environment = testData.getRepository().findOne("bar", "staging", "master");
				}
				catch (Exception e) {
					errorCount.incrementAndGet();
					e.printStackTrace();
				}
				JGitEnvironmentRepositoryConcurrencyTests.this.logger.info("client end.");
			}
		});

		Thread refresh = new Thread(new Runnable() {
			@Override
			public void run() {
				try {
					JGitEnvironmentRepositoryConcurrencyTests.this.logger.info("refresh start.");
					testData.getRepository().afterPropertiesSet();
					JGitEnvironmentRepositoryConcurrencyTests.this.logger.info("refresh end.");
				}
				catch (Exception e) {
					errorCount.incrementAndGet();
					e.printStackTrace();
				}
			}
		});

		// Start the parallel actions and wait till the end.
		refresh.start();
		client.start();
		refresh.join();
		client.join();

		assertThat(errorCount.get()).isEqualTo(0);
	}

	@Configuration(proxyBeanMethods = false)
	@EnableConfigurationProperties(ConfigServerProperties.class)
	@Import({ PropertyPlaceholderAutoConfiguration.class, EnvironmentRepositoryConfiguration.class })
	protected static class TestConfiguration {

	}

	private static class DelayedGitFactoryMock extends JGitEnvironmentRepository.JGitFactory {

		@Override
		public Git getGitByOpen(File file) throws IOException {
			Git originalGit = DelayedGitMock.open(file);
			return new DelayedGitMock(originalGit.getRepository());
		}

		@Override
		public CloneCommand getCloneCommandByCloneRepository() {
			return new DelayedCloneCommand();
		}

	}

	private static class DelayedGitMock extends Git {

		DelayedGitMock(Repository repo) {
			super(repo);
		}

		@Override
		public FetchCommand fetch() {
			return new DelayedFetchCommand(getRepository());
		}

		@Override
		public CheckoutCommand checkout() {
			return new DelayedCheckoutCommand(getRepository());
		}

	}

	private static class DelayedCloneCommand extends CloneCommand {

		@Override
		public Git call() throws GitAPIException, InvalidRemoteException, TransportException {
			try {
				Thread.sleep(250);
			}
			catch (InterruptedException e) {
				e.printStackTrace();
			}
			return super.call();
		}

	}

	private static class DelayedFetchCommand extends FetchCommand {

		DelayedFetchCommand(Repository repo) {
			super(repo);
		}

		@Override
		public FetchResult call() throws GitAPIException, InvalidRemoteException, TransportException {
			try {
				Thread.sleep(250);
			}
			catch (InterruptedException e) {
				e.printStackTrace();
			}
			return super.call();
		}

	}

	private static class DelayedCheckoutCommand extends CheckoutCommand {

		DelayedCheckoutCommand(Repository repo) {
			super(repo);
		}

		@Override
		public Ref call() throws GitAPIException, RefAlreadyExistsException, RefNotFoundException,
				InvalidRefNameException, CheckoutConflictException {
			try {
				Thread.sleep(250);
			}
			catch (InterruptedException e) {
				e.printStackTrace();
			}
			return super.call();
		}

	}

}
