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

import static apoc.util.MapUtil.map;
import static apoc.util.TestUtil.testCall;
import static apoc.util.TestUtil.testCallCount;
import static apoc.util.TestUtil.testCallEmpty;
import static apoc.util.TestUtil.testResult;
import static apoc.util.Util.isSelfRel;
import static java.util.Collections.emptyList;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.InstanceOfAssertFactories.list;
import static org.assertj.core.api.InstanceOfAssertFactories.type;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.neo4j.configuration.SettingImpl.newBuilder;
import static org.neo4j.configuration.SettingValueParsers.BOOL;
import static org.neo4j.graphdb.Label.label;

import apoc.util.ArrayBackedList;
import apoc.util.TestUtil;
import apoc.util.Util;
import apoc.util.collection.Iterators;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import junit.framework.TestCase;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.neo4j.graphdb.Direction;
import org.neo4j.graphdb.Label;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.QueryExecutionException;
import org.neo4j.graphdb.Relationship;
import org.neo4j.graphdb.RelationshipType;
import org.neo4j.graphdb.ResourceIterator;
import org.neo4j.test.rule.DbmsRule;
import org.neo4j.test.rule.ImpermanentDbmsRule;

/**
 * @author mh
 * @since 25.03.16
 */
public class GraphRefactoringTest {
    public static final String CLONE_NODES_QUERY = "match (n:MyBook) with n call apoc.refactor.cloneNodes([n], true) "
            + "YIELD output, error RETURN output, error";
    public static final String CLONE_SUBGRAPH_QUERY =
            "MATCH (n:MyBook) with n call apoc.refactor.cloneSubgraph([n], [], {}) YIELD output, error RETURN output, error";
    public static final String EXTRACT_QUERY =
            "MATCH p=(:Start)-[r:TO_MOVE]->(:End) with r call apoc.refactor.extractNode([r], ['MyBook'], 'OUT', 'IN') "
                    + "YIELD output, error RETURN output, error";

    @Rule
    public DbmsRule db = new ImpermanentDbmsRule()
            .withSetting(
                    newBuilder("internal.dbms.debug.track_cursor_close", BOOL, false)
                            .build(),
                    false)
            .withSetting(
                    newBuilder("internal.dbms.debug.trace_cursors", BOOL, false).build(), false);

    @Before
    public void setUp() {
        TestUtil.registerProcedure(db, GraphRefactoring.class);
    }

    @After
    public void tearDown() {
        db.shutdown();
    }

    @Test
    public void deleteAndReconnect() {
        db.executeTransactionally(
                "CREATE (f:One)-[:ALPHA {a:'b'}]->(b:Two)-[:BETA {c:'d', e:'f'}]->(c:Three)-[:GAMMA]->(d:Four)-[:DELTA {aa: 'bb', cc: 'dd', ee: 'ff'}]->(e:Five {foo: 'bar', baz: 'baa'})");

        TestUtil.testCall(db, Util.NODE_COUNT, (row) -> assertEquals(5L, row.get("result")));

        TestUtil.testCallEmpty(
                db,
                "MATCH p=(f:One)-->(b:Two)-->(c:Three), (d:Four), (e:Five) WITH p, [d,e] as list CALL apoc.refactor.deleteAndReconnect(p, list) YIELD nodes, relationships RETURN nodes, relationships",
                emptyMap());

        TestUtil.testCall(db, Util.NODE_COUNT, (row) -> assertEquals(5L, row.get("result")));

        TestUtil.testCall(
                db,
                "MATCH p=(f:One)-->(b:Two)-->(c:Three)-->(d:Four)-->(e:Five) WITH p, [b,d] as list CALL apoc.refactor.deleteAndReconnect(p, list) YIELD nodes, relationships RETURN nodes, relationships",
                (row) -> {
                    List<Node> nodes = (List<Node>) row.get("nodes");
                    assertEquals(3, nodes.size());
                    Node node1 = nodes.get(0);
                    assertEquals(singletonList(label("One")), node1.getLabels());
                    Node node2 = nodes.get(1);
                    assertEquals(singletonList(label("Three")), node2.getLabels());
                    Node node3 = nodes.get(2);
                    assertEquals(singletonList(label("Five")), node3.getLabels());
                    List<Relationship> rels = (List<Relationship>) row.get("relationships");
                    assertEquals(2, rels.size());
                    Relationship rel1 = rels.get(0);
                    assertEquals("ALPHA", rel1.getType().name());
                    assertEquals("b", rel1.getProperty("a"));
                    Relationship rel2 = rels.get(1);
                    assertEquals("GAMMA", rel2.getType().name());
                    assertTrue(rel2.getAllProperties().isEmpty());
                    assertNotNull(row.get("nodes"));
                });

        TestUtil.testCall(db, Util.NODE_COUNT, (row) -> assertEquals(3L, row.get("result")));
    }

