/*
 * Copyright (c) "Neo4j"
 * Neo4j Sweden AB [http://neo4j.com]
 *
 * This file is part of Neo4j.
 *
 * 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.
 */
package apoc.nodes;

import static apoc.nodes.NodesConfig.MAX_DEPTH_KEY;
import static apoc.nodes.NodesConfig.REL_TYPES_KEY;
import static apoc.util.Util.map;
import static apoc.util.collection.Iterators.asSet;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.neo4j.graphdb.Label.label;
import static org.neo4j.graphdb.RelationshipType.withName;

import apoc.create.Create;
import apoc.result.VirtualNode;
import apoc.result.VirtualRelationship;
import apoc.util.TestUtil;
import apoc.util.Util;
import apoc.util.collection.Iterables;
import apoc.util.collection.Iterators;
import com.neo4j.test.extension.EnterpriseDbmsExtension;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.neo4j.configuration.GraphDatabaseSettings;
import org.neo4j.graphdb.GraphDatabaseService;
import org.neo4j.graphdb.Label;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.Path;
import org.neo4j.graphdb.Relationship;
import org.neo4j.graphdb.RelationshipType;
import org.neo4j.test.TestDatabaseManagementServiceBuilder;
import org.neo4j.test.extension.ExtensionCallback;
import org.neo4j.test.extension.Inject;

@EnterpriseDbmsExtension(configurationCallback = "configure")
public class NodesTest {

    private static final List<String> FIRST_ALPHA_CYCLE_PROPS = List.of("alpha", "one", "two", "alpha");
    private static final List<String> SECOND_ALPHA_CYCLE_PROPS = List.of("alpha", "seven", "eight", "alpha");
    private static final List<String> BETA_CYCLE_PROPS = List.of("beta", "three", "four", "beta");
    private static final List<String> SELF_REL_PROPS = List.of("delta", "delta");
    private static final List<String> ONE_STEP_PROPS = List.of("epsilon", "seven", "epsilon");
    private static final String DEPEND_ON_REL_TYPE = "DEPENDS_ON";

    @Inject
    GraphDatabaseService db;

    @ExtensionCallback
    void configure(TestDatabaseManagementServiceBuilder builder) {
        builder.setConfig(
                GraphDatabaseSettings.procedure_unrestricted,
                List.of(
                        "apoc.nodes.link",
                        "apoc.node.relationship.exists",
                        "apoc.node.relationships.exist",
                        "apoc.nodes.connected",
                        "apoc.nodes.isDense"));
    }

    @BeforeAll
    void setUp() {
        TestUtil.registerProcedure(db, Nodes.class, Create.class, NodesRestricted.class);
    }

    @Test
    void isDense() {
        db.executeTransactionally(
                "CREATE (f:Foo) CREATE (b:Bar) WITH f UNWIND range(1,100) as id CREATE (f)-[:SELF]->(f)");

        TestUtil.testCall(
                db,
                "MATCH (n) WITH n, apoc.nodes.isDense(n) as dense "
                        + "WHERE n:Foo AND dense OR n:Bar AND NOT dense RETURN count(*) as c",
                (row) -> assertEquals(1L, row.get("c")));
    }

    @Test
    void isDenseWithRelType() {
        db.executeTransactionally(
                "CREATE (f:Foo) CREATE (b:Bar) WITH f UNWIND range(1,100) as id CREATE (f)-[:SELF]->(f)");

        TestUtil.testResult(
                db,
                """
                        CYPHER 25
                        MATCH (n)
                        WITH n, apoc.nodes.isDense(n, 'SELF') as dense
                        RETURN labels(n)[0] as labels, dense ORDER BY labels""",
                (result) -> {
                    assertTrue(result.hasNext());
                    var row1 = result.next();
                    assertEquals("Bar", row1.get("labels"));
                    assertFalse((boolean) row1.get("dense"));
                    assertTrue(result.hasNext());
                    var row2 = result.next();
                    assertEquals("Foo", row2.get("labels"));
                    assertTrue((boolean) row2.get("dense"));
                    assertFalse(result.hasNext());
                });
    }

