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

import static apoc.export.cypher.ExportCypherTest.ExportCypherResults.*;
import static apoc.export.util.ExportFormat.*;
import static apoc.util.BinaryTestUtil.getDecompressedData;
import static apoc.util.TestUtil.assertError;
import static apoc.util.TestUtil.testCall;
import static apoc.util.Util.INVALID_QUERY_MODE_ERROR;
import static apoc.util.Util.map;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.neo4j.configuration.SettingImpl.newBuilder;
import static org.neo4j.configuration.SettingValueParsers.BOOL;

import apoc.HelperProcedures;
import apoc.export.util.ExportConfig;
import apoc.util.BinaryTestUtil;
import apoc.util.CompressionAlgo;
import apoc.util.MapUtil;
import apoc.util.TestUtil;
import apoc.util.Util;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import junit.framework.TestCase;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;
import org.neo4j.configuration.GraphDatabaseSettings;
import org.neo4j.cypher.export.DatabaseSubGraph;
import org.neo4j.graphdb.QueryExecutionException;
import org.neo4j.graphdb.Relationship;
import org.neo4j.graphdb.ResourceIterator;
import org.neo4j.graphdb.Transaction;
import org.neo4j.test.rule.DbmsRule;
import org.neo4j.test.rule.ImpermanentDbmsRule;

/**
 * @author mh
 * @since 22.05.16
 */
public class ExportCypherTest {
    private static final String queryWithRelOnly = "MATCH (start:Foo)-[rel:KNOWS]->(end:Bar) RETURN rel";
    private static final String queryWithStartAndRel = "MATCH (start:Foo)-[rel:KNOWS]->(end:Bar) RETURN start, rel";
    private static final String exportQuery = "CALL apoc.export.cypher.query($query, $file, $config)";

    private static final String exportDataWithStartAndRel =
            """
                MATCH (start:Foo)-[rel:KNOWS]->(end:Bar)
                CALL apoc.export.cypher.data([start], [rel], $file, $config)
                YIELD nodes, relationships, properties RETURN *""";
    private static final String exportDataWithRelOnly =
            """
                MATCH (start:Foo)-[rel:KNOWS]->(end:Bar)
                CALL apoc.export.cypher.data([], [rel], $file, $config)
                YIELD nodes, relationships, properties RETURN *""";

    private static final Map<String, Object> exportConfig =
            map("useOptimizations", map("type", "none"), "separateFiles", true, "format", "neo4j-admin");

    private static final File directory = new File("target/import");

    static { //noinspection ResultOfMethodCallIgnored
        directory.mkdirs();
    }

    @Rule
    public DbmsRule db = new ImpermanentDbmsRule()
            .withSetting(
                    GraphDatabaseSettings.load_csv_file_url_root,
                    directory.toPath().toAbsolutePath())
            .withSetting(
                    newBuilder("internal.dbms.debug.track_cursor_close", BOOL, false)
                            .build(),
                    false)
            .withSetting(
                    newBuilder("internal.dbms.debug.trace_cursors", BOOL, false).build(), false)
            // Test assertions depends on sequential ids
            .withSetting(GraphDatabaseSettings.db_format, "aligned");

    @Rule
    public TestName testName = new TestName();

    @Before
    public void setUp() {
        ExportCypherTestUtils.setUp(db, testName);
    }

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

    @Test
    public void testRoundTripCypherExportAll() {
        final String cypherStatements = Util.readResourceFile("exportAll.cypher");
        db.executeTransactionally("CALL apoc.cypher.runMany($statements, {})", map("statements", cypherStatements));

        String fileName = "all.cypher";
        final String query =
                "CALL apoc.export.cypher.all($fileName, {batchSize: 100, format: 'cypher-shell', saveIndexNames: true, saveConstraintNames: true, multipleRelationshipsWithType: true})";

        TestUtil.testCall(db, query, map("fileName", fileName), (r) -> assertEquals(fileName, r.get("file")));

        assertEquals(cypherStatements, readFile("all.cypher").strip());
    }

