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

import static apoc.util.MapUtil.map;
import static apoc.util.TestUtil.testCall;
import static java.util.Arrays.asList;
import static junit.framework.TestCase.assertTrue;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.neo4j.graphdb.Label.label;

import apoc.HelperProcedures;
import apoc.graph.util.GraphsConfig;
import apoc.nodes.Nodes;
import apoc.util.JsonUtil;
import apoc.util.TestUtil;
import apoc.util.Util;
import com.neo4j.test.extension.EnterpriseDbmsExtension;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.neo4j.configuration.GraphDatabaseSettings;
import org.neo4j.configuration.SettingImpl;
import org.neo4j.configuration.SettingValueParsers;
import org.neo4j.graphdb.Entity;
import org.neo4j.graphdb.GraphDatabaseService;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.QueryExecutionException;
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;

@SuppressWarnings("unchecked")
@EnterpriseDbmsExtension(configurationCallback = "configure")
public class GraphsTest {

    private static final Map<String, Object> graph = map("name", "test", "properties", map("answer", 42L));

    boolean nonVirtual(Entity entity) {
        return !NumberUtils.isCreatable(entity.getElementId()) || entity.getId() > 0;
    }

    boolean virtual(Entity entity) {
        return entity.getId() < 0;
    }

    @Inject
    GraphDatabaseService db;

    @ExtensionCallback
    void configure(TestDatabaseManagementServiceBuilder builder) {
        builder.setConfig(
                        SettingImpl.newBuilder(
                                        "internal.dbms.debug.track_cursor_close", SettingValueParsers.BOOL, false)
                                .build(),
                        false)
                .setConfig(GraphDatabaseSettings.db_format, "aligned")
                .setConfig(
                        SettingImpl.newBuilder("internal.dbms.debug.trace_cursors", SettingValueParsers.BOOL, false)
                                .build(),
                        false);
    }

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

    @BeforeEach
    void beforeEach() {
        db.executeTransactionally(
                "CREATE (a:Actor {name:'Tom Hanks'})-[r:ACTED_IN {roles:'Forrest'}]->(m:Movie {title:'Forrest Gump'}) RETURN [a,m] as nodes, [r] as relationships",
                Collections.emptyMap(),
                result -> {
                    result.stream().forEach(graph::putAll);
                    return null;
                });
    }

    @Test
    public void testFromData() {
        TestUtil.testCall(
                db,
                "MATCH (n)-[r]->(m) CALL apoc.graph.fromData([n,m],[r],'test',{answer:42}) YIELD graph RETURN *",
                r -> assertEquals(graph, r.get("graph")));
    }

    @Test
    public void testFromPath() {
        TestUtil.testCall(
                db,
                "MATCH path = (n)-[r]->(m) CALL apoc.graph.fromPath(path,'test',{answer:42}) YIELD graph RETURN *",
                r -> assertEquals(graph, r.get("graph")));
    }

    @Test
    public void testFromPaths() {
        // given
        final Map<String, Object> myGraph = map("name", "test", "properties", map("answer", 42L));
        db.executeTransactionally(
                "MATCH (a:Actor {name:'Tom Hanks'}) CREATE (a)-[r:ACTED_IN {roles:'Dott. Henry Goose'}]->(m:Movie {title:'Cloud Atlas'}) RETURN [m] as nodes, [r] as relationships");
        db.executeTransactionally(
                "MATCH p = (a:Actor {name:'Tom Hanks'})-[r:ACTED_IN]->(m:Movie) RETURN reduce(output = [], n in collect(nodes(p)) | output + n) AS nodes, reduce(output = [], r in collect(relationships(p)) | output + r) AS relationships",
                Collections.emptyMap(),
                result -> {
                    result.stream()
                            .flatMap(m -> m.entrySet().stream())
                            .forEach(e -> myGraph.put(
                                    e.getKey(), new ArrayList<>(new HashSet<>((Collection<Object>) e.getValue()))));
                    return null;
                });
        // when
        TestUtil.testCall(
                db,
                "MATCH path = (n)-[r]->(m) WITH collect(path) AS paths CALL apoc.graph.fromPaths(paths,'test',{answer:42}) YIELD graph RETURN *",
                // then
                r -> assertEquals(myGraph, r.get("graph")));

        db.executeTransactionally("MATCH (m:Movie {title:'Cloud Atlas'}) DETACH DELETE m");
    }

