package com.lambdazen.bitsy;

import com.lambdazen.bitsy.store.FileBackedMemoryGraphStore;
import com.lambdazen.bitsy.util.CommittableFileLog;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.apache.tinkerpop.gremlin.structure.Direction;
import org.apache.tinkerpop.gremlin.structure.Edge;
import org.apache.tinkerpop.gremlin.structure.Graph;
import org.apache.tinkerpop.gremlin.structure.Property;
import org.apache.tinkerpop.gremlin.structure.Transaction.READ_WRITE_BEHAVIOR;
import org.apache.tinkerpop.gremlin.structure.Vertex;

public class BitsyGraphIT extends FileBasedTestCase {
    Graph graph;
    Path dbPath;
    Random rand = new Random();
    private Throwable toThrow;

    public boolean isPersistent() {
        return true;
    }

    public void setUp() throws IOException {
        CommittableFileLog.setLockMode(false);

        doSetUp(true);
    }

    public void doSetUp(boolean delete) throws IOException {
        System.out.println("Setting up graph");

        this.dbPath = tempDir("temp-bitsy-graph", delete);
        graph = new BitsyGraph(dbPath);

        //        this.dbPath = tempDir("temp-neo4j-graph", delete);
        //        graph = new Neo4jGraph(dbPath.toString());
    }

    protected static void deleteDirectory(final File directory) {
        deleteDirectory(directory, true);
    }

    protected static void deleteDirectory(final File directory, boolean deleteDir) {
        if (directory.exists()) {
            for (File file : directory.listFiles()) {
                if (file.isDirectory()) {
                    deleteDirectory(file);
                } else {
                    file.delete();
                }
            }

            if (deleteDir) {
                directory.delete();
            }
        }
    }

    public void tearDown() {
        System.out.println("Tearing down graph");
        try {
            graph.close();
        } catch (Exception e) {
            throw new RuntimeException("Got ex", e);
        }
    }

    private Vertex getVertex(Graph graph, Object id) {
        Iterator<Vertex> iter = graph.vertices(id);
        if (iter.hasNext()) {
            return iter.next();
        } else {
            return null;
        }
    }

    private Edge getEdge(Graph graph, Object id) {
        Iterator<Edge> iter = graph.edges(id);
        if (iter.hasNext()) {
            return iter.next();
        } else {
            return null;
        }
    }

    private Edge addEdge(Graph graph, Vertex vOut, Vertex vIn, String label) {
        return vOut.addEdge(label, vIn);
    }

    private void removeEdge(Graph graph, Edge edge) {
        edge.remove();
    }

    private void removeVertex(Graph graph, Vertex vertex) {
        vertex.remove();
    }