    @Test
    public void deleteAndReconnectWithTerminalNodes() {
        db.executeTransactionally(
                "CREATE (f:One)-[:ALPHA {a:'b'}]->(c:Two)-[:GAMMA]->(e:Three {foo: 'bar', baz: 'baa'})");

        // - terminal node
        TestUtil.testCall(
                db,
                "MATCH p=(f:One)-->(c:Two)-->(e:Three) WITH p, f CALL apoc.refactor.deleteAndReconnect(p, [f]) YIELD nodes, relationships RETURN nodes, relationships",
                (row) -> {
                    List<Node> nodes = (List<Node>) row.get("nodes");
                    assertEquals(2, nodes.size());
                    Node node1 = nodes.get(0);
                    assertEquals(singletonList(label("Two")), node1.getLabels());
                    Node node2 = nodes.get(1);
                    assertEquals(singletonList(label("Three")), node2.getLabels());
                    assertEquals("bar", node2.getProperty("foo"));
                    assertEquals("baa", node2.getProperty("baz"));
                    List<Relationship> rels = (List<Relationship>) row.get("relationships");
                    assertEquals(1, rels.size());
                    Relationship rel = rels.get(0);
                    assertEquals("GAMMA", rel.getType().name());
                    assertTrue(rel.getAllProperties().isEmpty());
                    assertNotNull(row.get("nodes"));
                });

        TestUtil.testCall(db, Util.NODE_COUNT, (row) -> assertEquals(2L, row.get("result")));

        TestUtil.testCall(
                db,
                "MATCH p=(f:Three) WITH p, [f] as list CALL apoc.refactor.deleteAndReconnect(p, list) YIELD nodes, relationships RETURN nodes, relationships",
                (row) -> {
                    assertEquals(0, ((List<Node>) row.get("nodes")).size());
                    assertEquals(0, ((List<Node>) row.get("relationships")).size());
                });

        TestUtil.testCall(db, Util.NODE_COUNT, (row) -> assertEquals(1L, row.get("result")));
    }

