/*******************************************************************************
 * Copyright (c) 2018 Aston University.
 * 
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the Eclipse
 * Public License, v. 2.0 are satisfied: GNU General Public License, version 3.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-3.0
 *
 * Contributors:
 *     Antonio Garcia-Dominguez - initial API and implementation
 ******************************************************************************/
package org.eclipse.hawk.integration.tests.emf;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeFalse;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.stream.Collectors;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.filefilter.TrueFileFilter;
import org.eclipse.emf.common.util.TreeIterator;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EPackage;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl;
import org.eclipse.emf.ecore.xmi.impl.XMIResourceFactoryImpl;
import org.eclipse.hawk.backend.tests.factories.IGraphDatabaseFactory;
import org.eclipse.hawk.core.query.IQueryEngine;
import org.eclipse.hawk.epsilon.emc.EOLQueryEngine;
import org.eclipse.hawk.graph.syncValidationListener.SyncValidationListener;
import org.eclipse.hawk.graph.syncValidationListener.SyncValidationListener.ValidationError;
import org.eclipse.hawk.integration.tests.IntegrationTestSuite;
import org.eclipse.hawk.integration.tests.ModelIndexingTest;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runners.Parameterized.Parameters;

/**
 * Integration test case that indexes a fragmented version of the GraBaTs'09
 * set0 model, and runs queries on containment subtrees with the
 * {@link IQueryEngine#PROPERTY_SUBTREECONTEXT} through
 * {@link EOLQueryEngine#getAllOf(String, String, String)} and
 * {@link EOLQueryEngine#getAllOfKind(String)}. Has a small program to do the
 * fragmentation at multiple levels.
 */
public class SubtreeContextTest extends ModelIndexingTest {

	private static class Fragmenter {
		private static final String pathToMetamodel = "resources/metamodels/JDTAST.ecore";
		private static final String pathToOriginal = "resources/models/set0-reduced/set0.xmi";
		private static final String pathToFragmented = "resources/models/set0-reduced-fragmented/set0.xmi";

		private ResourceSetImpl rs;

		public void run() throws IOException {
			rs = new ResourceSetImpl();
			rs.getResourceFactoryRegistry().getExtensionToFactoryMap().put("*", new XMIResourceFactoryImpl());

			Resource rEcoreMM = rs.createResource(URI.createFileURI(new File(pathToMetamodel).getAbsolutePath()));
			rEcoreMM.load(null);
			for (EObject rawEPackage : rEcoreMM.getContents()) {
				EPackage epackage = (EPackage) rawEPackage;
				rs.getPackageRegistry().put(epackage.getNsURI(), epackage);
			}
			rs.getResources().remove(rEcoreMM);

			final File fOriginal = new File(pathToOriginal);
			Resource rRoot = rs.createResource(URI.createFileURI(fOriginal.getAbsolutePath()));
			rRoot.load(null);

			final File fFragmented = new File(pathToFragmented);
			if (fFragmented.getParentFile().exists()) {
				FileUtils.deleteDirectory(fFragmented.getParentFile());
			}
			rRoot.setURI(URI.createFileURI(fFragmented.getAbsolutePath()));
			fragment(rRoot);

			for (Resource r : rs.getResources()) {
				r.save(null);
			}
			for (Resource r : rs.getResources()) {
				r.unload();
			}
			rs = null;
		}

		private void fragment(Resource rRoot) {
			File fRoot = new File(rRoot.getURI().toFileString());
			File fRootFolder = fRoot.getParentFile();

			for (TreeIterator<EObject> itContents = rRoot.getAllContents(); itContents.hasNext();) {
				EObject eob = itContents.next();
				if (rRoot.getContents().contains(eob)) {
					// No roots (otherwise, we'd have endless recursion)
					continue;
				}

				final String eClassName = eob.eClass().getName();
				switch (eClassName) {
				case "IJavaProject":
				case "BinaryPackageFragmentRoot":
				case "IPackageFragment":
					EStructuralFeature sf = eob.eClass().getEStructuralFeature("elementName");
					String name = eob.eGet(sf).toString();

					final File fFolder = new File(fRootFolder, String.format("%s_%s", eClassName, name));
					fFolder.mkdirs();
					final File fChild = new File(fFolder, String.format("%s_%s.xmi", eClassName, name));
					final URI uriChild = URI.createFileURI(fChild.getAbsolutePath());
					Resource r = rs.createResource(uriChild);
					itContents.prune();

					r.getContents().add(eob);
					fragment(r);
					break;
				}
			}

		}

	}