    @Test
    void isDenseWithRelTypeUnderThreshold() {
        db.executeTransactionally(
                "CREATE (f:Foo) CREATE (b:Bar) WITH f UNWIND range(1,49) as id CREATE (f)-[:SELF]->(f)");

        TestUtil.testResult(
                db,
                """
                        CYPHER 25
                        MATCH (n)
                        WITH n, apoc.nodes.isDense(n, 'SELF') as dense
                        RETURN labels(n)[0] as labels, dense ORDER BY labels""",
                (result) -> {
                    assertTrue(result.hasNext());
                    var row1 = result.next();
                    assertEquals("Bar", row1.get("labels"));
                    assertFalse((boolean) row1.get("dense"));
                    assertTrue(result.hasNext());
                    var row2 = result.next();
                    assertEquals("Foo", row2.get("labels"));
                    assertFalse((boolean) row2.get("dense"));
                    assertFalse(result.hasNext());
                });
    }

    @Test
    void nodesGetTest() {
        db.executeTransactionally("CREATE (f:Foo), (b:Bar), (c:Car)");
        TestUtil.testResult(
                db,
                """
                MATCH (f:Foo), (b:Bar), (c:Car)
                CALL apoc.nodes.get([elementId(f), id(b), c])
                YIELD node
                RETURN node
                """,
                (result) -> {
                    List<Label> returnedNodeLabels = new ArrayList<>();
                    List<Label> expectedLabels = List.of(label("Foo"), label("Bar"), label("Car"));
                    Map<String, Object> row = result.next();
                    ((Node) row.get("node")).getLabels().forEach(returnedNodeLabels::add);
                    row = result.next();
                    ((Node) row.get("node")).getLabels().forEach(returnedNodeLabels::add);
                    row = result.next();
                    ((Node) row.get("node")).getLabels().forEach(returnedNodeLabels::add);
                    assertFalse(result.hasNext());
                    assertTrue(expectedLabels.containsAll(returnedNodeLabels));
                });
    }

    @Test
    void relsGetTest() {
        db.executeTransactionally("CREATE ()-[:FOO]->(), ()-[:BAR]->(), ()-[:CAR]->()");
        TestUtil.testResult(
                db,
                """
                MATCH ()-[f:FOO]->(), ()-[b:BAR]->(), ()-[c:CAR]->()
                CALL apoc.nodes.rels([elementId(f), id(b), c])
                YIELD rel
                RETURN rel
                """,
                (result) -> {
                    List<RelationshipType> returnedRelTypes = new ArrayList<>();
                    List<RelationshipType> expectedTypes = List.of(withName("FOO"), withName("BAR"), withName("CAR"));
                    Map<String, Object> row = result.next();
                    returnedRelTypes.add(((Relationship) row.get("rel")).getType());
                    row = result.next();
                    returnedRelTypes.add(((Relationship) row.get("rel")).getType());
                    row = result.next();
                    returnedRelTypes.add(((Relationship) row.get("rel")).getType());
                    assertFalse(result.hasNext());
                    assertTrue(expectedTypes.containsAll(returnedRelTypes));
                });
    }

    @Test
    void cycles() {
        createDatasetForNodesCycles();

        // with all relationships
        TestUtil.testResult(
                db,
                "MATCH (m1:Start) WITH collect(m1) as nodes CALL apoc.nodes.cycles(nodes) YIELD path RETURN path",
                res -> {
                    List<Path> paths = Iterators.stream(res.<Path>columnAs("path"))
                            .sorted(Comparator.comparingLong(
                                    item -> (long) item.lastRelationship().getProperty("id")))
                            .toList();
                    assertEquals(5, paths.size());
                    assertionsCycle(paths.get(0), FIRST_ALPHA_CYCLE_PROPS);
                    assertionsCycle(paths.get(1), SECOND_ALPHA_CYCLE_PROPS);
                    assertionsCycle(paths.get(2), BETA_CYCLE_PROPS);
                    assertionsCycle(paths.get(3), SELF_REL_PROPS);
                    assertionsCycle(paths.get(4), ONE_STEP_PROPS);
                });
    }