    @Test
    public void deleteAndReconnectConsecutiveNodes() {
        db.executeTransactionally(
                "CREATE (f:Alpha)-[:REL_1 {a:'b'}]->(b:Beta)-[:REL_2 {c:'d', e:'f'}]->(c:Gamma)-[:REL_3]->(d:Delta)-[:REL_4 {aa: 'bb', cc: 'dd', ee: 'ff'}]->(e:Epsilon {foo: 'bar', baz: 'baa'})");

        TestUtil.testCall(db, Util.NODE_COUNT, (row) -> assertEquals(5L, row.get("result")));

        try (final var tx = db.beginTx()) {
            final var query =
                    "MATCH p=(f:Alpha)-->(b:Beta)-->(c:Gamma)-->(d:Delta)-->(e:Epsilon) WITH p, [b,c] as list CALL apoc.refactor.deleteAndReconnect(p, list) YIELD nodes, relationships RETURN nodes, relationships";
            assertThat(tx.execute(query).stream())
                    .singleElement(InstanceOfAssertFactories.map(String.class, Object.class))
                    .hasEntrySatisfying("nodes", nodes -> assertThat(nodes)
                            .asInstanceOf(InstanceOfAssertFactories.list(Node.class))
                            .satisfiesExactly(
                                    n -> assertThat(n.getLabels()).containsExactly(label("Alpha")),
                                    n -> assertThat(n.getLabels()).containsExactly(label("Delta")),
                                    n -> assertThat(n.getLabels()).containsExactly(label("Epsilon"))))
                    .hasEntrySatisfying("relationships", rels -> assertThat(rels)
                            .asInstanceOf(InstanceOfAssertFactories.list(Relationship.class))
                            .hasSize(2)
                            .satisfiesOnlyOnce(r -> {
                                assertThat(r.getType().name()).isEqualTo("REL_4");
                                assertThat(r.getAllProperties())
                                        .containsEntry("aa", "bb")
                                        .containsEntry("cc", "dd")
                                        .containsEntry("ee", "ff");
                            })
                            .satisfiesOnlyOnce(r -> {
                                assertThat(r.getType().name()).isEqualTo("REL_1");
                                assertThat(r.getAllProperties()).containsEntry("a", "b");
                            }));
            tx.commit();
        }

        TestUtil.testCall(db, Util.NODE_COUNT, (row) -> assertEquals(3L, row.get("result")));
    }

    @Test
    public void deleteAndReconnectWithIncomingRelConfig() {
        db.executeTransactionally(
                "CREATE (f:One)-[:ALPHA {a:'b'}]->(b:Two)-[:BETA {c:'d', e:'f'}]->(c:Three)-[:GAMMA]->(d:Four)-[:DELTA {aa: 'bb', cc: 'dd', ee: 'ff'}]->(e:Five {foo: 'bar', baz: 'baa'})");

        TestUtil.testCall(db, Util.NODE_COUNT, (row) -> assertEquals(5L, row.get("result")));

        TestUtil.testCall(
                db,
                "MATCH p=(f:One)-->(b:Two)-->(c:Three)-->(d:Four)-->(e:Five) WITH p, [b,d] as list CALL apoc.refactor.deleteAndReconnect(p, list, {relationshipSelectionStrategy: 'incoming'}) YIELD nodes, relationships RETURN nodes, relationships",
                (row) -> {
                    List<Node> nodes = (List<Node>) row.get("nodes");
                    assertEquals(3, nodes.size());
                    Node node1 = nodes.get(0);
                    assertEquals(singletonList(label("One")), node1.getLabels());
                    Node node2 = nodes.get(1);
                    assertEquals(singletonList(label("Three")), node2.getLabels());
                    Node node3 = nodes.get(2);
                    assertEquals(singletonList(label("Five")), node3.getLabels());
                    List<Relationship> rels = (List<Relationship>) row.get("relationships");
                    assertEquals(2, rels.size());
                    Relationship rel1 = rels.get(0);
                    assertEquals("ALPHA", rel1.getType().name());
                    assertEquals("b", rel1.getProperty("a"));
                    Relationship rel2 = rels.get(1);
                    assertEquals("GAMMA", rel2.getType().name());
                    assertNotNull(row.get("nodes"));
                });

        TestUtil.testCall(db, Util.NODE_COUNT, (row) -> assertEquals(3L, row.get("result")));
    }

