/*******************************************************************************
 * Copyright (c) 2015-2017 The University of York, 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.backend.tests;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;

import org.eclipse.hawk.backend.tests.factories.IGraphDatabaseFactory;
import org.eclipse.hawk.core.IModelIndexer;
import org.eclipse.hawk.core.graph.IGraphIterable;
import org.eclipse.hawk.core.graph.IGraphNode;
import org.eclipse.hawk.core.graph.IGraphNodeIndex;
import org.eclipse.hawk.core.graph.IGraphTransaction;
import org.junit.Assume;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runners.Parameterized.Parameters;

/**
 * Tests for populating and querying indices.
 */
public class IndexTest extends TemporaryDatabaseTest {

	@Rule
	public RedirectSystemErrorRule errRule = new RedirectSystemErrorRule();

	@Rule
	public LogbackOnlyErrorsRule logRule = new LogbackOnlyErrorsRule();

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

	public IndexTest(IGraphDatabaseFactory dbFactory) {
		super(dbFactory);
	}

	@Test
	public void query() throws Exception {
		final String mmBarURI = "http://foo/bar";
		final String mmFileURI = "file://a/b/c.d";
		final Map<String, Object> mmBarNodeProps = Collections.singletonMap(IModelIndexer.IDENTIFIER_PROPERTY, mmBarURI);
		final Map<String, Object> mmFileNodeProps = Collections.singletonMap(IModelIndexer.IDENTIFIER_PROPERTY, mmFileURI);

		try (IGraphTransaction tx = db.beginTransaction()) {
			IGraphNode mmNode = db.createNode(mmBarNodeProps, "metamodel");
			db.getMetamodelIndex().add(mmNode, Collections.singletonMap("id", mmBarURI));

			IGraphNode mmFileNode = db.createNode(mmFileNodeProps, "metamodel");
			db.getMetamodelIndex().add(mmFileNode, Collections.singletonMap("id", mmFileURI));

			tx.success();
		}

		// Query with partial wildcard
		try (IGraphTransaction tx = db.beginTransaction()) {
			final IGraphIterable<? extends IGraphNode> iter = db.getMetamodelIndex().query("id", "http://*");
			assertSame(db, iter.getSingle().getGraph());
			assertEquals(1, iter.size());
			assertTrue(iter.getSingle().getProperty(IModelIndexer.IDENTIFIER_PROPERTY).toString().startsWith("http://"));
			tx.success();
		}

		// Query with full value
		try (IGraphTransaction tx = db.beginTransaction()) {
			final IGraphIterable<? extends IGraphNode> iter3 = db.getMetamodelIndex().query("id", mmBarURI);
			assertEquals(1, iter3.size());

			final IGraphNode node = iter3.getSingle();
			assertSame(db, node.getGraph());
			assertEquals(mmBarURI, node.getProperty(IModelIndexer.IDENTIFIER_PROPERTY));
			tx.success();
		}

		// Query with wildcard on one field
		try (IGraphTransaction tx = db.beginTransaction()) {
			final IGraphIterable<? extends IGraphNode> iter4 = db.getMetamodelIndex().query("id", "*");
			assertSame(db, iter4.getSingle().getGraph());
			assertEquals(2, iter4.size());
			tx.success();
		}

		// Retrieve with full value
		try (IGraphTransaction tx = db.beginTransaction()) {
			final IGraphIterable<? extends IGraphNode> iter2 = db.getMetamodelIndex().get("id", mmFileURI);
			assertEquals(1, iter2.size());
			assertEquals(mmFileURI, iter2.getSingle().getProperty(IModelIndexer.IDENTIFIER_PROPERTY));
			tx.success();
		}

		// Use */* (all fields, all values)
		try (IGraphTransaction tx = db.beginTransaction()) {
			assertEquals(new HashSet<>(Arrays.asList(mmBarURI, mmFileURI)), db.getKnownMMUris());
			tx.success();
		}
	}