    public void testLoop() throws IOException {
        int numRuns = 3;
        for (int run = 0; run < numRuns; run++) {
            // System.out.println("Run #" + run);

            int numPerCommit = 10;
            int numCommit = 10;
            int numVertices = numPerCommit * numCommit;
            Object[] vids = new Object[numVertices];

            long ts = System.currentTimeMillis();
            for (int i = 0; i < numVertices; i++) {
                Vertex v = graph.addVertex();
                v.property("rand", rand.nextInt());
                v.property("count", i);
                vids[i] = v.id();

                if (i % numPerCommit == 0) {
                    graph.tx().commit();
                }

                // Make sure the vertex is there in the middle of the Tx
                assertEquals(Integer.valueOf(i), getVertex(graph, vids[i]).value("count"));
            }
            graph.tx().commit();

            double duration = System.currentTimeMillis() - ts;
            System.out.println("Took " + duration + "ms to insert " + numVertices + " vertices. Rate = "
                    + (duration / numVertices) + "ms per vertex");

            // Make sure vertices are there
            for (int i = 0; i < numVertices; i++) {
                Vertex v = getVertex(graph, vids[i]);
                assertEquals(i, (int) v.value("count"));
            }

            // if (rand.nextDouble() < 2) {
            // return;
            // }

            // Stop and start
            if (isPersistent()) {
                tearDown();
                doSetUp(false);
            }

            // Make sure vertices are still there
            for (int i = 0; i < numVertices; i++) {
                Vertex v = getVertex(graph, vids[i]);
                assertNotNull(v);
                assertEquals(i, (int) v.value("count"));
            }

            // Now add edges
            Object[] eids = new Object[numVertices];
            for (int i = 0; i < numVertices; i++) {
                Vertex vOut = getVertex(graph, vids[i]);
                Vertex vIn = getVertex(graph, vids[(i + 1) % numVertices]);
                Edge e = addEdge(graph, vOut, vIn, "foo");
                e.property("rand", rand.nextInt());
                e.property("count", i);

                eids[i] = e.id();

                if (i % numPerCommit == 0) {
                    graph.tx().commit();
                }

                // Make sure the edge is there in the middle of the Tx
                // Using toString() to get the edge
                Edge qEdge = getEdge(graph, eids[i].toString());
                assertEquals(Integer.valueOf(i), (Integer) qEdge.value("count"));
                assertEquals(vOut, qEdge.outVertex());
                assertEquals(vIn, qEdge.inVertex());
            }
            graph.tx().commit();

            // Make sure that the edges are there
            for (int i = 0; i < numVertices; i++) {
                Edge e = getEdge(graph, eids[i]);
                assertEquals(i, (int) e.value("count"));
            }

            // Now modify the edges. Delete even ones
            for (int i = 0; i < numVertices; i++) {
                Edge e = getEdge(graph, eids[i]);
                e.property("count", i + 1);

                eids[i] = e.id();

                if (i % numPerCommit == 0) {
                    graph.tx().commit();

                    try {
                        e.value("foo");
                    } catch (BitsyException ex) {
                        assertEquals(BitsyErrorCodes.ACCESS_OUTSIDE_TX_SCOPE, ex.getErrorCode());
                    }
                }

                // Make sure the edge is there in the middle of the Tx
                Edge qEdge = getEdge(graph, eids[i]);
                assertEquals(Integer.valueOf(i + 1), qEdge.value("count"));

                // Traverse to the vertices
                Vertex vOut = qEdge.outVertex();
                assertEquals(Integer.valueOf(i), vOut.value("count"));

                Vertex vIn = qEdge.inVertex();
                assertEquals(Integer.valueOf((i + 1) % numVertices), vIn.value("count"));

                // Run queries in the middle of the Tx
                assertEquals(qEdge, vIn.edges(Direction.IN).next());
                assertEquals(qEdge, vOut.edges(Direction.OUT).next());

                if (i % 2 == 0) {
                    removeEdge(graph, qEdge);

                    // Run queries in the middle of the Tx
                    if (rand.nextBoolean()) {
                        assertFalse(vIn.edges(Direction.IN).hasNext());
                        assertFalse(vOut.edges(Direction.OUT).hasNext());
                    } else {
                        try {
                            vIn.edges(Direction.IN).next();
                            fail("Expecting no element");
                        } catch (NoSuchElementException e1) {
                        }

                        try {
                            vOut.edges(Direction.OUT).next();
                            fail("Expecting no element");
                        } catch (NoSuchElementException e2) {
                        }
                    }

                    try {
                        vIn.edges(Direction.IN).next();
                        fail("Can't access a missing edge");
                    } catch (NoSuchElementException ex) {
                        // Good
                    }

                    try {
                        qEdge.value("count");
                        fail("Can not access deleted edges");
                    } catch (BitsyException ex) {
                        assertEquals(BitsyErrorCodes.ELEMENT_ALREADY_DELETED, ex.getErrorCode());
                    }
                }
            }
            graph.tx().commit();

            // Stop and start
            if (isPersistent()) {
                tearDown();
                // assertTrue(rand.nextDouble() > 5);
                doSetUp(false);
            }

            // Make sure vertices are still there
            for (int i = 0; i < numVertices; i++) {
                Vertex v = getVertex(graph, vids[i]);
                // System.out.println("Looking for " + vids[i]);

                assertEquals(i, (int) v.value("count"));
            }

            // Make sure that the edges are there
            for (int i = 0; i < numVertices; i++) {
                Edge e = getEdge(graph, eids[i]);

                if (i % 2 == 0) {
                    assertNull(e);
                } else {
                    // i was changed to i+1
                    assertNotNull("Could not find edge for UUID " + eids[i], e);
                    assertEquals(i + 1, (int) e.value("count"));
                }
            }

            // Stop and start
            if (isPersistent()) {
                tearDown();
                doSetUp(false);
            }

            // Make sure vertices are still there. Modify odd ones and delete even ones
            graph.tx().onReadWrite(READ_WRITE_BEHAVIOR.MANUAL);
            if (!graph.tx().isOpen()) graph.tx().open();

            for (int i = 0; i < numVertices; i++) {
                Vertex v = getVertex(graph, vids[i]);
                assertEquals(i, (int) v.value("count"));

                if (i % 2 == 0) {
                    removeVertex(graph, v);
                } else {
                    v.property("count", i + 1);
                }

                if (i % numPerCommit == 0) {
                    graph.tx().commit();

                    try {
                        v.value("count");
                    } catch (IllegalStateException ex) {
                        // That's expected
                        // TP2 behavior: assertEquals(BitsyErrorCodes.ACCESS_OUTSIDE_TX_SCOPE, ex.getErrorCode());
                    }

                    graph.tx().open();
                }

                // Check the vertex in the middle of the Tx
                Vertex qv = getVertex(graph, vids[i]);
                if (i % 2 == 0) {
                    assertNull(qv);
                } else {
                    assertEquals(i + 1, (int) v.value("count"));
                }
            }
            graph.tx().commit();
            graph.tx().onReadWrite(READ_WRITE_BEHAVIOR.AUTO);

            // Make sure that only odd vertices are still there
            for (int i = 0; i < numVertices; i++) {
                Vertex v = getVertex(graph, vids[i]);

                if (i % 2 == 0) {
                    assertNull(v);
                } else {
                    assertEquals(i + 1, (int) v.value("count"));
                }
            }

            // Make sure that the edges are gone (even vertices are deleted)
            for (int i = 0; i < numVertices; i++) {
                Edge e = getEdge(graph, eids[i]);
                assertNull(e);
            }

            // Stop and start
            if (isPersistent()) {
                tearDown();
                doSetUp(false);
            }

            // Make sure that the edges are gone (even vertices are deleted)
            for (int i = 0; i < numVertices; i++) {
                Edge e = getEdge(graph, eids[i]);
                assertNull(e);
            }

            // Make sure that only odd vertices are still there
            for (int i = 0; i < numVertices; i++) {
                Vertex v = getVertex(graph, vids[i]);

                if (i % 2 == 0) {
                    assertNull(v);
                } else {
                    assertEquals(i + 1, (int) v.value("count"));
                }
            }
        }
    }

