# Copyright DataStax, Inc.
#
# Licensed 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.

import sys
import logging
import inspect
from packaging.version import Version
import ipaddress
from uuid import UUID
from decimal import Decimal
import datetime

from cassandra.util import Point, LineString, Polygon, Duration

from cassandra.cluster import EXEC_PROFILE_GRAPH_DEFAULT, EXEC_PROFILE_GRAPH_ANALYTICS_DEFAULT
from cassandra.cluster import GraphAnalyticsExecutionProfile, GraphExecutionProfile, EXEC_PROFILE_GRAPH_SYSTEM_DEFAULT, \
    default_lbp_factory
from cassandra.policies import DSELoadBalancingPolicy

from cassandra.graph import GraphSON1Deserializer
from cassandra.graph.graphson import InetTypeIO, GraphSON2Deserializer, GraphSON3Deserializer
from cassandra.graph import Edge, Vertex, Path
from cassandra.graph.query import GraphOptions, GraphProtocol, graph_graphson2_row_factory, \
    graph_graphson3_row_factory

from tests.integration import DSE_VERSION
from tests.integration.advanced import *


def setup_module():
    if DSE_VERSION:
        dse_options = {'graph': {'realtime_evaluation_timeout_in_seconds': 60}}
        use_single_node_with_graph(dse_options=dse_options)


log = logging.getLogger(__name__)

MAX_LONG = 9223372036854775807
MIN_LONG = -9223372036854775808
ZERO_LONG = 0

MAKE_STRICT = "schema.config().option('graph.schema_mode').set('production')"
MAKE_NON_STRICT = "schema.config().option('graph.schema_mode').set('development')"
ALLOW_SCANS = "schema.config().option('graph.allow_scan').set('true')"

deserializer_plus_to_ipaddressv4 = lambda x: ipaddress.IPv4Address(GraphSON1Deserializer.deserialize_inet(x))
deserializer_plus_to_ipaddressv6 = lambda x: ipaddress.IPv6Address(GraphSON1Deserializer.deserialize_inet(x))


def generic_ip_deserializer(string_ip_address):
    if ":" in string_ip_address:
        return deserializer_plus_to_ipaddressv6(string_ip_address)
    return deserializer_plus_to_ipaddressv4(string_ip_address)


class GenericIpAddressIO(InetTypeIO):
    @classmethod
    def deserialize(cls, value, reader=None):
        return generic_ip_deserializer(value)

GraphSON2Deserializer._deserializers[GenericIpAddressIO.graphson_type] = GenericIpAddressIO
GraphSON3Deserializer._deserializers[GenericIpAddressIO.graphson_type] = GenericIpAddressIO

if DSE_VERSION:
    if DSE_VERSION >= Version('6.8.0'):
        CREATE_CLASSIC_GRAPH = "system.graph(name).engine(Classic).create()"
    else:
        CREATE_CLASSIC_GRAPH = "system.graph(name).create()"


def reset_graph(session, graph_name):
    ks = list(session.execute(
        "SELECT * FROM system_schema.keyspaces WHERE keyspace_name = '{}';".format(graph_name)))
    if ks:
        try:
            session.execute_graph('system.graph(name).drop()', {'name': graph_name},
                                  execution_profile=EXEC_PROFILE_GRAPH_SYSTEM_DEFAULT)
        except:
            pass

    session.execute_graph(CREATE_CLASSIC_GRAPH, {'name': graph_name},
                          execution_profile=EXEC_PROFILE_GRAPH_SYSTEM_DEFAULT)
    wait_for_graph_inserted(session, graph_name)


def wait_for_graph_inserted(session, graph_name):
    count = 0
    exists = session.execute_graph('system.graph(name).exists()', {'name': graph_name},
                                   execution_profile=EXEC_PROFILE_GRAPH_SYSTEM_DEFAULT)[0].value
    while not exists and count < 50:
        time.sleep(1)
        exists = session.execute_graph('system.graph(name).exists()', {'name': graph_name},
                                       execution_profile=EXEC_PROFILE_GRAPH_SYSTEM_DEFAULT)[0].value
    return exists


class BasicGraphUnitTestCase(BasicKeyspaceUnitTestCase):
    """
    This is basic graph unit test case that provides various utility methods that can be leveraged for testcase setup and tear
    down
    """

    @property
    def graph_name(self):
        return self._testMethodName.lower()

    def session_setup(self):
        lbp = DSELoadBalancingPolicy(default_lbp_factory())

        ep_graphson2 = GraphExecutionProfile(
            request_timeout=60,
            load_balancing_policy=lbp,
            graph_options=GraphOptions(
                graph_name=self.graph_name,
                graph_protocol=GraphProtocol.GRAPHSON_2_0
            ),
            row_factory=graph_graphson2_row_factory)

        ep_graphson3 = GraphExecutionProfile(
            request_timeout=60,
            load_balancing_policy=lbp,
            graph_options=GraphOptions(
                graph_name=self.graph_name,
                graph_protocol=GraphProtocol.GRAPHSON_3_0
            ),
            row_factory=graph_graphson3_row_factory)

        ep_graphson1 = GraphExecutionProfile(
            request_timeout=60,
            load_balancing_policy=lbp,
            graph_options=GraphOptions(
                graph_name=self.graph_name
            )
        )

        ep_analytics = GraphAnalyticsExecutionProfile(
            request_timeout=60,
            load_balancing_policy=lbp,
            graph_options=GraphOptions(
                graph_source=b'a',
                graph_language=b'gremlin-groovy',
                graph_name=self.graph_name
            )
        )

        self.cluster = TestCluster(execution_profiles={
            EXEC_PROFILE_GRAPH_DEFAULT: ep_graphson1,
            EXEC_PROFILE_GRAPH_ANALYTICS_DEFAULT: ep_analytics,
            "graphson1": ep_graphson1,
            "graphson2": ep_graphson2,
            "graphson3": ep_graphson3
        })

        self.session = self.cluster.connect()
        self.ks_name = self._testMethodName.lower()
        self.cass_version, self.cql_version = get_server_versions()

    def setUp(self):
        self.session_setup()
        self.reset_graph()
        self.clear_schema()
        # enable dev and scan modes
        self.session.execute_graph(MAKE_NON_STRICT)
        self.session.execute_graph(ALLOW_SCANS)

    def tearDown(self):
        self.cluster.shutdown()

    def clear_schema(self):
        self.session.execute_graph("""
            schema.clear();
        """)

    def reset_graph(self):
        reset_graph(self.session, self.graph_name)

    def wait_for_graph_inserted(self):
        wait_for_graph_inserted(self.session, self.graph_name)

    def _execute(self, query, graphson, params=None, execution_profile_options=None, **kwargs):
        queries = query if isinstance(query, list) else [query]
        ep = self.get_execution_profile(graphson)
        if execution_profile_options:
            ep = self.session.execution_profile_clone_update(ep, **execution_profile_options)

        results = []
        for query in queries:
            log.debug(query)
            rf = self.session.execute_graph_async(query, parameters=params, execution_profile=ep, **kwargs)
            results.append(rf.result())
            self.assertEqual(rf.message.custom_payload['graph-results'], graphson)

        return results[0] if len(results) == 1 else results

    def get_execution_profile(self, graphson, traversal=False):
        ep = 'graphson1'
        if graphson == GraphProtocol.GRAPHSON_2_0:
            ep = 'graphson2'
        elif graphson == GraphProtocol.GRAPHSON_3_0:
            ep = 'graphson3'

        return ep if traversal is False else 'traversal_' + ep

    def resultset_to_list(self, rs):
        results_list = []
        for result in rs:
            try:
                results_list.append(result.value)
            except:
                results_list.append(result)

        return results_list


