/*******************************************************************************
 * 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.timeaware.tests;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import java.io.File;
import java.util.Arrays;
import java.util.List;

import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.hawk.backend.tests.factories.IGraphDatabaseFactory;
import org.eclipse.hawk.core.graph.timeaware.ITimeAwareGraphNode;
import org.eclipse.hawk.epsilon.emc.EOLQueryEngine;
import org.eclipse.hawk.epsilon.emc.EOLQueryEngine.GraphNodeWrapper;
import org.eclipse.hawk.graph.Slot;
import org.eclipse.hawk.integration.tests.emf.EMFModelSupportFactory;
import org.eclipse.hawk.integration.tests.mm.Tree.Tree;
import org.eclipse.hawk.integration.tests.mm.Tree.TreeFactory;
import org.eclipse.hawk.integration.tests.mm.Tree.TreePackage;
import org.eclipse.hawk.svn.tests.rules.TemporarySVNRepository;
import org.eclipse.hawk.timeaware.queries.TimeAwareEOLQueryEngine.TimeAwareGraphNodeWrapper;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;

/**
 * Tests for the time-aware indexing of model element nodes, using Subversion.
 */
@RunWith(Parameterized.class)
public class SubversionNodeHistoryTest extends AbstractTimeAwareModelIndexingTest {
	@Rule
	public TemporarySVNRepository svnRepository = new TemporarySVNRepository();

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

	public SubversionNodeHistoryTest(File baseDir, IGraphDatabaseFactory dbFactory) {
		super(baseDir, dbFactory, new EMFModelSupportFactory());
	}

	@Override
	protected void setUpMetamodels() throws Exception {
		// Let Hawk pull in the metamodel through the registry
		TreePackage.eINSTANCE.getName();
	}

	@Test
	public void travelToMissingTimepointReturnsNull() throws Throwable {
		twoCommitTree();
		scheduleAndWait(() -> {
			GraphNodeWrapper gnw = (GraphNodeWrapper) timeAwareEOL("return Tree.latest.prev.all.first;");
			assertNotNull(gnw.getNode());
			assertNull(((ITimeAwareGraphNode) gnw.getNode()).travelInTime(ITimeAwareGraphNode.NO_SUCH_INSTANT));
			return null;
		});
	}

	@Test
	public void createDeleteNode() throws Throwable {
		twoCommitTree();
		scheduleAndWait(() -> {
			// .all works on revision 0
			assertEquals(0, timeAwareEOL("return Tree.all.size;"));

			// We also deleted everything in the latest revision
			assertEquals(0, timeAwareEOL("return Tree.latest.all.size;"));

			// .created can return instances that have been created from a certain moment in
			// time (even if not alive anymore)
			assertEquals(1, timeAwareEOL("return Tree.latest.prev.size;"));

			assertEquals("xy", timeAwareEOL("return Tree.latest.prev.all.first.label;"));

			return null;
		});
	}

	private void twoCommitTree() throws Exception {
		final File fTree = new File(svnRepository.getCheckoutDirectory(), "root.xmi");
		Resource rTree = rsTree.createResource(URI.createFileURI(fTree.getAbsolutePath()));

		Tree t = treeFactory.createTree();
		t.setLabel("xy");
		rTree.getContents().add(t);
		rTree.save(null);

		svnRepository.add(fTree);
		svnRepository.commit("First commit");
		svnRepository.remove(fTree);
		svnRepository.commit("Second commit - remove file");

		requestSVNIndex();
	}

	@Test
	public void countInstancesFromModelTypes() throws Throwable {
		twoCommitTree();
		scheduleAndWait(() -> {
			assertEquals(0, timeAwareEOL("return Model.types.selectOne(t|t.name='Tree').all.size;"));
			assertEquals(1, timeAwareEOL("return Model.types.selectOne(t|t.name='Tree').latest.prev.all.size;"));
			assertEquals(0, timeAwareEOL("return Model.types.selectOne(t|t.name='Tree').latest.prev.prev.all.size;"));
			assertEquals(0, timeAwareEOL("return Model.types.selectOne(t|t.name='Tree').earliest.all.size;"));
			return null;
		});
	}