	@Test
	public void queryWithPipes() throws Exception {
		// Pipe characters should be treated as such, not as "OR" (as in regexps)
		final String sN1 = "/a||/foo/bar", sN2 = "/b||/test";

		try (IGraphTransaction tx = db.beginTransaction()) {
			IGraphNode n1 = db.createNode(null, "metamodel");
			db.getMetamodelIndex().add(n1, "id", sN1);

			IGraphNode n2 = db.createNode(null, "metamodel");
			db.getMetamodelIndex().add(n2, "id", sN2);

			tx.success();
		}

		try (IGraphTransaction tx = db.beginTransaction()) {
			assertEquals(1, db.getMetamodelIndex().query("id", "/*||/foo/*").size());
			tx.success();
		}
	}

	@Test
	public void removeByNode() throws Exception {
 		final String mmBarURI = populateForRemove();

		// NOTE: changes in Lucene indexes are not complete until we commit the
		// transaction
		final IGraphIterable<? extends IGraphNode> iter = checkBeforeRemove(mmBarURI);
		try (IGraphTransaction tx = db.beginTransaction()) {
			db.getMetamodelIndex().remove(iter.getSingle());
			tx.success();
		}
		checkAfterRemove(mmBarURI);
	}

	@Test
	public void removeByNodeMultipleIndicesRemoveOne() throws Exception {
		IGraphNode n;
		try (IGraphTransaction tx = db.beginTransaction()) {
			n = db.createNode(null, "x");
			db.getMetamodelIndex().add(n, "a", 1);
			db.getFileIndex().add(n, "f", "/x/y");
			tx.success();
		}

		try (IGraphTransaction tx = db.beginTransaction()) {
			assertEquals(1, db.getMetamodelIndex().query("*", "*").size());
			assertEquals(1, db.getFileIndex().query("*", "*").size());
			db.getMetamodelIndex().remove(n);
			assertEquals(0, db.getMetamodelIndex().query("*", "*").size());
			assertEquals(1, db.getFileIndex().query("*", "*").size());

			tx.success();
		}
	}

	@Test
	public void removeByNodeMultipleIndicesRemoveAll() throws Exception {
		IGraphNode n;
		try (IGraphTransaction tx = db.beginTransaction()) {
			n = db.createNode(null, "x");
			db.getMetamodelIndex().add(n, "a", 1);
			db.getFileIndex().add(n, "f", "/x/y");
			tx.success();
		}

		try (IGraphTransaction tx = db.beginTransaction()) {
			assertEquals(1, db.getMetamodelIndex().query("*", "*").size());
			assertEquals(1, db.getFileIndex().query("*", "*").size());

			for (String idxName : db.getNodeIndexNames()) {
				db.getOrCreateNodeIndex(idxName).remove(n);
			}
			for (String idxName : db.getNodeIndexNames()) {
				assertEquals(0, db.getOrCreateNodeIndex(idxName).query("*", "*").size());				
			}
			tx.success();
		}

		try (IGraphTransaction tx = db.beginTransaction()) {
			for (String idxName : db.getNodeIndexNames()) {
				assertEquals(0, db.getOrCreateNodeIndex(idxName).query("*", "*").size());				
			}
			tx.success();
		}
	}

	@Test
	public void removeByFullKey() throws Exception {
		final String mmBarURI = populateForRemove();

		final IGraphIterable<? extends IGraphNode> iter = checkBeforeRemove(mmBarURI);
		try (IGraphTransaction tx = db.beginTransaction()) {
			db.getMetamodelIndex().remove(iter.getSingle(), "id", mmBarURI);
			tx.success();
		}
		checkAfterRemove(mmBarURI);
	}