class GraphUnitTestCase(BasicKeyspaceUnitTestCase):

    @property
    def graph_name(self):
        return self._testMethodName.lower()

    def session_setup(self):
        lbp = DSELoadBalancingPolicy(default_lbp_factory())

        ep_graphson2 = GraphExecutionProfile(
            request_timeout=60,
            load_balancing_policy=lbp,
            graph_options=GraphOptions(
                graph_name=self.graph_name,
                graph_protocol=GraphProtocol.GRAPHSON_2_0
            ),
            row_factory=graph_graphson2_row_factory)

        ep_graphson3 = GraphExecutionProfile(
            request_timeout=60,
            load_balancing_policy=lbp,
            graph_options=GraphOptions(
                graph_name=self.graph_name,
                graph_protocol=GraphProtocol.GRAPHSON_3_0
            ),
            row_factory=graph_graphson3_row_factory)

        ep_graphson1 = GraphExecutionProfile(
            request_timeout=60,
            load_balancing_policy=lbp,
            graph_options=GraphOptions(
                graph_name=self.graph_name,
                graph_language='gremlin-groovy'
            )
        )

        ep_analytics = GraphAnalyticsExecutionProfile(
            request_timeout=60,
            load_balancing_policy=lbp,
            graph_options=GraphOptions(
                graph_source=b'a',
                graph_language=b'gremlin-groovy',
                graph_name=self.graph_name
            )
        )

        self.cluster = TestCluster(execution_profiles={
            EXEC_PROFILE_GRAPH_DEFAULT: ep_graphson1,
            EXEC_PROFILE_GRAPH_ANALYTICS_DEFAULT: ep_analytics,
            "graphson1": ep_graphson1,
            "graphson2": ep_graphson2,
            "graphson3": ep_graphson3
        })

        self.session = self.cluster.connect()
        self.ks_name = self._testMethodName.lower()
        self.cass_version, self.cql_version = get_server_versions()

    def setUp(self):
        """basic setup only"""
        self.session_setup()

    def setup_graph(self, schema):
        """Config dependant setup"""
        schema.drop_graph(self.session, self.graph_name)
        schema.create_graph(self.session, self.graph_name)
        schema.clear(self.session)
        if schema is ClassicGraphSchema:
            # enable dev and scan modes
            self.session.execute_graph(MAKE_NON_STRICT)
            self.session.execute_graph(ALLOW_SCANS)

    def teardown_graph(self, schema):
        schema.drop_graph(self.session, self.graph_name)

    def tearDown(self):
        self.cluster.shutdown()

    def execute_graph_queries(self, queries, params=None, execution_profile=EXEC_PROFILE_GRAPH_DEFAULT,
                              verify_graphson=False, **kwargs):
        results = []
        for query in queries:
            log.debug(query)
            rf = self.session.execute_graph_async(query, parameters=params,
                                                  execution_profile=execution_profile, **kwargs)
            if verify_graphson:
                self.assertEqual(rf.message.custom_payload['graph-results'], verify_graphson)
            results.append(rf.result())

        return results

    def execute_graph(self, query, graphson, params=None, execution_profile_options=None, traversal=False, **kwargs):
        queries = query if isinstance(query, list) else [query]
        ep = self.get_execution_profile(graphson)
        if traversal:
            ep = 'traversal_' + ep
        if execution_profile_options:
            ep = self.session.execution_profile_clone_update(ep, **execution_profile_options)

        results = self.execute_graph_queries(queries, params, ep, verify_graphson=graphson, **kwargs)

        return results[0] if len(results) == 1 else results

    def get_execution_profile(self, graphson, traversal=False):
        ep = 'graphson1'
        if graphson == GraphProtocol.GRAPHSON_2_0:
            ep = 'graphson2'
        elif graphson == GraphProtocol.GRAPHSON_3_0:
            ep = 'graphson3'

        return ep if traversal is False else 'traversal_' + ep

    def resultset_to_list(self, rs):
        results_list = []
        for result in rs:
            try:
                results_list.append(result.value)
            except:
                results_list.append(result)

        return results_list


class BasicSharedGraphUnitTestCase(BasicKeyspaceUnitTestCase):
    """
    This is basic graph unit test case that provides various utility methods that can be leveraged for testcase setup and tear
    down
    """

    @classmethod
    def session_setup(cls):
        cls.cluster = TestCluster()
        cls.session = cls.cluster.connect()
        cls.ks_name = cls.__name__.lower()
        cls.cass_version, cls.cql_version = get_server_versions()
        cls.graph_name = cls.__name__.lower()

    @classmethod
    def setUpClass(cls):
        if DSE_VERSION:
            cls.session_setup()
            cls.reset_graph()
            profiles = cls.cluster.profile_manager.profiles
            profiles[EXEC_PROFILE_GRAPH_DEFAULT].request_timeout = 60
            profiles[EXEC_PROFILE_GRAPH_DEFAULT].graph_options.graph_name = cls.graph_name
            profiles[EXEC_PROFILE_GRAPH_ANALYTICS_DEFAULT].request_timeout = 60
            profiles[EXEC_PROFILE_GRAPH_ANALYTICS_DEFAULT].graph_options.graph_name = cls.graph_name

    @classmethod
    def tearDownClass(cls):
        if DSE_VERSION:
            cls.cluster.shutdown()

    @classmethod
    def clear_schema(self):
        self.session.execute_graph('schema.clear()')

    @classmethod
    def reset_graph(self):
        reset_graph(self.session, self.graph_name)

    def wait_for_graph_inserted(self):
        wait_for_graph_inserted(self.session, self.graph_name)


class GraphFixtures(object):

    @staticmethod
    def line(length, single_script=True):
        raise NotImplementedError()

    @staticmethod
    def classic():
        raise NotImplementedError()

    @staticmethod
    def multiple_fields():
        raise NotImplementedError()

    @staticmethod
    def large():
        raise NotImplementedError()


