/*******************************************************************************
 * Copyright (c) 2008, 2021 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *     Hannes Wellmann - Bug 577116: Improve test utility method reusability
 *******************************************************************************/
package org.eclipse.pde.api.tools.builder.tests;

import java.io.File;
import java.io.FileInputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IncrementalProjectBuilder;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.jdt.core.IJavaModelMarker;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.tests.builder.TestingEnvironment;
import org.eclipse.jdt.core.tests.util.AbstractCompilerTest;
import org.eclipse.pde.api.tools.internal.ApiBaselineManager;
import org.eclipse.pde.api.tools.internal.builder.ApiAnalysisBuilder;
import org.eclipse.pde.api.tools.internal.model.WorkspaceBaseline;
import org.eclipse.pde.api.tools.internal.provisional.ApiPlugin;
import org.eclipse.pde.api.tools.internal.provisional.IApiBaselineManager;
import org.eclipse.pde.api.tools.internal.provisional.IApiMarkerConstants;
import org.eclipse.pde.api.tools.internal.provisional.model.IApiBaseline;
import org.eclipse.pde.api.tools.model.tests.TestSuiteHelper;
import org.eclipse.pde.api.tools.tests.util.ProjectUtils;
import org.eclipse.pde.core.target.ITargetDefinition;
import org.eclipse.pde.core.target.ITargetPlatformService;
import org.eclipse.pde.internal.core.PDECore;
import org.eclipse.pde.internal.core.natures.PluginProject;
import org.eclipse.pde.ui.tests.util.TargetPlatformUtil;

/**
 * Environment used to test the {@link ApiAnalysisBuilder}. This environment
 * emulates a typical workbench environment
 *
 * @since 1.0.0
 */
public class ApiTestingEnvironment extends TestingEnvironment {

	protected static final IMarker[] NO_MARKERS = new IMarker[0];

	/**
	 * Whether to revert vs. reset the workspace
	 */
	private boolean fRevert = false;

	/**
	 * The default path to be used to revert the workspace to (if revert is
	 * enabled)
	 */
	private IPath fRevertSourcePath = null;

	/**
	 * Modified files for each build so that we can undo the changes
	 * incrementally rather than recreating the workspace for each test.
	 */
	private final List<IPath> fAdded = new ArrayList<>();
	private final List<IPath> fChanged = new ArrayList<>();
	private final List<IPath> fRemoved = new ArrayList<>();

	public ApiTestingEnvironment() throws Exception {
		super();
		setTargetPlatform();
	}

	@Override
	public IPath addProject(String projectName, String compliance) throws UnsupportedOperationException {
		IJavaProject javaProject = createProject(projectName);
		IProject project = javaProject.getProject();
		if (compliance != null) {
			setProjectCompliance(javaProject, compliance);
		}
		return project != null ? project.getFullPath() : IPath.EMPTY;
	}

	/**
	 * Sets the given compliance on the given project.
	 */
	public void setProjectCompliance(IJavaProject project, String compliance) {
		int requiredComplianceFlag = 0;
		String compilerVersion = null;
		if (JavaCore.VERSION_1_8.equals(compliance)) {
			requiredComplianceFlag = AbstractCompilerTest.F_1_8;
			compilerVersion = JavaCore.VERSION_1_8;
		} else {
			throw new UnsupportedOperationException("Test framework doesn't support compliance level: " + compliance); //$NON-NLS-1$
		}
		if (requiredComplianceFlag != 0) {
			if ((AbstractCompilerTest.getPossibleComplianceLevels() & requiredComplianceFlag) == 0) {
				throw new RuntimeException("This test requires a " + compliance + " JRE"); //$NON-NLS-1$ //$NON-NLS-2$
			}
			HashMap<String, String> options = new HashMap<>();
			options.put(JavaCore.COMPILER_COMPLIANCE, compilerVersion);
			options.put(JavaCore.COMPILER_SOURCE, compilerVersion);
			options.put(JavaCore.COMPILER_CODEGEN_TARGET_PLATFORM, compilerVersion);
			project.setOptions(options);
		}
	}