    @Test
    public void testFromDB() {
        TestUtil.testCall(db, " CALL apoc.graph.fromDB('test',{answer:42})", r -> assertEquals(graph, r.get("graph")));
    }

    @Test
    public void testFromCypher() {
        TestUtil.testCall(
                db,
                "CALL apoc.graph.fromCypher('MATCH (n)-[r]->(m) RETURN *',null,'test',{answer:42}) YIELD graph RETURN *",
                r -> assertEquals(graph, r.get("graph")));
    }

    @Test
    public void testFromDocument() throws Exception {
        Map<String, Object> artistGenesisMap = Util.map("type", "artist", "name", "Genesis", "id", 1L);
        Map<String, Object> albumGenesisMap =
                Util.map("type", "album", "producer", "Jonathan King", "id", 1L, "title", "From Genesis to Revelation");
        Map<String, Object> artistGenesisMapExt = new HashMap<>() {
            {
                putAll(artistGenesisMap);
                put("albums", List.of(albumGenesisMap));
            }
        };

        TestUtil.testResult(
                db,
                "CALL apoc.graph.fromDocument($json, $config) yield graph",
                Util.map(
                        "json",
                        JsonUtil.OBJECT_MAPPER.writeValueAsString(artistGenesisMapExt),
                        "config",
                        Util.map("write", true)),
                stringObjectMap -> {
                    Map<String, Object> map = stringObjectMap.next();
                    assertEquals("Graph", ((Map<String, Object>) map.get("graph")).get("name"));
                    Collection<Node> nodes = (Collection<Node>) ((Map<String, Object>) map.get("graph")).get("nodes");
                    Collection<Relationship> relationships =
                            (Collection<Relationship>) ((Map<String, Object>) map.get("graph")).get("relationships");
                    assertEquals(2, nodes.size());
                    assertEquals(1, relationships.size());
                    Iterator<Node> nodeIterator = nodes.iterator();
                    Iterator<Relationship> relationshipIterator = relationships.iterator();

                    Node albumGenesis = nodeIterator.next();
                    assertEquals(List.of(label("Album")), albumGenesis.getLabels());
                    // Check this is not a virtual node
                    assertTrue(nonVirtual(albumGenesis));
                    assertEquals(albumGenesisMap, albumGenesis.getAllProperties());
                    Node artistGenesis = nodeIterator.next();
                    assertEquals(List.of(label("Artist")), artistGenesis.getLabels());
                    // Check this is not a virtual node
                    assertTrue(nonVirtual(artistGenesis));
                    assertEquals(artistGenesisMap, artistGenesis.getAllProperties());
                    Relationship rel = relationshipIterator.next();
                    assertEquals("ALBUMS", rel.getType().name());
                    // Check this is not a virtual rel
                    assertTrue(nonVirtual(rel));
                });
        db.executeTransactionally("MATCH p = (a:Artist)-[r:ALBUMS]->(b:Album) detach delete p");
    }