    @Test
    void cyclesWithRelTypes() {
        createDatasetForNodesCycles();

        // with single specific relationship
        TestUtil.testResult(
                db,
                "MATCH (m1:Start) WITH collect(m1) as nodes CALL apoc.nodes.cycles(nodes, $config) YIELD path RETURN path",
                map("config", map(REL_TYPES_KEY, List.of(DEPEND_ON_REL_TYPE))),
                res -> {
                    List<Path> paths = Iterators.stream(res.<Path>columnAs("path"))
                            .sorted(Comparator.comparingLong(
                                    item -> (long) item.lastRelationship().getProperty("id")))
                            .toList();
                    assertEquals(3, paths.size());
                    assertionsCycle(paths.get(0), FIRST_ALPHA_CYCLE_PROPS, DEPEND_ON_REL_TYPE);
                    assertionsCycle(paths.get(1), SECOND_ALPHA_CYCLE_PROPS, DEPEND_ON_REL_TYPE);
                    assertionsCycle(paths.get(2), SELF_REL_PROPS, DEPEND_ON_REL_TYPE);
                });

        // with multiple specific relationship (without MY_REL_ANOTHER, shouldn't find `(:Start {bar: 'beta'})` cycle)
        TestUtil.testResult(
                db,
                "MATCH (m1:Start) WITH collect(m1) as nodes CALL apoc.nodes.cycles(nodes, $config) YIELD path RETURN path",
                map("config", map(REL_TYPES_KEY, List.of(DEPEND_ON_REL_TYPE, "MY_REL", "NOT_EXISTENT"))),
                res -> {
                    List<Path> paths = Iterators.stream(res.<Path>columnAs("path"))
                            .sorted(Comparator.comparingLong(
                                    item -> (long) item.lastRelationship().getProperty("id")))
                            .toList();
                    assertEquals(3, paths.size());
                    assertionsCycle(paths.get(0), FIRST_ALPHA_CYCLE_PROPS);
                    assertionsCycle(paths.get(1), SECOND_ALPHA_CYCLE_PROPS);
                    assertionsCycle(paths.get(2), SELF_REL_PROPS);
                });

        // with multiple specific relationship (with MY_REL_ANOTHER, should find `(:Start {bar: 'beta'})` cycle)
        TestUtil.testResult(
                db,
                "MATCH (m1:Start) WITH collect(m1) as nodes CALL apoc.nodes.cycles(nodes, $config) YIELD path RETURN path",
                map(
                        "config",
                        map(REL_TYPES_KEY, List.of(DEPEND_ON_REL_TYPE, "MY_REL", "MY_REL_ANOTHER", "NOT_EXISTENT"))),
                res -> {
                    List<Path> paths = Iterators.stream(res.<Path>columnAs("path"))
                            .sorted(Comparator.comparingLong(
                                    item -> (long) item.lastRelationship().getProperty("id")))
                            .toList();
                    assertEquals(4, paths.size());
                    assertionsCycle(paths.get(0), FIRST_ALPHA_CYCLE_PROPS);
                    assertionsCycle(paths.get(1), SECOND_ALPHA_CYCLE_PROPS);
                    assertionsCycle(paths.get(2), BETA_CYCLE_PROPS);
                    assertionsCycle(paths.get(3), SELF_REL_PROPS);
                });

        // with not existent relationship
        TestUtil.testCallEmpty(
                db,
                "MATCH (m1:Start) WITH collect(m1) as nodes CALL apoc.nodes.cycles(nodes, $config) YIELD path RETURN path",
                map("config", map(REL_TYPES_KEY, List.of("NOT_EXISTENT"))));
    }