    public void testClique() throws IOException {
        // Tests indexes in a clique graph
        int numPerCommit = 10;
        int numVertices = 29;
        int numRuns = 3;

        BitsyGraph kig = (BitsyGraph) graph;
        if (!kig.getIndexedKeys(Vertex.class).contains("vmod3")) {
            kig.createKeyIndex("vmod3", Vertex.class);
        } else {
            kig.dropKeyIndex("vmod3", Vertex.class);
            kig.createKeyIndex("vmod3", Vertex.class);
        }

        if (!kig.getIndexedKeys(Edge.class).contains("emod3")) {
            kig.createKeyIndex("emod3", Edge.class);
        } else {
            kig.dropKeyIndex("emod3", Edge.class);
            kig.createKeyIndex("emod3", Edge.class);
        }

        int txc = 0;
        Object[] vids = new Object[numVertices];
        Object[][] eids = new Object[numVertices][numVertices];
        long startLong = rand.nextLong();
        for (int run = 0; run < numRuns; run++) {
            if ((run == numRuns - 1) && ((BitsyGraph) graph).getStore().allowFullGraphScans()) {
                // Last run without index
                kig.dropKeyIndex("vmod3", Vertex.class);
                kig.dropKeyIndex("emod3", Edge.class);
            }

            // Change the starting long to look for index replacement logic
            long oldStartLong = startLong;
            startLong = rand.nextLong();
            int[] edgeCount = new int[] {0, 0, 0};

            for (int i = 0; i < numVertices; i++) {
                if (i == numVertices / 2) {
                    if (isPersistent()) {
                        tearDown();

                        doSetUp(false);
                    }
                }

                Vertex v;
                if (run == 0) {
                    v = graph.addVertex();
                    vids[i] = v.id();
                } else {
                    v = getVertex(graph, vids[i]);
                    assertEquals(vids[i], v.id());
                }

                v.property("vmod3", startLong + i % 3);

                // Make sure that the index works on committed and uncommitted data
                if ((txc++ % numPerCommit == 0) && rand.nextDouble() < 2) {
                    graph.tx().commit();

                    // Add a random vertex without this property
                    Vertex dummyV = graph.addVertex();
                    if (i % 2 == 0) dummyV.property("notvmod3", 1);
                }

                //                System.out.println("Testing vertex " + i + ". Run #" + run);

                // Negative test on index value in previous iter
                if (i > numVertices - 3) {
                    if (rand.nextBoolean()) {
                        assertFalse(((BitsyGraph) graph)
                                .verticesByIndex("vmod3", oldStartLong + i % 3)
                                .hasNext());
                    } else {
                        try {
                            ((BitsyGraph) graph)
                                    .verticesByIndex("vmod3", oldStartLong + i % 3)
                                    .next();
                            fail("Expecting no element");
                        } catch (NoSuchElementException e1) {
                        }
                    }
                }

                Iterator<BitsyVertex> viter = ((BitsyGraph) graph).verticesByIndex("vmod3", startLong + i % 3);
                int count = 0;
                while (viter.hasNext()) {
                    Vertex qv = viter.next();

                    boolean matchesSomething = false;
                    for (int div3 = i % 3; div3 <= i; div3 += 3) {
                        if (vids[div3].equals(qv.id())) {
                            matchesSomething = true;
                            // System.out.println("Found match for " + div3);
                        }
                        matchesSomething = true;
                    }
                    assertTrue(matchesSomething);

                    count++;
                    // System.out.println("Count = " + count);
                }

                assertEquals(1 + i / 3, count);

                // Now add edges
                for (int j = 0; j < i; j++) {
                    Edge e;
                    if (run == 0) {
                        Vertex outV = getVertex(graph, vids[j]);
                        Vertex inV = getVertex(graph, vids[i]);

                        e = addEdge(graph, outV, inV, "test");
                        eids[i][j] = e.id();
                    } else {
                        e = getEdge(graph, eids[i][j]);
                    }

                    e.property("emod3", "E" + (startLong + ((i - j) % 3)));
                    Object eid = e.id();

                    // Make sure that the index works on committed and uncommitted data
                    if (txc++ % numPerCommit == 0) {
                        graph.tx().commit();
                    }

                    // Negative test on index value in previous iter
                    if ((i == numVertices - 1) && (j >= numVertices - 3)) {
                        assertFalse(((BitsyGraph) graph)
                                .edgesByIndex("emod3", "E" + (oldStartLong + ((i - j) % 3)))
                                .hasNext());
                    }

                    // System.out.println("Testing edge from " + j + " to " + i);
                    Iterator<BitsyEdge> eiter =
                            ((BitsyGraph) graph).edgesByIndex("emod3", "E" + (startLong + ((i - j) % 3)));
                    edgeCount[(i - j) % 3]++;

                    int ecount = 0;
                    boolean matchesThis = false;
                    while (eiter.hasNext()) {
                        Edge qe = eiter.next();

                        if (eid.equals(qe.id())) {
                            matchesThis = true;
                            // System.out.println("Found edge match");
                        }

                        ecount++;
                    }

                    assertTrue(matchesThis);
                    assertEquals(edgeCount[(i - j) % 3], ecount);
                }

                graph.tx().commit();
            }
        }

        // Remove vertex props
        for (int i = 0; i < numVertices; i++) {
            Vertex v = getVertex(graph, vids[i]);
            v.property("vmod3").remove();

            Iterator<Edge> vOut = v.edges(Direction.OUT);
            while (vOut.hasNext()) {
                vOut.next().property("emod3").remove();
            }

            // Add a random vertex without this property
            Vertex dummyV = graph.addVertex();
            dummyV.property("notvmod3", 1);
            Object dummyVId = dummyV.id();

            // Commit & remove
            graph.tx().commit();
            dummyV = getVertex(graph, dummyVId);
            removeVertex(graph, dummyV);
        }

        // Make sure the index is empty
        for (int i = 0; i < 3; i++) {
            assertFalse(((BitsyGraph) graph)
                    .verticesByIndex("vmod3", startLong + i % 3)
                    .hasNext());
        }

        // Add it back
        for (int i = 0; i < numVertices; i++) {
            Vertex v = getVertex(graph, vids[i]);
            assertNull(v.value("vmod3"));
            v.property("vmod3", startLong);
        }

        // Make sure the index is non-empty before commit
        assertTrue(((BitsyGraph) graph).verticesByIndex("vmod3", startLong).hasNext());

        graph.tx().commit();

        // ... and after
        for (int i = 0; i < 3; i++) {
            assertTrue(((BitsyGraph) graph).verticesByIndex("vmod3", startLong).hasNext());
        }

        // Remove it again
        for (int i = 0; i < numVertices; i++) {
            Vertex v = getVertex(graph, vids[i]);
            v.property("vmod3").remove();
        }
        graph.tx().commit();

        for (int i = 0; i < 3; i++) {
            assertFalse(((BitsyGraph) graph)
                    .verticesByIndex("vmod3", startLong + i % 3)
                    .hasNext());
        }
        graph.tx().commit();

        // Remove edge props
        for (int i = 0; i < numVertices; i++) {
            for (int j = 0; j < i; j++) {
                if (eids[i][j] == null) {
                    continue;
                }

                Edge e = getEdge(graph, eids[i][j]);
                Property prop = e.property("emod3");
                if (prop.isPresent()) prop.remove();
            }

            // Add a random edge without this property
            Vertex dummyV1 = graph.addVertex();
            dummyV1.property("notvmod3", 1);

            Vertex dummyV2 = graph.addVertex();
            dummyV2.property("notvmod3", 1);

            Edge e = addEdge(graph, dummyV1, dummyV2, "test");
            e.property("notemod3", "E123");
        }

        for (int i = 0; i < numVertices; i++) {
            for (int j = 0; j < i; j++) {
                if (eids[i][j] == null) {
                    continue;
                }
                assertFalse(((BitsyGraph) graph)
                        .edgesByIndex("emod3", "E" + (startLong + ((i - j) % 3)))
                        .hasNext());
            }
        }

        graph.tx().commit();

        for (int i = 0; i < numVertices; i++) {
            for (int j = 0; j < i; j++) {
                if (eids[i][j] == null) {
                    continue;
                }
                assertFalse(((BitsyGraph) graph)
                        .edgesByIndex("emod3", "E" + (startLong + ((i - j) % 3)))
                        .hasNext());
            }
        }

        // Remove edges
        for (int i = 0; i < numVertices; i++) {
            for (int j = 0; j < i; j++) {
                if ((i + j) % 10 == 0) {
                    getEdge(graph, eids[i][j]).remove();
                }
            }
        }
        graph.tx().commit();

        // Remove vertices
        for (int i = 0; i < numVertices; i++) {
            getVertex(graph, vids[i]).remove();
        }

        graph.tx().commit();
    }

