# 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.
#

"""
Unit tests for the GremlinLang Class. Modified from deprecated Groovy Translator Tests.
"""
import uuid

from gremlin_python.process.strategies import ReadOnlyStrategy, SubgraphStrategy, OptionsStrategy, PartitionStrategy
from gremlin_python.process.traversal import within, eq, T, Order, Scope, Column, Operator, P, Pop, Cardinality, \
    between, inside, WithOptions, ShortestPath, starting_with, ending_with, containing, gt, lte, GValue
from gremlin_python.statics import SingleByte, short, long, bigint, BigDecimal, SingleChar
from gremlin_python.structure.graph import Graph, Vertex
from gremlin_python.process.anonymous_traversal import traversal
from gremlin_python.process.graph_traversal import __
from datetime import datetime, timezone, timedelta


class TestGremlinLang(object):

    def test_gremlin_lang(self):
        g = traversal().with_(None)

        tests = list()
        # 0
        tests.append([g.V(),
                      "g.V()"])
        # 1
        tests.append([g.V('1', '2', '3', '4'),
                      "g.V('1','2','3','4')"])
        # 2
        tests.append([g.V('3').value_map(True),
                      "g.V('3').valueMap(true)"])
        # 3
        tests.append([g.V().constant(5),
                      "g.V().constant(5)"])
        # 4
        tests.append([g.V().constant(1.5),
                      "g.V().constant(1.5D)"])
        # 5
        tests.append([g.V().constant('Hello'),
                      "g.V().constant('Hello')"])
        # 6
        tests.append([g.V().has_label('airport').limit(5),
                      "g.V().hasLabel('airport').limit(5)"])
        # 7
        tests.append([g.V().has_label(within('a', 'b', 'c')),
                      "g.V().hasLabel(within(['a','b','c']))"])
        # 8
        tests.append([g.V().has_label('airport', 'continent').out().limit(5),
                      "g.V().hasLabel('airport','continent').out().limit(5)"])
        # 9
        tests.append([g.V().has_label('airport').out().values('code').limit(5),
                      "g.V().hasLabel('airport').out().values('code').limit(5)"])
        # 10
        tests.append([g.V('3').as_('a').out('route').limit(10).where(eq('a')).by('region'),
                      "g.V('3').as('a').out('route').limit(10).where(eq('a')).by('region')"])
        # 11
        tests.append([g.V('3').repeat(__.out('route').simple_path()).times(2).path().by('code'),
                      "g.V('3').repeat(__.out('route').simplePath()).times(2).path().by('code')"])
        # 12
        tests.append([g.V().has_label('airport').out().has('region', 'US-TX').values('code').limit(5),
                      "g.V().hasLabel('airport').out().has('region','US-TX').values('code').limit(5)"])
        # 13
        tests.append([g.V().has_label('airport').union(__.values('city'), __.values('region')).limit(5),
                      "g.V().hasLabel('airport').union(__.values('city'),__.values('region')).limit(5)"])
        # 14
        tests.append([g.V('3').as_('a').out('route', 'routes'),
                      "g.V('3').as('a').out('route','routes')"])
        # 15
        tests.append([g.V().where(__.values('runways').is_(5)),
                      "g.V().where(__.values('runways').is(5))"])
        # 16
        tests.append([g.V('3').repeat(__.out().simple_path()).until(__.has('code', 'AGR')).path().by('code').limit(5),
                      "g.V('3').repeat(__.out().simplePath()).until(__.has('code','AGR')).path().by('code').limit(5)"])
        # 17
        tests.append([g.V().has_label('airport').order().by(__.id_()),
                      "g.V().hasLabel('airport').order().by(__.id())"])
        # 18
        tests.append([g.V().has_label('airport').order().by(T.id),
                      "g.V().hasLabel('airport').order().by(T.id)"])
        # 19
        tests.append([g.V().has_label('airport').order().by(__.id_(), Order.desc),
                      "g.V().hasLabel('airport').order().by(__.id(),Order.desc)"])
        # 20
        tests.append([g.V().has_label('airport').order().by('code', Order.desc),
                      "g.V().hasLabel('airport').order().by('code',Order.desc)"])
        # 21
        tests.append([g.V('1', '2', '3').local(__.out().out().dedup().fold()),
                      "g.V('1','2','3').local(__.out().out().dedup().fold())"])
        # 22
        tests.append([g.V('3').out().path().count(Scope.local),
                      "g.V('3').out().path().count(Scope.local)"])
        # 23
        tests.append([g.E().count(),
                      "g.E().count()"])
        # 24
        tests.append([g.V('5').out_e('route').in_v().path().limit(10),
                      "g.V('5').outE('route').inV().path().limit(10)"])
        # 25
        tests.append([g.V('5').property_map().select(Column.keys),
                      "g.V('5').propertyMap().select(Column.keys)"])
        # 26
        tests.append([g.V('5').property_map().select(Column.values),
                      "g.V('5').propertyMap().select(Column.values)"])
        # 27
        tests.append([g.V('3').values('runways').math('_ + 1'),
                      "g.V('3').values('runways').math('_ + 1')"])
        # 28
        tests.append([g.V('3').emit().repeat(__.out().simple_path()).times(3).limit(5).path(),
                      "g.V('3').emit().repeat(__.out().simplePath()).times(3).limit(5).path()"])
        # 29
        tests.append([g.V().match(__.as_('a').has('code', 'LHR').as_('b')).select('b').by('code'),
                      "g.V().match(__.as('a').has('code','LHR').as('b')).select('b').by('code')"])
        # 30
        tests.append([g.V().has('test-using-keyword-as-property', 'repeat'),
                      "g.V().has('test-using-keyword-as-property','repeat')"])
        # 31
        tests.append([g.V('1').add_e('test').to(__.V('4')),
                      "g.V('1').addE('test').to(__.V('4'))"])
        # 32
        tests.append([g.V().values('runways').max_(),
                      "g.V().values('runways').max()"])
        # 33
        tests.append([g.V().values('runways').min_(),
                      "g.V().values('runways').min()"])
        # 34
        tests.append([g.V().values('runways').sum_(),
                      "g.V().values('runways').sum()"])
        # 35
        tests.append([g.V().values('runways').mean(),
                      "g.V().values('runways').mean()"])
        # 36
        tests.append([g.with_sack(0).V('3', '5').sack(Operator.sum_).by('runways').sack(),
                      "g.withSack(0).V('3','5').sack(Operator.sum).by('runways').sack()"])
        # 37 - removed as store() was deprecated and removed in 3.8.0
        # 38
        tests.append([g.inject(3, 4, 5),
                      "g.inject(3,4,5)"])
        # 39
        tests.append([g.inject([3, 4, 5]),
                      "g.inject([3,4,5])"])
        # 40
        tests.append([g.inject(3, 4, 5).count(),
                      "g.inject(3,4,5).count()"])
        # 41
        tests.append([g.V().has('runways', gt(5)).count(),
                      "g.V().has('runways',gt(5)).count()"])
        # 42
        tests.append([g.V().has('runways', lte(5.3)).count(),
                      "g.V().has('runways',lte(5.3D)).count()"])
        # 43
        tests.append([g.V().has('code', within(123, 124)),
                      "g.V().has('code',within([123,124]))"])
        # 44
        tests.append([g.V().has('code', within(123, 'abc')),
                      "g.V().has('code',within([123,'abc']))"])
        # 45
        tests.append([g.V().has('code', within('abc', 123)),
                      "g.V().has('code',within(['abc',123]))"])
        # 46
        tests.append([g.V().has('code', within('abc', 'xyz')),
                      "g.V().has('code',within(['abc','xyz']))"])
        # 47
        tests.append([g.V('1', '2').has('region', P.within('US-TX', 'US-GA')),
                      "g.V('1','2').has('region',within(['US-TX','US-GA']))"])
        # 48
        tests.append([g.V().and_(__.has('runways', P.gt(5)), __.has('region', 'US-TX')),
                      "g.V().and(__.has('runways',gt(5)),__.has('region','US-TX'))"])
        # 49
        tests.append([g.V().union(__.has('runways', gt(5)), __.has('region', 'US-TX')),
                      "g.V().union(__.has('runways',gt(5)),__.has('region','US-TX'))"])
        # 50
        tests.append([g.V('3').choose(__.values('runways').is_(3), __.constant('three'), __.constant('not three')),
                      "g.V('3').choose(__.values('runways').is(3),__.constant('three'),__.constant('not three'))"])
        # 51
        tests.append(
            [g.V('3').choose(__.values('runways')).option(1, __.constant('three')).option(2, __.constant('not three')),
             "g.V('3').choose(__.values('runways')).option(1,__.constant('three')).option(2,__.constant('not three'))"])
        # 52
        tests.append([g.V('3').choose(__.values('runways')).option(1.5, __.constant('one and a half')).option(2, __.constant('not three')),
                      "g.V('3').choose(__.values('runways')).option(1.5D,__.constant('one and a half')).option(2,__.constant('not three'))"])
        # 53
        tests.append([g.V('3').repeat(__.out().simple_path()).until(__.loops().is_(1)).count(),
                      "g.V('3').repeat(__.out().simplePath()).until(__.loops().is(1)).count()"])
        # 54
        tests.append(
            [g.V().has_label('airport').limit(20).group().by('region').by('code').order(Scope.local).by(Column.keys),
             "g.V().hasLabel('airport').limit(20).group().by('region').by('code').order(Scope.local).by(Column.keys)"])
        # 55
        tests.append([g.V('1').as_('a').V('2').as_('a').select(Pop.all_, 'a'),
                      "g.V('1').as('a').V('2').as('a').select(Pop.all,'a')"])
        # 56
        tests.append([g.add_v('test').property(Cardinality.set_, 'p1', 10),
                      "g.addV('test').property(Cardinality.set,'p1',10)"])
        # 57
        tests.append([g.add_v('test').property(Cardinality.list_, 'p1', 10),
                      "g.addV('test').property(Cardinality.list,'p1',10)"])

        # 58
        tests.append([g.add_v('test').property(Cardinality.single, 'p1', 10),
                      "g.addV('test').property(Cardinality.single,'p1',10)"])
        # 59
        tests.append([g.V().limit(5).order().by(T.label),
                      "g.V().limit(5).order().by(T.label)"])

        # 60
        tests.append([g.V().range_(1, 5),
                      "g.V().range(1,5)"])

        # 61
        tests.append([g.add_v('test').property('p1', 123),
                      "g.addV('test').property('p1',123)"])

        # 62
        tests.append([g.add_v('test').property('date', datetime(2021, 2, 1, 9, 30)),
                      "g.addV('test').property('date',datetime(\"2021-02-01T09:30:00\"))"])
        # 63
        tests.append([g.add_v('test').property('date', datetime(2021, 2, 1)),
                      "g.addV('test').property('date',datetime(\"2021-02-01T00:00:00\"))"])
        # 64
        tests.append([g.add_e('route').from_(__.V('1')).to(__.V('2')),
                      "g.addE('route').from(__.V('1')).to(__.V('2'))"])
        # 65
        tests.append([g.with_side_effect('a', [1, 2]).V('3').select('a'),
                      "g.withSideEffect('a',[1,2]).V('3').select('a')"])
        # 66
        tests.append([g.with_side_effect('a', 1).V('3').select('a'),
                      "g.withSideEffect('a',1).V('3').select('a')"])
        # 67
        tests.append([g.with_side_effect('a', 'abc').V('3').select('a'),
                      "g.withSideEffect('a','abc').V('3').select('a')"])
        # 68
        tests.append([g.V().has('airport', 'region', 'US-NM').limit(3).values('elev').fold().index(),
                      "g.V().has('airport','region','US-NM').limit(3).values('elev').fold().index()"])
        # 69
        tests.append([g.V('3').repeat(__.time_limit(1000).out().simple_path()).until(__.has('code', 'AGR')).path(),
                      "g.V('3').repeat(__.timeLimit(1000).out().simplePath()).until(__.has('code','AGR')).path()"])

        # 70
        tests.append([g.V().has_label('airport').where(__.values('elev').is_(gt(14000))),
                      "g.V().hasLabel('airport').where(__.values('elev').is(gt(14000)))"])

        # 71
        tests.append([g.V().has_label('airport').where(__.out().count().is_(gt(250))).values('code'),
                      "g.V().hasLabel('airport').where(__.out().count().is(gt(250))).values('code')"])

        # 72
        tests.append([g.V().has_label('airport').filter_(__.out().count().is_(gt(250))).values('code'),
                      "g.V().hasLabel('airport').filter(__.out().count().is(gt(250))).values('code')"])
        # 73
        tests.append([g.with_sack(0).
                     V('3').
                     repeat(__.out_e('route').sack(Operator.sum_).by('dist').in_v()).
                     until(__.has('code', 'AGR').or_().loops().is_(4)).
                     has('code', 'AGR').
                     local(__.union(__.path().by('code').by('dist'), __.sack()).fold()).
                     limit(10),
                      "g.withSack(0).V('3').repeat(__.outE('route').sack(Operator.sum).by('dist').inV()).until(__.has('code','AGR').or().loops().is(4)).has('code','AGR').local(__.union(__.path().by('code').by('dist'),__.sack()).fold()).limit(10)"])

        # 74
        tests.append([g.add_v().as_('a').add_v().as_('b').add_e('knows').from_('a').to('b'),
                      "g.addV().as('a').addV().as('b').addE('knows').from('a').to('b')"])

        # 75
        tests.append([g.add_v('Person').as_('a').add_v('Person').as_('b').add_e('knows').from_('a').to('b'),
                      "g.addV('Person').as('a').addV('Person').as('b').addE('knows').from('a').to('b')"])

        # 76
        tests.append([g.V('3').project('Out', 'In').by(__.out().count()).by(__.in_().count()),
                      "g.V('3').project('Out','In').by(__.out().count()).by(__.in().count())"])

        # 77
        tests.append([g.V('44').out().aggregate('a').out().where(within('a')).path(),
                      "g.V('44').out().aggregate('a').out().where(within(['a'])).path()"])

        # 78
        tests.append([g.V().has('date', datetime(2021, 2, 22)),
                      "g.V().has('date',datetime(\"2021-02-22T00:00:00\"))"])

        # 79
        tests.append([g.V().has('date', within(datetime(2021, 2, 22), datetime(2021, 1, 1))),
                      "g.V().has('date',within([datetime(\"2021-02-22T00:00:00\"),datetime(\"2021-01-01T00:00:00\")]))"])

        # 80
        tests.append([g.V().has('date', between(datetime(2021, 1, 1), datetime(2021, 2, 22))),
                      "g.V().has('date',between(datetime(\"2021-01-01T00:00:00\"),datetime(\"2021-02-22T00:00:00\")))"])

        # 81
        tests.append([g.V().has('date', inside(datetime(2021, 1, 1), datetime(2021, 2, 22))),
                      "g.V().has('date',inside(datetime(\"2021-01-01T00:00:00\"),datetime(\"2021-02-22T00:00:00\")))"])

        # 82
        tests.append([g.V().has('date', P.gt(datetime(2021, 1, 1, 9, 30))),
                      "g.V().has('date',gt(datetime(\"2021-01-01T09:30:00\")))"])

        # 83
        tests.append([g.V().has('runways', between(3, 5)),
                      "g.V().has('runways',between(3,5))"])

        # 84
        tests.append([g.V().has('runways', inside(3, 5)),
                      "g.V().has('runways',inside(3,5))"])

        # 85
        tests.append([g.V('44').out_e().element_map(),
                      "g.V('44').outE().elementMap()"])

        # 86
        tests.append([g.V('44').value_map().by(__.unfold()),
                      "g.V('44').valueMap().by(__.unfold())"])

        # 87
        tests.append([g.V('44').value_map().with_(WithOptions.tokens, WithOptions.labels),
                      "g.V('44').valueMap().with('~tinkerpop.valueMap.tokens',2)"])

        # 88
        tests.append([g.V('44').value_map().with_(WithOptions.tokens),
                      "g.V('44').valueMap().with('~tinkerpop.valueMap.tokens')"])

        # 89
        tests.append([g.with_strategies(ReadOnlyStrategy()).add_v('test'),
                      "g.withStrategies(ReadOnlyStrategy).addV('test')"])
        # 90
        strategy = SubgraphStrategy(vertices=__.has('region', 'US-TX'), edges=__.has_label('route'))
        tests.append([g.with_strategies(strategy).V().count(),
                      "g.withStrategies(new SubgraphStrategy(vertices:__.has('region','US-TX'),edges:__.hasLabel('route'))).V().count()"])

        # 91
        strategy = SubgraphStrategy(vertex_properties=__.has_not('runways'))
        tests.append([g.with_strategies(strategy).V().count(),
                      "g.withStrategies(new SubgraphStrategy(vertexProperties:__.hasNot('runways'))).V().count()"])

        # 92
        strategy = SubgraphStrategy(vertices=__.has('region', 'US-TX'), vertex_properties=__.has_not('runways'))
        tests.append([g.with_strategies(strategy).V().count(),
                      "g.withStrategies(new SubgraphStrategy(vertices:__.has('region','US-TX'),vertexProperties:__.hasNot('runways'))).V().count()"])

        # 93
        strategy = SubgraphStrategy(vertices=__.has('region', 'US-TX'), edges=__.has_label('route'))
        tests.append([g.with_strategies(ReadOnlyStrategy(), strategy).V().count(),
                      "g.withStrategies(ReadOnlyStrategy,new SubgraphStrategy(vertices:__.has('region','US-TX'),edges:__.hasLabel('route'))).V().count()"])

        # 94
        strategy = SubgraphStrategy(vertices=__.has('region', 'US-TX'))
        tests.append([g.with_strategies(ReadOnlyStrategy(), strategy).V().count(),
                      "g.withStrategies(ReadOnlyStrategy,new SubgraphStrategy(vertices:__.has('region','US-TX'))).V().count()"])

        # 95 Note with_() options are now extracted into request message and is no longer sent with the script
        tests.append([g.with_('timeoutMillis', 500).V().count(),
                      "g.V().count()"])

        # 96 Note OptionsStrategy are now extracted into request message and is no longer sent with the script
        tests.append([g.with_strategies(OptionsStrategy(timeoutMillis=500)).V().count(),
                      "g.V().count()"])

        # 97
        tests.append([g.with_strategies(
            PartitionStrategy(partition_key="partition", write_partition="a", read_partitions=["a"])).add_v('test'),
                      "g.withStrategies(new PartitionStrategy(partitionKey:'partition',writePartition:'a',readPartitions:['a'])).addV('test')"])

        # 98
        tests.append([g.with_computer().V().shortest_path().with_(ShortestPath.target, __.has('name', 'peter')),
                      "g.withStrategies(VertexProgramStrategy).V().shortestPath().with('~tinkerpop.shortestPath.target',__.has('name','peter'))"])

        # 99
        tests.append([g.V().coalesce(__.E(), __.add_e('person')),
                      "g.V().coalesce(__.E(),__.addE('person'))"])

        # 100
        tests.append([g.inject(1).E(),
                      "g.inject(1).E()"])

        # 101
        tests.append([g.V().has("p1", starting_with("foo")),
                      "g.V().has('p1',startingWith('foo'))"])

        # 102
        tests.append([g.V().has("p1", ending_with("foo")),
                      "g.V().has('p1',endingWith('foo'))"])

        # 103
        class SuperStr(str):
            pass

        tests.append([g.V(SuperStr("foo_id")),
                      "g.V('foo_id')"])

        # 104
        tests.append([g.V().has("p1", containing(SuperStr("foo"))),
                      "g.V().has('p1',containing('foo'))"])

        # 105
        tests.append([g.V().has("p1", None),
                      "g.V().has('p1',null)"])

        # 106
        vertex = Vertex(0, "person")
        tests.append([g.V(vertex),
                      "g.V(0)"])

        # 107
        tests.append([g.inject({'name': 'java'}, {T.id: 0}, {},
                               {'age': float(10), 'pos_inf': float("inf"), 'neg_inf': float("-inf"),
                                'nan': float("nan")}),
                      "g.inject(['name':'java'],[(T.id):0],[:],['age':10.0D,'pos_inf':+Infinity,'neg_inf':-Infinity,'nan':NaN])"])

        # 108
        tests.append([g.inject(float(1)).is_(P.eq(1).or_(P.gt(2)).or_(P.lte(3)).or_(P.gte(4))),
                      "g.inject(1.0D).is(eq(1).or(gt(2)).or(lte(3)).or(gte(4)))"])

        # 109
        tests.append([g.V().has_label('person').has('age', P.gt(10).or_(P.gte(11).and_(P.lt(20))).and_(
            P.lt(29).or_(P.eq(35)))).name,
                      "g.V().hasLabel('person').has('age',gt(10).or(gte(11).and(lt(20))).and(lt(29).or(eq(35)))).values('name')"])

        # 110
        tests.append([g.inject(set('a')),
                      "g.inject({'a'})"])

        # 111
        tests.append([g.merge_v(None),
                      "g.mergeV(null)"])

        # 112
        tests.append([g.merge_e(None),
                      "g.mergeE(null)"])

        # 113
        tests.append([g.add_v('\"test\"'),
                      "g.addV('\"test\"')"])

        # 114
        tests.append([g.add_v('t\'"est'),
                      "g.addV('t\\'\"est')"])

        # 115
        tests.append([g.inject(True, SingleByte(1), short(2), 3, long(4), float(5),
                               float(6), bigint(7), BigDecimal(0, 8)),
                      "g.inject(true,1B,2S,3,4L,5.0D,6.0D,7N,8M)"])

        # 116
        tests.append([g.inject(uuid.UUID('9b8d8a9c-61c2-43e5-9cc8-c27b9261290e')),
                      'g.inject(UUID("9b8d8a9c-61c2-43e5-9cc8-c27b9261290e"))'])

        # 117
        tests.append([g.add_v('test').property('date', datetime(2021, 2, 1, 9, 30, tzinfo=timezone.utc)),
                      "g.addV('test').property('date',datetime(\"2021-02-01T09:30:00+00:00\"))"])

        # 118
        tests.append([g.add_v('test').property('date', datetime(2021, 2, 1, 9, 30, tzinfo=timezone(timedelta(hours=7)))),
                      "g.addV('test').property('date',datetime(\"2021-02-01T09:30:00+07:00\"))"])

        # 119
        tests.append([g.add_v('test').property('date', datetime(2021, 2, 1, 9, 30, tzinfo=timezone(timedelta(hours=-5)))),
                      "g.addV('test').property('date',datetime(\"2021-02-01T09:30:00-05:00\"))"])

        # 120 - Character backslash (not in feature file due to Gherkin escaping issues)
        tests.append([g.inject(SingleChar('\\')),
                      "g.inject('\\\\'c)"])

        # 121 - Character single quote (no feature equivalent)
        tests.append([g.inject(SingleChar("'")),
                      "g.inject(\"'\"c)"])

        for t in range(len(tests)):
            gremlin_lang = tests[t][0].gremlin_lang.get_gremlin()
            assert gremlin_lang == tests[t][1]

    def test_gvalue_name_cannot_be_null(self):
        try:
            GValue(None, [1, 2, 3])
            assert False, 'expected exception for null name'
        except Exception as ex:
            assert str(ex) == 'GValue name cannot be null.'

    def test_gvalue_name_mid_string_dollar_accepted(self):
        assert GValue('a$b', [1, 2, 3]).get_name() == 'a$b'

    def test_gvalue_name_unicode_letter_accepted(self):
        g = traversal().with_(None)
        p = GValue('café', 42)
        gremlin = g.V(p).gremlin_lang
        assert 'g.V(café)' == gremlin.get_gremlin()
        assert 42 == gremlin.get_parameters().get('café')

    def test_gvalue_underscore_name_accepted(self):
        assert GValue('_1', [1, 2, 3]).get_name() == '_1'

    def test_gvalue_is_not_duplicate(self):
        g = traversal().with_(None)
        try:
            g.inject(GValue('ids', [1, 2])).V(GValue('ids', [2, 3]))
        except Exception as ex:
            assert str(ex) == 'parameter with name ids already exists.'

    def test_gvalue_allow_parameter_reuse(self):
        g = traversal().with_(None)
        val = [1, 2, 3]
        p = GValue('ids', val)
        gremlin = g.inject(p).V(p).gremlin_lang
        assert 'g.inject(ids).V(ids)' == gremlin.get_gremlin()
        assert val == gremlin.get_parameters().get('ids')

    def test_gvalue_nested_in_child_traversal(self):
        g = traversal().with_(None)
        gremlin = g.V().where(__.is_(GValue('xx1', 1))).gremlin_lang
        assert 'g.V().where(__.is(xx1))' == gremlin.get_gremlin()
        assert 1 == gremlin.get_parameters().get('xx1')

    def test_gvalue_nested_across_multiple_child_traversals(self):
        g = traversal().with_(None)
        gremlin = g.V().union(__.V(GValue('vid1', 1)), __.V(GValue('vid4', 4))).gremlin_lang
        assert 'g.V().union(__.V(vid1),__.V(vid4))' == gremlin.get_gremlin()
        assert 1 == gremlin.get_parameters().get('vid1')
        assert 4 == gremlin.get_parameters().get('vid4')

    def test_gvalue_mid_string_underscore_accepted(self):
        g = traversal().with_(None)
        p = GValue('a_b', 42)
        gremlin = g.V(p).gremlin_lang
        assert 'g.V(a_b)' == gremlin.get_gremlin()
        assert 42 == gremlin.get_parameters().get('a_b')

    def test_gvalue_underscore_start_name_in_traversal(self):
        g = traversal().with_(None)
        gremlin = g.V(GValue('_1', [1, 2, 3])).gremlin_lang
        assert 'g.V(_1)' == gremlin.get_gremlin()
        assert [1, 2, 3] == gremlin.get_parameters().get('_1')

    def test_gvalue_dollar_name_in_traversal(self):
        g = traversal().with_(None)
        gremlin = g.V(GValue('a$b', 1)).gremlin_lang
        assert 'g.V(a$b)' == gremlin.get_gremlin()
        assert 1 == gremlin.get_parameters().get('a$b')

    def test_gvalue_invalid_name_raises_in_traversal(self):
        import pytest
        g = traversal().with_(None)
        with pytest.raises(Exception, match='Invalid parameter name'):
            g.V(GValue('1a', 1)).gremlin_lang.get_gremlin()

    def test_gvalue_construction_and_accessors(self):
        p = GValue('x', 1)
        assert 'x' == p.get_name()
        assert 1 == p.get()
        assert p.is_null() is False

    def test_gvalue_is_null(self):
        p = GValue('n', None)
        assert p.is_null() is True
        q = GValue('m', 0)
        assert q.is_null() is False

    def test_gvalue_string_representation(self):
        p = GValue('x', 1)
        assert repr(p) == 'x=1'
        assert str(p) == 'x=1'

    def test_gvalue_cannot_be_nested(self):
        try:
            GValue('x', GValue('y', 1))
        except Exception as ex:
            assert str(ex) == 'GValues cannot be nested'

    def test_unsupported_type_throws(self):
        g = traversal().with_(None)
        import pytest
        with pytest.raises(TypeError, match='cannot be represented as text'):
            g.inject(object()).gremlin_lang.get_gremlin()

    def test_unsupported_type_in_list_throws(self):
        g = traversal().with_(None)
        import pytest
        with pytest.raises(TypeError, match='cannot be represented as text'):
            g.inject([1, object()]).gremlin_lang.get_gremlin()

    def test_unsupported_type_in_dict_throws(self):
        g = traversal().with_(None)
        import pytest
        with pytest.raises(TypeError, match='cannot be represented as text'):
            g.inject({'key': object()}).gremlin_lang.get_gremlin()

    def test_unsupported_type_in_convert_parameters_to_string_throws(self):
        from gremlin_python.process.traversal import GremlinLang
        import pytest
        with pytest.raises(TypeError, match='cannot be represented as text'):
            GremlinLang.convert_parameters_to_string({'x': object()})

    def test_convert_parameters_to_string_empty(self):
        from gremlin_python.process.traversal import GremlinLang
        assert '[:]' == GremlinLang.convert_parameters_to_string({})
        assert '[:]' == GremlinLang.convert_parameters_to_string(None)

    def test_convert_parameters_to_string_basic(self):
        from gremlin_python.process.traversal import GremlinLang
        result = GremlinLang.convert_parameters_to_string({'x': 1, 'name': 'marko'})
        assert result.startswith('[')
        assert result.endswith(']')
        assert "'x':1" in result
        assert "'name':'marko'" in result

    def test_convert_parameters_to_string_nested_list(self):
        from gremlin_python.process.traversal import GremlinLang
        result = GremlinLang.convert_parameters_to_string({'ids': [1, 2, 3]})
        assert "'ids':[1,2,3]" in result

    def test_convert_parameters_to_string_escaped_string(self):
        from gremlin_python.process.traversal import GremlinLang
        result = GremlinLang.convert_parameters_to_string({'name': "it's a test"})
        assert "'name'" in result
        assert "it" in result

    def test_provider_defined_auto_dehydration(self):
        from gremlin_python.structure.graph import CompositePDT, provider_defined
        g = traversal().with_(None)

        @provider_defined(name="com.example.Point")
        class Point:
            def __init__(self, x, y):
                self.x = x
                self.y = y

        p = Point(1, 2)
        gremlin = g.inject(p).gremlin_lang.get_gremlin()
        assert "PDT('com.example.Point',['x':1,'y':2])" in gremlin

    def test_pdt_adapter_takes_precedence_over_decorator(self):
        from gremlin_python.structure.graph import PDTRegistry, provider_defined
        g = traversal().with_(None)

        @provider_defined(name="com.example.Point")
        class Point:
            def __init__(self, x, y):
                self.x = x
                self.y = y

        registry = PDTRegistry()
        registry.register("com.adapter.Point",
                          deserialize_fn=lambda fields: Point(fields["a"], fields["b"]),
                          serialize_fn=lambda p: {"a": p.x, "b": p.y},
                          target_class=Point)
        g.gremlin_lang.pdt_registry = registry

        p = Point(3, 4)
        gremlin = g.inject(p).gremlin_lang.get_gremlin()
        # The adapter's type_name and fields must win over the decorator's
        assert "PDT('com.adapter.Point',['a':3,'b':4])" in gremlin

    def test_pdt_special_characters_in_name(self):
        from gremlin_python.structure.graph import CompositePDT
        g = traversal().with_(None)

        pdt = CompositePDT('say"hello"', {'v': 1})
        gremlin = g.inject(pdt).gremlin_lang.get_gremlin()
        assert "PDT('say\"hello\"',['v':1])" in gremlin

        pdt2 = CompositePDT('back\\slash', {'v': 1})
        gremlin2 = g.inject(pdt2).gremlin_lang.get_gremlin()
        assert "PDT('back\\\\slash',['v':1])" in gremlin2

    def test_pdt_nested(self):
        from gremlin_python.structure.graph import CompositePDT
        g = traversal().with_(None)

        inner = CompositePDT('Inner', {'v': 1})
        outer = CompositePDT('Outer', {'inner': inner})
        gremlin = g.inject(outer).gremlin_lang.get_gremlin()
        assert "PDT('Outer',['inner':PDT('Inner',['v':1])])" in gremlin

    def test_dehydrate_inner_decorated_in_unregistered_outer(self):
        """A @provider_defined inner object must dehydrate to PDT form even when nested
        as a field value inside a raw (unregistered/undecorated) CompositePDT."""
        from gremlin_python.structure.graph import CompositePDT, provider_defined
        g = traversal().with_(None)

        @provider_defined(name="com.example.Inner")
        class Inner:
            def __init__(self, val):
                self.val = val

        inner_obj = Inner(99)
        # Outer is a raw CompositePDT — no adapter, no decorator
        outer_pdt = CompositePDT("com.example.Outer", {"child": inner_obj, "count": 7})

        gremlin = g.inject(outer_pdt).gremlin_lang.get_gremlin()
        # The inner decorated object MUST be dehydrated to its PDT representation
        assert "PDT('com.example.Outer',['child':PDT('com.example.Inner',['val':99]),'count':7])" in gremlin

    def test_match_gql_query_bytecode(self):
        g = traversal().with_(None)
        query = 'MATCH (p:person)-[e:knows]->(friend:person)'
        assert ("g.match('" + query + "')") == g.match(query).gremlin_lang.get_gremlin()
        params = {'name': 'marko'}
        assert ("g.match('" + query + "',['name':'marko'])") == g.match(query, params).gremlin_lang.get_gremlin()

    def test_eq_different_gremlin_steps_not_equal(self):
        # Same (empty) parameters but different gremlin steps must not compare equal.
        g = traversal().with_(None)
        count = g.V().count().gremlin_lang
        drop = g.V().drop().gremlin_lang
        assert count.parameters == drop.parameters
        assert count.get_gremlin() != drop.get_gremlin()
        assert count != drop
        assert drop != count

    def test_eq_equivalent_are_equal(self):
        g = traversal().with_(None)
        first = g.V().count().gremlin_lang
        second = g.V().count().gremlin_lang
        assert first == second

    def test_eq_equal_gremlin_different_parameters_not_equal(self):
        g = traversal().with_(None)
        first = g.V(GValue('ids', 1)).gremlin_lang
        second = g.V(GValue('ids', 2)).gremlin_lang
        assert first.get_gremlin() == second.get_gremlin()
        assert first != second

    def test_eq_non_gremlinlang_not_equal(self):
        g = traversal().with_(None)
        gl = g.V().count().gremlin_lang
        assert gl != 'g.V().count()'
        assert gl != 42
        assert gl != None