    @Test
    public void testFromDocumentWithCustomRelName() throws Exception {
        Map<String, Object> artistGenesisMap = map("type", "artist", "name", "Genesis", "id", 1L);
        Map<String, Object> albumGenesisMap =
                Util.map("type", "album", "producer", "Jonathan King", "id", 2L, "title", "From Genesis to Revelation");
        Map<String, Object> firstMemberMap = Util.map("type", "member", "name", "Steve Hackett", "id", 3L);
        Map<String, Object> secondMemberMap = Util.map("type", "member", "name", "Phil Collins", "id", 4L);
        Map<String, Object> genreMap = Util.map("type", "genre", "name", "Progressive rock", "id", 5L);

        Map<String, Object> artistGenesisMapExt = new HashMap<>(artistGenesisMap) {
            {
                put("toChange", List.of(genreMap));
                put("albums", List.of(albumGenesisMap));
                put("members", List.of(firstMemberMap, secondMemberMap));
            }
        };

        TestUtil.testResult(
                db,
                "CALL apoc.graph.fromDocument($json, $config) yield graph",
                map(
                        "json",
                        JsonUtil.OBJECT_MAPPER.writeValueAsString(artistGenesisMapExt),
                        "config",
                        map("relMapping", map("toChange", "GENRES"))),
                stringObjectMap -> {
                    Map<String, Object> map = stringObjectMap.next();
                    final Map<String, Object> graphMap = (Map<String, Object>) map.get("graph");
                    assertEquals("Graph", graphMap.get("name"));
                    List<Node> nodes = ((List<Node>) graphMap.get("nodes"));
                    assertEquals(5, nodes.size());

                    Set<String> relSet = ((List<Relationship>) graphMap.get("relationships"))
                            .stream()
                                    .map(Relationship::getType)
                                    .map(RelationshipType::name)
                                    .collect(Collectors.toSet());
                    assertEquals(Set.of("ALBUMS", "MEMBERS", "GENRES"), relSet);
                });
    }

    @Test
    public void testFromDocumentVirtual() throws Exception {
        Map<String, Object> artistGenesisMap = Util.map("type", "artist", "name", "Genesis", "id", 1L);
        Map<String, Object> albumGenesisMap =
                Util.map("type", "album", "producer", "Jonathan King", "id", 1L, "title", "From Genesis to Revelation");
        Map<String, Object> artistGenesisMapExt = new HashMap<>() {
            {
                putAll(artistGenesisMap);
                put("albums", List.of(albumGenesisMap));
            }
        };

        TestUtil.testResult(
                db,
                "CALL apoc.graph.fromDocument($json) yield graph",
                Util.map("json", JsonUtil.OBJECT_MAPPER.writeValueAsString(artistGenesisMapExt)),
                result -> {
                    Map<String, Object> map = result.next();
                    assertEquals("Graph", ((Map<String, Object>) map.get("graph")).get("name"));
                    Collection<Node> nodes = (Collection<Node>) ((Map<String, Object>) map.get("graph")).get("nodes");
                    Collection<Relationship> relationships =
                            (Collection<Relationship>) ((Map<String, Object>) map.get("graph")).get("relationships");
                    assertEquals(2, nodes.size());
                    assertEquals(1, relationships.size());
                    Iterator<Node> nodeIterator = nodes.iterator();
                    Iterator<Relationship> relationshipIterator = relationships.iterator();

                    Node artistGenesis = nodeIterator.next();
                    assertEquals(List.of(label("Artist")), artistGenesis.getLabels());
                    assertTrue(virtual(artistGenesis));
                    assertEquals(artistGenesisMap, artistGenesis.getAllProperties());

                    Node albumGenesis = nodeIterator.next();
                    assertEquals(List.of(label("Album")), albumGenesis.getLabels());
                    assertTrue(virtual(artistGenesis));
                    assertEquals(albumGenesisMap, albumGenesis.getAllProperties());

                    Relationship rel = relationshipIterator.next();
                    assertEquals(RelationshipType.withName("ALBUMS"), rel.getType());
                    assertTrue(virtual(rel));
                });
    }