	@Test
	public void countInstancesFromModel() throws Throwable {
		twoCommitTree();
		scheduleAndWait(() -> {
			assertEquals(0, timeAwareEOL("return Model.types.selectOne(t|t.name='Tree').all.size;"));
			assertEquals(1, timeAwareEOL("return Model.types.selectOne(t|t.name='Tree').latest.prev.all.size;"));
			assertEquals(0, timeAwareEOL("return Model.types.selectOne(t|t.name='Tree').latest.prev.prev.all.size;"));
			assertEquals(0, timeAwareEOL("return Model.types.selectOne(t|t.name='Tree').earliest.all.size;"));
			return null;
		});
	}

	@SuppressWarnings("unchecked")
	@Test
	public void countInstancesTimeline() throws Throwable {
		twoCommitTree();
		scheduleAndWait(() -> {
			List<List<Object>> results = (List<List<Object>>) timelineEOL("return Tree.all.size;");
			assertEquals(0, results.get(0).get(1));
			assertEquals(0, (int) timeAwareEOL("return Model.allInstancesAt(t).size;", "t", results.get(0).get(0)));

			/*
			 * This is required since Epsilon does not consider type promotions for
			 * reflective operation access (from int to long).
			 */
			assertEquals(0, (int) timeAwareEOL("return Model.allInstancesAt(" + results.get(0).get(0) + ").size;"));

			assertEquals(1, results.get(1).get(1));
			assertEquals(1, (int) timeAwareEOL("return Model.allInstancesAt(t).size;", "t", results.get(1).get(0)));

			assertEquals(0, results.get(2).get(1));
			assertEquals(0, (int) timeAwareEOL("return Model.allInstancesAt(t).size;", "t", results.get(2).get(0)));

			return null;
		});
	}

	@Test
	public void countInstancesModelAll() throws Throwable {
		final File fTree = new File(svnRepository.getCheckoutDirectory(), "root.xmi");
		Resource rTree = rsTree.createResource(URI.createFileURI(fTree.getAbsolutePath()));

		Tree t = treeFactory.createTree();
		t.setLabel("xy");
		rTree.getContents().add(t);
		rTree.save(null);

		svnRepository.add(fTree);
		svnRepository.commit("First commit");
		requestSVNIndex();

		scheduleAndWait(() -> {
			assertEquals(0, timeAwareEOL("return Model.allInstances.collect(t|t.label).size;"));
			assertEquals(0, timeAwareEOL("return Model.allInstances.size;"));
			assertEquals(1, timeAwareEOL("return Model.allInstancesNow.size;"));
			return null;
		});
	}

	private Tree keepAddingChildren() throws Exception {
		final File fTree = new File(svnRepository.getCheckoutDirectory(), "m.xmi");
		Resource rTree = rsTree.createResource(URI.createFileURI(fTree.getAbsolutePath()));

		Tree tRoot = treeFactory.createTree();
		tRoot.setLabel("Root");
		rTree.getContents().add(tRoot);
		rTree.save(null);
		svnRepository.add(fTree);
		svnRepository.commit("Create root");

		for (String childLabel : Arrays.asList("T1", "T2", "T3")) {
			Tree t1 = createChild(tRoot, childLabel);
			rTree.save(null);
			svnRepository.commit("Add " + childLabel);
		}

		requestSVNIndex();
		return tRoot;
	}