    @Test
    void cyclesWithMaxDepth() {
        createDatasetForNodesCycles();

        // with {maxDepth: 1} config
        TestUtil.testResult(
                db,
                "MATCH (m1:Start) WITH collect(m1) as nodes CALL apoc.nodes.cycles(nodes, $config) YIELD path RETURN path",
                map("config", map(MAX_DEPTH_KEY, 1)),
                res -> {
                    List<Path> paths = Iterators.stream(res.<Path>columnAs("path"))
                            .sorted(Comparator.comparingLong(
                                    item -> (long) item.lastRelationship().getProperty("id")))
                            .toList();
                    assertEquals(2, paths.size());
                    assertionsCycle(paths.get(0), SELF_REL_PROPS);
                    assertionsCycle(paths.get(1), ONE_STEP_PROPS);
                });

        // with {maxDepth: 0} config (only self-rel considered)
        TestUtil.testCall(
                db,
                "MATCH (m1:Start) WITH collect(m1) as nodes CALL apoc.nodes.cycles(nodes, $config) YIELD path RETURN path",
                map("config", map(REL_TYPES_KEY, List.of(DEPEND_ON_REL_TYPE), MAX_DEPTH_KEY, 0)),
                r -> assertionsCycle((Path) r.get("path"), SELF_REL_PROPS));
    }

    private void createDatasetForNodesCycles() {
        db.executeTransactionally("MATCH (n) DETACH DELETE n");

        // node with 2 'DEPENDS_ON' cycles and 1 mixed-rel-type cycle
        db.executeTransactionally(
                """
                CREATE (m1:Start {bar: 'alpha'}) with m1
                CREATE (m1)-[:DEPENDS_ON {id: 0}]->(m2:Module {bar: 'one'})-[:DEPENDS_ON {id: 1}]->(m3:Module {bar: 'two'})-[:DEPENDS_ON {id: 2}]->(m1)
                WITH m1, m2, m3
                CREATE (m1)-[:DEPENDS_ON {id: 3}]->(m2), (m2)-[:ANOTHER {id: 4}]->(m3), (m2)-[:DEPENDS_ON {id: 5}]->(m3)
                CREATE (m1)-[:DEPENDS_ON {id: 6}]->(:Module {bar: 'seven'})-[:DEPENDS_ON {id: 7}]->(:Module {bar: 'eight'})-[:DEPENDS_ON {id: 8}]->(m1)""");

        // node with 1 mixed-rel-type cycle
        db.executeTransactionally(
                "CREATE (m1:Start {bar: 'beta'}) with m1\n"
                        + "CREATE (m1)-[:MY_REL {id: 9}]->(m2:Module {bar: 'three'})-[:MY_REL_ANOTHER  {id: 10}]->(m3:Module {bar: 'four'})-[:MY_REL {id: 11}]->(m1)");

        // node without cycle
        db.executeTransactionally(
                "CREATE (m1:Start {bar: 'gamma'}) with m1\n"
                        + "CREATE (m1)-[:DEPENDS_ON {id: 12}]->(m2:Module {bar: 'five'})-[:DEPENDS_ON {id: 13}]->(m3:Module {bar: 'six'})");

        // node with self-rel
        db.executeTransactionally(
                "CREATE (m1:Start {bar: 'delta'}) with m1\n" + "CREATE (m1)-[:DEPENDS_ON {id: 20}]->(m1)");

        // node with depth 1
        db.executeTransactionally("CREATE (m1:Start {bar: 'epsilon'}) with m1\n"
                + "CREATE (m1)-[:DEPTH_ONE {id: 30}]->(:Module {bar: 'seven'})-[:DEPTH_ONE {id: 31}]->(m1)");
    }

    private void assertionsCycle(Path path, List<String> expectedProps, String customRelType) {
        List<String> propNodes = Iterables.stream(path.nodes())
                .map(node -> (String) node.getProperty("bar"))
                .collect(Collectors.toList());
        assertEquals(expectedProps, propNodes);
        if (customRelType != null) {
            assertTrue(Iterables.stream(path.relationships())
                    .allMatch(rel -> rel.getType().name().equals(customRelType)));
        }
    }

    private void assertionsCycle(Path path, List<String> expectedProps) {
        assertionsCycle(path, expectedProps, null);
    }

    @Test
    void link() {
        db.executeTransactionally(
                "UNWIND range(1,10) as id CREATE (n:Foo {id:id}) WITH collect(n) as nodes call apoc.nodes.link(nodes,'BAR') RETURN size(nodes) as len");

        long len = TestUtil.singleResultFirstColumn(db, "MATCH (n:Foo {id:1})-[r:BAR*9]->() RETURN size(r) as len");
        assertEquals(9L, len);
    }