class ClassicGraphFixtures(GraphFixtures):

    @staticmethod
    def datatypes():
        data = {
            "boolean1": ["Boolean()", True, None],
            "boolean2": ["Boolean()", False, None],
            "point1": ["Point()", Point(.5, .13), GraphSON1Deserializer.deserialize_point],
             "point2": ["Point()", Point(-5, .0), GraphSON1Deserializer.deserialize_point],

            "linestring1": ["Linestring()", LineString(((1.0, 2.0), (3.0, 4.0), (-89.0, 90.0))),
                                GraphSON1Deserializer.deserialize_linestring],
            "polygon1": ["Polygon()", Polygon([(10.0, 10.0), (80.0, 10.0), (80., 88.0), (10., 89.0), (10., 10.0)],
                                              [[(20., 20.0), (20., 30.0), (30., 30.0), (30., 20.0), (20., 20.0)],
                                               [(40., 20.0), (40., 30.0), (50., 30.0), (50., 20.0), (40., 20.0)]]),
                                GraphSON1Deserializer.deserialize_polygon],
            "int1": ["Int()", 2, GraphSON1Deserializer.deserialize_int],
            "smallint1": ["Smallint()", 1, GraphSON1Deserializer.deserialize_smallint],
            "bigint1": ["Bigint()", MAX_LONG, GraphSON1Deserializer.deserialize_bigint],
            "bigint2": ["Bigint()", MIN_LONG, GraphSON1Deserializer.deserialize_bigint],
            "bigint3": ["Bigint()", ZERO_LONG, GraphSON1Deserializer.deserialize_bigint],
            "varint1": ["Varint()", 2147483647, GraphSON1Deserializer.deserialize_varint],
            "int1": ["Int()", 100, GraphSON1Deserializer.deserialize_int],
            "float1": ["Float()", 0.3415681, GraphSON1Deserializer.deserialize_float],
            "double1": ["Double()", 0.34156811237335205, GraphSON1Deserializer.deserialize_double],
            "uuid1": ["Uuid()", UUID('12345678123456781234567812345678'), GraphSON1Deserializer.deserialize_uuid],
            "decimal1": ["Decimal()", Decimal(10), GraphSON1Deserializer.deserialize_decimal],
            "blob1": ["Blob()",  bytearray(b"Hello World"), GraphSON1Deserializer.deserialize_blob],

            "timestamp1": ["Timestamp()", datetime.datetime.utcnow().replace(microsecond=0),
                                GraphSON1Deserializer.deserialize_timestamp],
            "timestamp2": ["Timestamp()", datetime.datetime.max.replace(microsecond=0),
                                GraphSON1Deserializer.deserialize_timestamp],
            # These are valid values but are pending for DSP-14093 to be fixed
            #"timestamp3": ["Timestamp()", datetime.datetime(159, 1, 1, 23, 59, 59),
            #                                 GraphSON1TypeDeserializer.deserialize_timestamp],
            #"timestamp4": ["Timestamp()", datetime.datetime.min,
            #                                            GraphSON1TypeDeserializer.deserialize_timestamp],
            "inet1": ["Inet()", ipaddress.IPv4Address(u"127.0.0.1"), deserializer_plus_to_ipaddressv4],
            "inet2": ["Inet()", ipaddress.IPv6Address(u"2001:db8:85a3:8d3:1319:8a2e:370:7348"),
                      deserializer_plus_to_ipaddressv6],
            "duration1": ["Duration()", datetime.timedelta(1, 16, 0),
                          GraphSON1Deserializer.deserialize_duration],
            "duration2": ["Duration()", datetime.timedelta(days=1, seconds=16, milliseconds=15),
                          GraphSON1Deserializer.deserialize_duration],
            "blob3": ["Blob()", bytes(b"Hello World Again"), GraphSON1Deserializer.deserialize_blob],
            "blob4": ["Blob()", memoryview(b"And Again Hello World"), GraphSON1Deserializer.deserialize_blob]
            }

        if DSE_VERSION >= Version("5.1"):
            data["time1"] = ["Time()", datetime.time(12, 6, 12, 444), GraphSON1Deserializer.deserialize_time]
            data["time2"] = ["Time()", datetime.time(12, 6, 12), GraphSON1Deserializer.deserialize_time]
            data["time3"] = ["Time()", datetime.time(12, 6), GraphSON1Deserializer.deserialize_time]
            data["time4"] = ["Time()", datetime.time.min, GraphSON1Deserializer.deserialize_time]
            data["time5"] = ["Time()", datetime.time.max, GraphSON1Deserializer.deserialize_time]
            data["blob5"] = ["Blob()", bytearray(b"AKDLIElksadlaswqA" * 10000), GraphSON1Deserializer.deserialize_blob]
            data["datetime1"] = ["Date()", datetime.date.today(), GraphSON1Deserializer.deserialize_date]
            data["datetime2"] = ["Date()", datetime.date(159, 1, 3), GraphSON1Deserializer.deserialize_date]
            data["datetime3"] = ["Date()", datetime.date.min, GraphSON1Deserializer.deserialize_date]
            data["datetime4"] = ["Date()", datetime.date.max, GraphSON1Deserializer.deserialize_date]
            data["time1"] = ["Time()", datetime.time(12, 6, 12, 444), GraphSON1Deserializer.deserialize_time]
            data["time2"] = ["Time()", datetime.time(12, 6, 12), GraphSON1Deserializer.deserialize_time]
            data["time3"] = ["Time()", datetime.time(12, 6), GraphSON1Deserializer.deserialize_time]
            data["time4"] = ["Time()", datetime.time.min, GraphSON1Deserializer.deserialize_time]
            data["time5"] = ["Time()", datetime.time.max, GraphSON1Deserializer.deserialize_time]

        return data

    @staticmethod
    def line(length, single_script=False):
        queries = [ALLOW_SCANS + ';',
                   """schema.propertyKey('index').Int().ifNotExists().create();
                   schema.propertyKey('distance').Int().ifNotExists().create();
                   schema.vertexLabel('lp').properties('index').ifNotExists().create();
                   schema.edgeLabel('goesTo').properties('distance').connection('lp', 'lp').ifNotExists().create();"""]

        vertex_script = ["Vertex vertex0 = graph.addVertex(label, 'lp', 'index', 0);"]
        for index in range(1, length):
            if not single_script and len(vertex_script) > 25:
                queries.append("\n".join(vertex_script))
                vertex_script = [
                    "Vertex vertex{pindex} = g.V().hasLabel('lp').has('index', {pindex}).next()".format(
                        pindex=index-1)]

            vertex_script.append('''
                Vertex vertex{vindex} = graph.addVertex(label, 'lp', 'index', {vindex});
                vertex{pindex}.addEdge('goesTo', vertex{vindex}, 'distance', 5); '''.format(
                vindex=index, pindex=index - 1))

        queries.append("\n".join(vertex_script))
        return queries

    @staticmethod
    def classic():
        queries = [ALLOW_SCANS,
            '''schema.propertyKey('name').Text().ifNotExists().create();
            schema.propertyKey('age').Int().ifNotExists().create();
            schema.propertyKey('lang').Text().ifNotExists().create();
            schema.propertyKey('weight').Float().ifNotExists().create();
            schema.vertexLabel('person').properties('name', 'age').ifNotExists().create();
            schema.vertexLabel('software').properties('name', 'lang').ifNotExists().create();
            schema.edgeLabel('created').properties('weight').connection('person', 'software').ifNotExists().create();
            schema.edgeLabel('created').connection('software', 'software').add();
            schema.edgeLabel('knows').properties('weight').connection('person', 'person').ifNotExists().create();''',

            '''Vertex marko = graph.addVertex(label, 'person', 'name', 'marko', 'age', 29);
            Vertex vadas = graph.addVertex(label, 'person', 'name', 'vadas', 'age', 27);
            Vertex lop = graph.addVertex(label, 'software', 'name', 'lop', 'lang', 'java');
            Vertex josh = graph.addVertex(label, 'person', 'name', 'josh', 'age', 32);
            Vertex ripple = graph.addVertex(label, 'software', 'name', 'ripple', 'lang', 'java');
            Vertex peter = graph.addVertex(label, 'person', 'name', 'peter', 'age', 35);
            Vertex carl = graph.addVertex(label, 'person', 'name', 'carl', 'age', 35);
            marko.addEdge('knows', vadas, 'weight', 0.5f);
            marko.addEdge('knows', josh, 'weight', 1.0f);
            marko.addEdge('created', lop, 'weight', 0.4f);
            josh.addEdge('created', ripple, 'weight', 1.0f);
            josh.addEdge('created', lop, 'weight', 0.4f);
            peter.addEdge('created', lop, 'weight', 0.2f);''']

        return "\n".join(queries)

    @staticmethod
    def multiple_fields():
        query_params = {}
        queries= [ALLOW_SCANS,
                  '''schema.propertyKey('shortvalue').Smallint().ifNotExists().create();
                  schema.vertexLabel('shortvertex').properties('shortvalue').ifNotExists().create();
                  short s1 = 5000; graph.addVertex(label, "shortvertex", "shortvalue", s1);
                  schema.propertyKey('intvalue').Int().ifNotExists().create();
                  schema.vertexLabel('intvertex').properties('intvalue').ifNotExists().create();
                  int i1 = 1000000000; graph.addVertex(label, "intvertex", "intvalue", i1);
                  schema.propertyKey('intvalue2').Int().ifNotExists().create();
                  schema.vertexLabel('intvertex2').properties('intvalue2').ifNotExists().create();
                  Integer i2 = 100000000; graph.addVertex(label, "intvertex2", "intvalue2", i2);
                  schema.propertyKey('longvalue').Bigint().ifNotExists().create();
                  schema.vertexLabel('longvertex').properties('longvalue').ifNotExists().create();
                  long l1 = 9223372036854775807; graph.addVertex(label, "longvertex", "longvalue", l1);
                  schema.propertyKey('longvalue2').Bigint().ifNotExists().create();
                  schema.vertexLabel('longvertex2').properties('longvalue2').ifNotExists().create();
                  Long l2 = 100000000000000000L; graph.addVertex(label, "longvertex2", "longvalue2", l2);
                  schema.propertyKey('floatvalue').Float().ifNotExists().create();
                  schema.vertexLabel('floatvertex').properties('floatvalue').ifNotExists().create();
                  float f1 = 3.5f; graph.addVertex(label, "floatvertex", "floatvalue", f1);
                  schema.propertyKey('doublevalue').Double().ifNotExists().create();
                  schema.vertexLabel('doublevertex').properties('doublevalue').ifNotExists().create();
                  double d1 = 3.5e40; graph.addVertex(label, "doublevertex", "doublevalue", d1);
                  schema.propertyKey('doublevalue2').Double().ifNotExists().create();
                  schema.vertexLabel('doublevertex2').properties('doublevalue2').ifNotExists().create();
                  Double d2 = 3.5e40d; graph.addVertex(label, "doublevertex2", "doublevalue2", d2);''']

        if DSE_VERSION >= Version('5.1'):
            queries.append('''schema.propertyKey('datevalue1').Date().ifNotExists().create();
                         schema.vertexLabel('datevertex1').properties('datevalue1').ifNotExists().create();
                         schema.propertyKey('negdatevalue2').Date().ifNotExists().create();
                        schema.vertexLabel('negdatevertex2').properties('negdatevalue2').ifNotExists().create();''')

            for i in range(1, 4):
                queries.append('''schema.propertyKey('timevalue{0}').Time().ifNotExists().create();
                         schema.vertexLabel('timevertex{0}').properties('timevalue{0}').ifNotExists().create();'''.format(
                    i))

                queries.append('graph.addVertex(label, "datevertex1", "datevalue1", date1);')
            query_params['date1'] = '1999-07-29'

            queries.append('graph.addVertex(label, "negdatevertex2", "negdatevalue2", date2);')
            query_params['date2'] = '-1999-07-28'

            queries.append('graph.addVertex(label, "timevertex1", "timevalue1", time1);')
            query_params['time1'] = '14:02'
            queries.append('graph.addVertex(label, "timevertex2", "timevalue2", time2);')
            query_params['time2'] = '14:02:20'
            queries.append('graph.addVertex(label, "timevertex3", "timevalue3", time3);')
            query_params['time3'] = '14:02:20.222'

        return queries, query_params

    @staticmethod
    def large():
        query_parts = ['''
                int size = 2000;
                List ids = new ArrayList();
                schema.propertyKey('ts').Int().single().ifNotExists().create();
                schema.propertyKey('sin').Int().single().ifNotExists().create();
                schema.propertyKey('cos').Int().single().ifNotExists().create();
                schema.propertyKey('ii').Int().single().ifNotExists().create();
                schema.vertexLabel('lcg').properties('ts', 'sin', 'cos', 'ii').ifNotExists().create();
                schema.edgeLabel('linked').connection('lcg', 'lcg').ifNotExists().create();
                Vertex v = graph.addVertex(label, 'lcg');
                v.property("ts", 100001);
                v.property("sin", 0);
                v.property("cos", 1);
                v.property("ii", 0);
                ids.add(v.id());
                Random rand = new Random();
                for (int ii = 1; ii < size; ii++) {
                    v = graph.addVertex(label, 'lcg');
                    v.property("ii", ii);
                    v.property("ts", 100001 + ii);
                    v.property("sin", Math.sin(ii/5.0));
                    v.property("cos", Math.cos(ii/5.0));
                    Vertex u = g.V(ids.get(rand.nextInt(ids.size()))).next();
                    v.addEdge("linked", u);
                    ids.add(v.id());
                }
                g.V().count();''']

        return "\n".join(query_parts)

    @staticmethod
    def address_book():
        p1 = "Point()"
        p2 = "Point()"
        if DSE_VERSION >= Version('5.1'):
            p1 = "Point().withBounds(-100, -100, 100, 100)"
            p2 = "Point().withGeoBounds()"

        queries = [
            ALLOW_SCANS,
            "schema.propertyKey('name').Text().ifNotExists().create()",
            "schema.propertyKey('pointPropWithBoundsWithSearchIndex').{}.ifNotExists().create()".format(p1),
            "schema.propertyKey('pointPropWithBounds').{}.ifNotExists().create()".format(p1),
            "schema.propertyKey('pointPropWithGeoBoundsWithSearchIndex').{}.ifNotExists().create()".format(p2),
            "schema.propertyKey('pointPropWithGeoBounds').{}.ifNotExists().create()".format(p2),
            "schema.propertyKey('city').Text().ifNotExists().create()",
            "schema.propertyKey('state').Text().ifNotExists().create()",
            "schema.propertyKey('description').Text().ifNotExists().create()",
            "schema.vertexLabel('person').properties('name', 'city', 'state', 'description', 'pointPropWithBoundsWithSearchIndex', 'pointPropWithBounds', 'pointPropWithGeoBoundsWithSearchIndex', 'pointPropWithGeoBounds').ifNotExists().create()",
            "schema.vertexLabel('person').index('searchPointWithBounds').secondary().by('pointPropWithBounds').ifNotExists().add()",
            "schema.vertexLabel('person').index('searchPointWithGeoBounds').secondary().by('pointPropWithGeoBounds').ifNotExists().add()",

            "g.addV('person').property('name', 'Paul Thomas Joe').property('city', 'Rochester').property('state', 'MN').property('pointPropWithBoundsWithSearchIndex', Geo.point(-92.46295, 44.0234)).property('pointPropWithBounds', Geo.point(-92.46295, 44.0234)).property('pointPropWithGeoBoundsWithSearchIndex', Geo.point(-92.46295, 44.0234)).property('pointPropWithGeoBounds', Geo.point(-92.46295, 44.0234)).property('description', 'Lives by the hospital').next()",
            "g.addV('person').property('name', 'George Bill Steve').property('city', 'Minneapolis').property('state', 'MN').property('pointPropWithBoundsWithSearchIndex', Geo.point(-93.266667, 44.093333)).property('pointPropWithBounds', Geo.point(-93.266667, 44.093333)).property('pointPropWithGeoBoundsWithSearchIndex', Geo.point(-93.266667, 44.093333)).property('pointPropWithGeoBounds', Geo.point(-93.266667, 44.093333)).property('description', 'A cold dude').next()",
            "g.addV('person').property('name', 'James Paul Smith').property('city', 'Chicago').property('state', 'IL').property('pointPropWithBoundsWithSearchIndex', Geo.point(-87.684722, 41.836944)).property('description', 'Likes to hang out').next()",
            "g.addV('person').property('name', 'Jill Alice').property('city', 'Atlanta').property('state', 'GA').property('pointPropWithBoundsWithSearchIndex', Geo.point(-84.39, 33.755)).property('description', 'Enjoys a nice cold coca cola').next()"
        ]

        if not Version('5.0') <= DSE_VERSION < Version('5.1'):
            queries.append("schema.vertexLabel('person').index('search').search().by('pointPropWithBoundsWithSearchIndex').withError(0.00001, 0.0).by('pointPropWithGeoBoundsWithSearchIndex').withError(0.00001, 0.0).ifNotExists().add()")

        return "\n".join(queries)