    @Test
    public void deleteAndReconnectWithOutgoingRelConfig() {
        db.executeTransactionally(
                "CREATE (f:One)-[:ALPHA {a:'b'}]->(b:Two)-[:BETA {c:'d', e:'f'}]->(c:Three)-[:GAMMA]->(d:Four)-[:DELTA {aa: 'bb', cc: 'dd', ee: 'ff'}]->(e:Five {foo: 'bar', baz: 'baa'})");

        TestUtil.testCall(db, Util.NODE_COUNT, (row) -> assertEquals(5L, row.get("result")));

        TestUtil.testCall(
                db,
                "MATCH p=(f:One)-->(b:Two)-->(c:Three)-->(d:Four)-->(e:Five) WITH p, [b,d] as list CALL apoc.refactor.deleteAndReconnect(p, list, {relationshipSelectionStrategy: 'outgoing'}) YIELD nodes, relationships RETURN nodes, relationships",
                (row) -> {
                    List<Node> nodes = (List<Node>) row.get("nodes");
                    assertEquals(3, nodes.size());
                    Node node1 = nodes.get(0);
                    assertEquals(singletonList(label("One")), node1.getLabels());
                    Node node2 = nodes.get(1);
                    assertEquals(singletonList(label("Three")), node2.getLabels());
                    Node node3 = nodes.get(2);
                    assertEquals(singletonList(label("Five")), node3.getLabels());
                    List<Relationship> rels = (List<Relationship>) row.get("relationships");
                    assertEquals(2, rels.size());
                    Relationship rel1 = rels.get(0);
                    assertEquals("BETA", rel1.getType().name());
                    assertEquals("d", rel1.getProperty("c"));
                    assertEquals("f", rel1.getProperty("e"));
                    Relationship rel2 = rels.get(1);
                    assertEquals("DELTA", rel2.getType().name());
                    assertEquals("bb", rel2.getProperty("aa"));
                    assertEquals("dd", rel2.getProperty("cc"));
                    assertEquals("ff", rel2.getProperty("ee"));
                    assertNotNull(row.get("nodes"));
                });

        TestUtil.testCall(db, Util.NODE_COUNT, (row) -> assertEquals(3L, row.get("result")));
    }

    @Test
    public void deleteAndReconnectWithMergeRelConfig() {
        db.executeTransactionally(
                "CREATE (f:One)-[:ALPHA {a:'b'}]->(b:Two)-[:BETA {a:'d', e:'f', g: 'h'}]->(c:Three)-[:GAMMA {aa: 'one'}]->(d:Four)-[:DELTA {aa: 'bb', cc: 'dd', ee: 'ff'}]->(e:Five {foo: 'bar', baz: 'baa'})");

        TestUtil.testCall(db, Util.NODE_COUNT, (row) -> assertEquals(5L, row.get("result")));

        TestUtil.testCall(
                db,
                "MATCH p=(f:One)-->(b:Two)-->(c:Three)-->(d:Four)-->(e:Five) WITH p, [b,d] as list CALL apoc.refactor.deleteAndReconnect(p, list, {properties: 'discard', relationshipSelectionStrategy: 'merge'}) YIELD nodes, relationships RETURN nodes, relationships",
                (row) -> {
                    List<Node> nodes = (List<Node>) row.get("nodes");
                    assertEquals(3, nodes.size());
                    Node node1 = nodes.get(0);
                    assertEquals(singletonList(label("One")), node1.getLabels());
                    Node node2 = nodes.get(1);
                    assertEquals(singletonList(label("Three")), node2.getLabels());
                    Node node3 = nodes.get(2);
                    assertEquals(singletonList(label("Five")), node3.getLabels());
                    List<Relationship> rels = (List<Relationship>) row.get("relationships");
                    assertEquals(2, rels.size());
                    Relationship rel1 = rels.get(0);
                    assertEquals("ALPHA_BETA", rel1.getType().name());
                    assertEquals("f", rel1.getProperty("e"));
                    assertEquals("b", rel1.getProperty("a"));
                    assertEquals("h", rel1.getProperty("g"));
                    Relationship rel2 = rels.get(1);
                    assertEquals("GAMMA_DELTA", rel2.getType().name());
                    assertEquals("one", rel2.getProperty("aa"));
                    assertEquals("dd", rel2.getProperty("cc"));
                    assertEquals("ff", rel2.getProperty("ee"));
                    assertNotNull(row.get("nodes"));
                });

        TestUtil.testCall(db, Util.NODE_COUNT, (row) -> assertEquals(3L, row.get("result")));
    }

