/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.tinkerpop.gremlin.structure;

import org.apache.tinkerpop.gremlin.AbstractGremlinTest;
import org.apache.tinkerpop.gremlin.ExceptionCoverage;
import org.apache.tinkerpop.gremlin.FeatureRequirement;
import org.apache.tinkerpop.gremlin.FeatureRequirementSet;
import org.apache.tinkerpop.gremlin.structure.Graph.Features.EdgeFeatures;
import org.apache.tinkerpop.gremlin.structure.Graph.Features.EdgePropertyFeatures;
import org.apache.tinkerpop.gremlin.structure.Graph.Features.GraphFeatures;
import org.apache.tinkerpop.gremlin.structure.Graph.Features.VertexFeatures;
import org.apache.tinkerpop.gremlin.structure.Graph.Features.VertexPropertyFeatures;
import org.apache.tinkerpop.gremlin.util.iterator.IteratorUtils;
import org.junit.Before;
import org.junit.Test;
import org.junit.experimental.runners.Enclosed;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Date;
import java.util.UUID;

import static org.apache.tinkerpop.gremlin.structure.Graph.Features.ElementFeatures.FEATURE_ANY_IDS;
import static org.apache.tinkerpop.gremlin.structure.Graph.Features.ElementFeatures.FEATURE_NUMERIC_IDS;
import static org.apache.tinkerpop.gremlin.structure.Graph.Features.ElementFeatures.FEATURE_STRING_IDS;
import static org.apache.tinkerpop.gremlin.structure.Graph.Features.ElementFeatures.FEATURE_UUID_IDS;
import static org.apache.tinkerpop.gremlin.structure.Graph.Features.GraphFeatures.FEATURE_COMPUTER;
import static org.apache.tinkerpop.gremlin.structure.Graph.Features.GraphFeatures.FEATURE_THREADED_TRANSACTIONS;
import static org.apache.tinkerpop.gremlin.structure.Graph.Features.GraphFeatures.FEATURE_TRANSACTIONS;
import static org.apache.tinkerpop.gremlin.structure.Graph.Features.VariableFeatures.FEATURE_VARIABLES;
import static org.apache.tinkerpop.gremlin.structure.Graph.Features.VertexFeatures.FEATURE_USER_SUPPLIED_IDS;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeThat;

/**
 * Tests that do basic validation of proper Feature settings in Graph implementations.
 *
 * @author Stephen Mallette (http://stephen.genoprime.com)
 */
@RunWith(Enclosed.class)
@SuppressWarnings("ThrowableResultOfMethodCallIgnored")
public class FeatureSupportTest {
    private static final String INVALID_FEATURE_SPECIFICATION = "Features for %s specify that %s is false, but the feature appears to be implemented.  Reconsider this setting or throw the standard Exception.";
    private static final Logger logger = LoggerFactory.getLogger(FeatureSupportTest.class);

    public static class FeatureToStringTest extends AbstractGremlinTest {
        /**
         * A rough test to ensure that StringFactory is being used to toString Features.
         */
        @Test
        public void shouldHaveStandardToStringRepresentation() {
            assertTrue(graph.features().toString().startsWith("FEATURES"));
        }
    }

    /**
     * Feature checks that test {@link Graph} functionality to determine if a feature should be on when it is marked
     * as not supported.
     */
    @ExceptionCoverage(exceptionClass = Graph.Exceptions.class, methods = {
            "variablesNotSupported",
            "graphComputerNotSupported",
            "transactionsNotSupported"
    })
    public static class GraphFunctionalityTest extends AbstractGremlinTest {

        /**
         * This isn't really a test.  It just pretty prints the features for the graph for reference.  Of course,
         * if the implementing classes use anonymous inner classes it will
         */
        @Test
        public void shouldPrintTheFeatureList() {
            logger.info(String.format("Printing Features of %s for reference: ", g.getClass().getSimpleName()));
            logger.info(graph.features().toString());
            assertTrue(true);
        }

        /**
         * A {@link Graph} that does not support {@link Graph.Features.GraphFeatures#FEATURE_COMPUTER} must call
         * {@link Graph.Exceptions#graphComputerNotSupported()}.
         */
        @Test
        @FeatureRequirement(featureClass = GraphFeatures.class, feature = FEATURE_COMPUTER, supported = false)
        public void shouldSupportComputerIfAGraphCanCompute() throws Exception {
            try {
                graph.compute();
                fail(String.format(INVALID_FEATURE_SPECIFICATION, GraphFeatures.class.getSimpleName(), FEATURE_COMPUTER));
            } catch (Exception e) {
                validateException(Graph.Exceptions.graphComputerNotSupported(), e);
            }
        }

        /**
         * A {@link Graph} that does not support {@link Graph.Features.GraphFeatures#FEATURE_TRANSACTIONS} must call
         * {@link Graph.Exceptions#transactionsNotSupported()}.
         */
        @Test
        @FeatureRequirement(featureClass = GraphFeatures.class, feature = FEATURE_TRANSACTIONS, supported = false)
        @FeatureRequirement(featureClass = GraphFeatures.class, feature = FEATURE_THREADED_TRANSACTIONS, supported = false)
        public void shouldSupportTransactionsIfAGraphConstructsATx() {
            try {
                graph.tx();
                fail(String.format(INVALID_FEATURE_SPECIFICATION, GraphFeatures.class.getSimpleName(), FEATURE_TRANSACTIONS));
            } catch (Exception e) {
                validateException(Graph.Exceptions.transactionsNotSupported(), e);
            }
        }

        /**
         * A {@link Graph} that does not support {@link Graph.Features.VariableFeatures#FEATURE_VARIABLES} must call
         * {@link Graph.Exceptions#variablesNotSupported()}.
         */
        @Test
        @FeatureRequirement(featureClass = Graph.Features.VariableFeatures.class, feature = FEATURE_VARIABLES, supported = false)
        public void shouldSupportMemoryIfAGraphAcceptsMemory() throws Exception {
            try {
                graph.variables();
                fail(String.format(INVALID_FEATURE_SPECIFICATION, Graph.Features.VariableFeatures.class.getSimpleName(), FEATURE_VARIABLES));
            } catch (Exception e) {
                validateException(Graph.Exceptions.variablesNotSupported(), e);
            }
        }

        @Test
        @FeatureRequirement(featureClass = Graph.Features.GraphFeatures.class, feature = FEATURE_TRANSACTIONS)
        @FeatureRequirement(featureClass = Graph.Features.GraphFeatures.class, feature = FEATURE_THREADED_TRANSACTIONS, supported = false)
        public void shouldThrowOnThreadedTransactionNotSupported() {
            try {
                graph.tx().createThreadedTx();
                fail("An exception should be thrown since the threaded transaction feature is not supported");
            } catch (Exception e) {
                validateException(Transaction.Exceptions.threadedTransactionsNotSupported(), e);
            }
        }
    }

    /**
     * Feature checks that test {@link Vertex} functionality to determine if a feature
     * should be on when it is marked as not supported.
     */
    @ExceptionCoverage(exceptionClass = Vertex.Exceptions.class, methods = {
            "userSuppliedIdsNotSupported",
            "userSuppliedIdsOfThisTypeNotSupported",
            "vertexRemovalNotSupported"
    })
    @ExceptionCoverage(exceptionClass = Graph.Exceptions.class, methods = {
            "vertexAdditionsNotSupported"
    })
    @ExceptionCoverage(exceptionClass = Element.Exceptions.class, methods = {
            "propertyAdditionNotSupported"
    })
    @ExceptionCoverage(exceptionClass = Property.Exceptions.class, methods = {
            "propertyRemovalNotSupported"
    })
    public static class VertexFunctionalityTest extends AbstractGremlinTest {