class CoreGraphFixtures(GraphFixtures):

    @staticmethod
    def datatypes():
        data = ClassicGraphFixtures.datatypes()
        del data['duration1']
        del data['duration2']

        # Core Graphs only types
        data["map1"] = ["mapOf(Text, Text)", {'test': 'test'}, None]
        data["map2"] = ["mapOf(Text, Point)", {'test': Point(.5, .13)}, None]
        data["map3"] = ["frozen(mapOf(Int, Varchar))", {42: 'test'}, None]

        data["list1"] = ["listOf(Text)", ['test', 'hello', 'world'], None]
        data["list2"] = ["listOf(Int)", [42, 632, 32], None]
        data["list3"] = ["listOf(Point)", [Point(.5, .13), Point(42.5, .13)], None]
        data["list4"] = ["frozen(listOf(Int))", [42, 55, 33], None]

        data["set1"] = ["setOf(Text)", {'test', 'hello', 'world'}, None]
        data["set2"] = ["setOf(Int)", {42, 632, 32}, None]
        data["set3"] = ["setOf(Point)", {Point(.5, .13), Point(42.5, .13)}, None]
        data["set4"] = ["frozen(setOf(Int))", {42, 55, 33}, None]

        data["tuple1"] = ["tupleOf(Int, Text)", (42, "world"), None]
        data["tuple2"] = ["tupleOf(Int, tupleOf(Text, tupleOf(Text, Point)))", (42, ("world", ('this', Point(.5, .13)))), None]
        data["tuple3"] = ["tupleOf(Int, tupleOf(Text, frozen(mapOf(Text, Text))))", (42, ("world", {'test': 'test'})), None]
        data["tuple4"] = ["tupleOf(Int, tupleOf(Text, frozen(listOf(Int))))", (42, ("world", [65, 89])), None]
        data["tuple5"] = ["tupleOf(Int, tupleOf(Text, frozen(setOf(Int))))", (42, ("world", {65, 55})), None]
        data["tuple6"] = ["tupleOf(Int, tupleOf(Text, tupleOf(Text, LineString)))",
                          (42, ("world", ('this', LineString(((1.0, 2.0), (3.0, 4.0), (-89.0, 90.0)))))), None]

        data["tuple7"] = ["tupleOf(Int, tupleOf(Text, tupleOf(Text, Polygon)))",
                          (42, ("world", ('this', Polygon([(10.0, 10.0), (80.0, 10.0), (80., 88.0), (10., 89.0), (10., 10.0)],
                                              [[(20., 20.0), (20., 30.0), (30., 30.0), (30., 20.0), (20., 20.0)],
                                               [(40., 20.0), (40., 30.0), (50., 30.0), (50., 20.0), (40., 20.0)]])))), None]
        data["dse_duration1"] = ["Duration()", Duration(42, 12, 10303312), None]
        data["dse_duration2"] = ["Duration()", Duration(50, 32, 11), None]

        return data

    @staticmethod
    def line(length, single_script=False):
        queries = ["""
        schema.vertexLabel('lp').ifNotExists().partitionBy('index', Int).create();
        schema.edgeLabel('goesTo').ifNotExists().from('lp').to('lp').property('distance', Int).create();
        """]

        vertex_script = ["g.addV('lp').property('index', 0).next();"]
        for index in range(1, length):
            if not single_script and len(vertex_script) > 25:
                queries.append("\n".join(vertex_script))
                vertex_script = []

            vertex_script.append('''
                g.addV('lp').property('index', {index}).next();
                g.V().hasLabel('lp').has('index', {pindex}).as('pp').V().hasLabel('lp').has('index', {index}).as('p').
                   addE('goesTo').from('pp').to('p').property('distance', 5).next();
                '''.format(
                index=index, pindex=index - 1))

        queries.append("\n".join(vertex_script))
        return queries

    @staticmethod
    def classic():
        queries = [
            '''
            schema.vertexLabel('person').ifNotExists().partitionBy('name', Text).property('age', Int).create();
            schema.vertexLabel('software')ifNotExists().partitionBy('name', Text).property('lang', Text).create();
            schema.edgeLabel('created').ifNotExists().from('person').to('software').property('weight', Double).create();
            schema.edgeLabel('knows').ifNotExists().from('person').to('person').property('weight', Double).create();
            ''',

            '''
            Vertex marko = g.addV('person').property('name', 'marko').property('age', 29).next();
            Vertex vadas = g.addV('person').property('name', 'vadas').property('age', 27).next();
            Vertex lop = g.addV('software').property('name', 'lop').property('lang', 'java').next();
            Vertex josh = g.addV('person').property('name', 'josh').property('age', 32).next();
            Vertex peter = g.addV('person').property('name', 'peter').property('age', 35).next();
            Vertex carl = g.addV('person').property('name', 'carl').property('age', 35).next();
            Vertex ripple = g.addV('software').property('name', 'ripple').property('lang', 'java').next();

            // TODO, switch to VertexReference and use v.id()
            g.V().hasLabel('person').has('name', 'vadas').as('v').V().hasLabel('person').has('name', 'marko').as('m').addE('knows').from('m').to('v').property('weight', 0.5d).next();
            g.V().hasLabel('person').has('name', 'josh').as('j').V().hasLabel('person').has('name', 'marko').as('m').addE('knows').from('m').to('j').property('weight', 1.0d).next();
            g.V().hasLabel('software').has('name', 'lop').as('l').V().hasLabel('person').has('name', 'marko').as('m').addE('created').from('m').to('l').property('weight', 0.4d).next();
            g.V().hasLabel('software').has('name', 'ripple').as('r').V().hasLabel('person').has('name', 'josh').as('j').addE('created').from('j').to('r').property('weight', 1.0d).next();
            g.V().hasLabel('software').has('name', 'lop').as('l').V().hasLabel('person').has('name', 'josh').as('j').addE('created').from('j').to('l').property('weight', 0.4d).next();
            g.V().hasLabel('software').has('name', 'lop').as('l').V().hasLabel('person').has('name', 'peter').as('p').addE('created').from('p').to('l').property('weight', 0.2d).next();

            ''']

        return queries

    @staticmethod
    def multiple_fields():
        ## no generic test currently needs this
        raise NotImplementedError()

    @staticmethod
    def large():
        query_parts = [
            '''
            schema.vertexLabel('lcg').ifNotExists().partitionBy('ts', Int).property('sin', Double).
                property('cos', Double).property('ii', Int).create();
            schema.edgeLabel('linked').ifNotExists().from('lcg').to('lcg').create();
            ''',

            '''
            int size = 2000;
            List ids = new ArrayList();
            v = g.addV('lcg').property('ts', 100001).property('sin', 0d).property('cos', 1d).property('ii', 0).next();
            ids.add(v.id());
            Random rand = new Random();
            for (int ii = 1; ii < size; ii++) {
                v = g.addV('lcg').property('ts', 100001 + ii).property('sin', Math.sin(ii/5.0)).property('cos', Math.cos(ii/5.0)).property('ii', ii).next();

                uid = ids.get(rand.nextInt(ids.size()))
                g.V(v.id()).as('v').V(uid).as('u').addE('linked').from('v').to('u').next();
                ids.add(v.id());
            }
            g.V().count();'''
        ]

        return query_parts

    @staticmethod
    def address_book():
        queries = [
            "schema.vertexLabel('person').ifNotExists().partitionBy('name', Text)."
            "property('pointPropWithBoundsWithSearchIndex', Point)."
            "property('pointPropWithBounds', Point)."
            "property('pointPropWithGeoBoundsWithSearchIndex', Point)."
            "property('pointPropWithGeoBounds', Point)."
            "property('city', Text)."
            "property('state', Text)."
            "property('description', Text).create()",
            "schema.vertexLabel('person').searchIndex().by('name').by('pointPropWithBounds').by('pointPropWithGeoBounds').by('description').asText().create()",
            "g.addV('person').property('name', 'Paul Thomas Joe').property('city', 'Rochester').property('state', 'MN').property('pointPropWithBoundsWithSearchIndex', Geo.point(-92.46295, 44.0234)).property('pointPropWithBounds', Geo.point(-92.46295, 44.0234)).property('pointPropWithGeoBoundsWithSearchIndex', Geo.point(-92.46295, 44.0234)).property('pointPropWithGeoBounds', Geo.point(-92.46295, 44.0234)).property('description', 'Lives by the hospital').next()",
            "g.addV('person').property('name', 'George Bill Steve').property('city', 'Minneapolis').property('state', 'MN').property('pointPropWithBoundsWithSearchIndex', Geo.point(-93.266667, 44.093333)).property('pointPropWithBounds', Geo.point(-93.266667, 44.093333)).property('pointPropWithGeoBoundsWithSearchIndex', Geo.point(-93.266667, 44.093333)).property('pointPropWithGeoBounds', Geo.point(-93.266667, 44.093333)).property('description', 'A cold dude').next()",
            "g.addV('person').property('name', 'James Paul Smith').property('city', 'Chicago').property('state', 'IL').property('pointPropWithBoundsWithSearchIndex', Geo.point(-87.684722, 41.836944)).property('description', 'Likes to hang out').next()",
            "g.addV('person').property('name', 'Jill Alice').property('city', 'Atlanta').property('state', 'GA').property('pointPropWithBoundsWithSearchIndex', Geo.point(-84.39, 33.755)).property('description', 'Enjoys a nice cold coca cola').next()"
        ]

        if not Version('5.0') <= DSE_VERSION < Version('5.1'):
            queries.append("schema.vertexLabel('person').searchIndex().by('pointPropWithBoundsWithSearchIndex').by('pointPropWithGeoBounds')"
                           ".by('pointPropWithGeoBoundsWithSearchIndex').create()")

        return queries