    @Test
    public void deleteAndReconnectWithMergeRelConfigAndPropertiesCombine() {
        db.executeTransactionally(
                "CREATE (f:One)-[:ALPHA {a:'b'}]->(b:Two)-[:BETA {a:'d', e:'f', g: 'h'}]->(c:Three)-[:GAMMA {aa: 'one'}]->(d:Four)-[:DELTA {aa: 'bb', cc: 'dd', ee: 'ff'}]->(e:Five {foo: 'bar', baz: 'baa'})");

        TestUtil.testCall(db, Util.NODE_COUNT, (row) -> assertEquals(5L, row.get("result")));

        TestUtil.testCall(
                db,
                "MATCH p=(f:One)-->(b:Two)-->(c:Three)-->(d:Four)-->(e:Five) WITH p, [b,d] as list CALL apoc.refactor.deleteAndReconnect(p, list, {properties: 'combine', relationshipSelectionStrategy: 'merge'}) YIELD nodes, relationships RETURN nodes, relationships",
                (row) -> {
                    assertThat(row.get("nodes"))
                            .asInstanceOf(list(Node.class))
                            .satisfiesExactly(
                                    n -> assertThat(n.getLabels()).containsExactly(label("One")),
                                    n -> assertThat(n.getLabels()).containsExactly(label("Three")),
                                    n -> assertThat(n.getLabels()).containsExactly(label("Five")));
                    assertThat(row.get("relationships"))
                            .asInstanceOf(list(Relationship.class))
                            .satisfiesExactly(
                                    rel -> {
                                        assertThat(rel.getType().name()).isEqualTo("ALPHA_BETA");
                                        assertThat(rel.getAllProperties())
                                                .containsEntry("e", "f")
                                                .containsEntry("g", "h")
                                                .hasEntrySatisfying("a", v -> assertThat((String[]) v)
                                                        .containsExactlyInAnyOrder("b", "d"));
                                    },
                                    rel -> {
                                        assertThat(rel.getType().name()).isEqualTo("GAMMA_DELTA");
                                        assertThat(rel.getAllProperties())
                                                .containsEntry("ee", "ff")
                                                .containsEntry("cc", "dd")
                                                .hasEntrySatisfying("aa", v -> assertThat((String[]) v)
                                                        .containsExactlyInAnyOrder("one", "bb"));
                                    });
                });

        TestUtil.testCall(db, Util.NODE_COUNT, (row) -> assertEquals(3L, row.get("result")));
    }

    @Test
    public void deleteAndReconnectWithMergeRelConfigAndPropertiesOverride() {
        db.executeTransactionally(
                "CREATE (f:One)-[:ALPHA {a:'b'}]->(b:Two)-[:BETA {a:'d', e:'f', g: 'h'}]->(c:Three)-[:GAMMA {aa: 'one'}]->(d:Four)-[:DELTA {aa: 'bb', cc: 'dd', ee: 'ff'}]->(e:Five {foo: 'bar', baz: 'baa'})");

        TestUtil.testCall(db, Util.NODE_COUNT, (row) -> assertEquals(5L, row.get("result")));

        TestUtil.testCall(
                db,
                "MATCH p=(f:One)-->(b:Two)-->(c:Three)-->(d:Four)-->(e:Five) WITH p, [b,d] as list CALL apoc.refactor.deleteAndReconnect(p, list, {properties: 'override', relationshipSelectionStrategy: 'merge'}) YIELD nodes, relationships RETURN nodes, relationships",
                (row) -> {
                    assertThat(row.get("nodes"))
                            .asInstanceOf(list(Node.class))
                            .satisfiesExactly(
                                    n -> assertThat(n.getLabels()).containsExactly(label("One")),
                                    n -> assertThat(n.getLabels()).containsExactly(label("Three")),
                                    n -> assertThat(n.getLabels()).containsExactly(label("Five")));
                    assertThat(row.get("relationships"))
                            .asInstanceOf(list(Relationship.class))
                            .satisfiesExactly(
                                    rel -> {
                                        assertThat(rel.getType().name()).isEqualTo("ALPHA_BETA");
                                        assertThat(rel.getAllProperties())
                                                .containsEntry("e", "f")
                                                .containsEntry("a", "d")
                                                .containsEntry("g", "h");
                                    },
                                    rel -> {
                                        assertThat(rel.getType().name()).isEqualTo("GAMMA_DELTA");
                                        assertThat(rel.getAllProperties())
                                                .containsEntry("aa", "bb")
                                                .containsEntry("cc", "dd")
                                                .containsEntry("ee", "ff");
                                    });
                });

        TestUtil.testCall(db, Util.NODE_COUNT, (row) -> assertEquals(3L, row.get("result")));
    }