    @Test
    public void testFromArrayOfDocumentsVirtual() throws Exception {
        Map<String, Object> artistGenesisMap = Util.map("type", "artist", "name", "Genesis", "id", 1L);
        Map<String, Object> albumGenesisMap =
                Util.map("type", "album", "producer", "Jonathan King", "id", 1L, "title", "From Genesis to Revelation");
        Map<String, Object> artistGenesisMapExt = new HashMap<>() {
            {
                putAll(artistGenesisMap);
                put("albums", List.of(albumGenesisMap));
            }
        };
        Map<String, Object> artistDaftPunkMap = Util.map("id", 2L, "name", "Daft Punk", "type", "artist");
        Map<String, Object> albumDaftPunkMap =
                Util.map("id", 2L, "producer", "Daft Punk", "type", "album", "title", "Random Access Memory");
        Map<String, Object> artistDaftPunkMapExt = new HashMap<>() {
            {
                putAll(artistDaftPunkMap);
                put("albums", List.of(albumDaftPunkMap));
            }
        };

        TestUtil.testResult(
                db,
                "CALL apoc.graph.fromDocument($json) yield graph",
                Util.map(
                        "json",
                        JsonUtil.OBJECT_MAPPER.writeValueAsString(
                                Arrays.asList(artistGenesisMapExt, artistDaftPunkMapExt))),
                result -> {
                    Map<String, Object> map = result.next();
                    assertEquals("Graph", ((Map<String, Object>) map.get("graph")).get("name"));
                    Collection<Node> nodes = (Collection<Node>) ((Map<String, Object>) map.get("graph")).get("nodes");
                    Collection<Relationship> relationships =
                            (Collection<Relationship>) ((Map<String, Object>) map.get("graph")).get("relationships");
                    assertEquals(4, nodes.size());
                    assertEquals(2, relationships.size());
                    Iterator<Node> nodeIterator = nodes.iterator();
                    Iterator<Relationship> relationshipIterator = relationships.iterator();

                    Node artistGenesis = nodeIterator.next();
                    assertEquals(List.of(label("Artist")), artistGenesis.getLabels());
                    assertTrue(virtual(artistGenesis));
                    assertEquals(artistGenesisMap, artistGenesis.getAllProperties());

                    Node artistDaftPunk = nodeIterator.next();
                    assertEquals(List.of(label("Artist")), artistDaftPunk.getLabels());
                    assertTrue(virtual(artistDaftPunk));
                    assertEquals(artistDaftPunkMap, artistDaftPunk.getAllProperties());

                    Node albumGenesis = nodeIterator.next();
                    assertEquals(List.of(label("Album")), albumGenesis.getLabels());
                    assertTrue(virtual(albumGenesis));
                    assertEquals(albumGenesisMap, albumGenesis.getAllProperties());

                    Node albumDaftPunk = nodeIterator.next();
                    assertEquals(List.of(label("Album")), albumDaftPunk.getLabels());
                    assertTrue(virtual(albumDaftPunk));
                    assertEquals(albumDaftPunkMap, albumDaftPunk.getAllProperties());

                    Relationship rel = relationshipIterator.next();
                    assertEquals(RelationshipType.withName("ALBUMS"), rel.getType());
                    assertTrue(virtual(rel));

                    rel = relationshipIterator.next();
                    assertEquals(RelationshipType.withName("ALBUMS"), rel.getType());
                    assertTrue(virtual(rel));
                });
    }