def validate_classic_vertex(test, vertex):
    vertex_props = vertex.properties.keys()
    test.assertEqual(len(vertex_props), 2)
    test.assertIn('name', vertex_props)
    test.assertTrue('lang' in vertex_props or 'age' in vertex_props)


def validate_classic_vertex_return_type(test, vertex):
    validate_generic_vertex_result_type(vertex)
    vertex_props = vertex.properties
    test.assertIn('name', vertex_props)
    test.assertTrue('lang' in vertex_props or 'age' in vertex_props)


def validate_generic_vertex_result_type(test, vertex):
    test.assertIsInstance(vertex, Vertex)
    for attr in ('id', 'type', 'label', 'properties'):
        test.assertIsNotNone(getattr(vertex, attr))


def validate_classic_edge_properties(test, edge_properties):
    test.assertEqual(len(edge_properties.keys()), 1)
    test.assertIn('weight', edge_properties)
    test.assertIsInstance(edge_properties, dict)


def validate_classic_edge(test, edge):
    validate_generic_edge_result_type(test, edge)
    validate_classic_edge_properties(test, edge.properties)


def validate_line_edge(test, edge):
    validate_generic_edge_result_type(test, edge)
    edge_props = edge.properties
    test.assertEqual(len(edge_props.keys()), 1)
    test.assertIn('distance', edge_props)