	@Test
	public void removeByFullKeyInt() throws Exception {
		IGraphNode n1, n2;
		final String key = "int";
		try (IGraphTransaction tx = db.beginTransaction()) {
			n1 = db.createNode(null, "v");
			n2 = db.createNode(null, "v");
			db.getMetamodelIndex().add(n1, key, 1);
			db.getMetamodelIndex().add(n2, key, 2);

			tx.success();
		}

		try (IGraphTransaction tx = db.beginTransaction()) {
			final IGraphNodeIndex mmIdx = db.getMetamodelIndex();
			assertEquals(2, mmIdx.query(key, 1, 2, true, true).size());
			mmIdx.remove(n1, key, 1);
			tx.success();
		}

		try (IGraphTransaction tx = db.beginTransaction()) {
			assertEquals(0, db.getMetamodelIndex().get(key, 1).size());
			assertEquals(1, db.getMetamodelIndex().get(key, 2).size());
			assertEquals(1, db.getMetamodelIndex().query(key, 1, 2, true, true).size());
			assertEquals(1, db.getMetamodelIndex().query(key, "*").size());
			tx.success();
		}
	}

	@Test
	public void removeByFullKeyDouble() throws Exception {
		final String key = "double";

		IGraphNode n1, n2;
		try (IGraphTransaction tx = db.beginTransaction()) {
			n1 = db.createNode(null, "v");
			n2 = db.createNode(null, "v");
			db.getMetamodelIndex().add(n1, key, 1.1);
			db.getMetamodelIndex().add(n2, key, 2.3);

			tx.success();
		}

		try (IGraphTransaction tx = db.beginTransaction()) {
			final IGraphNodeIndex mmIdx = db.getMetamodelIndex();
			mmIdx.remove(n1, key, 1.1);
			tx.success();
		}

		try (IGraphTransaction tx = db.beginTransaction()) {
			assertEquals(0, db.getMetamodelIndex().get(key, 1.1).size());
			assertEquals(1, db.getMetamodelIndex().get(key, 2.3).size());
			assertEquals(1, db.getMetamodelIndex().query(key, 1.0, 3.0, false, false).size());
			assertEquals(1, db.getMetamodelIndex().query(key, "*").size());
			tx.success();
		}
	}
	
	@Test
	public void removeByValueNode() throws Exception {
		final String mmBarURI = populateForRemove();
		final IGraphIterable<? extends IGraphNode> iter = checkBeforeRemove(mmBarURI);
		try (IGraphTransaction tx = db.beginTransaction()) {
			db.getMetamodelIndex().remove(iter.getSingle(), null, mmBarURI);
			tx.success();
		} catch (UnsupportedOperationException ex) {
			Assume.assumeTrue("Removing a node by value with no key is not supported on this backend", false);
		}
		checkAfterRemove(mmBarURI);
	}

	@Test
	public void removeByFieldNode() throws Exception {
		final String mmBarURI = populateForRemove();

		final IGraphIterable<? extends IGraphNode> iter = checkBeforeRemove(mmBarURI);
		try (IGraphTransaction tx = db.beginTransaction()) {
			db.getMetamodelIndex().remove(iter.getSingle(), "id", null);
			tx.success();
		}
		checkAfterRemove(mmBarURI);
	}

	@Test
	public void removeByNode3Arg() throws Exception {
		final String mmBarURI = populateForRemove();

		// NOTE: changes in Lucene indexes are not complete until we commit the transaction
		final IGraphIterable<? extends IGraphNode> iter = checkBeforeRemove(mmBarURI);
		try (IGraphTransaction tx = db.beginTransaction()) {
			db.getMetamodelIndex().remove(iter.getSingle(), null, null);
			tx.success();
		}
		checkAfterRemove(mmBarURI);
	}