    @Test
    public void testFromArrayOfDocuments() throws Exception {
        Map<String, Object> artistGenesisMap = Util.map("type", "artist", "name", "Genesis", "id", 1L);
        Map<String, Object> albumGenesisMap =
                Util.map("type", "album", "producer", "Jonathan King", "id", 1L, "title", "From Genesis to Revelation");
        Map<String, Object> artistGenesisMapExt = new HashMap<>() {
            {
                putAll(artistGenesisMap);
                put("albums", List.of(albumGenesisMap));
            }
        };
        Map<String, Object> artistDaftPunkMap = Util.map("id", 2L, "name", "Daft Punk", "type", "artist");
        Map<String, Object> albumDaftPunkMap =
                Util.map("id", 2L, "producer", "Daft Punk", "type", "album", "title", "Random Access Memory");
        Map<String, Object> artistDaftPunkMapExt = new HashMap<>() {
            {
                putAll(artistDaftPunkMap);
                put("albums", List.of(albumDaftPunkMap));
            }
        };

        TestUtil.testResult(
                db,
                "CALL apoc.graph.fromDocument($json, $config) yield graph",
                Util.map(
                        "json",
                        JsonUtil.OBJECT_MAPPER.writeValueAsString(
                                Arrays.asList(artistGenesisMapExt, artistDaftPunkMapExt)),
                        "config",
                        Util.map("write", true)),
                result -> {
                    Map<String, Object> map = result.next();
                    assertEquals("Graph", ((Map<String, Object>) map.get("graph")).get("name"));
                    Collection<Node> nodes = (Collection<Node>) ((Map<String, Object>) map.get("graph")).get("nodes");
                    Collection<Relationship> relationships =
                            (Collection<Relationship>) ((Map<String, Object>) map.get("graph")).get("relationships");
                    assertEquals(4, nodes.size());
                    assertEquals(2, relationships.size());
                    Iterator<Node> nodeIterator = nodes.iterator();
                    Iterator<Relationship> relationshipIterator = relationships.iterator();

                    Node albumGenesis = nodeIterator.next();
                    assertEquals(List.of(label("Album")), albumGenesis.getLabels());
                    // Check this is not a virtual node
                    assertTrue(nonVirtual(albumGenesis));
                    assertEquals(albumGenesisMap, albumGenesis.getAllProperties());

                    Node albumDaftPunk = nodeIterator.next();
                    assertEquals(List.of(label("Album")), albumDaftPunk.getLabels());
                    // Check this is not a virtual node
                    assertTrue(nonVirtual(albumDaftPunk));
                    assertEquals(albumDaftPunkMap, albumDaftPunk.getAllProperties());

                    Node artistGenesis = nodeIterator.next();
                    assertEquals(List.of(label("Artist")), artistGenesis.getLabels());
                    // Check this is not a virtual node
                    assertTrue(nonVirtual(artistGenesis));
                    assertEquals(artistGenesisMap, artistGenesis.getAllProperties());

                    Node artistDaftPunk = nodeIterator.next();
                    assertEquals(List.of(label("Artist")), artistDaftPunk.getLabels());
                    // Check this is not a virtual node
                    assertTrue(nonVirtual(artistDaftPunk));
                    assertEquals(artistDaftPunkMap, artistDaftPunk.getAllProperties());

                    Relationship rel = relationshipIterator.next();
                    assertEquals("ALBUMS", rel.getType().name());
                    // Check this is not a virtual node
                    assertTrue(nonVirtual(rel));
                    rel = relationshipIterator.next();
                    assertEquals("ALBUMS", rel.getType().name());
                    // Check this is not a virtual rel
                    assertTrue(nonVirtual(rel));
                });
        db.executeTransactionally("MATCH p = (a:Artist)-[r:ALBUMS]->(b:Album) detach delete p");
        Long count = TestUtil.singleResultFirstColumn(
                db, "MATCH p = (a:Artist)-[r:ALBUMS]->(b:Album) RETURN count(p) AS count");
        assertEquals(0L, count.longValue());
    }

    @Test
    public void testFromDocumentVirtualWithCustomIdAndLabel() throws Exception {
        Map<String, Object> genesisMap = Util.map("myCustomType", "artist", "name", "Genesis", "myCustomId", 1L);
        Map<String, Object> albumMap = Util.map(
                "myCustomType",
                "album",
                "producer",
                "Jonathan King",
                "myCustomId",
                1L,
                "title",
                "From Genesis to Revelation");
        Map<String, Object> genesisExt = new HashMap<>() {
            {
                putAll(genesisMap);
                put("albums", List.of(albumMap));
            }
        };
        TestUtil.testResult(
                db,
                "CALL apoc.graph.fromDocument($json, $config) yield graph",
                Util.map(
                        "json",
                        JsonUtil.OBJECT_MAPPER.writeValueAsString(genesisExt),
                        "config",
                        Util.map("labelField", "myCustomType", "idField", "myCustomId")),
                stringObjectMap -> {
                    Map<String, Object> map = stringObjectMap.next();
                    assertEquals("Graph", ((Map<String, Object>) map.get("graph")).get("name"));
                    Collection<Node> nodes = (Collection<Node>) ((Map<String, Object>) map.get("graph")).get("nodes");
                    Collection<Relationship> relationships =
                            (Collection<Relationship>) ((Map<String, Object>) map.get("graph")).get("relationships");
                    assertEquals(2, nodes.size());
                    assertEquals(1, relationships.size());
                    Iterator<Node> nodeIterator = nodes.iterator();
                    Iterator<Relationship> relationshipIterator = relationships.iterator();

                    Node artistGenesis = nodeIterator.next();
                    assertEquals(List.of(label("Artist")), artistGenesis.getLabels());
                    assertTrue(virtual(artistGenesis));
                    assertEquals(genesisMap, artistGenesis.getAllProperties());

                    Node albumGenesis = nodeIterator.next();
                    assertEquals(List.of(label("Album")), albumGenesis.getLabels());
                    assertTrue(virtual(albumGenesis));
                    assertEquals(albumMap, albumGenesis.getAllProperties());

                    Relationship rel = relationshipIterator.next();
                    assertEquals(RelationshipType.withName("ALBUMS"), rel.getType());
                    assertTrue(virtual(rel));
                });
    }