def validate_generic_edge_result_type(test, edge):
    test.assertIsInstance(edge, Edge)
    for attr in ('properties', 'outV', 'outVLabel', 'inV', 'inVLabel', 'label', 'type', 'id'):
        test.assertIsNotNone(getattr(edge, attr))


def validate_path_result_type(test, path):
    test.assertIsInstance(path, Path)
    test.assertIsNotNone(path.labels)
    for obj in path.objects:
        if isinstance(obj, Edge):
            validate_classic_edge(test, obj)
        elif isinstance(obj, Vertex):
            validate_classic_vertex(test, obj)
        else:
            test.fail("Invalid object found in path " + str(object.type))


class GraphTestConfiguration(object):
    """Possible Configurations:
        ClassicGraphSchema:
            graphson1
            graphson2
            graphson3

        CoreGraphSchema
            graphson3
    """

    @classmethod
    def schemas(cls):
        schemas = [ClassicGraphSchema]
        if DSE_VERSION >= Version("6.8"):
            schemas.append(CoreGraphSchema)
        return schemas

    @classmethod
    def graphson_versions(cls):
        graphson_versions = [GraphProtocol.GRAPHSON_1_0]
        if DSE_VERSION >= Version("6.0"):
            graphson_versions.append(GraphProtocol.GRAPHSON_2_0)
        if DSE_VERSION >= Version("6.8"):
            graphson_versions.append(GraphProtocol.GRAPHSON_3_0)
        return graphson_versions

    @classmethod
    def schema_configurations(cls, schema=None):
        schemas = cls.schemas() if schema is None else [schema]
        configurations = []
        for s in schemas:
                configurations.append(s)

        return configurations

    @classmethod
    def configurations(cls, schema=None, graphson=None):
        schemas = cls.schemas() if schema is None else [schema]
        graphson_versions = cls.graphson_versions() if graphson is None else [graphson]

        configurations = []
        for s in schemas:
            for g in graphson_versions:
                if s is CoreGraphSchema and g != GraphProtocol.GRAPHSON_3_0:
                    continue
                configurations.append((s, g))

        return configurations

    @staticmethod
    def _make_graph_schema_test_method(func, schema):
        def test_input(self):
            self.setup_graph(schema)
            try:
                func(self, schema)
            except:
                raise
            finally:
                self.teardown_graph(schema)

        schema_name = 'classic' if schema is ClassicGraphSchema else 'core'
        test_input.__name__ = '{func}_{schema}'.format(
            func=func.__name__.lstrip('_'), schema=schema_name)
        return test_input

    @staticmethod
    def _make_graph_test_method(func, schema, graphson):
        def test_input(self):
            self.setup_graph(schema)
            try:
                func(self, schema, graphson)
            except:
                raise
            finally:
                self.teardown_graph(schema)

        graphson_name = 'graphson1'
        if graphson == GraphProtocol.GRAPHSON_2_0:
            graphson_name = 'graphson2'
        elif graphson == GraphProtocol.GRAPHSON_3_0:
            graphson_name = 'graphson3'

        schema_name = 'classic' if schema is ClassicGraphSchema else 'core'

        # avoid keyspace name too long issue
        if DSE_VERSION < Version('6.7'):
            schema_name = schema_name[0]
            graphson_name = 'g' + graphson_name[-1]

        test_input.__name__ = '{func}_{schema}_{graphson}'.format(
            func=func.__name__.lstrip('_'), schema=schema_name, graphson=graphson_name)
        return test_input

    @classmethod
    def generate_tests(cls, schema=None, graphson=None, traversal=False):
        """Generate tests for a graph configuration"""
        def decorator(klass):
            if DSE_VERSION:
                predicate = inspect.isfunction
                for name, func in inspect.getmembers(klass, predicate=predicate):
                    if not name.startswith('_test'):
                        continue
                    for _schema, _graphson in cls.configurations(schema, graphson):
                        if traversal and _graphson == GraphProtocol.GRAPHSON_1_0:
                            continue
                        test_input = cls._make_graph_test_method(func, _schema, _graphson)
                        log.debug("Generated test '{}.{}'".format(klass.__name__, test_input.__name__))
                        setattr(klass, test_input.__name__, test_input)
                return klass

        return decorator

    @classmethod
    def generate_schema_tests(cls, schema=None):
        """Generate schema tests for a graph configuration"""
        def decorator(klass):
            if DSE_VERSION:
                predicate = inspect.isfunction
                for name, func in inspect.getmembers(klass, predicate=predicate):
                    if not name.startswith('_test'):
                        continue
                    for _schema in cls.schema_configurations(schema):
                        test_input = cls._make_graph_schema_test_method(func, _schema)
                        log.debug("Generated test '{}.{}'".format(klass.__name__, test_input.__name__))
                        setattr(klass, test_input.__name__, test_input)
            return klass

        return decorator