    @Test
    void linkWithAvoidDuplicateTrue() {
        db.executeTransactionally(
                "CREATE (n:Foo {id:1}), (m:Foo {id:2}) WITH [n,m] as nodes CALL apoc.nodes.link(nodes,'BAR') RETURN 1");
        TestUtil.testCall(
                db, "MATCH (n:Foo)-[r]->() RETURN count(r) as count", row -> assertEquals(1L, row.get("count")));

        // with avoidDuplicates
        db.executeTransactionally(
                "MATCH (n:Foo) WITH collect(n) as nodes CALL apoc.nodes.link(nodes,'BAR', {avoidDuplicates: true}) RETURN 1");
        TestUtil.testCall(
                db, "MATCH (n:Foo)-[r]->() RETURN count(r) as count", row -> assertEquals(1L, row.get("count")));

        // without avoidDuplicates
        db.executeTransactionally("MATCH (n:Foo) WITH collect(n) as nodes CALL apoc.nodes.link(nodes,'BAR') RETURN 1");
        TestUtil.testCall(
                db, "MATCH (n:Foo)-[r]->() RETURN count(r) as count", row -> assertEquals(2L, row.get("count")));
    }

    @Test
    void delete() {
        db.executeTransactionally("UNWIND range(1,100) as id CREATE (n:Foo {id:id})-[:X]->(n)");

        long count = TestUtil.singleResultFirstColumn(
                db,
                "MATCH (n:Foo) WITH collect(n) as nodes call apoc.nodes.delete(nodes,1) YIELD value as count RETURN count");
        assertEquals(100L, count);

        count = TestUtil.singleResultFirstColumn(db, "MATCH (n:Foo) RETURN count(*) as count");
        assertEquals(0L, count);
    }

    @Test
    void nodesDeleteTest() {
        db.executeTransactionally("CREATE (:FOO), (:BAR), (:BAZ)");

        long count = TestUtil.singleResultFirstColumn(
                db,
                """
                MATCH (foo:FOO), (bar:BAR), (baz:BAZ)
                CALL apoc.nodes.delete([foo, id(bar), elementId(baz)],1)
                YIELD value AS count
                RETURN count""");
        assertEquals(3L, count);

        count = TestUtil.singleResultFirstColumn(db, "MATCH (n) RETURN count(*) as count");
        assertEquals(0L, count);
    }

    @Test
    void types() {
        db.executeTransactionally("CREATE (f:Foo) CREATE (b:Bar) CREATE (f)-[:Y]->(f) CREATE (f)-[:X]->(f)");
        TestUtil.testCall(
                db,
                "MATCH (n:Foo) RETURN apoc.node.relationship.types(n) AS value",
                (r) -> assertEquals(asSet("X", "Y"), asSet(((List) r.get("value")).toArray())));
        TestUtil.testCall(
                db,
                "MATCH (n:Foo) RETURN apoc.node.relationship.types(n,'X') AS value",
                (r) -> assertEquals(asList("X"), r.get("value")));
        TestUtil.testCall(
                db,
                "MATCH (n:Foo) RETURN apoc.node.relationship.types(n,'X|Z') AS value",
                (r) -> assertEquals(asList("X"), r.get("value")));
        TestUtil.testCall(
                db,
                "MATCH (n:Bar) RETURN apoc.node.relationship.types(n) AS value",
                (r) -> assertEquals(Collections.emptyList(), r.get("value")));
        TestUtil.testCall(db, "RETURN apoc.node.relationship.types(null) AS value", (r) -> assertNull(r.get("value")));
    }

    @Test
    void nodesTypes() {
        // given
        db.executeTransactionally("CREATE (f:Foo), (f)-[:Y]->(f), (f)-[:X]->(f)");
        db.executeTransactionally("CREATE (f:Bar), (f)-[:YY]->(f), (f)-[:XX]->(f)");

        // when
        TestUtil.testCall(
                db,
                "MATCH (f:Foo), (b:Bar) RETURN apoc.nodes.relationship.types([f, elementId(b)]) AS value",
                (result) -> {
                    // then
                    List<Map<String, Object>> list = (List<Map<String, Object>>) result.get("value");
                    assertFalse(list.isEmpty());
                    list.forEach(map -> {
                        Node node = (Node) map.get("node");
                        List<String> data = (List<String>) map.get("types");
                        if (node.hasLabel(Label.label("Foo"))) {
                            assertEquals(asSet("X", "Y"), asSet(data.iterator()));
                        } else {
                            assertEquals(asSet("XX", "YY"), asSet(data.iterator()));
                        }
                    });
                });
    }

