/*
 * 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.neo4j.process;

import org.apache.tinkerpop.gremlin.LoadGraphWith;
import org.apache.tinkerpop.gremlin.neo4j.AbstractNeo4jGremlinTest;
import org.apache.tinkerpop.gremlin.neo4j.structure.Neo4jGraph;
import org.apache.tinkerpop.gremlin.process.traversal.P;
import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal;
import org.apache.tinkerpop.gremlin.structure.T;
import org.apache.tinkerpop.gremlin.structure.Vertex;
import org.apache.tinkerpop.gremlin.util.TimeUtil;
import org.junit.Ignore;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;

import static org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__.as;
import static org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__.where;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

/**
 * @author Marko A. Rodriguez (http://markorodriguez.com)
 * @author Stephen Mallette (http://stephen.genoprime.com)
 */
public class NativeNeo4jCypherCheck extends AbstractNeo4jGremlinTest {
    private static final Logger logger = LoggerFactory.getLogger(NativeNeo4jCypherCheck.class);

    @Test
    @LoadGraphWith(LoadGraphWith.GraphData.MODERN)
    public void shouldExecuteCypher() throws Exception {
        this.graph.addVertex("name", "marko");
        this.graph.tx().commit();
        final Iterator<Map<String, Object>> result = this.getGraph().cypher("MATCH (a {name:\"marko\"}) RETURN a", Collections.emptyMap());
        assertNotNull(result);
        assertTrue(result.hasNext());
    }

    @Test
    @LoadGraphWith(LoadGraphWith.GraphData.MODERN)
    public void shouldExecuteCypherWithArgs() throws Exception {
        this.graph.addVertex("name", "marko");
        this.graph.tx().commit();
        final Map<String, Object> bindings = new HashMap<>();
        bindings.put("n", "marko");
        final Iterator<Map<String, Object>> result = this.getGraph().cypher("MATCH (a {name:{n}}) RETURN a", bindings);
        assertNotNull(result);
        assertTrue(result.hasNext());
    }

    @Test
    @LoadGraphWith(LoadGraphWith.GraphData.MODERN)
    public void shouldExecuteCypherWithArgsUsingVertexIdList() throws Exception {
        final Vertex v = this.graph.addVertex("name", "marko");
        final List<Object> idList = Arrays.asList(v.id());
        this.graph.tx().commit();

        final Map<String, Object> bindings = new HashMap<>();
        bindings.put("ids", idList);
        final Iterator<String> result = this.getGraph().cypher("START n=node({ids}) RETURN n", bindings).select("n").values("name");
        assertNotNull(result);
        assertTrue(result.hasNext());
        assertEquals("marko", result.next());
    }

    @Test
    @LoadGraphWith(LoadGraphWith.GraphData.MODERN)
    public void shouldExecuteCypherAndBackToGremlin() throws Exception {
        this.graph.addVertex("name", "marko", "age", 29, "color", "red");
        this.graph.addVertex("name", "marko", "age", 30, "color", "yellow");

        this.graph.tx().commit();
        final Traversal result = this.getGraph().cypher("MATCH (a {name:\"marko\"}) RETURN a").select("a").has("age", 29).values("color");
        assertNotNull(result);
        assertTrue(result.hasNext());
        assertEquals("red", result.next().toString());
    }

    @Test
    @LoadGraphWith(LoadGraphWith.GraphData.MODERN)
    public void shouldExecuteMultiIdWhereCypher() throws Exception {
        this.graph.addVertex("name", "marko", "age", 29, "color", "red");
        this.graph.addVertex("name", "marko", "age", 30, "color", "yellow");
        this.graph.addVertex("name", "marko", "age", 30, "color", "orange");
        this.graph.tx().commit();

        final List<Object> result = this.getGraph().cypher("MATCH (n) WHERE id(n) IN [1,2] RETURN n").select("n").id().toList();
        assertNotNull(result);
        assertEquals(2, result.size());
        assertTrue(result.contains(1l));
        assertTrue(result.contains(2l));
    }

    @Test
    @LoadGraphWith(LoadGraphWith.GraphData.MODERN)
    public void shouldExecuteMultiIdWhereWithParamCypher() throws Exception {
        final Vertex v1 = this.graph.addVertex("name", "marko", "age", 29, "color", "red");
        final Vertex v2 = this.graph.addVertex("name", "marko", "age", 30, "color", "yellow");
        this.graph.addVertex("name", "marko", "age", 30, "color", "orange");
        this.graph.tx().commit();

        final List<Object> ids = Arrays.asList(v1.id(), v2.id());
        final Map<String, Object> m = new HashMap<>();
        m.put("ids", ids);
        final List<Object> result = this.getGraph().cypher("MATCH (n) WHERE id(n) IN {ids} RETURN n", m).select("n").id().toList();
        assertNotNull(result);
        assertEquals(2, result.size());
        assertTrue(result.contains(v1.id()));
        assertTrue(result.contains(v2.id()));
    }