class VertexLabel(object):
    """
    Helper that represents a new VertexLabel:

    VertexLabel(['Int()', 'Float()'])  # a vertex with 2 properties named property1 and property2
    VertexLabel([('int1', 'Int()'), 'Float()'])  # a vertex with 2 properties named int1 and property1
    """

    id = 0
    label = None
    properties = None

    def __init__(self, properties):
        VertexLabel.id += 1
        self.id = VertexLabel.id
        self.label = "vertex{}".format(self.id)
        self.properties = {'pkid': self.id}
        property_count = 0
        for p in properties:
            if isinstance(p, tuple):
                name, typ = p
            else:
                property_count += 1
                name = "property-v{}-{}".format(self.id, property_count)
                typ = p
            self.properties[name] = typ

    @property
    def non_pk_properties(self):
        return {p: v for p, v in self.properties.items() if p != 'pkid'}


class GraphSchema(object):

    has_geo_bounds = DSE_VERSION and DSE_VERSION >= Version('5.1')
    fixtures = GraphFixtures

    @classmethod
    def sanitize_type(cls, typ):
        if typ.lower().startswith("point"):
            return cls.sanitize_point_type()
        elif typ.lower().startswith("line"):
            return cls.sanitize_line_type()
        elif typ.lower().startswith("poly"):
            return cls.sanitize_polygon_type()
        else:
            return typ

    @classmethod
    def sanitize_point_type(cls):
        return "Point().withGeoBounds()" if cls.has_geo_bounds else "Point()"

    @classmethod
    def sanitize_line_type(cls):
        return "Linestring().withGeoBounds()" if cls.has_geo_bounds else "Linestring()"

    @classmethod
    def sanitize_polygon_type(cls):
        return "Polygon().withGeoBounds()" if cls.has_geo_bounds else "Polygon()"

    @staticmethod
    def drop_graph(session, graph_name):
        ks = list(session.execute(
            "SELECT * FROM system_schema.keyspaces WHERE keyspace_name = '{}';".format(graph_name)))
        if not ks:
            return

        try:
            session.execute_graph('system.graph(name).drop()', {'name': graph_name},
                                  execution_profile=EXEC_PROFILE_GRAPH_SYSTEM_DEFAULT)
        except:
            pass

    @staticmethod
    def create_graph(session, graph_name):
        raise NotImplementedError()

    @staticmethod
    def clear(session):
        pass

    @staticmethod
    def create_vertex_label(session, vertex_label, execution_profile=EXEC_PROFILE_GRAPH_DEFAULT):
        raise NotImplementedError()

    @staticmethod
    def add_vertex(session, vertex_label, name, value, execution_profile=EXEC_PROFILE_GRAPH_DEFAULT):
        raise NotImplementedError()

    @classmethod
    def ensure_properties(cls, session, obj, execution_profile=EXEC_PROFILE_GRAPH_DEFAULT):
        if not isinstance(obj, (Vertex, Edge)):
            return

        # This pre-processing is due to a change in TinkerPop
        # properties are not returned automatically anymore
        # with some queries.
        if not obj.properties:
            if isinstance(obj, Edge):
                obj.properties = {}
                for p in cls.get_edge_properties(session, obj, execution_profile=execution_profile):
                    obj.properties.update(p)
            elif isinstance(obj, Vertex):
                obj.properties = {
                    p.label: p
                    for p in cls.get_vertex_properties(session, obj, execution_profile=execution_profile)
                }

    @staticmethod
    def get_vertex_properties(session, vertex, execution_profile=EXEC_PROFILE_GRAPH_DEFAULT):
        return session.execute_graph("g.V(vertex_id).properties().toList()", {'vertex_id': vertex.id},
                                     execution_profile=execution_profile)

    @staticmethod
    def get_edge_properties(session, edge, execution_profile=EXEC_PROFILE_GRAPH_DEFAULT):
        v = session.execute_graph("g.E(edge_id).properties().toList()", {'edge_id': edge.id},
                                     execution_profile=execution_profile)
        return v