    @Test
    public void testEagernessMergeNodesFails() {
        db.executeTransactionally("CREATE INDEX FOR (n:Person) ON (n.ID)");
        String id = db.executeTransactionally(
                "CREATE (p1:Person {ID:1}), (p2:Person {ID:2}) RETURN elementId(p1) as id",
                emptyMap(),
                result -> Iterators.single(result.columnAs("id")));
        testCall(
                db,
                "MATCH (o:Person {ID:$oldID}), (n:Person {ID:$newID}) CALL apoc.refactor.mergeNodes([o,n]) yield node return node",
                map("oldID", 1L, "newID", 2L),
                (r) -> {
                    Node node = (Node) r.get("node");
                    assertEquals(id, node.getElementId());
                    assertTrue(node.hasLabel(label("Person")));
                    assertEquals(2L, node.getProperty("ID"));
                });
    }

    @Test
    public void testMergingOfEmptyNodeListProps() {
        db.executeTransactionally(
                "MERGE (t:TEST {prop: []})-[r:ACCESS {prop: 1}]->(t2:BLA )<-[r2:ACCESS {prop: 1}]-(t3:TEST {prop: []})");
        testCall(
                db,
                """
                        MATCH (t:TEST)
                        WITH collect(t) AS tests
                        CALL apoc.refactor.mergeNodes(tests, {properties:"combine", mergeRels: true, singleElementAsArray: true})
                        YIELD node
                        RETURN node
                       """,
                (r) -> {
                    Node node = (Node) r.get("node");
                    assertTrue(node.hasLabel(label("TEST")));
                    assertArrayEquals(new String[0], (String[]) node.getProperty("prop"));
                });
    }

    @Test
    public void testMergingOfOneEmptyNodeListProps() {
        db.executeTransactionally(
                "MERGE (t:TEST {prop: ['hi']})-[r:ACCESS {prop: 1}]->(t2:BLA )<-[r2:ACCESS {prop: 1}]-(t3:TEST {prop: []})");
        testCall(
                db,
                """
                        MATCH (t:TEST)
                        WITH collect(t) AS tests
                        CALL apoc.refactor.mergeNodes(tests, {properties:"combine", mergeRels: true, singleElementAsArray: true})
                        YIELD node
                        RETURN node
                       """,
                (r) -> {
                    Node node = (Node) r.get("node");
                    assertTrue(node.hasLabel(label("TEST")));
                    assertArrayEquals(new String[] {"hi"}, (String[]) node.getProperty("prop"));
                });
    }

    @Test
    public void testMergingOfEmptyRelListProps() {
        db.executeTransactionally("MERGE (t:TEST)-[r:ACCESS {prop: []}]->(t2:BLA )<-[r2:ACCESS {prop: []}]-(t3:TEST)");
        testCall(
                db,
                """
                        MATCH (t:TEST)
                        WITH collect(t) AS tests
                        CALL apoc.refactor.mergeNodes(tests, {properties:"combine", mergeRels: true, singleElementAsArray: true})
                        YIELD node
                        MATCH (node)-[r]->()
                        RETURN r
                       """,
                (r) -> {
                    Relationship rel = (Relationship) r.get("r");
                    assertTrue(rel.isType(RelationshipType.withName("ACCESS")));
                    assertArrayEquals(new String[0], (String[]) rel.getProperty("prop"));
                });
    }