    @Test
    void nodesHasRelationship() {
        // given
        db.executeTransactionally("CREATE (f:Foo), (f)-[:X]->(f)");
        db.executeTransactionally("CREATE (b:Bar), (b)-[:Y]->(b)");

        // when
        TestUtil.testCall(
                db,
                "MATCH (n:Foo) RETURN apoc.nodes.relationships.exist(collect(elementId(n)), 'X|Y') AS value",
                (result) -> {
                    // then
                    List<Map<String, Object>> list = (List<Map<String, Object>>) result.get("value");
                    assertFalse(list.isEmpty());
                    list.forEach(map -> {
                        Map<String, Boolean> data = (Map<String, Boolean>) map.get("exists");
                        assertEquals(map("X", true, "Y", false), data);
                    });
                });

        TestUtil.testCall(
                db, "MATCH (n:Bar) RETURN apoc.nodes.relationships.exist(collect(n), 'X|Y') AS value", (result) -> {
                    // then
                    List<Map<String, Object>> list = (List<Map<String, Object>>) result.get("value");
                    assertFalse(list.isEmpty());
                    list.forEach(map -> {
                        Map<String, Boolean> data = (Map<String, Boolean>) map.get("exists");
                        assertEquals(map("X", false, "Y", true), data);
                    });
                });
    }

    @Test
    void hasRelationship() {
        db.executeTransactionally(
                "CREATE (:Foo)-[:Y]->(:Bar),(n:FooBar) WITH n UNWIND range(1,100) as _ CREATE (n)-[:X]->(n)");
        TestUtil.testCall(
                db,
                "MATCH (n:Foo) RETURN apoc.node.relationship.exists(n,'Y') AS value",
                (r) -> assertEquals(true, r.get("value")));
        TestUtil.testCall(
                db,
                "MATCH (n:Foo) RETURN apoc.node.relationship.exists(n,'Y>') AS value",
                (r) -> assertEquals(true, r.get("value")));
        TestUtil.testCall(
                db,
                "MATCH (n:Foo) RETURN apoc.node.relationship.exists(n,'<Y') AS value",
                (r) -> assertEquals(false, r.get("value")));
        TestUtil.testCall(
                db,
                "MATCH (n:Foo) RETURN apoc.node.relationship.exists(n,'X') AS value",
                (r) -> assertEquals(false, r.get("value")));

        TestUtil.testCall(
                db,
                "MATCH (n:Bar) RETURN apoc.node.relationship.exists(n,'Y') AS value",
                (r) -> assertEquals(true, r.get("value")));
        TestUtil.testCall(
                db,
                "MATCH (n:Bar) RETURN apoc.node.relationship.exists(n,'Y>') AS value",
                (r) -> assertEquals(false, r.get("value")));
        TestUtil.testCall(
                db,
                "MATCH (n:Bar) RETURN apoc.node.relationship.exists(n,'<Y') AS value",
                (r) -> assertEquals(true, r.get("value")));
        TestUtil.testCall(
                db,
                "MATCH (n:Bar) RETURN apoc.node.relationship.exists(n,'X') AS value",
                (r) -> assertEquals(false, r.get("value")));

        TestUtil.testCall(
                db,
                "MATCH (n:FooBar) RETURN apoc.node.relationship.exists(n,'X') AS value",
                (r) -> assertEquals(true, r.get("value")));
        TestUtil.testCall(
                db,
                "MATCH (n:FooBar) RETURN apoc.node.relationship.exists(n,'X>') AS value",
                (r) -> assertEquals(true, r.get("value")));
        TestUtil.testCall(
                db,
                "MATCH (n:FooBar) RETURN apoc.node.relationship.exists(n,'<X') AS value",
                (r) -> assertEquals(true, r.get("value")));
        TestUtil.testCall(
                db,
                "MATCH (n:FooBar) RETURN apoc.node.relationship.exists(n,'Y') AS value",
                (r) -> assertEquals(false, r.get("value")));
    }