	@Test
	public void integerExact() throws Exception {
		final String mmBarURI = "http://foo/bar";
		final Map<String, Object> mmBarNodeProps = Collections.singletonMap(IModelIndexer.IDENTIFIER_PROPERTY, mmBarURI);

		final IGraphNodeIndex idx = db.getMetamodelIndex();
		try (IGraphTransaction tx = db.beginTransaction()) {
			IGraphNode mmNode = db.createNode(mmBarNodeProps, "metamodel");
			idx.add(mmNode, Collections.singletonMap("value", 5));

			IGraphNode mmNode2 = db.createNode(mmBarNodeProps, "metamodel");
			idx.add(mmNode2, Collections.singletonMap("value", 8));

			IGraphNode mmNode3 = db.createNode(mmBarNodeProps, "metamodel");
			idx.add(mmNode3, Collections.singletonMap("value", 8));

			tx.success();
		}

		try (IGraphTransaction tx = db.beginTransaction()) {
			assertEquals(0, idx.get("value", 1).size());
			assertEquals(0, idx.query("value", 1).size());

			assertEquals(1, idx.get("value", 5).size());
			assertEquals(1, idx.query("value", 5).size());

			assertEquals(2, idx.get("value", 8).size());
			assertEquals(2, idx.query("value", 8).size());

			assertEquals(3, idx.query("value", "*").size());
			assertEquals(3, idx.query("*", "*").size());

			tx.success();
		}
	}

	@Test
	public void doubleExact() throws Exception {
		final String mmBarURI = "http://foo/bar";
		final Map<String, Object> mmBarNodeProps = Collections.singletonMap(IModelIndexer.IDENTIFIER_PROPERTY, mmBarURI);

		final IGraphNodeIndex idx = db.getMetamodelIndex();
		try (IGraphTransaction tx = db.beginTransaction()) {
			IGraphNode mmNode = db.createNode(mmBarNodeProps, "metamodel");
			idx.add(mmNode, Collections.singletonMap("value", 5.3));

			IGraphNode mmNode2 = db.createNode(mmBarNodeProps, "metamodel");
			idx.add(mmNode2, Collections.singletonMap("value", 8.1));

			IGraphNode mmNode3 = db.createNode(mmBarNodeProps, "metamodel");
			idx.add(mmNode3, Collections.singletonMap("value", 8.1));

			tx.success();
		}

		try (IGraphTransaction tx = db.beginTransaction()) {
			assertEquals(0, idx.get("value", 5.2).size());
			assertEquals(0, idx.query("value", 5.2).size());
			assertEquals(0, idx.get("value", 8.4).size());
			assertEquals(0, idx.query("value", 8.4).size());

			assertEquals(1, idx.get("value", 5.3).size());
			assertEquals(1, idx.query("value", 5.3).size());
			assertEquals(2, idx.get("value", 8.1).size());
			assertEquals(2, idx.query("value", 8.1).size());

			assertEquals(3, idx.query("value", "*").size());
			assertEquals(3, idx.query("*", "*").size());

			tx.success();
		}
	}

	@Test
	public void integerRanges() throws Exception {
		final String mmBarURI = "http://foo/bar";
		final Map<String, Object> mmBarNodeProps = Collections.singletonMap(IModelIndexer.IDENTIFIER_PROPERTY, mmBarURI);

		final IGraphNodeIndex idx = db.getMetamodelIndex();
		try (IGraphTransaction tx = db.beginTransaction()) {
			IGraphNode mmNode = db.createNode(mmBarNodeProps, "metamodel");
			idx.add(mmNode, Collections.singletonMap("value", 5));

			IGraphNode mmNode2 = db.createNode(mmBarNodeProps, "metamodel");
			idx.add(mmNode2, Collections.singletonMap("value", 8));

			IGraphNode mmNode3 = db.createNode(mmBarNodeProps, "metamodel");
			idx.add(mmNode3, Collections.singletonMap("value", 1));
			tx.success();
		}

		try (IGraphTransaction tx = db.beginTransaction()) {
			assertEquals(3, idx.query("value", 0, 10, false, false).size());
			assertEquals(1, idx.query("value", 1, 8, false, false).size());
			assertEquals(2, idx.query("value", 1, 8, true, false).size());
			assertEquals(2, idx.query("value", 1, 8, false, true).size());
			assertEquals(3, idx.query("value", 1, 8, true, true).size());
			assertEquals(2, idx.query("value", 2, 8, true, true).size());
			assertEquals(2, idx.query("value", 1, 7, true, true).size());
			assertEquals(1, idx.query("value", 4, 6, false, false).size());
			assertEquals(1, idx.query("value", 5, 5, true, true).size());
			assertEquals(0, idx.query("value", 5, 5, false, false).size());
			assertEquals(0, idx.query("value", 5, 4, false, false).size());
			tx.success();
		}
	}