    @Test
    public void testFromDocumentVirtualWithDuplicates() throws Exception {
        Map<String, Object> genesisMap = Util.map("type", "artist", "name", "Genesis", "id", 1L);
        Map<String, Object> albumMap =
                Util.map("type", "album", "producer", "Jonathan King", "id", 1L, "title", "From Genesis to Revelation");
        Map<String, Object> genesisExt = new HashMap<>() {
            {
                putAll(genesisMap);
                put("albums", Arrays.asList(albumMap, albumMap));
            }
        };
        TestUtil.testResult(
                db,
                "CALL apoc.graph.fromDocument($json) yield graph",
                Util.map("json", JsonUtil.OBJECT_MAPPER.writeValueAsString(genesisExt)),
                stringObjectMap -> {
                    Map<String, Object> map = stringObjectMap.next();
                    assertEquals("Graph", ((Map<String, Object>) map.get("graph")).get("name"));
                    Collection<Node> nodes = (Collection<Node>) ((Map<String, Object>) map.get("graph")).get("nodes");
                    Collection<Relationship> relationships =
                            (Collection<Relationship>) ((Map<String, Object>) map.get("graph")).get("relationships");
                    assertEquals(2, nodes.size());
                    assertEquals(1, relationships.size());
                    Iterator<Node> nodeIterator = nodes.iterator();
                    Iterator<Relationship> relationshipIterator = relationships.iterator();

                    Node artistGenesis = nodeIterator.next();
                    assertEquals(List.of(label("Artist")), artistGenesis.getLabels());
                    assertTrue(virtual(artistGenesis));
                    assertEquals(genesisMap, artistGenesis.getAllProperties());

                    Node albumGenesis = nodeIterator.next();
                    assertEquals(List.of(label("Album")), albumGenesis.getLabels());
                    assertTrue(virtual(albumGenesis));
                    assertEquals(albumMap, albumGenesis.getAllProperties());

                    Relationship rel = relationshipIterator.next();
                    assertEquals(RelationshipType.withName("ALBUMS"), rel.getType());
                    assertTrue(virtual(rel));
                });
    }