        @Test
        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = Graph.Features.VertexFeatures.FEATURE_ADD_VERTICES, supported = false)
        public void shouldSupportAddVerticesIfAVertexCanBeAdded() throws Exception {
            try {
                graph.addVertex();
                fail(String.format(INVALID_FEATURE_SPECIFICATION, VertexFeatures.class.getSimpleName(), VertexFeatures.FEATURE_ADD_VERTICES));
            } catch (Exception e) {
                validateException(Graph.Exceptions.vertexAdditionsNotSupported(), e);
            }
        }

        @Test
        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = Graph.Features.VertexFeatures.FEATURE_ADD_VERTICES)
        @FeatureRequirement(featureClass = VertexFeatures.class, feature = FEATURE_USER_SUPPLIED_IDS, supported = false)
        public void shouldSupportUserSuppliedIdsIfAnIdCanBeAssignedToVertex() throws Exception {
            try {
                graph.addVertex(T.id, graphProvider.convertId(99999943835l, Vertex.class));
                fail(String.format(INVALID_FEATURE_SPECIFICATION, VertexFeatures.class.getSimpleName(), FEATURE_USER_SUPPLIED_IDS));
            } catch (Exception e) {
                validateException(Vertex.Exceptions.userSuppliedIdsNotSupported(), e);
            }
        }

        @Test
        @FeatureRequirement(featureClass = VertexFeatures.class, feature = FEATURE_USER_SUPPLIED_IDS, supported = false)
        public void shouldNotAllowAnyIdsIfUserSuppliedIdsIsFalse() throws Exception {
            assertFalse(graph.features().vertex().willAllowId(graphProvider.convertId(99999943835l, Vertex.class)));
        }

        @Test
        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = Graph.Features.VertexFeatures.FEATURE_ADD_VERTICES)
        @FeatureRequirement(featureClass = VertexFeatures.class, feature = FEATURE_USER_SUPPLIED_IDS)
        @FeatureRequirement(featureClass = VertexFeatures.class, feature = FEATURE_STRING_IDS, supported = false)
        public void shouldSupportUserSuppliedIdsOfTypeString() throws Exception {
            final String id = "this-is-a-valid-id";

            // a graph can "allow" an id without internally supporting it natively and therefore doesn't need
            // to throw the exception
            assumeFalse(graph.features().vertex().willAllowId(id));

            try {
                graph.addVertex(T.id, id);
                fail(String.format(INVALID_FEATURE_SPECIFICATION, VertexFeatures.class.getSimpleName(), FEATURE_STRING_IDS));
            } catch (Exception e) {
                validateException(Vertex.Exceptions.userSuppliedIdsOfThisTypeNotSupported(), e);
            }
        }

        @Test
        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = Graph.Features.VertexFeatures.FEATURE_ADD_VERTICES)
        @FeatureRequirement(featureClass = VertexFeatures.class, feature = FEATURE_USER_SUPPLIED_IDS)
        @FeatureRequirement(featureClass = VertexFeatures.class, feature = FEATURE_NUMERIC_IDS, supported = false)
        public void shouldSupportUserSuppliedIdsOfTypeNumericInt() throws Exception {
            final int id = 123456;

            // a graph can "allow" an id without internally supporting it natively and therefore doesn't need
            // to throw the exception
            assumeFalse(graph.features().vertex().willAllowId(id));

            try {
                graph.addVertex(T.id, id);
                fail(String.format(INVALID_FEATURE_SPECIFICATION, VertexFeatures.class.getSimpleName(), FEATURE_NUMERIC_IDS));
            } catch (Exception e) {
                validateException(Vertex.Exceptions.userSuppliedIdsOfThisTypeNotSupported(), e);
            }
        }

        @Test
        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = Graph.Features.VertexFeatures.FEATURE_ADD_VERTICES)
        @FeatureRequirement(featureClass = VertexFeatures.class, feature = FEATURE_USER_SUPPLIED_IDS)
        @FeatureRequirement(featureClass = VertexFeatures.class, feature = FEATURE_NUMERIC_IDS, supported = false)
        public void shouldSupportUserSuppliedIdsOfTypeNumericLong() throws Exception {
            final long id = 123456l;

            // a graph can "allow" an id without internally supporting it natively and therefore doesn't need
            // to throw the exception
            assumeFalse(graph.features().vertex().willAllowId(id));

            try {
                graph.addVertex(T.id, id);
                fail(String.format(INVALID_FEATURE_SPECIFICATION, VertexFeatures.class.getSimpleName(), FEATURE_NUMERIC_IDS));
            } catch (Exception e) {
                validateException(Vertex.Exceptions.userSuppliedIdsOfThisTypeNotSupported(), e);
            }
        }

        @Test
        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = Graph.Features.VertexFeatures.FEATURE_ADD_VERTICES)
        @FeatureRequirement(featureClass = VertexFeatures.class, feature = FEATURE_USER_SUPPLIED_IDS)
        @FeatureRequirement(featureClass = VertexFeatures.class, feature = FEATURE_UUID_IDS, supported = false)
        public void shouldSupportUserSuppliedIdsOfTypeUuid() throws Exception {
            final UUID id = UUID.randomUUID();

            // a graph can "allow" an id without internally supporting it natively and therefore doesn't need
            // to throw the exception
            assumeFalse(graph.features().vertex().willAllowId(id));

            try {
                graph.addVertex(T.id, id);
                fail(String.format(INVALID_FEATURE_SPECIFICATION, VertexFeatures.class.getSimpleName(), FEATURE_UUID_IDS));
            } catch (Exception e) {
                validateException(Vertex.Exceptions.userSuppliedIdsOfThisTypeNotSupported(), e);
            }
        }

        @Test
        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = Graph.Features.VertexFeatures.FEATURE_ADD_VERTICES)
        @FeatureRequirement(featureClass = VertexFeatures.class, feature = FEATURE_USER_SUPPLIED_IDS)
        @FeatureRequirement(featureClass = VertexFeatures.class, feature = FEATURE_ANY_IDS, supported = false)
        public void shouldSupportUserSuppliedIdsOfTypeAny() throws Exception {
            try {
                final Date id = new Date();
                graph.addVertex(T.id, id);

                // a graph can "allow" an id without internally supporting it natively and therefore doesn't need
                // to throw the exception
                if (!graph.features().vertex().willAllowId(id))
                    fail(String.format(INVALID_FEATURE_SPECIFICATION, VertexFeatures.class.getSimpleName(), FEATURE_ANY_IDS));
            } catch (Exception e) {
                validateException(Vertex.Exceptions.userSuppliedIdsOfThisTypeNotSupported(), e);
            }
        }

        @Test
        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = Graph.Features.VertexFeatures.FEATURE_ADD_VERTICES)
        @FeatureRequirement(featureClass = VertexFeatures.class, feature = FEATURE_USER_SUPPLIED_IDS, supported = false)
        @FeatureRequirement(featureClass = VertexFeatures.class, feature = FEATURE_STRING_IDS, supported = false)
        public void shouldSupportStringIdsIfStringIdsAreGeneratedFromTheGraph() throws Exception {
            final Vertex v = graph.addVertex();
            if (v.id() instanceof String)
                fail(String.format(INVALID_FEATURE_SPECIFICATION, VertexFeatures.class.getSimpleName(), FEATURE_STRING_IDS));
        }

        @Test
        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = Graph.Features.VertexFeatures.FEATURE_ADD_VERTICES)
        @FeatureRequirement(featureClass = VertexFeatures.class, feature = FEATURE_USER_SUPPLIED_IDS, supported = false)
        @FeatureRequirement(featureClass = VertexFeatures.class, feature = FEATURE_UUID_IDS, supported = false)
        public void shouldSupportUuidIdsIfUuidIdsAreGeneratedFromTheGraph() throws Exception {
            final Vertex v = graph.addVertex();
            if (v.id() instanceof UUID)
                fail(String.format(INVALID_FEATURE_SPECIFICATION, VertexFeatures.class.getSimpleName(), FEATURE_UUID_IDS));
        }

        @Test
        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = Graph.Features.VertexFeatures.FEATURE_ADD_VERTICES)
        @FeatureRequirement(featureClass = VertexFeatures.class, feature = FEATURE_USER_SUPPLIED_IDS, supported = false)
        @FeatureRequirement(featureClass = VertexFeatures.class, feature = FEATURE_NUMERIC_IDS, supported = false)
        public void shouldSupportNumericIdsIfNumericIdsAreGeneratedFromTheGraph() throws Exception {
            final Vertex v = graph.addVertex();
            if (v.id() instanceof Number)
                fail(String.format(INVALID_FEATURE_SPECIFICATION, VertexFeatures.class.getSimpleName(), FEATURE_NUMERIC_IDS));
        }

        @Test
        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = Graph.Features.VertexFeatures.FEATURE_ADD_VERTICES)
        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = VertexFeatures.FEATURE_ADD_PROPERTY, supported = false)
        public void shouldSupportAddVertexPropertyIfItCanBeAdded() throws Exception {
            try {
                final Vertex v = graph.addVertex();
                v.property(VertexProperty.Cardinality.single, "should", "not-add-property");
                fail(String.format(INVALID_FEATURE_SPECIFICATION, VertexFeatures.class.getSimpleName(), VertexFeatures.FEATURE_ADD_PROPERTY));
            } catch (Exception e) {
                validateException(Element.Exceptions.propertyAdditionNotSupported(), e);
            }
        }

        @Test
        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = Graph.Features.VertexFeatures.FEATURE_REMOVE_VERTICES, supported = false)
        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = Graph.Features.VertexFeatures.FEATURE_ADD_VERTICES)
        public void shouldSupportRemoveVerticesIfAVertexCanBeRemoved() throws Exception {
            try {
                graph.addVertex().remove();
                fail(String.format(INVALID_FEATURE_SPECIFICATION, VertexFeatures.class.getSimpleName(), VertexFeatures.FEATURE_REMOVE_VERTICES));
            } catch (Exception e) {
                validateException(Vertex.Exceptions.vertexRemovalNotSupported(), e);
            }
        }

        @Test
        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = VertexFeatures.FEATURE_REMOVE_PROPERTY, supported = false)
        @FeatureRequirementSet(FeatureRequirementSet.Package.VERTICES_ONLY)
        public void shouldSupportRemovePropertyIfAPropertyCanBeRemoved() throws Exception {
            try {
                final Vertex v = graph.addVertex("name", "me");
                v.property("name").remove();
                fail(String.format(INVALID_FEATURE_SPECIFICATION, VertexFeatures.class.getSimpleName(), VertexFeatures.FEATURE_REMOVE_PROPERTY));
            } catch (Exception e) {
                validateException(Property.Exceptions.propertyRemovalNotSupported(), e);
            }
        }
    }

    /**
     * Feature checks that test {@link Edge} functionality to determine if a feature
     * should be on when it is marked as not supported.
     */
    @ExceptionCoverage(exceptionClass = Vertex.Exceptions.class, methods = {
            "edgeAdditionsNotSupported"
    })
    @ExceptionCoverage(exceptionClass = Edge.Exceptions.class, methods = {
            "edgeRemovalNotSupported",
            "userSuppliedIdsNotSupported",
            "userSuppliedIdsOfThisTypeNotSupported"
    })
    @ExceptionCoverage(exceptionClass = Element.Exceptions.class, methods = {
            "propertyAdditionNotSupported"
    })
    @ExceptionCoverage(exceptionClass = Property.Exceptions.class, methods = {
            "propertyRemovalNotSupported"
    })
    public static class EdgeFunctionalityTest extends AbstractGremlinTest {

        @Test
        @FeatureRequirement(featureClass = Graph.Features.EdgeFeatures.class, feature = Graph.Features.EdgeFeatures.FEATURE_ADD_EDGES, supported = false)
        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = Graph.Features.VertexFeatures.FEATURE_ADD_VERTICES)
        public void shouldSupportAddEdgesIfEdgeCanBeAdded() throws Exception {
            try {
                final Vertex v = graph.addVertex();
                v.addEdge("friend", v);
                fail(String.format(INVALID_FEATURE_SPECIFICATION, EdgeFeatures.class.getSimpleName(), EdgeFeatures.FEATURE_ADD_EDGES));
            } catch (Exception e) {
                validateException(Vertex.Exceptions.edgeAdditionsNotSupported(), e);
            }
        }

        @Test
        @FeatureRequirement(featureClass = Graph.Features.EdgeFeatures.class, feature = Graph.Features.EdgeFeatures.FEATURE_ADD_EDGES)
        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = Graph.Features.VertexFeatures.FEATURE_ADD_VERTICES)
        @FeatureRequirement(featureClass = EdgeFeatures.class, feature = EdgeFeatures.FEATURE_USER_SUPPLIED_IDS, supported = false)
        public void shouldSupportUserSuppliedIdsIfAnIdCanBeAssignedToEdge() throws Exception {
            try {
                final Vertex v = graph.addVertex();
                v.addEdge("friend", v, T.id, graphProvider.convertId(99999943835l, Edge.class));
                fail(String.format(INVALID_FEATURE_SPECIFICATION, EdgeFeatures.class.getSimpleName(), EdgeFeatures.FEATURE_USER_SUPPLIED_IDS));
            } catch (Exception e) {
                validateException(Edge.Exceptions.userSuppliedIdsNotSupported(), e);
            }
        }

        @Test
        @FeatureRequirement(featureClass = EdgeFeatures.class, feature = EdgeFeatures.FEATURE_USER_SUPPLIED_IDS, supported = false)
        public void shouldNotAllowAnyIdsIfUserSuppliedIdsIsFalse() throws Exception {
            assertFalse(graph.features().edge().willAllowId(graphProvider.convertId(99999943835l, Edge.class)));
        }

        @Test
        @FeatureRequirement(featureClass = Graph.Features.EdgeFeatures.class, feature = Graph.Features.EdgeFeatures.FEATURE_ADD_EDGES)
        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = Graph.Features.VertexFeatures.FEATURE_ADD_VERTICES)
        @FeatureRequirement(featureClass = EdgeFeatures.class, feature = FEATURE_USER_SUPPLIED_IDS, supported = false)
        @FeatureRequirement(featureClass = EdgeFeatures.class, feature = FEATURE_STRING_IDS, supported = false)
        public void shouldSupportStringIdsIfStringIdsAreGeneratedFromTheGraph() throws Exception {
            final Vertex v = graph.addVertex();
            final Edge e = v.addEdge("knows", v);
            if (e.id() instanceof String)
                fail(String.format(INVALID_FEATURE_SPECIFICATION, EdgeFeatures.class.getSimpleName(), FEATURE_STRING_IDS));
        }

        @Test
        @FeatureRequirement(featureClass = Graph.Features.EdgeFeatures.class, feature = Graph.Features.EdgeFeatures.FEATURE_ADD_EDGES)
        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = Graph.Features.VertexFeatures.FEATURE_ADD_VERTICES)
        @FeatureRequirement(featureClass = EdgeFeatures.class, feature = FEATURE_USER_SUPPLIED_IDS, supported = false)
        @FeatureRequirement(featureClass = EdgeFeatures.class, feature = FEATURE_UUID_IDS, supported = false)
        public void shouldSupportUuidIdsIfUuidIdsAreGeneratedFromTheGraph() throws Exception {
            final Vertex v = graph.addVertex();
            final Edge e = v.addEdge("knows", v);
            if (e.id() instanceof UUID)
                fail(String.format(INVALID_FEATURE_SPECIFICATION, EdgeFeatures.class.getSimpleName(), FEATURE_UUID_IDS));
        }

        @Test
        @FeatureRequirement(featureClass = Graph.Features.EdgeFeatures.class, feature = Graph.Features.EdgeFeatures.FEATURE_ADD_EDGES)
        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = Graph.Features.VertexFeatures.FEATURE_ADD_VERTICES)
        @FeatureRequirement(featureClass = EdgeFeatures.class, feature = FEATURE_USER_SUPPLIED_IDS, supported = false)
        @FeatureRequirement(featureClass = EdgeFeatures.class, feature = FEATURE_NUMERIC_IDS, supported = false)
        public void shouldSupportNumericIdsIfNumericIdsAreGeneratedFromTheGraph() throws Exception {
            final Vertex v = graph.addVertex();
            final Edge e = v.addEdge("knows", v);
            if (e.id() instanceof Number)
                fail(String.format(INVALID_FEATURE_SPECIFICATION, EdgeFeatures.class.getSimpleName(), FEATURE_NUMERIC_IDS));
        }

        @Test
        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = Graph.Features.VertexFeatures.FEATURE_ADD_VERTICES)
        @FeatureRequirement(featureClass = VertexFeatures.class, feature = FEATURE_USER_SUPPLIED_IDS)
        @FeatureRequirement(featureClass = VertexFeatures.class, feature = FEATURE_STRING_IDS, supported = false)
        public void shouldSupportUserSuppliedIdsOfTypeString() throws Exception {
            final String id = "this-is-a-valid-id";

            // a graph can "allow" an id without internally supporting it natively and therefore doesn't need
            // to throw the exception
            assumeFalse(graph.features().edge().willAllowId(id));

            try {
                final Vertex v = graph.addVertex();
                v.addEdge("test", v, T.id, id);
                fail(String.format(INVALID_FEATURE_SPECIFICATION, EdgeFeatures.class.getSimpleName(), FEATURE_STRING_IDS));
            } catch (Exception e) {
                validateException(Edge.Exceptions.userSuppliedIdsOfThisTypeNotSupported(), e);
            }
        }

        @Test
        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = Graph.Features.VertexFeatures.FEATURE_ADD_VERTICES)
        @FeatureRequirement(featureClass = VertexFeatures.class, feature = FEATURE_USER_SUPPLIED_IDS)
        @FeatureRequirement(featureClass = VertexFeatures.class, feature = FEATURE_NUMERIC_IDS, supported = false)
        public void shouldSupportUserSuppliedIdsOfTypeNumericInt() throws Exception {
            final int id = 123456;

            // a graph can "allow" an id without internally supporting it natively and therefore doesn't need
            // to throw the exception
            assumeFalse(graph.features().edge().willAllowId(id));

            try {
                final Vertex v = graph.addVertex();
                v.addEdge("test", v, T.id, id);
                fail(String.format(INVALID_FEATURE_SPECIFICATION, EdgeFeatures.class.getSimpleName(), FEATURE_NUMERIC_IDS));
            } catch (Exception e) {
                validateException(Edge.Exceptions.userSuppliedIdsOfThisTypeNotSupported(), e);
            }
        }

        @Test
        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = Graph.Features.VertexFeatures.FEATURE_ADD_VERTICES)
        @FeatureRequirement(featureClass = VertexFeatures.class, feature = FEATURE_USER_SUPPLIED_IDS)
        @FeatureRequirement(featureClass = VertexFeatures.class, feature = FEATURE_NUMERIC_IDS, supported = false)
        public void shouldSupportUserSuppliedIdsOfTypeNumericLong() throws Exception {
            final long id = 123456l;

            // a graph can "allow" an id without internally supporting it natively and therefore doesn't need
            // to throw the exception
            assumeFalse(graph.features().edge().willAllowId(id));

            try {
                final Vertex v = graph.addVertex();
                v.addEdge("test", v, T.id, id);
                fail(String.format(INVALID_FEATURE_SPECIFICATION, EdgeFeatures.class.getSimpleName(), FEATURE_NUMERIC_IDS));
            } catch (Exception e) {
                validateException(Edge.Exceptions.userSuppliedIdsOfThisTypeNotSupported(), e);
            }
        }

        @Test
        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = Graph.Features.VertexFeatures.FEATURE_ADD_VERTICES)
        @FeatureRequirement(featureClass = VertexFeatures.class, feature = FEATURE_USER_SUPPLIED_IDS)
        @FeatureRequirement(featureClass = VertexFeatures.class, feature = FEATURE_UUID_IDS, supported = false)
        public void shouldSupportUserSuppliedIdsOfTypeUuid() throws Exception {
            final UUID id = UUID.randomUUID();

            // a graph can "allow" an id without internally supporting it natively and therefore doesn't need
            // to throw the exception
            assumeFalse(graph.features().edge().willAllowId(id));

            try {
                final Vertex v = graph.addVertex();
                v.addEdge("test", v, T.id, id);
                fail(String.format(INVALID_FEATURE_SPECIFICATION, EdgeFeatures.class.getSimpleName(), FEATURE_UUID_IDS));
            } catch (Exception e) {
                validateException(Edge.Exceptions.userSuppliedIdsOfThisTypeNotSupported(), e);
            }
        }

        @Test
        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = Graph.Features.VertexFeatures.FEATURE_ADD_VERTICES)
        @FeatureRequirement(featureClass = Graph.Features.EdgeFeatures.class, feature = Graph.Features.EdgeFeatures.FEATURE_ADD_EDGES)
        @FeatureRequirement(featureClass = VertexFeatures.class, feature = FEATURE_USER_SUPPLIED_IDS)
        @FeatureRequirement(featureClass = VertexFeatures.class, feature = FEATURE_ANY_IDS, supported = false)
        public void shouldSupportUserSuppliedIdsOfTypeAny() throws Exception {
            final Date id = new Date();

            // a graph can "allow" an id without internally supporting it natively and therefore doesn't need
            // to throw the exception
            assumeFalse(graph.features().edge().willAllowId(id));

            try {
                final Vertex v = graph.addVertex();
                v.addEdge("test", v, T.id, id);
                fail(String.format(INVALID_FEATURE_SPECIFICATION, EdgeFeatures.class.getSimpleName(), FEATURE_ANY_IDS));
            } catch (Exception e) {
                validateException(Edge.Exceptions.userSuppliedIdsOfThisTypeNotSupported(), e);
            }
        }

        @Test
        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = Graph.Features.VertexFeatures.FEATURE_ADD_VERTICES)
        @FeatureRequirement(featureClass = Graph.Features.EdgeFeatures.class, feature = Graph.Features.EdgeFeatures.FEATURE_ADD_EDGES)
        @FeatureRequirement(featureClass = Graph.Features.EdgeFeatures.class, feature = EdgeFeatures.FEATURE_ADD_PROPERTY, supported = false)
        public void shouldSupportAddEdgePropertyIfItCanBeAdded() throws Exception {
            try {
                final Vertex v = graph.addVertex();
                final Edge e = v.addEdge("test", v);
                e.property("should", "not-add-property");
                fail(String.format(INVALID_FEATURE_SPECIFICATION, EdgePropertyFeatures.class.getSimpleName(), EdgeFeatures.FEATURE_ADD_PROPERTY));
            } catch (Exception e) {
                validateException(Element.Exceptions.propertyAdditionNotSupported(), e);
            }
        }

        @Test
        @FeatureRequirement(featureClass = Graph.Features.EdgeFeatures.class, feature = Graph.Features.EdgeFeatures.FEATURE_ADD_EDGES)
        @FeatureRequirement(featureClass = Graph.Features.EdgeFeatures.class, feature = Graph.Features.EdgeFeatures.FEATURE_REMOVE_EDGES, supported = false)
        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = Graph.Features.VertexFeatures.FEATURE_ADD_VERTICES)
        public void shouldSupportRemoveEdgesIfEdgeCanBeRemoved() throws Exception {
            try {
                final Vertex v = graph.addVertex();
                v.addEdge("friend", v);
                v.remove();
                fail(String.format(INVALID_FEATURE_SPECIFICATION, VertexFeatures.class.getSimpleName(), EdgeFeatures.FEATURE_REMOVE_EDGES));
            } catch (Exception ex) {
                validateException(Edge.Exceptions.edgeRemovalNotSupported(), ex);
            }
        }

        @Test
        @FeatureRequirement(featureClass = Graph.Features.EdgeFeatures.class, feature = EdgeFeatures.FEATURE_REMOVE_PROPERTY, supported = false)
        @FeatureRequirementSet(FeatureRequirementSet.Package.SIMPLE)
        public void shouldSupportRemovePropertyIfAPropertyCanBeRemoved() throws Exception {
            try {
                final Vertex v = graph.addVertex();
                final Edge e = v.addEdge("self", v, "name", "foo");
                e.property("name").remove();
                fail(String.format(INVALID_FEATURE_SPECIFICATION, EdgeFeatures.class.getSimpleName(), EdgeFeatures.FEATURE_REMOVE_PROPERTY));
            } catch (Exception ex) {
                validateException(Property.Exceptions.propertyRemovalNotSupported(), ex);
            }
        }
    }

    /**
     * Feature checks that test {@link Element} {@link Property} functionality to determine if a feature should be on
     * when it is marked as not supported.
     */
    @RunWith(Parameterized.class)
    @ExceptionCoverage(exceptionClass = Property.Exceptions.class, methods = {
            "dataTypeOfPropertyValueNotSupported"
    })
    public static class ElementPropertyDataTypeFunctionalityTest extends AbstractGremlinTest {
        private static final String INVALID_FEATURE_SPECIFICATION = "Features for %s specify that %s is false, but the feature appears to be implemented.  Reconsider this setting or throw the standard Exception.";

        @Parameterized.Parameters(name = "supports{0}({1})")
        public static Iterable<Object[]> data() {
            return PropertyTest.PropertyFeatureSupportTest.data();
        }

        @Parameterized.Parameter(value = 0)
        public String featureName;

        @Parameterized.Parameter(value = 1)
        public Object value;

        @Test
        @FeatureRequirement(featureClass = Graph.Features.EdgeFeatures.class, feature = Graph.Features.EdgeFeatures.FEATURE_ADD_EDGES)
        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = Graph.Features.VertexFeatures.FEATURE_ADD_VERTICES)
        @FeatureRequirement(featureClass = Graph.Features.EdgeFeatures.class, feature = EdgeFeatures.FEATURE_ADD_PROPERTY)
        public void shouldEnableFeatureOnEdgeIfNotEnabled() throws Exception {
            assumeThat(graph.features().supports(EdgePropertyFeatures.class, featureName), is(false));
            try {
                final Edge edge = createEdgeForPropertyFeatureTests();
                edge.property("aKey", value);
                fail(String.format(INVALID_FEATURE_SPECIFICATION, EdgePropertyFeatures.class.getSimpleName(), featureName));
            } catch (Exception e) {
                validateException(Property.Exceptions.dataTypeOfPropertyValueNotSupported(value), e);
            }
        }

        @Test
        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = Graph.Features.VertexFeatures.FEATURE_ADD_VERTICES)
        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = Graph.Features.VertexFeatures.FEATURE_ADD_PROPERTY)
        public void shouldEnableFeatureOnVertexIfNotEnabled() throws Exception {
            assumeThat(graph.features().supports(VertexPropertyFeatures.class, featureName), is(false));
            try {
                graph.addVertex("aKey", value);
                fail(String.format(INVALID_FEATURE_SPECIFICATION, VertexPropertyFeatures.class.getSimpleName(), featureName));
            } catch (Exception e) {
                validateException(Property.Exceptions.dataTypeOfPropertyValueNotSupported(value), e);
            }
        }

        private Edge createEdgeForPropertyFeatureTests() {
            final Vertex vertexA = graph.addVertex();
            final Vertex vertexB = graph.addVertex();
            return vertexA.addEdge(graphProvider.convertLabel("knows"), vertexB);
        }
    }

    /**
     * Feature checks that tests if {@link Graph.Variables}
     * functionality to determine if a feature should be on when it is marked as not supported.
     */
    @RunWith(Parameterized.class)
    @ExceptionCoverage(exceptionClass = Graph.Variables.Exceptions.class, methods = {
            "dataTypeOfVariableValueNotSupported"
    })
    public static class GraphVariablesFunctionalityTest extends AbstractGremlinTest {
        private static final String INVALID_FEATURE_SPECIFICATION = "Features for %s specify that %s is false, but the feature appears to be implemented.  Reconsider this setting or throw the standard Exception.";

        @Parameterized.Parameters(name = "supports{0}({1})")
        public static Iterable<Object[]> data() {
            return VariablesTest.GraphVariablesFeatureSupportTest.data();
        }

        @Parameterized.Parameter(value = 0)
        public String featureName;

        @Parameterized.Parameter(value = 1)
        public Object value;

        /**
         * In this case, the feature requirement for sideEffects is checked, because it means that at least one aspect of
         * sideEffects is supported so we need to test other features to make sure they work properly.
         */
        @Test
        @FeatureRequirement(featureClass = Graph.Features.VariableFeatures.class, feature = Graph.Features.VariableFeatures.FEATURE_VARIABLES)
        public void shouldEnableFeatureOnGraphIfNotEnabled() throws Exception {
            assumeThat(graph.features().supports(Graph.Features.VariableFeatures.class, featureName), is(false));
            try {
                final Graph.Variables variables = graph.variables();
                variables.set("aKey", value);
                fail(String.format(INVALID_FEATURE_SPECIFICATION, Graph.Features.VariableFeatures.class.getSimpleName(), featureName));
            } catch (Exception e) {
                validateException(Graph.Variables.Exceptions.dataTypeOfVariableValueNotSupported(value), e);
            }
        }
    }

    @ExceptionCoverage(exceptionClass = VertexProperty.Exceptions.class, methods = {
            "multiPropertiesNotSupported",
            "metaPropertiesNotSupported",
            "userSuppliedIdsNotSupported",
            "userSuppliedIdsOfThisTypeNotSupported",
            "identicalMultiPropertiesNotSupported"
    })
    @ExceptionCoverage(exceptionClass = Element.Exceptions.class, methods = {
            "propertyRemovalNotSupported"
    })
    public static class VertexPropertyFunctionalityTest extends AbstractGremlinTest {

        @Test
        @FeatureRequirementSet(FeatureRequirementSet.Package.VERTICES_ONLY)
        @FeatureRequirement(featureClass = VertexPropertyFeatures.class, feature = VertexPropertyFeatures.FEATURE_USER_SUPPLIED_IDS, supported = false)
        public void shouldSupportUserSuppliedIdsIfAnIdCanBeAssigned() throws Exception {
            try {
                final Vertex v = graph.addVertex();
                v.property(VertexProperty.Cardinality.single, "name", "me", T.id, graphProvider.convertId(99999943835l, VertexProperty.class));
                fail(String.format(INVALID_FEATURE_SPECIFICATION, VertexPropertyFeatures.class.getSimpleName(), VertexPropertyFeatures.FEATURE_USER_SUPPLIED_IDS));
            } catch (Exception ex) {
                validateException(VertexProperty.Exceptions.userSuppliedIdsNotSupported(), ex);
            }
        }

        @Test
        @FeatureRequirement(featureClass = VertexPropertyFeatures.class, feature = FEATURE_USER_SUPPLIED_IDS, supported = false)
        public void shouldNotAllowAnyIdsIfUserSuppliedIdsIsFalse() throws Exception {
            assertFalse(graph.features().vertex().properties().willAllowId(graphProvider.convertId(99999943835l, VertexProperty.class)));
        }

        @Test
        @FeatureRequirementSet(FeatureRequirementSet.Package.VERTICES_ONLY)
        @FeatureRequirement(featureClass = VertexPropertyFeatures.class, feature = FEATURE_USER_SUPPLIED_IDS)
        @FeatureRequirement(featureClass = VertexPropertyFeatures.class, feature = FEATURE_STRING_IDS, supported = false)
        public void shouldSupportUserSuppliedIdsOfTypeString() throws Exception {
            final String id = "this-is-a-valid-id";

            // a graph can "allow" an id without internally supporting it natively and therefore doesn't need
            // to throw the exception
            assumeFalse(graph.features().vertex().properties().willAllowId(id));

            try {
                final Vertex v = graph.addVertex();
                v.property(VertexProperty.Cardinality.single, "test", "me", T.id, id);
                fail(String.format(INVALID_FEATURE_SPECIFICATION, VertexPropertyFeatures.class.getSimpleName(), FEATURE_STRING_IDS));
            } catch (Exception ex) {
                validateException(VertexProperty.Exceptions.userSuppliedIdsOfThisTypeNotSupported(), ex);
            }
        }

        @Test
        @FeatureRequirementSet(FeatureRequirementSet.Package.VERTICES_ONLY)
        @FeatureRequirement(featureClass = VertexPropertyFeatures.class, feature = FEATURE_USER_SUPPLIED_IDS)
        @FeatureRequirement(featureClass = VertexPropertyFeatures.class, feature = FEATURE_NUMERIC_IDS, supported = false)
        public void shouldSupportUserSuppliedIdsOfTypeNumericInt() throws Exception {
            final int id = 123456;

            // a graph can "allow" an id without internally supporting it natively and therefore doesn't need
            // to throw the exception
            assumeFalse(graph.features().vertex().properties().willAllowId(id));

            try {
                final Vertex v = graph.addVertex();
                v.property(VertexProperty.Cardinality.single, "test", "me", T.id, id);
                fail(String.format(INVALID_FEATURE_SPECIFICATION, VertexPropertyFeatures.class.getSimpleName(), FEATURE_NUMERIC_IDS));
            } catch (Exception ex) {
                validateException(VertexProperty.Exceptions.userSuppliedIdsOfThisTypeNotSupported(), ex);
            }
        }

        @Test
        @FeatureRequirementSet(FeatureRequirementSet.Package.VERTICES_ONLY)
        @FeatureRequirement(featureClass = VertexPropertyFeatures.class, feature = FEATURE_USER_SUPPLIED_IDS)
        @FeatureRequirement(featureClass = VertexPropertyFeatures.class, feature = FEATURE_NUMERIC_IDS, supported = false)
        public void shouldSupportUserSuppliedIdsOfTypeNumericLong() throws Exception {
            final long id = 123456l;

            // a graph can "allow" an id without internally supporting it natively and therefore doesn't need
            // to throw the exception
            assumeFalse(graph.features().vertex().properties().willAllowId(id));

            try {
                final Vertex v = graph.addVertex();
                v.property(VertexProperty.Cardinality.single, "test", "me", T.id, id);
                fail(String.format(INVALID_FEATURE_SPECIFICATION, VertexPropertyFeatures.class.getSimpleName(), FEATURE_NUMERIC_IDS));
            } catch (Exception ex) {
                validateException(VertexProperty.Exceptions.userSuppliedIdsOfThisTypeNotSupported(), ex);
            }
        }

        @Test
        @FeatureRequirementSet(FeatureRequirementSet.Package.VERTICES_ONLY)
        @FeatureRequirement(featureClass = VertexPropertyFeatures.class, feature = FEATURE_USER_SUPPLIED_IDS)
        @FeatureRequirement(featureClass = VertexPropertyFeatures.class, feature = FEATURE_UUID_IDS, supported = false)
        public void shouldSupportUserSuppliedIdsOfTypeUuid() throws Exception {
            final UUID id = UUID.randomUUID();

            // a graph can "allow" an id without internally supporting it natively and therefore doesn't need
            // to throw the exception
            assumeFalse(graph.features().vertex().properties().willAllowId(id));

            try {
                final Vertex v = graph.addVertex();
                v.property(VertexProperty.Cardinality.single, "test", "me", T.id, id);
                fail(String.format(INVALID_FEATURE_SPECIFICATION, VertexPropertyFeatures.class.getSimpleName(), FEATURE_UUID_IDS));
            } catch (Exception ex) {
                validateException(VertexProperty.Exceptions.userSuppliedIdsOfThisTypeNotSupported(), ex);
            }
        }

        @Test
        @FeatureRequirementSet(FeatureRequirementSet.Package.VERTICES_ONLY)
        @FeatureRequirement(featureClass = Graph.Features.EdgeFeatures.class, feature = Graph.Features.EdgeFeatures.FEATURE_ADD_EDGES)
        @FeatureRequirement(featureClass = VertexPropertyFeatures.class, feature = FEATURE_USER_SUPPLIED_IDS)
        @FeatureRequirement(featureClass = VertexPropertyFeatures.class, feature = FEATURE_ANY_IDS, supported = false)
        public void shouldSupportUserSuppliedIdsOfTypeAny() throws Exception {
            final Date id = new Date();

            // a graph can "allow" an id without internally supporting it natively and therefore doesn't need
            // to throw the exception
            assumeFalse(graph.features().vertex().properties().willAllowId(id));

            try {
                final Vertex v = graph.addVertex();
                v.property(VertexProperty.Cardinality.single, "test", "me", T.id, id);
                fail(String.format(INVALID_FEATURE_SPECIFICATION, VertexPropertyFeatures.class.getSimpleName(), FEATURE_ANY_IDS));
            } catch (Exception ex) {
                validateException(VertexProperty.Exceptions.userSuppliedIdsOfThisTypeNotSupported(), ex);
            }
        }

        @Test
        @FeatureRequirementSet(FeatureRequirementSet.Package.VERTICES_ONLY)
        @FeatureRequirement(featureClass = VertexPropertyFeatures.class, feature = FEATURE_USER_SUPPLIED_IDS, supported = false)
        @FeatureRequirement(featureClass = VertexPropertyFeatures.class, feature = FEATURE_STRING_IDS, supported = false)
        public void shouldSupportStringIdsIfStringIdsAreGeneratedFromTheGraph() throws Exception {
            final Vertex v = graph.addVertex();
            final VertexProperty p = v.property(VertexProperty.Cardinality.single, "name", "stephen");
            if (p.id() instanceof String)
                fail(String.format(INVALID_FEATURE_SPECIFICATION, VertexPropertyFeatures.class.getSimpleName(), VertexPropertyFeatures.FEATURE_STRING_IDS));
        }

        @Test
        @FeatureRequirementSet(FeatureRequirementSet.Package.VERTICES_ONLY)
        @FeatureRequirement(featureClass = VertexPropertyFeatures.class, feature = FEATURE_USER_SUPPLIED_IDS, supported = false)
        @FeatureRequirement(featureClass = VertexPropertyFeatures.class, feature = FEATURE_UUID_IDS, supported = false)
        public void shouldSupportUuidIdsIfUuidIdsAreGeneratedFromTheGraph() throws Exception {
            final Vertex v = graph.addVertex();
            final VertexProperty p = v.property(VertexProperty.Cardinality.single, "name", "stephen");
            if (p.id() instanceof UUID)
                fail(String.format(INVALID_FEATURE_SPECIFICATION, VertexPropertyFeatures.class.getSimpleName(), VertexPropertyFeatures.FEATURE_UUID_IDS));
        }

        @Test
        @FeatureRequirementSet(FeatureRequirementSet.Package.VERTICES_ONLY)
        @FeatureRequirement(featureClass = VertexPropertyFeatures.class, feature = FEATURE_USER_SUPPLIED_IDS, supported = false)
        @FeatureRequirement(featureClass = VertexPropertyFeatures.class, feature = FEATURE_NUMERIC_IDS, supported = false)
        public void shouldSupportNumericIdsIfNumericIdsAreGeneratedFromTheGraph() throws Exception {
            final Vertex v = graph.addVertex();
            final VertexProperty p = v.property(VertexProperty.Cardinality.single, "name", "stephen");
            if (p.id() instanceof Number)
                fail(String.format(INVALID_FEATURE_SPECIFICATION, VertexPropertyFeatures.class.getSimpleName(), VertexPropertyFeatures.FEATURE_NUMERIC_IDS));
        }


        @Test
        @FeatureRequirement(featureClass = Graph.Features.VertexPropertyFeatures.class, feature = VertexPropertyFeatures.FEATURE_REMOVE_PROPERTY, supported = false)
        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = VertexFeatures.FEATURE_META_PROPERTIES)
        @FeatureRequirementSet(FeatureRequirementSet.Package.VERTICES_ONLY)
        public void shouldSupportRemovePropertyIfAPropertyCanBeRemoved() throws Exception {
            try {
                final Vertex v = graph.addVertex();
                final VertexProperty p = v.property(VertexProperty.Cardinality.single, "name", "me", "test", "this");
                p.property("test").remove();
                fail(String.format(INVALID_FEATURE_SPECIFICATION, VertexPropertyFeatures.class.getSimpleName(), VertexPropertyFeatures.FEATURE_REMOVE_PROPERTY));
            } catch (Exception ex) {
                validateException(Property.Exceptions.propertyRemovalNotSupported(), ex);
            }
        }

        @Test
        @FeatureRequirement(featureClass = Graph.Features.EdgeFeatures.class, feature = Graph.Features.EdgeFeatures.FEATURE_ADD_EDGES)
        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = Graph.Features.VertexFeatures.FEATURE_ADD_VERTICES)
        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = Graph.Features.VertexFeatures.FEATURE_ADD_PROPERTY)
        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = VertexFeatures.FEATURE_MULTI_PROPERTIES)
        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = VertexFeatures.FEATURE_DUPLICATE_MULTI_PROPERTIES, supported = false)
        public void shouldSupportIdenticalMultiPropertyIfTheSameKeyCanBeAssignedSameValueMoreThanOnce() throws Exception {
            try {
                final Vertex v = graph.addVertex("name", "stephen", "name", "stephen");
                if (2 == IteratorUtils.count(v.properties()))
                    fail(String.format(INVALID_FEATURE_SPECIFICATION, VertexFeatures.class.getSimpleName(), VertexFeatures.FEATURE_DUPLICATE_MULTI_PROPERTIES));
            } catch (Exception ex) {
                validateException(VertexProperty.Exceptions.identicalMultiPropertiesNotSupported(), ex);
            }
        }

        @Test
        @FeatureRequirement(featureClass = Graph.Features.EdgeFeatures.class, feature = Graph.Features.EdgeFeatures.FEATURE_ADD_EDGES)
        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = Graph.Features.VertexFeatures.FEATURE_ADD_VERTICES)
        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = Graph.Features.VertexFeatures.FEATURE_ADD_PROPERTY)
        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = VertexFeatures.FEATURE_MULTI_PROPERTIES, supported = false)
        public void shouldSupportMultiPropertyIfTheSameKeyCanBeAssignedMoreThanOnce() throws Exception {
            try {
                final Vertex v = graph.addVertex("name", "stephen", "name", "steve");
                if (2 == IteratorUtils.count(v.properties()))
                    fail(String.format(INVALID_FEATURE_SPECIFICATION, VertexFeatures.class.getSimpleName(), VertexFeatures.FEATURE_MULTI_PROPERTIES));
            } catch (Exception ex) {
                validateException(VertexProperty.Exceptions.multiPropertiesNotSupported(), ex);
            }
        }

        @Test
        @FeatureRequirement(featureClass = Graph.Features.EdgeFeatures.class, feature = Graph.Features.EdgeFeatures.FEATURE_ADD_EDGES)
        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = Graph.Features.VertexFeatures.FEATURE_ADD_VERTICES)
        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = Graph.Features.VertexFeatures.FEATURE_ADD_PROPERTY)
        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = VertexFeatures.FEATURE_META_PROPERTIES, supported = false)
        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = VertexFeatures.FEATURE_META_PROPERTIES)
        public void shouldSupportMetaPropertyIfPropertiesCanBePutOnProperties() throws Exception {
            try {
                final Vertex v = graph.addVertex();
                v.property(VertexProperty.Cardinality.single, "name", "stephen", "p", "on-property");
                fail(String.format(INVALID_FEATURE_SPECIFICATION, VertexFeatures.class.getSimpleName(), VertexFeatures.FEATURE_META_PROPERTIES));
            } catch (Exception ex) {
                validateException(VertexProperty.Exceptions.metaPropertiesNotSupported(), ex);
            }
        }

        @Test
        @FeatureRequirement(featureClass = Graph.Features.EdgeFeatures.class, feature = Graph.Features.EdgeFeatures.FEATURE_ADD_EDGES)
        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = Graph.Features.VertexFeatures.FEATURE_ADD_VERTICES)
        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = Graph.Features.VertexFeatures.FEATURE_ADD_PROPERTY)
        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = VertexFeatures.FEATURE_META_PROPERTIES, supported = false)
        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = VertexFeatures.FEATURE_META_PROPERTIES)
        public void shouldSupportMetaPropertyIfPropertiesCanBePutOnPropertiesViaVertexProperty() throws Exception {
            try {
                final Vertex v = graph.addVertex("name", "stephen");
                v.property("name").property("p", "on-property");
                fail(String.format(INVALID_FEATURE_SPECIFICATION, VertexFeatures.class.getSimpleName(), VertexFeatures.FEATURE_META_PROPERTIES));
            } catch (Exception ex) {
                validateException(VertexProperty.Exceptions.metaPropertiesNotSupported(), ex);
            }
        }

        @Test
        @FeatureRequirement(featureClass = Graph.Features.EdgeFeatures.class, feature = Graph.Features.EdgeFeatures.FEATURE_ADD_EDGES)
        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = Graph.Features.VertexFeatures.FEATURE_ADD_VERTICES)
        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = Graph.Features.VertexFeatures.FEATURE_ADD_PROPERTY)
        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = VertexFeatures.FEATURE_META_PROPERTIES, supported = false)
        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = VertexFeatures.FEATURE_META_PROPERTIES)
        public void shouldSupportMetaPropertyIfPropertiesHaveAnIteratorViaVertexProperty() throws Exception {
            try {
                final Vertex v = graph.addVertex("name", "stephen");
                v.property("name").properties();
                fail(String.format(INVALID_FEATURE_SPECIFICATION, VertexFeatures.class.getSimpleName(), VertexFeatures.FEATURE_META_PROPERTIES));
            } catch (Exception ex) {
                validateException(VertexProperty.Exceptions.metaPropertiesNotSupported(), ex);
            }
        }
    }

    /**
     * Feature checks that simply evaluate conflicting feature definitions without evaluating the actual implementation
     * itself.
     */
    public static class LogicalFeatureSupportTest extends AbstractGremlinTest {

        private EdgeFeatures edgeFeatures;
        private EdgePropertyFeatures edgePropertyFeatures;
        private GraphFeatures graphFeatures;
        private Graph.Features.VariableFeatures variablesFeatures;
        private VertexFeatures vertexFeatures;
        private VertexPropertyFeatures vertexPropertyFeatures;

        @Before
        public void innerSetup() {
            final Graph.Features f = graph.features();
            edgeFeatures = f.edge();
            edgePropertyFeatures = edgeFeatures.properties();
            graphFeatures = f.graph();
            variablesFeatures = graphFeatures.variables();
            vertexFeatures = f.vertex();
            vertexPropertyFeatures = vertexFeatures.properties();
        }

        @Test
        @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = FEATURE_ANY_IDS)
        public void shouldNotSupportAnyIdsUnlessUserSuppliedIdsIsSupportedOnVertex() {
            assertTrue(vertexFeatures.supportsUserSuppliedIds());
        }

        @Test
        @FeatureRequirement(featureClass = Graph.Features.EdgeFeatures.class, feature = FEATURE_ANY_IDS)
        public void shouldNotSupportAnyIdsUnlessUserSuppliedIdsIsSupportedOnEdge() {
            assertTrue(edgeFeatures.supportsUserSuppliedIds());
        }

        @Test
        @FeatureRequirement(featureClass = Graph.Features.VertexPropertyFeatures.class, feature = VertexPropertyFeatures.FEATURE_ANY_IDS)
        public void shouldNotSupportAnyIdsUnlessUserSuppliedIdsIsSupportedOnVertexProperty() {
            assertTrue(vertexPropertyFeatures.supportsUserSuppliedIds());
        }

        @Test
        public void shouldSupportADataTypeIfGraphHasVariablesEnabled() {
            assertEquals(variablesFeatures.supportsVariables(), (variablesFeatures.supportsBooleanValues() || variablesFeatures.supportsDoubleValues()
                    || variablesFeatures.supportsFloatValues() || variablesFeatures.supportsIntegerValues()
                    || variablesFeatures.supportsLongValues() || variablesFeatures.supportsMapValues()
                    || variablesFeatures.supportsMixedListValues() || variablesFeatures.supportsByteValues()
                    || variablesFeatures.supportsBooleanArrayValues() || variablesFeatures.supportsByteArrayValues()
                    || variablesFeatures.supportsDoubleArrayValues() || variablesFeatures.supportsFloatArrayValues()
                    || variablesFeatures.supportsIntegerArrayValues() || variablesFeatures.supportsLongArrayValues()
                    || variablesFeatures.supportsSerializableValues() || variablesFeatures.supportsStringValues()
                    || variablesFeatures.supportsUniformListValues() || variablesFeatures.supportsStringArrayValues()));
        }

        @Test
        @FeatureRequirement(featureClass = Graph.Features.EdgePropertyFeatures.class, feature = EdgePropertyFeatures.FEATURE_PROPERTIES)
        public void shouldSupportADataTypeIfEdgeHasPropertyEnabled() {
            assertEquals(edgePropertyFeatures.supportsProperties(), (edgePropertyFeatures.supportsBooleanValues() || edgePropertyFeatures.supportsDoubleValues()
                    || edgePropertyFeatures.supportsFloatValues() || edgePropertyFeatures.supportsIntegerValues()
                    || edgePropertyFeatures.supportsLongValues() || edgePropertyFeatures.supportsMapValues()
                    || edgePropertyFeatures.supportsMixedListValues() || edgePropertyFeatures.supportsByteValues()
                    || edgePropertyFeatures.supportsBooleanArrayValues() || edgePropertyFeatures.supportsByteArrayValues()
                    || edgePropertyFeatures.supportsDoubleArrayValues() || edgePropertyFeatures.supportsFloatArrayValues()
                    || edgePropertyFeatures.supportsIntegerArrayValues() || edgePropertyFeatures.supportsLongArrayValues()
                    || edgePropertyFeatures.supportsSerializableValues() || edgePropertyFeatures.supportsStringValues()
                    || edgePropertyFeatures.supportsUniformListValues() || edgePropertyFeatures.supportsStringArrayValues()));
        }

        @Test
        @FeatureRequirement(featureClass = Graph.Features.VertexPropertyFeatures.class, feature = VertexPropertyFeatures.FEATURE_PROPERTIES)
        public void shouldSupportADataTypeIfVertexHasPropertyEnabled() {
            assertEquals(vertexPropertyFeatures.supportsProperties(), (vertexPropertyFeatures.supportsBooleanValues() || vertexPropertyFeatures.supportsDoubleValues()
                    || vertexPropertyFeatures.supportsFloatValues() || vertexPropertyFeatures.supportsIntegerValues()
                    || vertexPropertyFeatures.supportsLongValues() || vertexPropertyFeatures.supportsMapValues()
                    || vertexPropertyFeatures.supportsMixedListValues() || vertexPropertyFeatures.supportsByteValues()
                    || vertexPropertyFeatures.supportsBooleanArrayValues() || vertexPropertyFeatures.supportsByteArrayValues()
                    || vertexPropertyFeatures.supportsDoubleArrayValues() || vertexPropertyFeatures.supportsFloatArrayValues()
                    || vertexPropertyFeatures.supportsIntegerArrayValues() || vertexPropertyFeatures.supportsLongArrayValues()
                    || vertexPropertyFeatures.supportsSerializableValues() || vertexPropertyFeatures.supportsStringValues()
                    || vertexPropertyFeatures.supportsUniformListValues() || vertexPropertyFeatures.supportsStringArrayValues()));
        }

        @Test
        public void shouldSupportRegularTransactionsIfThreadedTransactionsAreEnabled() {
            if (graphFeatures.supportsThreadedTransactions())
                assertThat(graphFeatures.supportsThreadedTransactions(), is(true));
        }

        @Test
        public void shouldSupportMultiPropertiesIfSupportingIdenticalMultiProperties() {
            if (vertexFeatures.supportsDuplicateMultiProperties())
                assertThat(vertexFeatures.supportsMultiProperties(), is(true));
        }
    }
}