    public void testTypes() throws IOException {
        int numPerCommit = 10;
        int numCommit = 10;
        int numVertices = numPerCommit * numCommit;
        Object[] vids = new Object[numVertices];

        long ts = System.currentTimeMillis();
        for (int i = 0; i < numVertices; i++) {
            Vertex v = graph.addVertex();
            v.property("int", i);
            v.property("long", (long) i);
            v.property("Integer", i);
            v.property("Long", (long) i);
            v.property("double", (double) i + 0.1);
            v.property("Double", i + 0.1);
            v.property("float", (float) i + 0.1);
            v.property("Float", (float) (i + 0.1));
            v.property("string", "String");
            v.property("date", new Date(ts));

            v.property("stringArr", new String[] {"foo", "bar"});
            v.property("intArr", new int[] {1, 2});
            v.property("longArr", new long[] {1, 2});
            v.property("doubleArr", new double[] {1.1, 2.1});
            v.property("floatArr", new float[] {1.1f, 2.1f});
            v.property("booleanArr", new boolean[] {false, true});

            vids[i] = v.id();

            if (i % numPerCommit == 0) {
                graph.tx().commit();
            }
        }
        graph.tx().commit();

        double duration = System.currentTimeMillis() - ts;
        System.out.println("Took " + duration + "ms to insert " + numVertices + " vertices. Rate = "
                + (duration / numVertices) + "ms per vertex");

        // Stop and start
        if (isPersistent()) {
            tearDown();
            doSetUp(false);
        }

        // Make sure vertices are still there
        for (int i = 0; i < numVertices; i++) {
            Vertex v = getVertex(graph, vids[i]);

            assertNotNull("Could not find " + vids[i], v);
            // System.out.println("Index " + v.getId() + ". All props: " + v.getPropertyKeys());
            assertEquals(i, (int) v.value("int"));
            assertEquals((long) i, (long) v.value("long"));
            assertEquals(Integer.valueOf(i), (Integer) v.value("Integer"));
            assertEquals(Long.valueOf(i), (Long) v.value("Long"));
            assertEquals((double) i + 0.1, (double) v.value("double"));
            assertEquals((double) i + 0.1, (Double) v.value("Double"));
            //            assertEquals((float) i + 0.1, (float)v.value("float"));  TP3 doesn't support raw types
            // property
            assertEquals((float) (i + 0.1), (Float) v.value("Float"));
            assertEquals("String", v.value("string"));

            assertEquals("foo", ((String[]) v.value("stringArr"))[0]);
            assertEquals("bar", ((String[]) v.value("stringArr"))[1]);

            assertEquals(1, ((int[]) v.value("intArr"))[0]);
            assertEquals(2, ((int[]) v.value("intArr"))[1]);

            assertEquals(1, ((long[]) v.value("longArr"))[0]);
            assertEquals(2, ((long[]) v.value("longArr"))[1]);

            assertEquals(1.1d, ((double[]) v.value("doubleArr"))[0]);
            assertEquals(2.1d, ((double[]) v.value("doubleArr"))[1]);

            assertEquals(1.1f, ((float[]) v.value("floatArr"))[0]);
            assertEquals(2.1f, ((float[]) v.value("floatArr"))[1]);

            assertEquals(false, ((boolean[]) v.value("booleanArr"))[0]);
            assertEquals(true, ((boolean[]) v.value("booleanArr"))[1]);

            assertEquals(new Date(ts), v.value("date"));
        }
    }