    @Test
    public void testMergingOfOneEmptyRelListProps() {
        db.executeTransactionally(
                "MERGE (t:TEST)-[r:ACCESS {prop: ['hi']}]->(t2:BLA )<-[r2:ACCESS {prop: []}]-(t3:TEST)");
        testCall(
                db,
                """
                        MATCH (t:TEST)
                        WITH collect(t) AS tests
                        CALL apoc.refactor.mergeNodes(tests, {properties:"combine", mergeRels: true, singleElementAsArray: true})
                        YIELD node
                        MATCH (node)-[r]->()
                        RETURN r
                       """,
                (r) -> {
                    Relationship rel = (Relationship) r.get("r");
                    assertTrue(rel.isType(RelationshipType.withName("ACCESS")));
                    assertArrayEquals(new String[] {"hi"}, (String[]) rel.getProperty("prop"));
                });
    }

    @Test
    public void testMergeNodesShouldNotFailWithSamePropKeysConstraints() {
        db.executeTransactionally("CREATE CONSTRAINT FOR (a:A) REQUIRE a.prop1 IS UNIQUE");
        db.executeTransactionally("CREATE CONSTRAINT FOR (a:B) REQUIRE a.prop1 IS UNIQUE");
        String id = db.executeTransactionally(
                "CREATE (a:A {prop1: 1}), (:B {prop1: 1}) RETURN elementId(a) as id",
                emptyMap(),
                r -> Iterators.single(r.columnAs("id")));
        testCall(
                db,
                "MATCH (a:A {prop1:1}), (b:B {prop1:1})\n"
                        + "CALL apoc.refactor.mergeNodes([a, b]) YIELD node RETURN node;",
                (r) -> {
                    Node node = (Node) r.get("node");
                    assertEquals(id, node.getElementId());
                    assertTrue(node.hasLabel(label("A")));
                    assertTrue(node.hasLabel(label("B")));
                    assertEquals(1L, node.getProperty("prop1"));
                });
    }

    @Test
    public void testMergeNodesEagerAggregation() {
        String id = db.executeTransactionally(
                "CREATE (p1:Person {ID:1}), (p2:Person {ID:2}) RETURN elementId(p1) as id ",
                emptyMap(),
                result -> Iterators.single(result.columnAs("id")));
        testCall(
                db,
                "MATCH (o:Person {ID:$oldID}), (n:Person {ID:$newID}) WITH head(collect([o,n])) as nodes CALL apoc.refactor.mergeNodes(nodes) yield node return node",
                map("oldID", 1L, "newID", 2L),
                (r) -> {
                    Node node = (Node) r.get("node");
                    assertEquals(id, node.getElementId());
                    assertTrue(node.hasLabel(label("Person")));
                    assertEquals(2L, node.getProperty("ID"));
                });
    }

    @Test
    public void testMergeNodesEagerIndex() {
        db.executeTransactionally("CREATE INDEX FOR (n:Person) ON (n.ID)");
        db.executeTransactionally("CALL db.awaitIndexes()");
        String id = db.executeTransactionally(
                "CREATE (p1:Person {ID:1}), (p2:Person {ID:2}) RETURN elementId(p1) as id ",
                emptyMap(),
                result -> Iterators.single(result.columnAs("id")));
        testCall(
                db,
                "MATCH (o:Person {ID:$oldID}), (n:Person {ID:$newID}) USING INDEX o:Person(ID) USING INDEX n:Person(ID) CALL apoc.refactor.mergeNodes([o,n]) yield node return node",
                map("oldID", 1L, "newID", 2L),
                (r) -> {
                    Node node = (Node) r.get("node");
                    assertEquals(id, node.getElementId());
                    assertTrue(node.hasLabel(label("Person")));
                    assertEquals(2L, node.getProperty("ID"));
                });
    }