	@Test
	public void commitMessages() throws Throwable {
		keepAddingChildren();
		scheduleAndWait(() -> {
			assertEquals("Create root",
				timeAwareEOL("return Model.getRepository(Tree.latest.all.selectOne(t|t.label='Root').earliest).message;"));
			assertEquals("Add T1",
					timeAwareEOL("return Model.getRepository(Tree.latest.all.selectOne(t|t.label='T1').earliest).message;"));
			assertEquals("Add T2",
					timeAwareEOL("return Model.getRepository(Tree.latest.all.selectOne(t|t.label='T2').earliest).message;"));
			assertEquals("Add T3",
					timeAwareEOL("return Model.getRepository(Tree.latest.all.selectOne(t|t.label='T3').earliest).message;"));

			return null;
		});
	}

	@Test
	public void followReferences() throws Throwable {
		keepAddingChildren();
		scheduleAndWait(() -> {
			Object taGNW = timeAwareEOL("return Tree.latest.all.selectOne(t|t.label='Root').earliest.next.children.first;");
			assertTrue("Following a reference in a time-aware backend should result in a time-aware graph node wrapper",
				taGNW instanceof TimeAwareGraphNodeWrapper);

			return null;
		});
	}

	@Test
	public void rangesAreBothInclusive() throws Throwable {
		keepAddingChildren();
		scheduleAndWait(() -> {
			GraphNodeWrapper gnw = (GraphNodeWrapper) timeAwareEOL(
				"return Tree.latest.all.selectOne(t|t.latest.label = 'Root');"
			);

			ITimeAwareGraphNode taNode = (ITimeAwareGraphNode) gnw.getNode();
			final long earliestInstant = taNode.getEarliestInstant();
			final long latestInstant = taNode.getLatestInstant();

			final List<ITimeAwareGraphNode> allVersions = taNode.getAllVersions();
			assertEquals(earliestInstant, allVersions.get(allVersions.size() - 1).getTime());
			assertEquals(latestInstant, allVersions.get(0).getTime());

			final List<ITimeAwareGraphNode> versionsUpTo = taNode.getVersionsUpTo(latestInstant);
			assertEquals(earliestInstant, versionsUpTo.get(versionsUpTo.size() - 1).getTime());
			assertEquals(latestInstant, versionsUpTo.get(0).getTime());

			final List<ITimeAwareGraphNode> versionsFrom = taNode.getVersionsFrom(earliestInstant);
			assertEquals(earliestInstant, versionsFrom.get(versionsFrom.size() - 1).getTime());
			assertEquals(latestInstant, versionsFrom.get(0).getTime());
	
			final List<ITimeAwareGraphNode> versionsBW = taNode.getVersionsBetween(earliestInstant, latestInstant);
			assertEquals(earliestInstant, versionsBW.get(versionsFrom.size() - 1).getTime());
			assertEquals(latestInstant, versionsBW.get(0).getTime());
			
			return null;
		});

	}

	@Test
	public void alwaysTrue() throws Throwable {
		keepAddingChildren();
		scheduleAndWait(() -> {
			assertTrue((boolean) timeAwareEOL(
				"return Tree.latest.all.selectOne(t|t.label='Root').always(v|v.label = 'Root');"
			));
			assertTrue((boolean) timeAwareEOL(
				"return Tree.latest.all.selectOne(t|t.label='Root').never(v|v.label <> 'Root');"
			));
			assertTrue((boolean) timeAwareEOL(
				"return Tree.latest.all.selectOne(t|t.label='Root').eventually(v|v.children.size > 2);"
			));
			assertFalse((boolean) timeAwareEOL(
				"return Tree.latest.all.selectOne(t|t.label='Root').eventually(v|v.children.size > 3);"
			));
			assertTrue((boolean) timeAwareEOL(
				"return Tree.latest.all.selectOne(t|t.label='Root').eventuallyAtMost(v | v.children.size > 2, 2);"
			));
			assertFalse((boolean) timeAwareEOL(
				"return Tree.latest.all.selectOne(t|t.label='Root').eventuallyAtMost(v | v.children.size > 0, 2);"
			));
			assertFalse((boolean) timeAwareEOL(
				"return Tree.latest.all.selectOne(t|t.label='Root').eventuallyAtLeast(v | v.children.size > 2, 2);"
			));
			assertTrue((boolean) timeAwareEOL(
				"return Tree.latest.all.selectOne(t|t.label='Root').eventuallyAtLeast(v | v.children.size > 0, 2);"
			));
			assertTrue((boolean) timeAwareEOL(
				"return Tree.latest.all.selectOne(t|t.label='Root').since(v|v.children.size > 1).always(v | v.children.size>1);"
			));
			assertFalse((boolean) timeAwareEOL(
				"return Tree.latest.all.selectOne(t|t.label='Root').since(v|v.children.size > 1).eventually(v | v.children.size<1);"
			));

			return null;
		});
	}