    @Test
    void hasRelationships() {
        db.executeTransactionally(
                "CREATE (:Foo)-[:Y]->(:Bar),(n:FooBar) WITH n UNWIND range(1,100) as _ CREATE (n)-[:X]->(n)");
        TestUtil.testCall(
                db,
                "MATCH (n:Foo) RETURN apoc.node.relationships.exist(n,'Y') AS value",
                (r) -> assertEquals(map("Y", true), r.get("value")));
        TestUtil.testCall(
                db,
                "MATCH (n:Foo) RETURN apoc.node.relationships.exist(n,'Y>') AS value",
                (r) -> assertEquals(map("Y>", true), r.get("value")));
        TestUtil.testCall(
                db,
                "MATCH (n:Foo) RETURN apoc.node.relationships.exist(n,'<Y') AS value",
                (r) -> assertEquals(map("<Y", false), r.get("value")));
        TestUtil.testCall(
                db,
                "MATCH (n:Foo) RETURN apoc.node.relationships.exist(n,'X') AS value",
                (r) -> assertEquals(map("X", false), r.get("value")));

        TestUtil.testCall(
                db,
                "MATCH (n:Bar) RETURN apoc.node.relationships.exist(n,'Y') AS value",
                (r) -> assertEquals(map("Y", true), r.get("value")));
        TestUtil.testCall(
                db,
                "MATCH (n:Bar) RETURN apoc.node.relationships.exist(n,'Y>') AS value",
                (r) -> assertEquals(map("Y>", false), r.get("value")));
        TestUtil.testCall(
                db,
                "MATCH (n:Bar) RETURN apoc.node.relationships.exist(n,'<Y') AS value",
                (r) -> assertEquals(map("<Y", true), r.get("value")));
        TestUtil.testCall(
                db,
                "MATCH (n:Bar) RETURN apoc.node.relationships.exist(n,'X') AS value",
                (r) -> assertEquals(map("X", false), r.get("value")));

        TestUtil.testCall(
                db,
                "MATCH (n:FooBar) RETURN apoc.node.relationships.exist(n,'X') AS value",
                (r) -> assertEquals(map("X", true), r.get("value")));
        TestUtil.testCall(
                db,
                "MATCH (n:FooBar) RETURN apoc.node.relationships.exist(n,'X>') AS value",
                (r) -> assertEquals(map("X>", true), r.get("value")));
        TestUtil.testCall(
                db,
                "MATCH (n:FooBar) RETURN apoc.node.relationships.exist(n,'<X') AS value",
                (r) -> assertEquals(map("<X", true), r.get("value")));
        TestUtil.testCall(
                db,
                "MATCH (n:FooBar) RETURN apoc.node.relationships.exist(n,'Y') AS value",
                (r) -> assertEquals(map("Y", false), r.get("value")));
    }