class ClassicGraphSchema(GraphSchema):

    fixtures = ClassicGraphFixtures

    @staticmethod
    def create_graph(session, graph_name):
        session.execute_graph(CREATE_CLASSIC_GRAPH, {'name': graph_name},
                              execution_profile=EXEC_PROFILE_GRAPH_SYSTEM_DEFAULT)
        wait_for_graph_inserted(session, graph_name)

    @staticmethod
    def clear(session):
        session.execute_graph('schema.clear()')

    @classmethod
    def create_vertex_label(cls, session, vertex_label, execution_profile=EXEC_PROFILE_GRAPH_DEFAULT):
        statements = ["schema.propertyKey('pkid').Int().ifNotExists().create();"]
        for k, v in vertex_label.non_pk_properties.items():
            typ = cls.sanitize_type(v)
            statements.append("schema.propertyKey('{name}').{type}.create();".format(
                name=k, type=typ
            ))

        statements.append("schema.vertexLabel('{label}').partitionKey('pkid').properties(".format(
            label=vertex_label.label))
        property_names = [name for name in vertex_label.non_pk_properties.keys()]
        statements.append(", ".join(["'{}'".format(p) for p in property_names]))
        statements.append(").create();")

        to_run = "\n".join(statements)
        session.execute_graph(to_run, execution_profile=execution_profile)

    @staticmethod
    def add_vertex(session, vertex_label, name, value, execution_profile=EXEC_PROFILE_GRAPH_DEFAULT):
        statement = "g.addV('{label}').property('pkid', {pkid}).property('{property_name}', val);".format(
            pkid=vertex_label.id, label=vertex_label.label, property_name=name)
        parameters = {'val': value}
        return session.execute_graph(statement, parameters, execution_profile=execution_profile)


class CoreGraphSchema(GraphSchema):

    fixtures = CoreGraphFixtures

    @classmethod
    def sanitize_type(cls, typ):
        typ = super(CoreGraphSchema, cls).sanitize_type(typ)
        return typ.replace('()', '')

    @classmethod
    def sanitize_point_type(cls):
        return "Point"

    @classmethod
    def sanitize_line_type(cls):
        return "LineString"

    @classmethod
    def sanitize_polygon_type(cls):
        return "Polygon"

    @staticmethod
    def create_graph(session, graph_name):
        session.execute_graph('system.graph(name).create()', {'name': graph_name},
                              execution_profile=EXEC_PROFILE_GRAPH_SYSTEM_DEFAULT)
        wait_for_graph_inserted(session, graph_name)

    @classmethod
    def create_vertex_label(cls, session, vertex_label, execution_profile=EXEC_PROFILE_GRAPH_DEFAULT):
        statements = ["schema.vertexLabel('{label}').partitionBy('pkid', Int)".format(
            label=vertex_label.label)]

        for name, typ in vertex_label.non_pk_properties.items():
            typ = cls.sanitize_type(typ)
            statements.append(".property('{name}', {type})".format(name=name, type=typ))
        statements.append(".create();")

        to_run = "\n".join(statements)
        session.execute_graph(to_run, execution_profile=execution_profile)

    @staticmethod
    def add_vertex(session, vertex_label, name, value, execution_profile=EXEC_PROFILE_GRAPH_DEFAULT):
        statement = "g.addV('{label}').property('pkid', {pkid}).property('{property_name}', val);".format(
            pkid=vertex_label.id, label=vertex_label.label, property_name=name)
        parameters = {'val': value}
        return session.execute_graph(statement, parameters, execution_profile=execution_profile)