    @Test
    public void testCypherRoundTrip() {
        // Set up
        db.executeTransactionally(
                """
                CREATE CONSTRAINT composite_unique
                FOR (n:Node)
                REQUIRE (n.propA, n.propB) IS UNIQUE;
                """);

        db.executeTransactionally(
                """
                CREATE (a:Node {propA: "X", propB: "1"})
                CREATE (b:Node {propA: "Y"})
                CREATE (a)-[:LINKED_TO]->(b);
                """);

        String fileName = "allWithConstraints.cypher";

        // Run export
        testCall(
                db,
                "CALL apoc.export.cypher.all($fileName, {writeNodeProperties: true})",
                map("fileName", fileName),
                (r) -> assertEquals(fileName, r.get("file")));

        db.executeTransactionally("""
                DROP CONSTRAINT composite_unique;
                """);

        db.executeTransactionally("""
                MATCH (n) DETACH DELETE n;
                """);

        final String cypherStatements = readFile(fileName);
        db.executeTransactionally("CALL apoc.cypher.runMany($statements, {})", map("statements", cypherStatements));

        testCall(
                db,
                """
                        RETURN EXISTS {
                            MATCH (a:Node {propA: "X", propB: "1"})-[:LINKED_TO]->(b:Node {propA: "Y"})
                        }
                        AND COUNT { MATCH (n) RETURN 1 } = 2
                        AND NOT EXISTS { MATCH (n:`UNIQUE IMPORT LABEL`) RETURN 1 } AS dbIsCorrect
                        """,
                (r) -> assertTrue((Boolean) r.get("dbIsCorrect")));

        // Clean up
        db.executeTransactionally("MATCH (n) DETACH DELETE n;");
    }

    @Test
    public void testUniqueNodeLabels() {
        // Check the unique node test doesn't fail if the node has extra non-unique constrained labels
        String createExtraNodes = "MATCH (n) SET n:EXTRA_LABEL";
        db.executeTransactionally(createExtraNodes);

        try (Transaction tx = db.beginTx()) {
            MultiStatementCypherSubGraphExporter exporter =
                    new MultiStatementCypherSubGraphExporter(new DatabaseSubGraph(tx), new ExportConfig(null), db);

            assertEquals(exporter.countArtificialUniqueNodes(tx.getAllNodes()), 2);
        }
    }

    @Test
    public void testExportAllCypherResults() {
        TestUtil.testCall(
                db,
                "CALL apoc.export.cypher.all(null,{useOptimizations: { type: 'none'}, format: 'neo4j-shell'})",
                (r) -> {
                    assertResults(null, r, "database");
                    assertEquals(EXPECTED_NEO4J_SHELL, r.get("cypherStatements"));
                });
    }

    @Test
    public void testExportAllCypherStreaming() {
        final String query =
                "CALL apoc.export.cypher.all(null,{useOptimizations: { type: 'none'}, streamStatements:true,batchSize:3, format: 'neo4j-shell'})";
        assertExportAllCypherStreaming(CompressionAlgo.NONE, query);
    }

    @Test
    public void testExportAllWithCompressionCypherStreaming() {
        final CompressionAlgo algo = CompressionAlgo.BZIP2;
        final String query = "CALL apoc.export.cypher.all(null,{compression: '" + algo.name()
                + "', useOptimizations: { type: 'none'}, streamStatements:true,batchSize:3, format: 'neo4j-shell'})";
        assertExportAllCypherStreaming(algo, query);
    }

    @Test
    public void testExportAllWithStreamingSeparatedAndCompressedFile() {
        final CompressionAlgo algo = CompressionAlgo.BZIP2;
        final Map<String, Object> config = map(
                "compression",
                algo.name(),
                "useOptimizations",
                map("type", "none"),
                "stream",
                true,
                "separateFiles",
                true,
                "format",
                "neo4j-admin");

        TestUtil.testCall(db, "CALL apoc.export.cypher.all(null, $config)", map("config", config), r -> {
            assertEquals(EXPECTED_NODES, getDecompressedData(algo, r.get("nodeStatements")));
            assertEquals(EXPECTED_RELATIONSHIPS, getDecompressedData(algo, r.get("relationshipStatements")));
            assertEquals(EXPECTED_SCHEMA, getDecompressedData(algo, r.get("schemaStatements")));
            assertEquals(EXPECTED_CLEAN_UP, getDecompressedData(algo, r.get("cleanupStatements")));
        });
    }