	/**
	 * Creates a new plug-in project with the given name. If a project with the
	 * same name already exists in the testing workspace it will be deleted and
	 * new project created.
	 *
	 * @return the newly created {@link IJavaProject} or <code>null</code> if
	 *         there is an exception creating the project
	 */
	protected IJavaProject createProject(String projectName) {
		IJavaProject jproject = null;
		try {
			IProject project = getWorkspace().getRoot().getProject(projectName);
			if (project.exists()) {
				project.delete(true, new NullProgressMonitor());
			}
			jproject = ProjectUtils.createPluginProject(projectName,
					new String[] { PluginProject.NATURE, ApiPlugin.NATURE_ID });
			addProject(jproject.getProject());
		} catch (CoreException ce) {
			ApiPlugin.log(ce);
			ce.printStackTrace();
		}
		return jproject;
	}

	/**
	 * Performs a clean build on the project using the builder with the given
	 * builder id
	 */
	public void cleanBuild(IProject project, String builderid) {
		try {
			getProject(project.getName()).build(IncrementalProjectBuilder.CLEAN_BUILD, builderid, null, null);
		} catch (CoreException e) {
		}
	}

	/**
	 * Incrementally builds the given project using the builder with the given
	 * builder id
	 */
	public void incrementalBuild(IProject project, String builderid) {
		try {
			getProject(project.getName()).build(IncrementalProjectBuilder.INCREMENTAL_BUILD, builderid, null, null);
		} catch (CoreException e) {
		}
	}

	/**
	 * Performs a full build on the given project using the builder with the
	 * given builder id
	 */
	public void fullBuild(IProject project, String builderid) {
		try {
			getProject(project.getName()).build(IncrementalProjectBuilder.FULL_BUILD, builderid, null, null);
		} catch (CoreException e) {
		}
	}

	/**
	 * returns all of the usage markers for the specified resource and its
	 * children
	 *
	 * @return all API usage problem markers
	 * @throws CoreException
	 *
	 * @see IApiMarkerConstants#API_USAGE_PROBLEM_MARKER
	 */
	protected IMarker[] getAllUsageMarkers(IResource resource) throws CoreException {
		if (resource == null) {
			return NO_MARKERS;
		}
		if (!resource.isAccessible()) {
			return NO_MARKERS;
		}
		return resource.findMarkers(IApiMarkerConstants.API_USAGE_PROBLEM_MARKER, true, IResource.DEPTH_INFINITE);
	}

	/**
	 * returns all of the usage markers for the specified resource and its
	 * children
	 *
	 * @return all JDT problem markers that are on the resource backing the
	 *         given path
	 */
	public IMarker[] getAllJDTMarkers(IPath path) throws CoreException {
		return getAllJDTMarkers(getResource(path));
	}

	/**
	 * returns all of the usage markers for the specified resource and its
	 * children
	 *
	 * @return all JDT problem markers on the given {@link IResource}
	 */
	protected IMarker[] getAllJDTMarkers(IResource resource) throws CoreException {
		if (resource == null) {
			return NO_MARKERS;
		}
		if (!resource.isAccessible()) {
			return NO_MARKERS;
		}
		IMarker[] javaModelMarkers = resource.findMarkers(IJavaModelMarker.JAVA_MODEL_PROBLEM_MARKER, true, IResource.DEPTH_INFINITE);
		IMarker[] buildpathMarkers = resource.findMarkers(IJavaModelMarker.BUILDPATH_PROBLEM_MARKER, true, IResource.DEPTH_INFINITE);
		int javaModelMarkersLength = javaModelMarkers.length;
		int buildpathMarkersLength = buildpathMarkers.length;
		if (javaModelMarkersLength == 0) {
			return buildpathMarkers;
		} else if (buildpathMarkersLength == 0) {
			return javaModelMarkers;
		}
		IMarker[] allMarkers = new IMarker[javaModelMarkersLength + buildpathMarkersLength];
		System.arraycopy(javaModelMarkers, 0, allMarkers, 0, javaModelMarkersLength);
		System.arraycopy(buildpathMarkers, 0, allMarkers, javaModelMarkersLength, buildpathMarkersLength);
		return allMarkers;
	}