    @Test
    public void testMergeNodesIndexConflict() {
        db.executeTransactionally("CREATE CONSTRAINT FOR (a:A) REQUIRE a.prop1 IS UNIQUE;");
        db.executeTransactionally("CREATE CONSTRAINT FOR (b:B) REQUIRE b.prop2 IS UNIQUE;");
        db.executeTransactionally("CALL db.awaitIndexes()");
        String id = db.executeTransactionally(
                "CREATE (a:A) SET a.prop1 = 1 CREATE (b:B) SET b.prop2 = 99 RETURN elementId(a) as id ",
                emptyMap(),
                result -> Iterators.single(result.columnAs("id")));
        testCall(
                db,
                "MATCH (a:A {prop1:1}) MATCH (b:B {prop2:99}) CALL apoc.refactor.mergeNodes([a, b]) YIELD node RETURN node",
                map("oldID", 1L, "newID", 2L),
                (r) -> {
                    Node node = (Node) r.get("node");
                    assertEquals(id, node.getElementId());
                    assertTrue(node.hasLabel(Label.label("A")));
                    assertTrue(node.hasLabel(Label.label("B")));
                    assertEquals(1L, node.getProperty("prop1"));
                    assertEquals(99L, node.getProperty("prop2"));
                });
    }

    /*
    ISSUE #590
     */
    @Test
    public void testMergeMultipleNodesRelationshipDirection() {
        db.executeTransactionally("create (a1:ALabel {name:'a1'})-[:HAS_REL]->(b1:BLabel {name:'b1'}),"
                + "          (a2:ALabel {name:'a2'})-[:HAS_REL]->(b2:BLabel {name:'b2'}),"
                + "          (a3:ALabel {name:'a3'})-[:HAS_REL]->(b3:BLabel {name:'b3'}), "
                + "          (a4:ALabel {name:'a4'})-[:HAS_REL]->(b4:BLabel {name:'b4'})");

        testCall(
                db,
                "MATCH (b1:BLabel {name:'b1'}), (b2:BLabel {name:'b2'}), (b3:BLabel {name:'b3'}), (b4:BLabel {name:'b4'}) "
                        + "     WITH head(collect([b1,b2,b3,b4])) as nodes CALL apoc.refactor.mergeNodes(nodes) yield node return node",
                row -> {
                    assertNotNull(row.get("node"));
                    assertTrue(row.get("node") instanceof Node);
                    Node resultingNode = (Node) (row.get("node"));
                    assertEquals(0, resultingNode.getDegree(Direction.OUTGOING));
                    assertEquals(4, resultingNode.getDegree(Direction.INCOMING));
                });
    }

    @Test
    public void testMergeNodesWithNonDistinct() {
        db.executeTransactionally("create (a1:ALabel {name:'a1'})-[:HAS_REL]->(b1:BLabel {name:'b1'}),"
                + "          (a2:ALabel {name:'a2'})-[:HAS_REL]->(b2:BLabel {name:'b2'}),"
                + "          (a3:ALabel {name:'a3'})-[:HAS_REL]->(b3:BLabel {name:'b3'}) ");

        testCall(
                db,
                "MATCH (a1:ALabel{name:'a1'}),(a2:ALabel{name:'a2'}),(a3:ALabel{name:'a3'}) " +
                        //                 | here we're using a2 two times!
                        //                \/
                        "WITH [a1,a2,a2,a3] as nodes limit 1 "
                        + "CALL apoc.refactor.mergeNodes(nodes) yield node return node",
                row -> {
                    Node node = (Node) row.get("node");
                    assertNotNull(node);
                    assertEquals(3, node.getDegree(Direction.OUTGOING));
                    assertEquals(0, node.getDegree(Direction.INCOMING));
                });

        testResult(
                db,
                "MATCH (a:ALabel) return count(*) as count",
                result -> assertEquals(