	@Rule
	public GraphChangeListenerRule<SyncValidationListener> syncValidation = new GraphChangeListenerRule<>(
			new SyncValidationListener());

	@Rule
	public TemporaryFolder modelFolder = new TemporaryFolder();

	private File folderOriginal, folderFragmented;
	private String originalRepoURI, fragmentedRepoURI;

	@Parameters(name="{1}")
	public static Iterable<Object[]> params() {
		return IntegrationTestSuite.caseParams();
	}

	public SubtreeContextTest(File baseDir, IGraphDatabaseFactory dbf) {
		super(baseDir, dbf, new EMFModelSupportFactory());
	}

	@Before
	public void setUp() throws Throwable {
		// Skip for deprecated backends - it is an expensive test
		assumeFalse(db.getClass().isAnnotationPresent(Deprecated.class));
		
		indexer.registerMetamodels(new File(baseDir, "resources/metamodels/Ecore.ecore"),
				new File(baseDir, "resources/metamodels/JDTAST.ecore"));

		folderOriginal = new File(baseDir, "resources/models/set0-reduced").getCanonicalFile();
		folderFragmented = new File(baseDir, "resources/models/set0-reduced-fragmented").getCanonicalFile();
		originalRepoURI = folderOriginal.toPath().toUri().toString();
		fragmentedRepoURI = folderFragmented.toPath().toUri().toString();
	}

	protected void prepareBoth() throws Exception, Throwable {
		prepareOriginal();
		prepareFragmented();
	}

	protected void prepareFragmented() throws Exception, Throwable {
		requestFolderIndex(folderFragmented);
		scheduleAndWait(() -> {
			assertNoValidationErrors("There should be no validation errors on the fragmented model");
			return null;
		});
	}

	private void assertNoValidationErrors(String message) {
		List<ValidationError> errors = syncValidation.getListener().getErrors();
		if (!errors.isEmpty()) {
			List<String> messages = errors.stream()
				.map(e -> e.getMessage())
				.collect(Collectors.toList());
			fail(message + ":\n" + String.join("\n\n", messages));
		}
	}

	protected void prepareOriginal() throws Exception, Throwable {
		requestFolderIndex(folderOriginal);
		scheduleAndWait(() -> {
			assertNoValidationErrors("There should be no validation errors on the original model");
			return null;
		});
	}

	@Test
	public void allContents() throws Throwable {
		prepareBoth();

		// Sanity check for .allContents (repo-based)
		final int originalSize = (int) eol("return Model.allContents.size;",
				ctx(IQueryEngine.PROPERTY_REPOSITORYCONTEXT, originalRepoURI));
		final int fragmentedSize = (int) eol("return Model.allContents.size;",
				ctx(IQueryEngine.PROPERTY_REPOSITORYCONTEXT, fragmentedRepoURI));
		assertEquals(originalSize, fragmentedSize);
		assertEquals(originalSize * 2, eol("return Model.allContents.size;"));

		// Now doing it with the subtree context (will be replaced by a breadth-first traversal)
		final int subtreeOriginalSize = (int) eol("return Model.allContents.size;", ctx(
			IQueryEngine.PROPERTY_REPOSITORYCONTEXT, originalRepoURI,
			IQueryEngine.PROPERTY_SUBTREECONTEXT, "/set0.xmi"
		));
		assertEquals(originalSize, subtreeOriginalSize);

		final int subtreeFragmentedSize = (int) eol("return Model.allContents.size;", ctx(
			IQueryEngine.PROPERTY_REPOSITORYCONTEXT, fragmentedRepoURI,
			IQueryEngine.PROPERTY_SUBTREECONTEXT, "/set0.xmi"
		));
		assertEquals(subtreeFragmentedSize, subtreeOriginalSize);

		// Now limit to the contents of the first IJavaProject
		final int originalJavaContents = (int) eol("return 1 + IJavaProject.all.first.eAllContents.size;",
				ctx(IQueryEngine.PROPERTY_REPOSITORYCONTEXT, originalRepoURI));
		final int fileBasedJavaContents = (int) eol("return Model.allContents.size;", ctx(
			IQueryEngine.PROPERTY_REPOSITORYCONTEXT, fragmentedRepoURI,
			IQueryEngine.PROPERTY_FILECONTEXT, "/IJavaProject_org.eclipse.jdt.apt.pluggable.core/*"));
		final int subtreeJavaContents = (int) eol("return Model.allContents.size;", ctx(
			IQueryEngine.PROPERTY_REPOSITORYCONTEXT, fragmentedRepoURI,
			IQueryEngine.PROPERTY_SUBTREECONTEXT, "/IJavaProject_org.eclipse.jdt.apt.pluggable.core/IJavaProject_org.eclipse.jdt.apt.pluggable.core.xmi"
		));
		assertEquals(originalJavaContents, fileBasedJavaContents);
		assertEquals(originalJavaContents, subtreeJavaContents);
	}