	/**
	 * Returns all of the unsupported Javadoc tag markers on the specified
	 * resource and all of its children.
	 *
	 * @return all unsupported tag problem markers
	 * @throws CoreException
	 *
	 * @see IApiMarkerConstants#UNSUPPORTED_TAG_PROBLEM_MARKER
	 */
	protected IMarker[] getAllUnsupportedTagMarkers(IResource resource) throws CoreException {
		if (resource == null) {
			return NO_MARKERS;
		}
		if (!resource.isAccessible()) {
			return NO_MARKERS;
		}
		return resource.findMarkers(IApiMarkerConstants.UNSUPPORTED_TAG_PROBLEM_MARKER, true, IResource.DEPTH_INFINITE);
	}

	/**
	 * Returns all of the unsupported annotation markers on the given resource
	 * and all of its children
	 *
	 * @return all unsupported annotation markers
	 * @since 1.0.400
	 */
	protected IMarker[] getAllUnsupportedAnnotationMarkers(IResource resource) throws CoreException {
		if (resource == null) {
			return NO_MARKERS;
		}
		if (!resource.isAccessible()) {
			return NO_MARKERS;
		}
		return resource.findMarkers(IApiMarkerConstants.UNSUPPORTED_ANNOTATION_PROBLEM_MARKER, true, IResource.DEPTH_INFINITE);
	}

	/**
	 * Returns all of the compatibility markers on the given resource and its
	 * children
	 *
	 * @return all compatibility problem markers
	 * @throws CoreException
	 *
	 * @see IApiMarkerConstants#COMPATIBILITY_PROBLEM_MARKER
	 */
	protected IMarker[] getAllCompatibilityMarkers(IResource resource) throws CoreException {
		if (resource == null) {
			return NO_MARKERS;
		}
		if (!resource.isAccessible()) {
			return NO_MARKERS;
		}
		return resource.findMarkers(IApiMarkerConstants.COMPATIBILITY_PROBLEM_MARKER, true, IResource.DEPTH_INFINITE);
	}

	/**
	 * Returns all of the API profile markers on the given resource and its
	 * children
	 *
	 * @return all API baseline problem markers
	 * @throws CoreException
	 *
	 * @see IApiMarkerConstants#DEFAULT_API_BASELINE_PROBLEM_MARKER
	 */
	protected IMarker[] getAllApiBaselineMarkers(IResource resource) throws CoreException {
		if (resource == null) {
			return NO_MARKERS;
		}
		if (!resource.isAccessible()) {
			return NO_MARKERS;
		}
		return resource.findMarkers(IApiMarkerConstants.DEFAULT_API_BASELINE_PROBLEM_MARKER, true, IResource.DEPTH_INFINITE);
	}

	/**
	 * Returns all of the since tag markers on the given resource and its
	 * children
	 *
	 * @return all since tag problem markers
	 * @throws CoreException
	 *
	 * @see IApiMarkerConstants#SINCE_TAGS_PROBLEM_MARKER
	 */
	protected IMarker[] getAllSinceTagMarkers(IResource resource) throws CoreException {
		if (resource == null) {
			return NO_MARKERS;
		}
		if (!resource.isAccessible()) {
			return NO_MARKERS;
		}
		return resource.findMarkers(IApiMarkerConstants.SINCE_TAGS_PROBLEM_MARKER, true, IResource.DEPTH_INFINITE);
	}