    // TODO: Uncomment after supporting threaded transaction
    /*
        public void testConcurrency() {
            // Create a vertex
            Vertex v = graph.addVertex();
            v.property("foo", "bar");

            Edge e = addEdge(graph, v, v, "self");
            e.property("foo", "bar");
            Object eid = e.id();

            Object vid = v.id();
            graph.tx().commit();

            // Create a threaded transaction
            //TransactionalGraph graph2 = ((ThreadedTransactionalGraph)graph).newTransaction(); // was startTransaction prior to 2.3.0 port
            Graph graph2 = graph.tx().createThreadedTx(); // TP 3.0 version

            Vertex v1 = getVertex(graph, vid);

            Edge e2 = getEdge(graph2, eid);
            Vertex v2 = e2.outVertex();

            assertEquals("bar", v1.value("foo"));
            v1.property("foo", "baz");

            assertEquals("bar", v2.value("foo"));
            v2.property("foo", "bart");

            // Should succeed
            graph.tx().commit();

            try {
                // Should fail
                graph2.tx().commit();

                fail("Failed optimistic concurrency test");
            } catch (BitsyRetryException ex) {
                assertEquals(BitsyErrorCodes.CONCURRENT_MODIFICATION, ex.getErrorCode());
            }

            Vertex v2retry = graph2.vertices(vid).next();
            assertEquals("baz", v2retry.value("foo"));

            v2retry.property("foo", "bart");
            graph2.tx().commit();

            try {
            	graph2.close();
            } catch (Exception ex) {
            	ex.printStackTrace();
            	fail("Couldn't close graph2" + ex);
            }

            // Ensure that the retried transaction came through
            Vertex v3 = getVertex(graph, vid);
            assertEquals("bart", v3.value("foo"));

            Edge e3 = v3.edges(Direction.OUT).next();
            assertEquals("self", e3.label());
            assertEquals("bar", e3.value("foo"));

            e3.property("foo", "baz");
            assertEquals("self", e3.label());
            assertEquals("baz", e3.value("foo"));

            //TransactionalGraph graph3 = ((ThreadedTransactionalGraph)graph).newTransaction(); // WAS startTransaction prior to 2.3.0 port
            Graph graph3 = graph.tx().createThreadedTx(); // TP3 version
            Edge e4 = getEdge(graph3, eid);

            assertEquals("self", e4.label());
            assertEquals("bar", e4.value("foo"));

            e4.property("foo", "bart");
            assertEquals("self", e4.label());
            assertEquals("bart", e4.value("foo"));

            assertEquals(e4 + " doesnt' have foo: bart", "bart", e4.value("foo"));

            // Should pass
            graph3.tx().commit();

            // See if the changes from graph3 are seen here
            Vertex v51 = graph3.vertices(vid).next();
            Edge e51 = v51.edges(Direction.OUT).next();

            assertEquals("self", e51.label());
            assertEquals(e51 + " doesnt' have foo: bart", "bart", e51.value("foo"));

            try {
                graph.tx().commit();
                fail("Failed optimistic concurrency test");
            } catch (BitsyRetryException ex) {
                assertEquals(BitsyErrorCodes.CONCURRENT_MODIFICATION, ex.getErrorCode());
            }

            // See if the changes from graph3 are seen here
            Vertex v5 = getVertex(graph, vid);
            Edge e5 = v5.edges(Direction.OUT).next();

            assertEquals("self", e5.label());
            assertEquals(e5 + " doesnt' have foo: bart", "bart", e5.value("foo"));

            // All old vertices should be dead
            for (Element eN : new Element[] {v1, v2, v2retry, v3, e, e2, e3, e4}) {
                try {
                    eN.value("foo");
                    fail("dead Tx");
                } catch (BitsyException ex) {
                    assertEquals(BitsyErrorCodes.ACCESS_OUTSIDE_TX_SCOPE, ex.getErrorCode());
                }
            }

            try {
            	graph.close();
            } catch (Exception ex) {
            	fail("Couldn't close graph " + ex);
            }
        }
    */
    // Pun intended
    public void testEdgeCases() {
        assertEquals(BitsyIsolationLevel.READ_COMMITTED, ((BitsyGraph) graph).getDefaultIsolationLevel());
        for (BitsyIsolationLevel level :
                new BitsyIsolationLevel[] {BitsyIsolationLevel.READ_COMMITTED, BitsyIsolationLevel.REPEATABLE_READ}) {
            ((BitsyGraph) graph).setDefaultIsolationLevel(level);
            graph.tx().commit();

            int numVertices = 4;

            Object[] vids = new Object[numVertices];
            Vertex[] verts = new Vertex[numVertices];

            for (int i = 0; i < numVertices; i++) {
                Vertex v = graph.addVertex();
                vids[i] = v.id();
                verts[i] = v;
            }

            // Add an edge from 0 to 1
            Edge e01 = addEdge(graph, verts[0], verts[1], "one");
            Object e01Id = e01.id();

            Edge e01Alt = addEdge(graph, verts[0], verts[1], "two");
            Object e01AltId = e01Alt.id();

            Edge e12 = addEdge(graph, verts[1], verts[2], "empty"); // Empty string no longer allowed in TP3
            Object e12Id = e12.id();

            Edge e23 = addEdge(graph, verts[2], verts[3], "three");
            Object e23Id = e23.id();

            // Check if the edges are in
            checkIterCount(verts[0].edges(Direction.OUT), 2);
            checkIterCount(verts[0].edges(Direction.OUT, "one"), 1);
            checkIterCount(verts[0].edges(Direction.OUT, "two"), 1);

            checkIterCount(verts[1].edges(Direction.OUT), 1);
            checkIterCount(verts[1].edges(Direction.IN), 2);

            checkIterCount(verts[2].edges(Direction.OUT), 1);
            checkIterCount(verts[2].edges(Direction.IN), 1);

            checkIterCount(verts[3].edges(Direction.IN, "three"), 1);
            checkIterCount(verts[3].edges(Direction.IN, "threeX"), 0);