    @Test
    void testConnected() {
        db.executeTransactionally("CREATE (st:StartThin),(et:EndThin),(ed:EndDense)");
        int relCount = 20;
        for (int rel = 0; rel < relCount; rel++) {
            db.executeTransactionally(
                    "MATCH (st:StartThin),(et:EndThin),(ed:EndDense) " + " CREATE (st)-[:REL"
                            + rel + "]->(et) " + " WITH * UNWIND RANGE(1,$count) AS id CREATE (st)-[:REL"
                            + rel + "]->(ed)",
                    map("count", relCount - rel));
        }

        TestUtil.testCall(
                db,
                "MATCH (s:StartThin),(e:EndThin)  RETURN apoc.nodes.connected(s,e) as value",
                (r) -> assertEquals(true, r.get("value")));
        TestUtil.testCall(
                db,
                "MATCH (s:StartThin),(e:EndThin)  RETURN apoc.nodes.connected(s,e,'REL3') as value",
                (r) -> assertEquals(true, r.get("value")));
        TestUtil.testCall(
                db,
                "MATCH (s:StartThin),(e:EndThin)  RETURN apoc.nodes.connected(s,e,'REL10>') as value",
                (r) -> assertEquals(true, r.get("value")));
        TestUtil.testCall(
                db,
                "MATCH (s:StartThin),(e:EndThin)  RETURN apoc.nodes.connected(s,e,'REL20') as value",
                (r) -> assertEquals(false, r.get("value")));
        TestUtil.testCall(
                db,
                "MATCH (s:StartThin),(e:EndThin)  RETURN apoc.nodes.connected(s,e,'REL15>|REL20') as value",
                (r) -> assertEquals(true, r.get("value")));

        TestUtil.testCall(
                db,
                "MATCH (s:StartThin),(e:EndDense) RETURN apoc.nodes.connected(s,e) as value",
                (r) -> assertEquals(true, r.get("value")));
        TestUtil.testCall(
                db,
                "MATCH (s:StartThin),(e:EndDense) RETURN apoc.nodes.connected(s,e,'REL3') as value",
                (r) -> assertEquals(true, r.get("value")));
        TestUtil.testCall(
                db,
                "MATCH (s:StartThin),(e:EndDense) RETURN apoc.nodes.connected(s,e,'REL10>') as value",
                (r) -> assertEquals(true, r.get("value")));
        TestUtil.testCall(
                db,
                "MATCH (s:StartThin),(e:EndDense) RETURN apoc.nodes.connected(s,e,'REL20') as value",
                (r) -> assertEquals(false, r.get("value")));
        TestUtil.testCall(
                db,
                "MATCH (s:StartThin),(e:EndDense) RETURN apoc.nodes.connected(s,e,'REL15>|REL20') as value",
                (r) -> assertEquals(true, r.get("value")));

        TestUtil.testCall(
                db,
                "MATCH (s:StartThin),(e:EndThin)  RETURN apoc.nodes.connected(e,s) as value",
                (r) -> assertEquals(true, r.get("value")));
        TestUtil.testCall(
                db,
                "MATCH (s:StartThin),(e:EndThin)  RETURN apoc.nodes.connected(e,s,'REL3') as value",
                (r) -> assertEquals(true, r.get("value")));
        TestUtil.testCall(
                db,
                "MATCH (s:StartThin),(e:EndThin)  RETURN apoc.nodes.connected(e,s,'REL10<') as value",
                (r) -> assertEquals(true, r.get("value")));
        TestUtil.testCall(
                db,
                "MATCH (s:StartThin),(e:EndThin)  RETURN apoc.nodes.connected(e,s,'REL20') as value",
                (r) -> assertEquals(false, r.get("value")));
        TestUtil.testCall(
                db,
                "MATCH (s:StartThin),(e:EndThin)  RETURN apoc.nodes.connected(e,s,'REL15<|REL20') as value",
                (r) -> assertEquals(true, r.get("value")));

        TestUtil.testCall(
                db,
                "MATCH (s:StartThin),(e:EndDense) RETURN apoc.nodes.connected(e,s) as value",
                (r) -> assertEquals(true, r.get("value")));
        TestUtil.testCall(
                db,
                "MATCH (s:StartThin),(e:EndDense) RETURN apoc.nodes.connected(e,s,'REL3') as value",
                (r) -> assertEquals(true, r.get("value")));
        TestUtil.testCall(
                db,
                "MATCH (s:StartThin),(e:EndDense) RETURN apoc.nodes.connected(e,s,'REL10<') as value",
                (r) -> assertEquals(true, r.get("value")));
        TestUtil.testCall(
                db,
                "MATCH (s:StartThin),(e:EndDense) RETURN apoc.nodes.connected(e,s,'REL20') as value",
                (r) -> assertEquals(false, r.get("value")));
        TestUtil.testCall(
                db,
                "MATCH (s:StartThin),(e:EndDense) RETURN apoc.nodes.connected(e,s,'REL15<|REL20') as value",
                (r) -> assertEquals(true, r.get("value")));

        // todo inverse e,s then also incoming
    }

    @Test
    void testDegreeTypeAndDirection() {
        db.executeTransactionally(