	@Test
	public void floatingRanges() throws Exception {
		final String mmBarURI = "http://foo/bar";
		final Map<String, Object> mmBarNodeProps = Collections.singletonMap(IModelIndexer.IDENTIFIER_PROPERTY, mmBarURI);

		final IGraphNodeIndex idx = db.getMetamodelIndex();
		try (IGraphTransaction tx = db.beginTransaction()) {
			IGraphNode mmNode = db.createNode(mmBarNodeProps, "metamodel");
			idx.add(mmNode, Collections.singletonMap("value", 5.3));

			IGraphNode mmNode2 = db.createNode(mmBarNodeProps, "metamodel");
			idx.add(mmNode2, Collections.singletonMap("value", 8.1d));

			IGraphNode mmNode3 = db.createNode(mmBarNodeProps, "metamodel");
			idx.add(mmNode3, Collections.singletonMap("value", 1.34));
			tx.success();
		}

		try (IGraphTransaction tx = db.beginTransaction()) {
			assertEquals(3, idx.query("value", 0.0, 10.0, false, false).size());
			assertEquals(3, idx.query("value", 1.3, 8.2, false, false).size());
			assertEquals(2, idx.query("value", 1.4, 8.2, false, false).size());
			assertEquals(2, idx.query("value", 1.3, 8.0, false, false).size());
			assertEquals(1, idx.query("value", 1.4, 8.0, false, false).size());
			assertEquals(1, idx.query("value", 5.2, 5.4, false, false).size());
			assertEquals(0, idx.query("value", 5.2, 5.2, false, false).size());
			assertEquals(0, idx.query("value", 5.2, 5.1, false, false).size());
			tx.success();
		}
	}

	@Test
	public void invalidIndexNames() throws Exception {
		// OIndexManagerShared#createIndex checks for these
		final char[] invalidChars = ":,; %=/\\".toCharArray();

		IGraphNode n;
		try (IGraphTransaction tx = db.beginTransaction()) {
			n = db.createNode(null, "eobject");
			tx.success();
		}
		for (char invalidChar : invalidChars) {
			final String name = "my" + invalidChar + "index";
			try (IGraphTransaction tx = db.beginTransaction()) {
				assertFalse(db.nodeIndexExists(name));
				IGraphNodeIndex idx = db.getOrCreateNodeIndex(name);
				idx.add(n, "id", 1);
				tx.success();
			}

			try (IGraphTransaction tx = db.beginTransaction()) {
				assertTrue("Index " + name + " should be listed", db.getNodeIndexNames().contains(name));
				assertTrue(db.nodeIndexExists(name));
				IGraphNodeIndex idx = db.getOrCreateNodeIndex(name);

				IGraphIterable<? extends IGraphNode> results = idx.query("id", 1);
				assertEquals("Exact query for index " + name + " should produce one result", 1, results.size());
				results = idx.query("id", 0, 1, true, true);
				assertEquals("Range query for index " + name + " should produce one result", 1, results.size());

				final IGraphIterable<? extends IGraphNode> allID = idx.query("id", "*");
				assertEquals("Should find exactly one match in " + name, 1, allID.size());

				tx.success();
			}
		}
	}