    @Test
    public void testFromDocumentWithDuplicates() throws Exception {
        Map<String, Object> genesisMap = Util.map("type", "artist", "name", "Genesis", "id", 1L);
        Map<String, Object> albumMap =
                Util.map("type", "album", "producer", "Jonathan King", "id", 1L, "title", "From Genesis to Revelation");
        Map<String, Object> genesisExt = new HashMap<>() {
            {
                putAll(genesisMap);
                put("albums", Arrays.asList(albumMap, albumMap));
            }
        };
        TestUtil.testResult(
                db,
                "CALL apoc.graph.fromDocument($json, $config) yield graph",
                Util.map(
                        "json",
                        JsonUtil.OBJECT_MAPPER.writeValueAsString(genesisExt),
                        "config",
                        Util.map("write", true)),
                stringObjectMap -> {
                    Map<String, Object> map = stringObjectMap.next();
                    assertEquals("Graph", ((Map<String, Object>) map.get("graph")).get("name"));
                    Collection<Node> nodes = (Collection<Node>) ((Map<String, Object>) map.get("graph")).get("nodes");
                    Collection<Relationship> relationships =
                            (Collection<Relationship>) ((Map<String, Object>) map.get("graph")).get("relationships");
                    assertEquals(2, nodes.size());
                    assertEquals(1, relationships.size());
                    Iterator<Node> nodeIterator = nodes.iterator();
                    Iterator<Relationship> relationshipIterator = relationships.iterator();

                    Node albumGenesis = nodeIterator.next();
                    assertEquals(List.of(label("Album")), albumGenesis.getLabels());
                    // Check this is not a virtual node
                    assertTrue(nonVirtual(albumGenesis));
                    assertEquals(albumMap, albumGenesis.getAllProperties());
                    Node artistGenesis = nodeIterator.next();
                    assertEquals(List.of(label("Artist")), artistGenesis.getLabels());
                    // Check this is not a virtual node
                    assertTrue(nonVirtual(artistGenesis));
                    assertEquals(genesisMap, artistGenesis.getAllProperties());
                    Relationship rel = relationshipIterator.next();
                    assertEquals("ALBUMS", rel.getType().name());
                    // Check this is not a virtual rel
                    assertTrue(nonVirtual(rel));
                });
        db.executeTransactionally("MATCH p = (a:Artist)-[r:ALBUMS]->(b:Album) detach delete p");
        Long count = TestUtil.singleResultFirstColumn(
                db, "MATCH p = (a:Artist)-[r:ALBUMS]->(b:Album) RETURN count(p) AS count");
        assertEquals(0L, count.longValue());
    }

    @Test
    public void testCreateVirtualSimpleNodeWithErrorId() throws Exception {
        Map<String, Object> genesisMap = Util.map("type", "artist", "name", "Genesis");
        try {
            TestUtil.testCall(
                    db,
                    "CALL apoc.graph.fromDocument($json) yield graph",
                    Util.map("json", JsonUtil.OBJECT_MAPPER.writeValueAsString(genesisMap)),
                    stringObjectMap -> {});
        } catch (QueryExecutionException e) {
            Throwable except = ExceptionUtils.getRootCause(e);
            assertTrue(except instanceof RuntimeException);
            assertEquals(
                    "The object `{\"type\":\"artist\",\"name\":\"Genesis\"}` must have `id` as id-field name",
                    except.getMessage());
        }
    }

    @Test
    public void testValidateDocument() throws Exception {
        List<Object> list = Arrays.asList(
                Util.map("type", "artist", "name", "Daft Punk"),
                Util.map("id", 1, "type", "artist", "name", "Daft Punk"),
                Util.map("id", 1, "name", "Daft Punk"),
                Util.map("name", "Daft Punk"));

        TestUtil.testResult(
                db,
                "CALL apoc.graph.validateDocument($json, $config) yield row",
                Util.map(
                        "json",
                        JsonUtil.OBJECT_MAPPER.writeValueAsString(list),
                        "config",
                        Util.map("generateId", false)),
                result -> {
                    Map<String, Object> row =
                            (Map<String, Object>) result.next().get("row");
                    assertEquals(0L, row.get("index"));
                    assertEquals(
                            "The object `{\"type\":\"artist\",\"name\":\"Daft Punk\"}` must have `id` as id-field name",
                            row.get("message"));
                    row = (Map<String, Object>) result.next().get("row");
                    assertEquals(2L, row.get("index"));
                    assertEquals(
                            "The object `{\"id\":1,\"name\":\"Daft Punk\"}` must have `type` as label-field name",
                            row.get("message"));
                    row = (Map<String, Object>) result.next().get("row");
                    assertEquals(3L, row.get("index"));
                    assertEquals(
                            "The object `{\"name\":\"Daft Punk\"}` must have `id` as id-field name and `type` as label-field name",
                            row.get("message"));
                    assertFalse(result.hasNext());
                });
    }

    @Test
    public void testValidateDocumentWithCutErrorFormatter() {
        String json =