	/**
	 * Returns all of the version markers on the given resource and its children
	 *
	 * @return all version problem markers
	 * @throws CoreException
	 *
	 * @see IApiMarkerConstants#VERSION_NUMBERING_PROBLEM_MARKER
	 */
	protected IMarker[] getAllVersionMarkers(IResource resource) throws CoreException {
		if (resource == null) {
			return NO_MARKERS;
		}
		if (!resource.isAccessible()) {
			return NO_MARKERS;
		}
		return resource.findMarkers(IApiMarkerConstants.VERSION_NUMBERING_PROBLEM_MARKER, true, IResource.DEPTH_INFINITE);
	}

	/**
	 * Returns all of the unused API problem filters markers on the given
	 * resource to infinite depth
	 *
	 * @return all unused problem filter markers
	 * @throws CoreException
	 *
	 * @see IApiMarkerConstants#UNUSED_FILTER_PROBLEM_MARKER
	 */
	protected IMarker[] getAllUnusedApiProblemFilterMarkers(IResource resource) throws CoreException {
		if (resource == null) {
			return NO_MARKERS;
		}
		if (!resource.isAccessible()) {
			return NO_MARKERS;
		}
		return resource.findMarkers(IApiMarkerConstants.UNUSED_FILTER_PROBLEM_MARKER, true, IResource.DEPTH_INFINITE);
	}

	/**
	 * Returns all of the markers from the testing workspace
	 *
	 * @return all {@link IMarker}s currently set in the workspace
	 */
	public IMarker[] getMarkers() {
		return getMarkersFor(getWorkspaceRootPath());
	}

	/**
	 * Returns the collection of API problem markers for the given element
	 *
	 * @return the array of {@link IMarker}s found on the resource that
	 *         corresponds to the given path
	 */
	public IMarker[] getMarkersFor(IPath root) {
		return getMarkersFor(root, null);
	}

	/**
	 * Return all problems with the specified element.
	 *
	 * @return the array of {@link IMarker}s found on the resource that
	 *         corresponds to the given path
	 */
	public IMarker[] getMarkersFor(IPath path, String additionalMarkerType) {
		IResource resource = getResource(path);
		try {
			List<Object> problems = new ArrayList<>();
			addToList(problems, getAllUsageMarkers(resource));
			addToList(problems, getAllCompatibilityMarkers(resource));
			addToList(problems, getAllApiBaselineMarkers(resource));
			addToList(problems, getAllSinceTagMarkers(resource));
			addToList(problems, getAllVersionMarkers(resource));
			addToList(problems, getAllUnsupportedTagMarkers(resource));
			addToList(problems, getAllUnsupportedAnnotationMarkers(resource));
			addToList(problems, getAllUnusedApiProblemFilterMarkers(resource));

			// additional markers
			if (additionalMarkerType != null) {
				problems.addAll(Arrays.asList(resource.findMarkers(additionalMarkerType, true, IResource.DEPTH_INFINITE)));
			}
			return problems.toArray(new IMarker[problems.size()]);
		} catch (CoreException e) {
			// ignore
		}
		return new IMarker[0];
	}

	/**
	 * Looks up the {@link IResource} in the workspace from the given path
	 *
	 * @return the {@link IResource} handle for the given path
	 */
	public IResource getResource(IPath path) {
		IResource resource;
		if (path.equals(getWorkspaceRootPath())) {
			resource = getWorkspace().getRoot();
		} else {
			IProject p = getProject(path);
			if (p != null && path.equals(p.getFullPath())) {
				resource = getProject(path.lastSegment());
			} else if (path.getFileExtension() == null) {
				resource = getWorkspace().getRoot().getFolder(path);
			} else {
				resource = getWorkspace().getRoot().getFile(path);
			}
		}
		return resource;
	}

	/**
	 * Adds the array of objects to the given list
	 */
	private void addToList(List<Object> list, Object[] objects) {
		if (list == null || objects == null) {
			return;
		}
		if (objects.length == 0) {
			return;
		}
		for (Object object : objects) {
			list.add(object);
		}
	}

	@Override
	public ApiProblem[] getProblems() {
		return (ApiProblem[]) super.getProblems();
	}