	@Test
	public void after() throws Throwable {
		keepAddingChildren();
		scheduleAndWait(() -> {
			assertEquals(".after is an open left range, i.e. excludes matching version", 2, (int) timeAwareEOL(
				"return Tree.latest.all.selectOne(t|t.label='Root').earliest.after(v|v.children.size > 0).children.size;"
			));
			assertNull(".after with no match returns null", timeAwareEOL(
				"return Tree.latest.all.selectOne(t|t.label='Root').after(v|v.children.size > 5);"
			));
			return null;
		});
	}

	@Test
	public void until() throws Throwable {
		Tree tRoot = keepAddingChildren();
		Tree tFourthChild = TreeFactory.eINSTANCE.createTree();
		tFourthChild.setLabel("T4");
		tRoot.getChildren().add(tFourthChild);
		tRoot.eResource().save(null);
		svnRepository.commit("Added fourth child");
		indexer.requestImmediateSync();

		scheduleAndWait(() -> {
			assertEquals(".until is a closed end range, i.e. includes matching version", 2, (int) timeAwareEOL(
				"return Tree.earliest.next.all.selectOne(t|t.label='Root').until(v|v.children.size > 1).latest.children.size;"
			));
			assertNull(".until with no match returns null", timeAwareEOL(
				"return Tree.earliest.next.all.selectOne(t|t.label='Root').until(v|v.children.size > 5);"
			));
			assertEquals(".since + .until works", 2, (int) timeAwareEOL(
				"return Tree.earliest.next.all.selectOne(t|t.label='Root').since(v|v.children.size > 1).until(v|v.children.size > 2).versions.size;"
			));
			return null;
		});
	}

	@Test
	public void before() throws Throwable {
		Tree tRoot = keepAddingChildren();
		Tree tFourthChild = TreeFactory.eINSTANCE.createTree();
		tFourthChild.setLabel("T4");
		tRoot.getChildren().add(tFourthChild);
		tRoot.eResource().save(null);
		svnRepository.commit("Added fourth child");
		indexer.requestImmediateSync();

		scheduleAndWait(() -> {
			assertEquals(".before is a open end range, i.e. excludes matching version", 1, (int) timeAwareEOL(
				"return Tree.earliest.next.all.selectOne(t|t.label='Root').before(v|v.children.size > 1).latest.children.size;"
			));
			assertNull(".before with no match returns null", timeAwareEOL(
				"return Tree.earliest.next.all.selectOne(t|t.label='Root').before(v|v.children.size > 5);"
			));
			assertEquals(".after + .before works", 1, (int) timeAwareEOL(
				"return Tree.earliest.next.all.selectOne(t|t.label='Root').after(v|v.children.size.println('after for ' + v.time + ': ') > 0).before(v|v.children.size.println('before for ' + v.time + ': ') > 2).versions.size;"
			));
			assertFalse(".after + .before can give an undefined node", (boolean) timeAwareEOL(
				"return Tree.earliest.next.all.selectOne(t|t.label='Root').after(v|v.children.size > 0).before(v|v.children.size > 1).isDefined();"
			));

			return null;
		});
	}