    private void assertExportAllCypherStreaming(CompressionAlgo algo, String query) {
        StringBuilder sb = new StringBuilder();
        TestUtil.testResult(db, query, (res) -> {
            Map<String, Object> r = res.next();
            assertEquals(3L, r.get("batchSize"));
            assertEquals(1L, r.get("batches"));
            assertEquals(3L, r.get("nodes"));
            assertEquals(3L, r.get("rows"));
            assertEquals(0L, r.get("relationships"));
            assertEquals(5L, r.get("properties"));
            assertNull("Should get file", r.get("file"));
            assertEquals("cypher", r.get("format"));
            assertTrue("Should get time greater than 0", ((long) r.get("time")) >= 0);
            sb.append(getDecompressedData(algo, r.get("cypherStatements")));
            r = res.next();
            assertEquals(3L, r.get("batchSize"));
            assertEquals(2L, r.get("batches"));
            assertEquals(3L, r.get("nodes"));
            assertEquals(4L, r.get("rows"));
            assertEquals(1L, r.get("relationships"));
            assertEquals(6L, r.get("properties"));
            assertTrue("Should get time greater than 0", ((long) r.get("time")) >= 0);
            sb.append(getDecompressedData(algo, r.get("cypherStatements")));
        });
        assertEquals(EXPECTED_NEO4J_SHELL.replace("LIMIT 20000", "LIMIT 3"), sb.toString());
    }

    // -- Whole file test -- //
    @Test
    public void testExportAllCypherDefault() {
        String fileName = "all.cypher";
        TestUtil.testCall(
                db,
                "CALL apoc.export.cypher.all($fileName,{useOptimizations: { type: 'none'}, format: 'neo4j-shell'})",
                map("fileName", fileName),
                (r) -> assertResults(fileName, r, "database"));
        assertEquals(EXPECTED_NEO4J_SHELL, readFile(fileName));
    }

    @Test
    public void testExportAllCypherForCypherShell() {
        String fileName = "all.cypher";
        TestUtil.testCall(
                db,
                "CALL apoc.export.cypher.all($file,$config)",
                map("file", fileName, "config", map("useOptimizations", map("type", "none"), "format", "cypher-shell")),
                (r) -> assertResults(fileName, r, "database"));
        assertEquals(EXPECTED_CYPHER_SHELL, readFile(fileName));
    }

    @Test
    public void testExportQueryCypherForNeo4j() {
        String fileName = "all.cypher";
        String query = "MATCH (n) OPTIONAL MATCH p = (n)-[r]-(m) RETURN n,r,m";
        TestUtil.testCall(
                db,
                exportQuery,
                map(
                        "file",
                        fileName,
                        "query",
                        query,
                        "config",
                        map("useOptimizations", map("type", "none"), "format", "neo4j-shell")),
                (r) -> {});
        assertEquals(EXPECTED_NEO4J_SHELL, readFile(fileName));
    }

    @Test
    public void testExportQueryOnlyRel() {
        final Map<String, Object> config = withoutOptimization(map("format", "neo4j-shell"));
        Map<String, Object> params = Map.of("query", queryWithRelOnly, "config", config);
        assertExportRelOnly(params, exportQuery);

        // check that apoc.export.cypher.data returns consistent results
        assertExportRelOnly(params, exportDataWithRelOnly);
    }

    @Test
    public void testExportQueryOnlyRelWithNodeOfRelsTrue() {
        // check that `nodesOfRelationships: true` doesn't change the result
        final Map<String, Object> config =
                withoutOptimization(map("format", "neo4j-shell", "nodesOfRelationships", true));
        Map<String, Object> params = Map.of("query", queryWithRelOnly, "config", config);
        assertExportRelOnly(params, exportQuery);

        // check that apoc.export.cypher.data returns consistent results
        assertExportRelOnly(params, exportDataWithRelOnly);
    }

    @Test
    public void testExportQueryOnlyRelAndStart() {
        final Map<String, Object> config = withoutOptimization(map("format", "neo4j-shell"));
        Map<String, Object> params = Map.of("query", queryWithStartAndRel, "config", config);
        assertExportWithoutEndNode(params, exportQuery);

        // check that apoc.export.cypher.data returns consistent results
        assertExportWithoutEndNode(params, exportDataWithStartAndRel);
    }