	/**
	 * Returns the current workspace {@link IApiBaseline}
	 *
	 * @return the workspace baseline
	 */
	protected IApiBaseline getWorkspaceProfile() {
		return ApiPlugin.getDefault().getApiBaselineManager().getWorkspaceBaseline();
	}

	public static void dispose(IApiBaseline apiBaseline) {
		if (apiBaseline instanceof WorkspaceBaseline) {
			IApiBaselineManager apiBaselineManager = ApiPlugin.getDefault().getApiBaselineManager();
			((ApiBaselineManager) apiBaselineManager).disposeWorkspaceBaseline();
		} else if (apiBaseline != null) {
			apiBaseline.dispose();
		}
	}

	@Override
	public ApiProblem[] getProblemsFor(IPath path, String additionalMarkerType) {
		IMarker[] markers = getMarkersFor(path, additionalMarkerType);
		ArrayList<ApiProblem> problems = new ArrayList<>();
		for (IMarker marker : markers) {
			problems.add(new ApiProblem(marker));
		}
		return problems.toArray(new ApiProblem[problems.size()]);
	}

	@Override
	public void removeProject(IPath projectPath) {
		IJavaProject project = getJavaProject(projectPath);
		if (project != null) {
			try {
				project.getProject().delete(true, new NullProgressMonitor());
			} catch (CoreException ce) {

			}
		}
	}

	@Override
	public void resetWorkspace() {
		try {
			if (fRevert) {
				try {
					revertWorkspace();
				} catch (Exception e) {
					// in case we have an exception reverting a file, just toast
					// it all
					deleteWorkspace();
				}
			} else {
				deleteWorkspace();
			}
		} catch (Exception e) {
			// dump the trace
			// https://bugs.eclipse.org/bugs/show_bug.cgi?id=275005
			e.printStackTrace();
		} finally {
			// clear all changes
			fAdded.clear();
			fChanged.clear();
			fRemoved.clear();
		}
	}

	/**
	 * Completely deletes the workspace
	 *
	 * @since 1.1
	 */
	void deleteWorkspace() {
		super.resetWorkspace();
		// clean up any left over projects from other tests
		IProject[] projects = getWorkspace().getRoot().getProjects();
		for (IProject project : projects) {
			try {
				project.delete(true, new NullProgressMonitor());
			} catch (CoreException ce) {
				// help with debugging
				ce.printStackTrace();
			}
		}
	}

	/**
	 * Sets the default revert source path to the given path.
	 */
	public void setRevertSourcePath(IPath path) {
		fRevertSourcePath = path;
	}

	/**
	 * @return the currently set source path to use when reverting changes to
	 *         the workspace.
	 */
	public IPath getRevertSourcePath() {
		return fRevertSourcePath;
	}

	/**
	 * Reverts changes in the workspace - added, removed, changed files
	 *
	 * @throws Exception if something happens trying to revert the workspace
	 *             contents
	 */
	public void revertWorkspace() throws Exception {
		// remove each added file
		Iterator<IPath> iterator = fAdded.iterator();
		while (iterator.hasNext()) {
			IPath path = iterator.next();
			deleteWorkspaceFile(path);
		}

		// revert each changed file
		iterator = fChanged.iterator();
		IPath revert = getRevertSourcePath();
		if (revert != null) {
			IPath path = null;
			while (iterator.hasNext()) {
				path = iterator.next();
				updateWorkspaceFile(path, TestSuiteHelper.getPluginDirectoryPath().append(ApiBuilderTest.TEST_SOURCE_ROOT).append(getRevertSourcePath()).append(path));
			}

			// replace each deleted file
			iterator = fRemoved.iterator();
			while (iterator.hasNext()) {
				path = iterator.next();
				createWorkspaceFile(path, TestSuiteHelper.getPluginDirectoryPath().append(ApiBuilderTest.TEST_SOURCE_ROOT).append(getRevertSourcePath()).append(path));
			}
		}
	}