	@Test
	public void sinceThen() throws Throwable {
		keepAddingChildren();

		scheduleAndWait(() -> {
			assertFalse("Type node - Without .sinceThen, always uses all versions", (boolean) timeAwareEOL(
				"return Tree.earliest.next.always(v|v.all.size > 0);"
			));
			assertTrue("Type node - With .sinceThen, scope is limited to that version onwards", (boolean) timeAwareEOL(
				"return Tree.earliest.next.sinceThen.always(v|v.all.size > 0);"
			));

			assertFalse("Model element - Without .sinceThen, always uses all versions", (boolean) timeAwareEOL(
				"return Tree.earliest.next.all.selectOne(t|t.label = 'Root').next.always(v|v.children.size > 0);"
			));
			assertTrue("Model element - With .sinceThen, scope is limited to that version onwards", (boolean) timeAwareEOL(
				"return Tree.earliest.next.all.selectOne(t|t.label = 'Root').next.sinceThen.always(v|v.children.size > 0);"
			));

			return null;
		});
	}

	@Test
	public void afterThen() throws Throwable {
		keepAddingChildren();

		scheduleAndWait(() -> {
			assertFalse("Type node - Without .afterThen, always uses all versions", (boolean) timeAwareEOL(
				"return Tree.earliest.next.always(v|v.all.size > 1);"
			));
			assertTrue("Type node - With .afterThen, scope is limited to that version onwards", (boolean) timeAwareEOL(
				"return Tree.earliest.next.afterThen.always(v|v.all.size > 1);"
			));

			assertFalse("Model element - Without .afterThen, always uses all versions", (boolean) timeAwareEOL(
				"return Tree.earliest.next.all.selectOne(t|t.label = 'Root').next.always(v|v.children.size > 1);"
			));
			assertTrue("Model element - With .sinceThen, scope is limited to that version onwards", (boolean) timeAwareEOL(
				"return Tree.earliest.next.all.selectOne(t|t.label = 'Root').next.afterThen.always(v|v.children.size > 1);"
			));

			return null;
		});
	}

	@Test
	public void untilThen() throws Throwable {
		keepAddingChildren();

		scheduleAndWait(() -> {
			assertTrue("Positive combination of .sinceThen + .untilThen", (boolean) timeAwareEOL(
				"return Tree.earliest.next.sinceThen.latest.prev.untilThen.always(v|v.all.size > 0 and v.all.size < 4);"
			));
			assertFalse("Negative combination of .sinceThen + .untilThen", (boolean) timeAwareEOL(
				"return Tree.earliest.next.sinceThen.latest.untilThen.always(v|v.all.size > 0 and v.all.size < 4);"
			));
			return null;
		});
	}

	@Test
	public void beforeThen() throws Throwable {
		keepAddingChildren();

		scheduleAndWait(() -> {
			assertTrue("Positive combination of .afterThen + .beforeThen", (boolean) timeAwareEOL(
				"return Tree.earliest.afterThen.latest.beforeThen.always(v|v.all.size > 0 and v.all.size < 4);"
			));
			assertFalse("Negative combination of .sinceThen + .beforeThen", (boolean) timeAwareEOL(
				"return Tree.earliest.sinceThen.latest.beforeThen.always(v|v.all.size > 0 and v.all.size < 4);"
			));
			return null;
		});
	}