	@Test
	public void getMultipleMatches() throws Exception {
		final String objLabel = "object";
		final String fileIdxKey = "file";
		final String fileNodeLabel = fileIdxKey;
		final String fileEdgeLabel = "inFile";

		IGraphNodeIndex idxRoots;
		try (IGraphTransaction tx = db.beginTransaction()) {
			idxRoots = db.getOrCreateNodeIndex("roots");

			IGraphNode n1 = db.createNode(null, objLabel);
			IGraphNode n2 = db.createNode(null, objLabel);
			IGraphNode n3 = db.createNode(null, objLabel);
			IGraphNode f1 = db.createNode(null, fileNodeLabel);
			IGraphNode f2 = db.createNode(null, fileNodeLabel);

			db.createRelationship(n1, f1, fileEdgeLabel);
			db.createRelationship(n2, f1, fileEdgeLabel);
			db.createRelationship(n3, f2, fileEdgeLabel);
			idxRoots.add(n1, fileIdxKey, "a.xmi");
			idxRoots.add(n2, fileIdxKey, "a.xmi");
			idxRoots.add(n3, fileIdxKey, "b.xmi");

			tx.success();
		}

		try (IGraphTransaction tx = db.beginTransaction()) {
			final IGraphIterable<? extends IGraphNode> aRoots = idxRoots.get(fileIdxKey, "a.xmi");
			assertEquals(2, aRoots.size());
			assertEquals(2, iteratorSize(aRoots.iterator()));
			assertNotNull(aRoots.getSingle());
			tx.success();
		}

		try (IGraphTransaction tx = db.beginTransaction()) {
			final IGraphIterable<? extends IGraphNode> bRoots = idxRoots.get(fileIdxKey, "b.xmi");
			assertEquals(1, bRoots.size());
			assertEquals(1, iteratorSize(bRoots.iterator()));
			assertNotNull(bRoots.getSingle());
			tx.success();
		}

		try (IGraphTransaction tx = db.beginTransaction()) {
			final IGraphIterable<? extends IGraphNode> cRoots = idxRoots.get(fileIdxKey, "c.xmi");
			assertEquals(0, cRoots.size());
			assertEquals(0, iteratorSize(cRoots.iterator()));
			try {
				cRoots.getSingle();
				fail("NoSuchElementException should have been thrown");
			} catch (NoSuchElementException ex) {
				// good!
			}
			tx.success();
		}
	}

	@Test
	public void addNullValue() throws Exception {
		try (IGraphTransaction tx = db.beginTransaction()) {
			IGraphNodeIndex idxRoots = db.getOrCreateNodeIndex("roots");
			IGraphNode n1 = db.createNode(null, "eobject");
			idxRoots.add(n1, "file", null);
			tx.success();
		}
	}

	@Test
	public void addNullMap() throws Exception {
		try (IGraphTransaction tx = db.beginTransaction()) {
			IGraphNodeIndex idxRoots = db.getOrCreateNodeIndex("roots");
			IGraphNode n1 = db.createNode(null, "eobject");
			idxRoots.add(n1, null);
			tx.success();
		}
	}

	@Test
	public void addPreservesPriorValue() throws Exception {
		/*
		 * IMPORTANT - Don't pull an Antonio and think that a single (node, key) pair
		 * should only be associated with one value. Derived attribute acces indices
		 * need to track all the attributes (values) that a (node = source node, key =
		 * accessed node) accessed!
		 */

		IGraphNodeIndex idxRoots;
		IGraphNode x;
		try (IGraphTransaction tx = db.beginTransaction()) {
			idxRoots = db.getOrCreateNodeIndex("roots");
			x = db.createNode(null, "eobject");
			idxRoots.add(x, "a", "1");
			idxRoots.add(x, "a", "2");
			tx.success();
		}

		try (IGraphTransaction tx = db.beginTransaction()) {
			assertEquals(1, idxRoots.query("a", "1").size());
			assertEquals(1, idxRoots.query("a", "2").size());
			tx.success();
		}
	}

	@Test
	public void addThenReuseIterable() throws Exception {
		IGraphNodeIndex idxRoots;
		IGraphNode x;
		
		try (IGraphTransaction tx = db.beginTransaction()) {
			idxRoots = db.getOrCreateNodeIndex("roots");
			x = db.createNode(null, "eobject");

			IGraphIterable<? extends IGraphNode> iterNodes = idxRoots.get("a", "1");
			assertFalse(iterNodes.iterator().hasNext());
			idxRoots.add(x, "a", "1");
			assertTrue(iterNodes.iterator().hasNext());
			tx.success();
		}
	}