	@Test
	public void getAllOf() throws Throwable {
		prepareBoth();
		final String eolQuery = "return IType.all.size;";

		/*
		 * First do the most straightforward approach: find all the files under a folder,
		 * go to the type, then go through all instances filtering by file.
		 */
		final int fileClasses = (int) eol(eolQuery, ctx(
			IQueryEngine.PROPERTY_REPOSITORYCONTEXT, fragmentedRepoURI,
			IQueryEngine.PROPERTY_FILECONTEXT, "/IJavaProject_org.eclipse.jdt.apt.pluggable.core/*"));

		/* This version goes to the file nodes first, then checks types. */
		final int fileFirstClasses = (int) eol(eolQuery, ctx(
				IQueryEngine.PROPERTY_REPOSITORYCONTEXT, fragmentedRepoURI,
				IQueryEngine.PROPERTY_FILECONTEXT, "/IJavaProject_org.eclipse.jdt.apt.pluggable.core/*",
				IQueryEngine.PROPERTY_FILEFIRST, "true"));
		assertEquals(fileClasses, fileFirstClasses);

		/* This version finds out the files in a containment subtree. */
		final int subtreeClasses = (int) eol(eolQuery, ctx(
			IQueryEngine.PROPERTY_REPOSITORYCONTEXT, fragmentedRepoURI,
			IQueryEngine.PROPERTY_SUBTREECONTEXT, "/IJavaProject_org.eclipse.jdt.apt.pluggable.core/IJavaProject_org.eclipse.jdt.apt.pluggable.core.xmi"
		));
		assertEquals(fileClasses, subtreeClasses);

		/* This version finds out the files in a containment subtree, and uses file-first traversal. */
		final int subtreeFileFirstClasses = (int) eol(eolQuery, ctx(
			IQueryEngine.PROPERTY_REPOSITORYCONTEXT, fragmentedRepoURI,
			IQueryEngine.PROPERTY_SUBTREECONTEXT, "/IJavaProject_org.eclipse.jdt.apt.pluggable.core/IJavaProject_org.eclipse.jdt.apt.pluggable.core.xmi",
			IQueryEngine.PROPERTY_FILEFIRST, "true"
		));
		assertEquals(fileClasses, subtreeFileFirstClasses);

		/* This version finds out the files in a containment subtree, and registers derived edges to quickly find instances of a type within a containment subtree. */
		final int subtreeClassesDerived = (int) eol(eolQuery, ctx(
			IQueryEngine.PROPERTY_REPOSITORYCONTEXT, fragmentedRepoURI,
			IQueryEngine.PROPERTY_SUBTREECONTEXT, "/IJavaProject_org.eclipse.jdt.apt.pluggable.core/IJavaProject_org.eclipse.jdt.apt.pluggable.core.xmi",
			IQueryEngine.PROPERTY_SUBTREE_DERIVEDALLOF, "true"
		));
		assertEquals(fileClasses, subtreeClassesDerived);
	}