	@Test
	public void whenPoints() throws Throwable {
		Tree tRoot = keepAddingChildren();
		tRoot.getChildren().remove(2);
		tRoot.eResource().save(null);
		svnRepository.commit("Removed third child");
		indexer.requestImmediateSync();

		scheduleAndWait(() -> {
			assertEquals(".when with all versions", 5, (int) timeAwareEOL(
				"return Tree.earliest.next.all.selectOne(t|t.label='Root').when(v|v.children.size >= 0).versions.size;"
			));
			assertFalse(".when with no versions returns null", (boolean) timeAwareEOL(
				"return Tree.earliest.next.all.selectOne(t|t.label='Root').when(v|v.children.size > 5).isDefined();"
			));
			assertEquals(".when with some contiguous versions", 2, (int) timeAwareEOL(
				"return Tree.earliest.next.all.selectOne(t|t.label='Root').when(v|v.children.size < 2).versions.size;"
			));
			assertEquals(".when with one version", 1, (int) timeAwareEOL(
				"return Tree.earliest.next.all.selectOne(t|t.label='Root').when(v|v.children.size = 3).versions.size;"
			));
			assertEquals(".when with some non-contiguous versions", 2, (int) timeAwareEOL(
				"return Tree.earliest.next.all.selectOne(t|t.label='Root').when(v|v.children.size = 2).versions.size;"
			));
			assertEquals(".when with non-contiguous versions + back and forth", 2, (int) timeAwareEOL(
				"return Tree.earliest.next.all.selectOne(t|t.label='Root').when(v|v.children.size = 2).next.prev.children.size;"
			));

			return null;
		});
	}

	@Test
	public void unscope() throws Throwable {
		/*
		 * This test uses a combination of .when, .afterThen, .untilThen
		 * and .unscoped to check properties inside all intervals that match
		 * a condition.
		 */
		final File fTree = new File(svnRepository.getCheckoutDirectory(), "m.xmi");
		Resource rTree = rsTree.createResource(URI.createFileURI(fTree.getAbsolutePath()));

		Tree tRoot = treeFactory.createTree();
		tRoot.setLabel("Empty");
		rTree.getContents().add(tRoot);
		rTree.save(null);
		svnRepository.add(fTree);
		svnRepository.commit("Create root");

		// First interval
		Tree t1A = treeFactory.createTree(); t1A.setLabel("T1A");
		tRoot.setLabel("NotEmpty");
		tRoot.getChildren().add(t1A);
		rTree.save(null);
		svnRepository.commit("First non-empty interval, commit 1");
		Tree t1B = treeFactory.createTree(); t1B.setLabel("T1B");
		tRoot.getChildren().add(t1B);
		rTree.save(null);
		svnRepository.commit("First non-empty interval, commit 2");

		// Intermediate gap, empty node
		tRoot.setLabel("Empty");
		tRoot.getChildren().clear();
		rTree.save(null);
		svnRepository.commit("Gap revision, empty root");

		// Second interval
		tRoot.setLabel("NotEmpty");
		Tree t1C = treeFactory.createTree(); t1C.setLabel("T1C");
		tRoot.getChildren().add(t1C);
		rTree.save(null);
		svnRepository.commit("Second non-empty interval, commit 1");
		tRoot.getChildren().add(t1A);
		tRoot.getChildren().remove(t1C);
		rTree.save(null);
		svnRepository.commit("Third non-empty interval, commit 2");

		/*
		 * Final gap, empty node (would need .weakBefore/.weakUntil or tweaked .until
		 * condition without it).
		 */
		tRoot.setLabel("Empty");
		tRoot.getChildren().clear();
		rTree.save(null);
		svnRepository.commit("Final gap revision, empty root");

		requestSVNIndex();

		// END OF TEST SETUP

		scheduleAndWait(() -> {
			assertEquals("There are three versions with an empty root node", 3, (int)timeAwareEOL(
				"return Tree.latest.all.selectOne(t|not t.eContainer.isDefined())"
				+ ".earliest.when(v|v.children.isEmpty).versions.size;"
			));
			assertTrue("Root element labels are not empty when they say so - .when version", (boolean)timeAwareEOL(
				"return Tree.latest.all.selectOne(t|not t.eContainer.isDefined())"
				+ ".earliest.when(v|v.label = 'NotEmpty')"
				+ ".always(v|not v.children.isEmpty);"
			));
			assertTrue("Interval version without .unscope would give null value", (boolean)timeAwareEOL(
				"return Tree.latest.all.selectOne(t|not t.eContainer.isDefined())"
				+ ".earliest.when(v|v.label = 'NotEmpty' and v.prev.label = 'Empty')"
				+ ".always(v | not v.sinceThen.before(v|v.label = 'Empty').isDefined());"
			));
			assertTrue("Root element labels are not empty when they say so - interval version with .unscope, .sinceThen and .before", (boolean)timeAwareEOL(
				"return Tree.latest.all.selectOne(t|not t.eContainer.isDefined())"
				+ ".earliest.when(v|v.label.println('label: ') = 'NotEmpty' and v.prev.label = 'Empty')"
				+ ".always(v| v.unscoped.sinceThen.before(v | v.label = 'Empty')"
					+ ".always(v | v.children.size.println('Children of ' + v.label + ' at ' + v.time + ': ') > 0)"
				+ ");"
			));
			assertTrue("Root element labels are not empty when they say so - interval version with .unscope, .sinceThen and .until", (boolean)timeAwareEOL(
				"return Tree.latest.all.selectOne(t|not t.eContainer.isDefined())"
				+ ".earliest.when(v|v.label = 'NotEmpty' and v.prev.label = 'Empty')"
				+ ".always(v| v.unscoped.sinceThen.until(v | not v.next.isDefined() or v.next.label = 'Empty')"
					+ ".always(v | v.children.size.println('Children of ' + v.label + ' at ' + v.time + ': ') > 0)"
				+ ");"
			));

			assertTrue("Type node - scoped .always returns true", (boolean) timeAwareEOL(
				"return Tree.earliest.next.sinceThen.always(v|v.all.size > 0);"
			));
			assertFalse("Type node - unscoped .always returns true", (boolean) timeAwareEOL(
				"return Tree.earliest.next.sinceThen.unscoped.always(v|v.all.size > 0);"
			));
			
			return null;
		});

	}