	/*
	 * TODO test adding multiple values and removing one value.
	 */

	@Test
	public void deleteRecreate() throws Exception {
		IGraphNodeIndex idxRoots;
		IGraphNode x;
		try (IGraphTransaction tx = db.beginTransaction()) {
			idxRoots = db.getOrCreateNodeIndex("roots");
			x = db.createNode(null, "eobject");
			idxRoots.add(x, "a", "1");
			tx.success();
		}

		try (IGraphTransaction tx = db.beginTransaction()) {
			assertEquals(1, idxRoots.query("a", "1").size());
			idxRoots.delete();
			tx.success();
		}

		try (IGraphTransaction tx = db.beginTransaction()) {
			idxRoots = db.getOrCreateNodeIndex("roots");
			idxRoots.flush();
			assertEquals(0, idxRoots.query("a", "*").size());
			idxRoots.add(x, "a", "2");
			tx.success();
		}
		try (IGraphTransaction tx = db.beginTransaction()) {
			assertEquals(0, idxRoots.query("a", "1").size());
			assertEquals(1, idxRoots.query("a", "2").size());
		}
	}

	@Test
	public void nodeDelete() throws Exception {
		try (IGraphTransaction tx = db.beginTransaction()) {
			IGraphNode n = db.createNode(null, "x");
			db.getMetamodelIndex().add(n, "a", 1);
			tx.success();
		}

		try (IGraphTransaction tx = db.beginTransaction()) {
			final IGraphIterable<? extends IGraphNode> results = db.getMetamodelIndex().get("a", 1);
			assertEquals(1, results.size());
			results.getSingle().delete();
			tx.success();
		}

		try (IGraphTransaction tx = db.beginTransaction()) {
			final IGraphIterable<? extends IGraphNode> results = db.getMetamodelIndex().get("a", 1);
			assertEquals(0, results.size());
			tx.success();
		}
	}

	@Test
	public void indexKeyAdditionRollback() throws Exception {
		IGraphNode n;
		try (IGraphTransaction tx = db.beginTransaction()) {
			n = db.createNode(null, "x");
			db.getMetamodelIndex().add(n, "b", 1);
			tx.success();
		}

		try (IGraphTransaction tx = db.beginTransaction()) {
			db.getMetamodelIndex().add(n, "a", 1);
			tx.failure();
		}

		try (IGraphTransaction tx = db.beginTransaction()) {
			assertEquals(0, db.getMetamodelIndex().query("a", "*").size());
			assertEquals(1, db.getMetamodelIndex().query("b", "*").size());
			tx.success();
		}
	}

	@Test
	public void indexKeyRemovalRollback() throws Exception {
		IGraphNode n;
		try (IGraphTransaction tx = db.beginTransaction()) {
			n = db.createNode(null, "x");
			db.getMetamodelIndex().add(n, "b", 1);
			db.getMetamodelIndex().add(n, "c", "x");
			tx.success();
		}

		try (IGraphTransaction tx = db.beginTransaction()) {
			assertEquals(1, db.getMetamodelIndex().query("b", 1).size());
			assertEquals(1, db.getMetamodelIndex().query("c", "x").size());
			tx.success();
		}

		try (IGraphTransaction tx = db.beginTransaction()) {
			db.getMetamodelIndex().remove(n, "b", 1);
			db.getMetamodelIndex().remove(n, "c", "x");
			// NOTE: GreyCat and Orient expose changes immediately, but Neo4j 2.0.5 does not
			//	assertEquals(0, db.getMetamodelIndex().query("b", 1).size());
			//	assertEquals(0, db.getMetamodelIndex().query("c", "x").size());
			tx.failure();
		}

		try (IGraphTransaction tx = db.beginTransaction()) {
			assertEquals(1, db.getMetamodelIndex().query("b", 1).size());
			assertEquals(1, db.getMetamodelIndex().query("c", "x").size());
			tx.success();
		}
	}