    @Test
    public void testExportQueryOnlyRelAndStartWithNodeOfRelsTrue() {
        // check that with {nodesOfRelationships: true} the end node is returned as well
        final Map<String, Object> config =
                withoutOptimization(map("format", "neo4j-shell", "nodesOfRelationships", true));
        Map<String, Object> params = Map.of("query", queryWithStartAndRel, "config", config);

        String expectedNodesWithEndNode = String.format(EXPECTED_BEGIN_AND_FOO
                + "CREATE (:Bar:`UNIQUE IMPORT LABEL` {age:42, name:\"bar\", `UNIQUE IMPORT ID`:1});%n"
                + "COMMIT%n");
        String expectedWithEndNode =
                expectedNodesWithEndNode + EXPECTED_SCHEMA_ONLY_START + EXPECTED_REL_ONLY + EXPECTED_CLEAN_UP;

        commonDataAndQueryAssertions(exportQuery, params, expectedWithEndNode, 2L, 5L);

        // apoc.export.cypher.data doesn't accept `nodesOfRelationships` config,
        // so it returns the same result as the above one
        assertExportWithoutEndNode(params, exportDataWithStartAndRel);
    }

    private void assertExportWithoutEndNode(Map<String, Object> params, String exportData) {
        commonDataAndQueryAssertions(exportData, params, EXPECTED_WITHOUT_END_NODE, 1L, 3L);
    }

    private void assertExportRelOnly(Map<String, Object> params, String exportData) {
        commonDataAndQueryAssertions(exportData, params, EXPECTED_REL_ONLY, 0L, 1L);
    }

    private void commonDataAndQueryAssertions(
            String query, Map<String, Object> config, String expectedOutput, long nodes, long properties) {
        String fileName = "testFile.cypher";

        Map<String, Object> params = map("file", fileName);
        params.putAll(config);

        TestUtil.testCall(db, query, params, r -> {
            assertEquals(nodes, r.get("nodes"));
            assertEquals(1L, r.get("relationships"));
            assertEquals(properties, r.get("properties"));
        });
        assertEquals(expectedOutput, readFile(fileName));
    }

    @Test
    public void testExportCypherAdminOperationErrorMessage() {
        String filename = "test.cypher";
        List<String> invalidQueries =
                List.of("SHOW CONSTRAINTS YIELD id, name, type RETURN *", "SHOW INDEXES YIELD id, name, type RETURN *");

        for (String query : invalidQueries) {
            QueryExecutionException e = Assert.assertThrows(
                    QueryExecutionException.class,
                    () -> TestUtil.testCall(
                            db,
                            exportQuery,
                            MapUtil.map(
                                    "query", query,
                                    "file", filename,
                                    "config", map("useOptimizations", map("type", "none"), "format", "neo4j-shell")),
                            (r) -> assertResults(filename, r, "database")));

            assertError(e, INVALID_QUERY_MODE_ERROR, RuntimeException.class, "apoc.export.cypher.query");
        }
    }

    private static String readFile(String fileName) {
        return readFile(fileName, CompressionAlgo.NONE);
    }

    private static String readFile(String fileName, CompressionAlgo algo) {
        return BinaryTestUtil.readFileToString(new File(directory, fileName), UTF_8, algo);
    }

    @Test
    public void testExportGraphCypher() {
        String fileName = "graph.cypher";
        TestUtil.testCall(
                db,
                "CALL apoc.graph.fromDB('test',{}) yield graph "
                        + "CALL apoc.export.cypher.graph(graph, $file,$exportConfig) "
                        + "YIELD nodes, relationships, properties, file, source,format, time "
                        + "RETURN *",
                map(
                        "file",
                        fileName,
                        "exportConfig",
                        map("useOptimizations", map("type", "none"), "format", "neo4j-shell")),
                (r) -> assertResults(fileName, r, "graph"));
        assertEquals(EXPECTED_NEO4J_SHELL, readFile(fileName));
    }

    // -- Separate files tests -- //
    @Test
    public void testExportAllCypherNodes() {
        String fileName = "all.cypher";
        TestUtil.testCall(
                db,
                "CALL apoc.export.cypher.all($file,$exportConfig)",
                map("file", fileName, "exportConfig", exportConfig),
                (r) -> assertResults(fileName, r, "database"));
        assertEquals(EXPECTED_NODES, readFile("all.nodes.cypher"));
    }