	@Test
	public void onceFalse() throws Throwable {
		Tree tRoot = keepAddingChildren();
		tRoot.setLabel("SomethingElse");
		tRoot.eResource().save(null);
		svnRepository.commit("Changed label");
		indexer.requestImmediateSync();
		
		scheduleAndWait(() -> {
			assertFalse((boolean) timeAwareEOL(
				"return Tree.latest.all.selectOne(t|t.latest.label='SomethingElse').always(v|v.label = 'Root');"
			));
			assertFalse((boolean) timeAwareEOL(
				"return Tree.latest.prev.all.selectOne(t|t.label='Root').always(v|v.label = 'Root');"
			));

			assertFalse((boolean) timeAwareEOL(
				"return Tree.latest.prev.all.selectOne(t|t.label='Root').never(v|v.label = 'Root');"
			));
			assertFalse((boolean) timeAwareEOL(
				"return Tree.latest.prev.all.selectOne(t|t.label='Root').never(v|v.label <> 'Root');"
			));

			assertTrue((boolean) timeAwareEOL(
				"return Tree.latest.prev.all.selectOne(t|t.label='Root').eventually(v|v.label <> 'Root');"
			));
			assertTrue((boolean) timeAwareEOL(
				"return Tree.latest.prev.all.selectOne(t|t.label='Root').eventually(v|v.label = 'Root');"
			));

			assertTrue((boolean) timeAwareEOL(
				"return Tree.latest.prev.all.selectOne(t|t.label='Root').eventuallyAtMost(v|v.label <> 'Root', 1);"
			));
			assertFalse((boolean) timeAwareEOL(
				"return Tree.latest.prev.all.selectOne(t|t.label='Root').eventuallyAtMost(v|v.label = 'Root', 2);"
			));

			assertFalse((boolean) timeAwareEOL(
				"return Tree.latest.prev.all.selectOne(t|t.label='Root').eventuallyAtLeast(v|v.label <> 'Root', 2);"
			));
			assertTrue((boolean) timeAwareEOL(
				"return Tree.latest.prev.all.selectOne(t|t.label='Root').eventuallyAtLeast(v|v.label = 'Root', 2);"
			));

			assertNotNull(".since by itself returns a node", timeAwareEOL(
				"return Tree.latest.all.selectOne(t|t.latest.label='SomethingElse').earliest.since(v|v.label <> 'Root');"
			));
			assertFalse(".since + .eventually works", (boolean) timeAwareEOL(
				"return Tree.latest.all.selectOne(t|t.label='Root').earliest.since(v|v.label <> 'Root').eventually(v|v.label = 'Root');"
			));
			assertTrue(".since + .never works", (boolean) timeAwareEOL(
				"return Tree.latest.all.selectOne(t|t.label='Root').earliest.since(v|v.label <> 'Root').never(v|v.label.println('Label at ' + v.time + ': ') = 'Root');"
			));
			assertTrue(".since + .always works", (boolean) timeAwareEOL(
				"return Tree.latest.all.selectOne(t|t.label='Root').earliest.since(v|v.label <> 'Root').always(v|v.children.size > 1);"
			));
			assertFalse(".since can be chained", (boolean) timeAwareEOL(
				"return Tree.latest.all.selectOne(t|t.latest.label='SomethingElse').earliest.since(v|v.label <> 'Root').since(v|v.children.size = 0).isDefined();"
			));

			return null;
		});
	}