	/**
	 * Deletes the workspace file at the specified location (full path).
	 */
	private void deleteWorkspaceFile(IPath workspaceLocation) throws Exception {
		IFile file = getWorkspace().getRoot().getFile(workspaceLocation);
		try {
			file.delete(true, null);
		} catch (CoreException e) {
			// try to bring the resource in to sync an re-delete
			file.refreshLocal(IResource.DEPTH_ONE, null);
			file.delete(true, null);
		}
	}

	/**
	 * Updates the contents of a workspace file at the specified location (full
	 * path), with the contents of a local file at the given replacement
	 * location (absolute path).
	 */
	private void updateWorkspaceFile(IPath workspaceLocation, IPath replacementLocation) throws Exception {
		IFile file = getWorkspace().getRoot().getFile(workspaceLocation);
		File replacement = replacementLocation.toFile();
		try (FileInputStream stream = new FileInputStream(replacement);) {
			file.setContents(stream, false, true, null);
		}
	}

	/**
	 * Updates the contents of a workspace file at the specified location (full
	 * path), with the contents of a local file at the given replacement
	 * location (absolute path).
	 */
	private void createWorkspaceFile(IPath workspaceLocation, IPath replacementLocation) throws Exception {
		IFile file = getWorkspace().getRoot().getFile(workspaceLocation);
		File replacement = replacementLocation.toFile();
		try (FileInputStream stream = new FileInputStream(replacement)) {
			file.create(stream, false, null);
		}
	}

	/**
	 * Notes a file was added during the test, to be undone
	 */
	public void added(IPath path) {
		fAdded.add(path);
	}

	public void changed(IPath path) {
		fChanged.add(path);
	}

	public void removed(IPath path) {
		fRemoved.add(path);
	}

	@Override
	public IPath addFile(IPath root, String fileName, String contents) {
		IPath path = root.append(fileName);
		IFile file = getWorkspace().getRoot().getFile(path);
		if (file.exists()) {
			changed(path);
		} else {
			added(path);
		}
		return super.addFile(root, fileName, contents);
	}

	/**
	 * Returns the listing of projects in the order the workspace has computed
	 * they should be built. This method calls out to
	 * {@link org.eclipse.core.resources.IWorkspace#computeProjectOrder(IProject[])}
	 * , which can slow down testing with successive calls.
	 *
	 * @return a build-ordered listing of the workspace projects
	 */
	public IProject[] getProjectBuildOrder() {
		return getWorkspace().computeProjectOrder(getWorkspace().getRoot().getProjects()).projects;
	}

	@Override
	public IPath addClass(IPath packagePath, String className, String contents) {
		IPath filePath = packagePath.append(className + ".java"); //$NON-NLS-1$
		if (getWorkspace().getRoot().getFile(filePath).exists()) {
			changed(filePath);
		} else {
			added(filePath);
		}
		return super.addClass(packagePath, className, contents);
	}

	/**
	 * Sets whether to revert the workspace rather than reset.
	 */
	public void setRevert(boolean revert) {
		fRevert = revert;
	}

	public static void setTargetPlatform() throws CoreException, InterruptedException {
		ITargetPlatformService tpService = PDECore.getDefault().acquireService(ITargetPlatformService.class);
		ITargetDefinition workspaceTarget = tpService.getWorkspaceTargetDefinition();
		if (workspaceTarget != null && workspaceTarget.getBundles() == null) {
			TargetPlatformUtil.loadAndSetTarget(workspaceTarget);
		}
		boolean coreRuntimeFound = Arrays.stream(workspaceTarget.getBundles())
				.anyMatch(bundle -> "org.eclipse.core.runtime".equals(bundle.getBundleInfo().getSymbolicName())); //$NON-NLS-1$
		if (!coreRuntimeFound) {
			TargetPlatformUtil.setRunningPlatformSubSetAsTarget("Current bundles target platform", null); //$NON-NLS-1$
		}
	}
}