	@Test
	public void getAllOfCountsSubtypes() throws Throwable {
		prepareFragmented();

		/* This version finds out the files in a containment subtree, and registers derived edges to quickly find instances of a type within a containment subtree. */
		final int binpkgCount = (int) eol("return BinaryPackageFragmentRoot.all.size;", ctx(
			IQueryEngine.PROPERTY_REPOSITORYCONTEXT, fragmentedRepoURI,
			IQueryEngine.PROPERTY_SUBTREECONTEXT, "/set0.xmi",
			IQueryEngine.PROPERTY_SUBTREE_DERIVEDALLOF, "true"
		));
		final int ipkgCount = (int) eol("return IPackageFragmentRoot.all.size;", ctx(
			IQueryEngine.PROPERTY_REPOSITORYCONTEXT, fragmentedRepoURI,
			IQueryEngine.PROPERTY_SUBTREECONTEXT, "/set0.xmi",
			IQueryEngine.PROPERTY_SUBTREE_DERIVEDALLOF, "true"
		));

		assertTrue(String.format(
			"IPackageFragmentRoot.all.size should be > than BinaryPackageFragmentRoot.all.size: checking %d > %d", ipkgCount, binpkgCount),
			ipkgCount > binpkgCount);

		List<String> pathsTouched = new ArrayList<>();
		final Random rnd = new Random(42);
		for (File f : FileUtils.listFiles(folderFragmented, TrueFileFilter.INSTANCE, TrueFileFilter.INSTANCE)) {
			if (rnd.nextDouble() < 0.2) {
				FileUtils.touch(f);
				pathsTouched.add(f.getName());
			}
		}
		indexer.requestImmediateSync();
		scheduleAndWait(() -> {
			final int ipkgCount2 = (int) eol("return IPackageFragmentRoot.all.size;", ctx(
				IQueryEngine.PROPERTY_REPOSITORYCONTEXT, fragmentedRepoURI,
				IQueryEngine.PROPERTY_SUBTREECONTEXT, "/set0.xmi",
				IQueryEngine.PROPERTY_SUBTREE_DERIVEDALLOF, "true"
			));
			assertEquals(
				"IPackageFragmentRoot.all.size should stay the same after touching " + pathsTouched,
				ipkgCount, ipkgCount2);
		
			return null;
		});
	}

	@Test
	public void subtreeTraversalScoping() throws Throwable {
		prepareBoth();

		// None of the external package fragment roots should be visible with traversal scoping on
		final String eolQuery = "return IJavaProject.all.first.externalPackageFragmentRoots.size;";
		final int fileClasses = (int) eol(eolQuery, ctx(
			IQueryEngine.PROPERTY_REPOSITORYCONTEXT, fragmentedRepoURI,
			IQueryEngine.PROPERTY_FILECONTEXT, "/IJavaProject_org.eclipse.jdt.apt.pluggable.core/*",
			IQueryEngine.PROPERTY_ENABLE_TRAVERSAL_SCOPING, "true"));
		assertEquals(0, fileClasses);

		// Same should happen with the subtree context
		final int subtreeClasses = (int) eol(eolQuery, ctx(
			IQueryEngine.PROPERTY_REPOSITORYCONTEXT, fragmentedRepoURI,
			IQueryEngine.PROPERTY_SUBTREECONTEXT, "/IJavaProject_org.eclipse.jdt.apt.pluggable.core/IJavaProject_org.eclipse.jdt.apt.pluggable.core.xmi",
			IQueryEngine.PROPERTY_ENABLE_TRAVERSAL_SCOPING, "true"));
		assertEquals(0, subtreeClasses);
	}

	@Test
	public void getFiles() throws Throwable {
		prepareBoth();

		final int fragmentedFilesCount = (int) eol("return Model.files.size;", ctx(
			IQueryEngine.PROPERTY_REPOSITORYCONTEXT, fragmentedRepoURI,
			IQueryEngine.PROPERTY_FILECONTEXT, "/IJavaProject_org.eclipse.jdt.apt.pluggable.core/*"));
		final int subtreeFilesCount = (int) eol("return Model.files.size;", ctx(
			IQueryEngine.PROPERTY_REPOSITORYCONTEXT, fragmentedRepoURI,
			IQueryEngine.PROPERTY_SUBTREECONTEXT, "/IJavaProject_org.eclipse.jdt.apt.pluggable.core/IJavaProject_org.eclipse.jdt.apt.pluggable.core.xmi"
		));
		assertEquals(fragmentedFilesCount, subtreeFilesCount);
	}

	private static Map<String, Object> ctx(String... opts) {
		Map<String, Object> ctx = new HashMap<>();
		for (int i = 0; i + 1 < opts.length; i += 2) {
			final String key = opts[i], value = opts[i + 1];
			ctx.put(key, value);
		}
		return ctx;
	}

	/**
	 * Creates the fragmented version of set0, to help with testing.
	 */
	public static void main(String[] args) throws IOException {
		new Fragmenter().run();
	}

}