	@Test
	public void whenAnnotated() throws Throwable {
		indexer.registerMetamodels(new File(baseDir, TREE_MM_PATH));
		indexer.addDerivedAttribute(TreePackage.eNS_URI , "Tree", "hasNoChildren",
			Slot.ATTR_TYPE_TIMEANNOTATION, false, false, false,
			EOLQueryEngine.TYPE, "return self.children.isEmpty;");
		
		final File fTree = new File(svnRepository.getCheckoutDirectory(), "m.xmi");
		Resource rTree = rsTree.createResource(URI.createFileURI(fTree.getAbsolutePath()));

		// Version 1: no children
		Tree tRoot = treeFactory.createTree();
		tRoot.setLabel("Root");
		rTree.getContents().add(tRoot);
		rTree.save(null);
		svnRepository.add(fTree);
		svnRepository.commit("Create root");

		// V2: has children
		Tree t1 = createChild(tRoot, "t1");
		rTree.save(null);
		svnRepository.commit("Add child");

		// V3: no children again
		tRoot.getChildren().remove(t1);
		rTree.save(null);
		svnRepository.commit("Remove child");

		requestSVNIndex();

		scheduleAndWait(() -> {
			assertEquals(3, timeAwareEOL(
					"return Tree.latest.all.selectOne(t|t.label='Root').versions.size;"));
			System.out.println(timeAwareEOL("return Tree.latest.all.selectOne(t|t.label='Root').versions.collect(v|v.children.size);"));

			// .when uses the current timepoint as the start
			assertEquals(2, timeAwareEOL(
				"return Tree.latest.all.selectOne(t|t.label='Root').earliest.when(v|v.children.size = 0).versions.size;"));
			assertEquals(1, timeAwareEOL(
					"return Tree.latest.all.selectOne(t|t.label='Root').when(v|v.children.size = 0).versions.size;"));

			// .whenAnnotated should do the same
			assertEquals(2, timeAwareEOL(
				"return Tree.latest.all.selectOne(t|t.label='Root').earliest.whenAnnotated('hasNoChildren').versions.size;"));
			assertEquals(1, timeAwareEOL(
					"return Tree.latest.all.selectOne(t|t.label='Root').whenAnnotated('hasNoChildren').versions.size;"));

			return null;
		});
	}

	private Tree createChild(Tree tRoot, String label) {
		Tree t1 = treeFactory.createTree();
		t1.setLabel(label);
		tRoot.getChildren().add(t1);
		return t1;
	}

	private void requestSVNIndex() throws Exception {
		requestSVNIndex(svnRepository);
	}

}