    @Test
    @Ignore
    @LoadGraphWith(LoadGraphWith.GraphData.GRATEFUL)
    public void benchmarkCypherAndMatch() throws Exception {
        final Neo4jGraph n = (Neo4jGraph) graph;
        final List<Supplier<GraphTraversal<?, ?>>> traversals = Arrays.asList(
                () -> g.V().match(
                        as("a").in("sungBy").as("b"),
                        as("a").in("writtenBy").as("b")).select("a", "b").by("name"),
                () -> n.cypher("MATCH (a)<-[:sungBy]-(b), (a)<-[:writtenBy]-(b) RETURN a.name, b.name"),
                ///
                () -> g.V().match(
                        as("a").out("followedBy").as("b"),
                        as("b").out("followedBy").as("a")).select("a", "b").by("name"),
                () -> n.cypher("MATCH (a)-[:followedBy]->(b), (b)-[:followedBy]->(a) RETURN a.name, b.name"),
                ///
                () -> g.V().match(
                        as("a").out("followedBy").count().is(P.gt(10)).as("b"),
                        as("a").in("followedBy").count().is(P.gt(10)).as("b")).
                        select("a").by("name"),
                () -> n.cypher("MATCH (a)-[:followedBy]->(b) WITH a, COUNT(b) AS bc WHERE bc > 10 MATCH (a)<-[:followedBy]-(c) WITH a, bc, COUNT(c) AS cc WHERE bc = cc RETURN a.name"),
                ///
                () -> g.V().match(
                        as("a").in("sungBy").count().as("b"),
                        as("a").in("sungBy").as("c"),
                        as("c").out("followedBy").as("d"),
                        as("d").out("sungBy").as("e"),
                        as("e").in("sungBy").count().as("b"),
                        where("a", P.neq("e"))).select("a", "e").by("name"),
                () -> n.cypher("MATCH (a)<-[:sungBy]-()-[:followedBy]->()-[:sungBy]->(e) WHERE a <> e WITH a, e MATCH (a)<-[:sungBy]-(b) WITH a, e, COUNT(DISTINCT b) as bc MATCH (e)<-[:sungBy]-(f) WITH a, e, bc, COUNT(DISTINCT f) as fc WHERE bc = fc RETURN a.name, e.name"),
                ///
                () -> g.V().match(
                        as("a").in("followedBy").as("b"),
                        as("a").out("sungBy").as("c"),
                        as("b").out("sungBy").as("c")).select("a", "b"),
                () -> n.cypher("MATCH (a)<-[:followedBy]-(b), (a)-[:sungBy]->(c), (b)-[:sungBy]->(c) RETURN a, b"),
                ///
                () -> g.V().match(
                        as("a").in("followedBy").as("b"),
                        as("a").out("sungBy").as("c"),
                        as("a").out("writtenBy").as("d"),
                        as("b").out("sungBy").as("c"),
                        as("b").out("writtenBy").as("d"),
                        where("c", P.neq("d"))).select("a", "b").by("name"),
                () -> n.cypher("MATCH (a)<-[:followedBy]-(b), (a)-[:sungBy]->(c), (a)-[:writtenBy]->(d), (b)-[:sungBy]->(c), (b)-[:writtenBy]->(d) WHERE c <> d RETURN a.name, b.name"),
                ///
                () -> g.V().match(
                        as("a").in("sungBy").as("b"),
                        as("a").in("writtenBy").as("b"),
                        as("b").out("followedBy").as("c"),
                        as("c").out("sungBy").as("a"),
                        as("c").out("writtenBy").as("a")).select("a").by("name"),
                () -> n.cypher("MATCH (a)<-[:sungBy]-(b), (a)<-[:writtenBy]-(b), (b)-[:followedBy]->(c), (c)-[:sungBy]->(a), (c)-[:writtenBy]->(a) RETURN a.name"),
                ///
                () -> g.V().match(
                        as("a").has("name", "Garcia").has(T.label, "artist"),
                        as("a").in("writtenBy").as("b"),
                        as("b").out("followedBy").as("c"),
                        as("c").out("writtenBy").as("d"),
                        as("d").where(P.neq("a"))).select("a", "d").by("name"),
                () -> n.cypher("MATCH (a)<-[:writtenBy]-(b), (b)-[:followedBy]->(c), (c)-[:writtenBy]->(d) WHERE a <> d AND a.name = 'Garcia' AND 'artist' IN labels(a) RETURN a.name, d.name"),
                ///
                () -> g.V().match(
                        as("a").out("followedBy").as("b"),
                        as("b").out("followedBy").as("c")).select("a").by("name"),
                () -> n.cypher("MATCH (a)-[:followedBy]->(b), (b)-[:followedBy]->(c) RETURN a.name"),
                ///
                () -> g.V().match(
                        as("a").out("followedBy").as("b"),
                        as("a").has(T.label, "song").has("performances", P.gt(10)),
                        as("a").out("writtenBy").as("c"),
                        as("b").out("writtenBy").as("c")).select("a", "b", "c").by("name"),
                () -> n.cypher("MATCH (a)-[:followedBy]->(b), (a)-[:writtenBy]->(c), (b)-[:writtenBy]->(c) WHERE a.performances > 10 AND 'song' IN labels(a) RETURN a, b, c").select("a", "b", "c").by("name")
        );

        for (int i = 0; i < traversals.size(); i = i + 2) {
            final Supplier<GraphTraversal<?, ?>> gremlin = traversals.get(i);
            final Supplier<GraphTraversal<?, ?>> cypher = traversals.get(i + 1);
            for (final Supplier<GraphTraversal<?, ?>> sub : Arrays.asList(cypher, gremlin)) {
                logger.info("pre-strategy:  {}", sub.get());
                logger.info("post-strategy: {}", sub.get().iterate());
                logger.info(TimeUtil.clockWithResult(25, () -> sub.get().count().next()).toString());
                logger.info("------------------");
            }
        }
    }

}