    @Test
    public void testExportAllCypherRelationships() {
        String fileName = "all.cypher";
        TestUtil.testCall(
                db,
                "CALL apoc.export.cypher.all($file,$exportConfig)",
                map("file", fileName, "exportConfig", exportConfig),
                (r) -> assertResults(fileName, r, "database"));
        assertEquals(EXPECTED_RELATIONSHIPS, readFile("all.relationships.cypher"));
    }

    @Test
    public void testExportAllCypherSchema() {
        String fileName = "all.cypher";
        TestUtil.testCall(
                db,
                "CALL apoc.export.cypher.all($file,$exportConfig)",
                map("file", fileName, "exportConfig", exportConfig),
                (r) -> assertResults(fileName, r, "database"));
        assertEquals(EXPECTED_SCHEMA, readFile("all.schema.cypher"));
    }

    @Test
    public void testExportAllCypherSchemaWithSaveIdxNames() {
        final Map<String, Object> config = new HashMap<>(ExportCypherTest.exportConfig);
        config.put("saveIndexNames", true);
        String fileName = "all.cypher";
        TestUtil.testCall(
                db,
                "CALL apoc.export.cypher.all($file,$config)",
                map("file", fileName, "config", config),
                (r) -> assertResults(fileName, r, "database"));
        assertEquals(EXPECTED_SCHEMA_WITH_NAMES, readFile("all.schema.cypher"));
    }

    @Test
    public void testExportAllCypherCleanUp() {
        String fileName = "all.cypher";
        TestUtil.testCall(
                db,
                "CALL apoc.export.cypher.all($file,$exportConfig)",
                map("file", fileName, "exportConfig", exportConfig),
                (r) -> assertResults(fileName, r, "database"));
        assertEquals(EXPECTED_CLEAN_UP, readFile("all.cleanup.cypher"));
    }

    @Test
    public void testExportGraphCypherNodes() {
        String fileName = "graph.cypher";
        TestUtil.testCall(
                db,
                "CALL apoc.graph.fromDB('test',{}) yield graph "
                        + "CALL apoc.export.cypher.graph(graph, $file,$exportConfig) "
                        + "YIELD nodes, relationships, properties, file, source,format, time "
                        + "RETURN *",
                map("file", fileName, "exportConfig", exportConfig),
                (r) -> assertResults(fileName, r, "graph"));
        assertEquals(EXPECTED_NODES, readFile("graph.nodes.cypher"));
    }

    @Test
    public void testExportGraphCypherRelationships() {
        String fileName = "graph.cypher";
        TestUtil.testCall(
                db,
                "CALL apoc.graph.fromDB('test',{}) yield graph "
                        + "CALL apoc.export.cypher.graph(graph, $file,$exportConfig) "
                        + "YIELD nodes, relationships, properties, file, source,format, time "
                        + "RETURN *",
                map("file", fileName, "exportConfig", exportConfig),
                (r) -> assertResults(fileName, r, "graph"));
        assertEquals(EXPECTED_RELATIONSHIPS, readFile("graph.relationships.cypher"));
    }

    @Test
    public void testExportGraphCypherSchema() {
        String fileName = "graph.cypher";
        TestUtil.testCall(
                db,
                "CALL apoc.graph.fromDB('test',{}) yield graph "
                        + "CALL apoc.export.cypher.graph(graph, $file,$exportConfig) "
                        + "YIELD nodes, relationships, properties, file, source,format, time "
                        + "RETURN *",
                map("file", fileName, "exportConfig", exportConfig),
                (r) -> assertResults(fileName, r, "graph"));
        assertEquals(EXPECTED_SCHEMA, readFile("graph.schema.cypher"));
    }

    @Test
    public void testExportGraphCypherCleanUp() {
        String fileName = "graph.cypher";
        TestUtil.testCall(
                db,
                "CALL apoc.graph.fromDB('test',{}) yield graph "
                        + "CALL apoc.export.cypher.graph(graph, $file,$exportConfig) "
                        + "YIELD nodes, relationships, properties, file, source,format, time "
                        + "RETURN *",
                map("file", fileName, "exportConfig", exportConfig),
                (r) -> assertResults(fileName, r, "graph"));
        assertEquals(EXPECTED_CLEAN_UP, readFile("graph.cleanup.cypher"));
    }