	@Test
	public void indexDeletionRollback() throws Exception {
		try (IGraphTransaction tx = db.beginTransaction()) {
			IGraphNode n1 = db.createNode(null, "x"), n2 = db.createNode(null, "y");
			db.getMetamodelIndex().add(n1, "b", 1);
			db.getMetamodelIndex().add(n2, "b", 3);
			tx.success();
		}

		try (IGraphTransaction tx = db.beginTransaction()) {
			assertEquals(2, db.getMetamodelIndex().query("b", "*").size());
			db.getMetamodelIndex().delete();
			db.getMetamodelIndex().flush();			
			assertEquals(0, db.getMetamodelIndex().query("b", "*").size());
			tx.failure();
		}

		try (IGraphTransaction tx = db.beginTransaction()) {
			assertEquals(2, db.getMetamodelIndex().query("b", "*").size());
			tx.success();
		}
	}

	@Test
	public void indexNodeDeletionRollback() throws Exception {
		IGraphNode n1, n2;
		try (IGraphTransaction tx = db.beginTransaction()) {
			n1 = db.createNode(null, "x");
			n2 = db.createNode(null, "y");
			db.getMetamodelIndex().add(n1, "b", 1);
			db.getMetamodelIndex().add(n2, "b", 3);
			tx.success();
		}

		try (IGraphTransaction tx = db.beginTransaction()) {
			assertEquals(2, db.getMetamodelIndex().query("b", "*").size());
			db.getMetamodelIndex().remove(n2);
			assertEquals(1, db.getMetamodelIndex().query("b", "*").size());
			tx.failure();
		}

		try (IGraphTransaction tx = db.beginTransaction()) {
			assertEquals(2, db.getMetamodelIndex().query("b", "*").size());
			tx.success();
		}
	}

	private String populateForRemove() throws Exception {
		final String mmBarURI = "http://foo/bar";
		final Map<String, Object> mmBarNodeProps = Collections.singletonMap(IModelIndexer.IDENTIFIER_PROPERTY, mmBarURI);

		try (IGraphTransaction tx = db.beginTransaction()) {
			IGraphNode mmNode = db.createNode(mmBarNodeProps, "metamodel");
			final IGraphNodeIndex mmIdx = db.getMetamodelIndex();
			mmIdx.add(mmNode, Collections.singletonMap("id", mmBarURI));

			IGraphNode mmNode2 = db.createNode(mmBarNodeProps, "metamodel");
			mmIdx.add(mmNode2, Collections.singletonMap("id", "file://foo"));

			tx.success();
		}
		return mmBarURI;
	}

	private void checkAfterRemove(final String mmBarURI) throws Exception {
		try (IGraphTransaction tx = db.beginTransaction()) {
			final IGraphNodeIndex mmIdx = db.getMetamodelIndex();

			assertEquals(0, mmIdx.query("id", mmBarURI).size());
			assertEquals(0, mmIdx.query("id", "http://*").size());
			assertEquals(1, mmIdx.query("id", "file://*").size());
			assertEquals(1, mmIdx.query("id", "fil*://*").size());
			assertEquals(1, mmIdx.query("id", "*file://*").size());
		}
	}

	private IGraphIterable<? extends IGraphNode> checkBeforeRemove(final String mmBarURI) throws Exception {
		try (IGraphTransaction tx = db.beginTransaction()) {
			final IGraphIterable<? extends IGraphNode> iter = db.getMetamodelIndex().query("id", mmBarURI);
			assertEquals(1, iter.size());
			tx.success();
			return iter;
		}
	}

	private int iteratorSize(Iterator<?> it) {
		int count = 0;
		while (it.hasNext()) {
			it.next();
			count++;
		}
		return count;
	}
}
