/*******************************************************************************
 * Copyright (c) 2016, 2018 Red Hat Inc. and others.
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     Red Hat - Initial Contribution
 *******************************************************************************/

package org.eclipse.linuxtools.internal.docker.ui.testutils;

import static org.junit.Assert.fail;

import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IProjectDescription;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IncrementalProjectBuilder;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.ui.dialogs.IOverwriteQuery;
import org.eclipse.ui.wizards.datatransfer.ImportOperation;
import org.junit.rules.ExternalResource;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;

/**
 * JUnit {@link TestRule} to initialize a {@link IProject} in the test
 * workspace, by reading the {@link RunWithProject} annotation on the test
 * method.
 */
public class ProjectInitializationRule extends ExternalResource {

	private Description description;

	private IProject project = null;

	public IProject getProject() {
		return this.project;
	}

	@Override
	public Statement apply(final Statement base, final Description description) {
		this.description = description;
		return super.apply(base, description);
	}

	@Override
	protected void before() throws Throwable {
		if (description == null) {
			fail("No method description available while trying to setup test project");
		}
		final String projectName = getProjectName();
		final IWorkspace junitWorkspace = ResourcesPlugin.getWorkspace();
		this.project = getTargetWorkspaceProject(getSampleProjectPath(projectName), junitWorkspace);
	}

	/**
	 * @return the name of the project to setup before running the JUnit test.
	 * @throws NoSuchMethodException
	 * @throws ClassNotFoundException
	 */
	private String getProjectName() throws NoSuchMethodException, ClassNotFoundException {
		final String className = description.getClassName();
		final String methodName = description.getMethodName();
		final Class<?> testClass = Class.forName(className);
		final Method testMethod = testClass.getMethod(methodName);
		final RunWithProject runWithProjectMethodAnnotation = testMethod.getAnnotation(RunWithProject.class);
		if (runWithProjectMethodAnnotation != null) {
			return runWithProjectMethodAnnotation.value();
		}
		final RunWithProject runWithProjectTypeAnnotation = testClass.getAnnotation(RunWithProject.class);
		if (runWithProjectTypeAnnotation != null) {
			return runWithProjectTypeAnnotation.value();
		}
		fail("No '@RunWithProject' annotation found while running test " + className + "." + methodName);
		return null;
	}

	/**
	 * @param projectName
	 *            the name of the project
	 * @return the {@link IPath} to the given project, in an absolute form.
	 */
	public static IPath getSampleProjectPath(final String projectName) {
		if (System.getProperty("user.dir") != null) {
			return new Path(System.getProperty("user.dir")).append("projects").append(projectName).makeAbsolute();
		}
		fail("The sample project was not found in the launcher workspace under name '" + projectName + "'");
		return null;
	}

	/**
	 * Creates or opens the project in the target/JUnit workspace.
	 *
	 * @param projectSourcePath
	 *            the absolute path to the project
	 * @param targetWorkspace
	 *            the target workspace in which the project should be created
	 * @return the project
	 * @throws CoreException
	 * @throws InvocationTargetException
	 * @throws InterruptedException
	 */
	public static IProject getTargetWorkspaceProject(final IPath projectSourcePath, final IWorkspace targetWorkspace)
			throws CoreException, InvocationTargetException, InterruptedException {
		final IPath dotProjectPath = projectSourcePath.addTrailingSeparator().append(".project");
		final IProjectDescription description = targetWorkspace.loadProjectDescription(dotProjectPath);
		final String projectName = description.getName();
		final IProject project = targetWorkspace.getRoot().getProject(projectName);
		if (project.exists()
				&& !targetWorkspace.getRoot().getFile(project.getFile(".project").getFullPath()).exists()) {
			project.delete(true, null);
		} else if (project.exists() && !project.isOpen()) {
			project.open(null);
		} else if (!project.exists()) {
			createProject(description, projectName, targetWorkspace, project);
			final SyncFileSystemStructureProvider syncFileSystemStructureProvider = new SyncFileSystemStructureProvider.Builder(
					projectSourcePath, project.getLocation()).ignoreRelativeSourcePaths(".svn", ".git", "target", "bin")
							.build();
			final List<File> filesToImport = syncFileSystemStructureProvider.getChildren(projectSourcePath.toFile());
			if (filesToImport != null && filesToImport.size() > 0) {
				ImportOperation operation = new ImportOperation(project.getFullPath(), projectSourcePath.toFile(),
						syncFileSystemStructureProvider, pathString -> IOverwriteQuery.YES, filesToImport);
				operation.setContext(null);
				// need to overwrite modified files
				operation.setOverwriteResources(true);
				operation.setCreateContainerStructure(false);
				operation.run(null);
			}
		}
		return project;
	}

	/**
	 * @param description
	 * @param projectName
	 * @param workspace
	 * @param project
	 * @throws InvocationTargetException
	 * @throws InterruptedException
	 * @throws CoreException
	 * @throws OperationCanceledException
	 */
	static void createProject(final IProjectDescription description, final String projectName,
			final IWorkspace workspace, final IProject project)
			throws InvocationTargetException, OperationCanceledException, CoreException, InterruptedException {
		// import from file system

		// import project from location copying files - use default project
		// location for this workspace
		// if location is null, project already exists in this location or
		// some error condition occurred.
		final IProjectDescription desc = workspace.newProjectDescription(projectName);
		desc.setBuildSpec(description.getBuildSpec());
		desc.setComment(description.getComment());
		desc.setDynamicReferences(description.getDynamicReferences());
		desc.setNatureIds(description.getNatureIds());
		desc.setReferencedProjects(description.getReferencedProjects());
		try {
			project.create(desc, null);
			project.open(IResource.BACKGROUND_REFRESH, null);
		} catch (CoreException e) {
			throw new InvocationTargetException(e);
		}
		buildProject(project);
	}

	/**
	 * Builds the given project
	 *
	 * @param project
	 * @throws CoreException
	 * @throws OperationCanceledException
	 * @throws InterruptedException
	 */
	public static void buildProject(final IProject project)
			throws CoreException, OperationCanceledException, InterruptedException {
		project.refreshLocal(IResource.DEPTH_INFINITE, null);
		project.build(IncrementalProjectBuilder.FULL_BUILD, null);
		Job.getJobManager().join(ResourcesPlugin.FAMILY_MANUAL_BUILD, null);
	}
}