    private void assertResults(String fileName, Map<String, Object> r, final String source) {
        assertEquals(3L, r.get("nodes"));
        assertEquals(1L, r.get("relationships"));
        assertEquals(6L, r.get("properties"));
        assertEquals(fileName, r.get("file"));
        assertEquals(source + ": nodes(3), rels(1)", r.get("source"));
        assertEquals("cypher", r.get("format"));
        assertTrue("Should get time greater than 0", ((long) r.get("time")) >= 0);
    }

    @Test
    public void testExportQueryCypherPlainFormat() {
        String fileName = "all.cypher";
        String query = "MATCH (n) OPTIONAL MATCH p = (n)-[r]-(m) RETURN n,r,m";
        TestUtil.testCall(
                db,
                exportQuery,
                map(
                        "file",
                        fileName,
                        "query",
                        query,
                        "config",
                        map("useOptimizations", map("type", "none"), "format", "plain")),
                (r) -> {});
        assertEquals(EXPECTED_PLAIN, readFile(fileName));
    }

    @Test
    public void testExportQueryCypherFormatUpdateAll() {
        String fileName = "all.cypher";
        String query = "MATCH (n) OPTIONAL MATCH p = (n)-[r]-(m) RETURN n,r,m";
        TestUtil.testCall(
                db,
                exportQuery,
                map(
                        "file",
                        fileName,
                        "query",
                        query,
                        "config",
                        map(
                                "useOptimizations",
                                map("type", "none"),
                                "format",
                                "neo4j-shell",
                                "cypherFormat",
                                "updateAll")),
                (r) -> {});
        assertEquals(EXPECTED_NEO4J_MERGE, readFile(fileName));
    }

    @Test
    public void testExportQueryCypherFormatAddStructure() {
        String fileName = "all.cypher";
        String query = "MATCH (n) OPTIONAL MATCH p = (n)-[r]-(m) RETURN n,r,m";
        TestUtil.testCall(
                db,
                exportQuery,
                map(
                        "file",
                        fileName,
                        "query",
                        query,
                        "config",
                        map(
                                "useOptimizations",
                                map("type", "none"),
                                "format",
                                "neo4j-shell",
                                "cypherFormat",
                                "addStructure")),
                (r) -> {});
        assertEquals(
                EXPECTED_NODES_MERGE_ON_CREATE_SET
                        + EXPECTED_SCHEMA_EMPTY
                        + EXPECTED_RELATIONSHIPS
                        + EXPECTED_CLEAN_UP_EMPTY,
                readFile(fileName));
    }

    @Test
    public void testExportQueryCypherFormatUpdateStructure() {
        String fileName = "all.cypher";
        String query = "MATCH (n) OPTIONAL MATCH p = (n)-[r]-(m) RETURN n,r,m";
        TestUtil.testCall(
                db,
                exportQuery,
                map(
                        "file",
                        fileName,
                        "query",
                        query,
                        "config",
                        map(
                                "useOptimizations",
                                map("type", "none"),
                                "format",
                                "neo4j-shell",
                                "cypherFormat",
                                "updateStructure")),
                (r) -> {});
        assertEquals(
                EXPECTED_NODES_EMPTY
                        + EXPECTED_SCHEMA_EMPTY
                        + EXPECTED_RELATIONSHIPS_MERGE_ON_CREATE_SET
                        + EXPECTED_CLEAN_UP_EMPTY,
                readFile(fileName));
    }

    @Test
    public void testExportSchemaCypher() {
        String fileName = "onlySchema.cypher";
        TestUtil.testCall(
                db,
                "CALL apoc.export.cypher.schema($file,$exportConfig)",
                map("file", fileName, "exportConfig", exportConfig),
                (r) -> {});
        assertEquals(EXPECTED_ONLY_SCHEMA_NEO4J_SHELL, readFile(fileName));
    }

    @Test
    public void testExportSchemaCypherWithStreamTrue() {
        Consumer<Map<String, Object>> resultAssertion = (r) -> {
            Object actual = r.get("cypherStatements");
            assertEquals(EXPECTED_ONLY_SCHEMA_CYPHER_SHELL, actual);
        };

        // with {stream: true}
        TestUtil.testCall(db, "CALL apoc.export.cypher.schema(null, {stream: true})", resultAssertion);

        // with {streamStatements: true}
        TestUtil.testCall(db, "CALL apoc.export.cypher.schema(null, {streamStatements: true})", resultAssertion);

        // assert `schemaStatements` of `apoc.export.cypher.all`
        // is equal to `cypher.schema` output plus the `CREATE CONSTRAINT UNIQUE_IMPORT_NAME ...` statement
        TestUtil.testCall(db, "CALL apoc.export.cypher.all(null, { separateFiles: true, stream: true})", r -> {
            Object actual = r.get("schemaStatements");
            assertEquals(convertToCypherShellFormat(EXPECTED_SCHEMA), actual);
        });
    }

    @Test
    public void testExportSchemaCypherWithIdxNames() {
        final Map<String, Object> config = new HashMap<>(ExportCypherTest.exportConfig);
        config.putAll(map("saveIndexNames", true));
        String fileName = "onlySchema.cypher";
        TestUtil.testCall(
                db,
                "CALL apoc.export.cypher.schema($file,$config)",
                map("file", fileName, "config", config),
                (r) -> {});
        assertEquals(EXPECTED_ONLY_SCHEMA_NEO4J_SHELL_WITH_NAMES, readFile(fileName));
    }

    @Test
    public void testExportSchemaCypherShell() {
        String fileName = "onlySchema.cypher";
        TestUtil.testCall(
                db,
                "CALL apoc.export.cypher.schema($file,$exportConfig)",
                map(
                        "file",
                        fileName,
                        "exportConfig",
                        map("useOptimizations", map("type", "none"), "format", "cypher-shell")),
                (r) -> {});
        assertEquals(EXPECTED_ONLY_SCHEMA_CYPHER_SHELL, readFile(fileName));
    }

    @Test
    public void testExportCypherNodePoint() {
        db.executeTransactionally("CREATE (f:Test {name:'foo'," + "place2d:point({ x: 2.3, y: 4.5 }),"
                + "place3d1:point({ x: 2.3, y: 4.5 , z: 1.2})})"
                + "-[:FRIEND_OF {place2d:point({ longitude: 56.7, latitude: 12.78 })}]->"
                + "(:Bar {place3d:point({ longitude: 12.78, latitude: 56.7, height: 100 })})");
        String fileName = "temporalPoint.cypher";
        String query = "MATCH (n:Test)-[r]-(m) RETURN n,r,m";
        TestUtil.testCall(
                db,
                exportQuery,
                map(
                        "file",
                        fileName,
                        "query",
                        query,
                        "config",
                        map("useOptimizations", map("type", "none"), "format", "neo4j-shell")),
                (r) -> {});
        assertEquals(EXPECTED_CYPHER_POINT, readFile(fileName));
    }

    @Test
    public void testExportCypherNodeDate() {
        db.executeTransactionally("CREATE (f:Test {name:'foo', " + "date:date('2018-10-30'), "
                + "datetime:datetime('2018-10-30T12:50:35.556+0100'), "
                + "localTime:localdatetime('20181030T19:32:24')})"
                + "-[:FRIEND_OF {date:date('2018-10-30')}]->"
                + "(:Bar {datetime:datetime('2018-10-30T12:50:35.556')})");
        String fileName = "temporalDate.cypher";
        String query = "MATCH (n:Test)-[r]-(m) RETURN n,r,m";
        TestUtil.testCall(
                db,
                exportQuery,
                map(
                        "file",
                        fileName,
                        "query",
                        query,
                        "config",
                        map("useOptimizations", map("type", "none"), "format", "neo4j-shell")),
                (r) -> {});
        assertEquals(EXPECTED_CYPHER_DATE, readFile(fileName));
    }

    @Test
    public void testExportCypherNodeTime() {
        db.executeTransactionally("CREATE (f:Test {name:'foo', " + "local:localtime('12:50:35.556'),"
                + "t:time('125035.556+0100')})"
                + "-[:FRIEND_OF {t:time('125035.556+0100')}]->"
                + "(:Bar {datetime:datetime('2018-10-30T12:50:35.556+0100')})");
        String fileName = "temporalTime.cypher";
        String query = "MATCH (n:Test)-[r]-(m) RETURN n,r,m";
        TestUtil.testCall(
                db,
                exportQuery,
                map(
                        "file",
                        fileName,
                        "query",
                        query,
                        "config",
                        map("useOptimizations", map("type", "none"), "format", "neo4j-shell")),
                (r) -> {});
