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

import static apoc.ApocConfig.EXPORT_TO_FILE_ERROR;
import static apoc.export.graphml.ExportGraphMLTestUtil.*;
import static apoc.util.BinaryTestUtil.fileToBinary;
import static apoc.util.BinaryTestUtil.getDecompressedData;
import static apoc.util.MapUtil.map;
import static apoc.util.TestUtil.assertError;
import static apoc.util.TestUtil.testCall;
import static apoc.util.TestUtil.testResult;
import static apoc.util.TransactionTestUtil.checkTerminationGuard;
import static apoc.util.Util.INVALID_QUERY_MODE_ERROR;
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.junit.Assert.fail;
import static org.neo4j.graphdb.Label.label;

import apoc.HelperProcedures;
import apoc.util.BinaryTestUtil;
import apoc.util.CompressionAlgo;
import apoc.util.CompressionConfig;
import apoc.util.TestUtil;
import apoc.util.Util;
import apoc.util.collection.Iterables;
import java.io.File;
import java.io.FileWriter;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import junit.framework.TestCase;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.assertj.core.api.Assertions;
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.graphdb.Node;
import org.neo4j.graphdb.QueryExecutionException;
import org.neo4j.graphdb.Relationship;
import org.neo4j.graphdb.ResourceIterator;
import org.neo4j.test.rule.DbmsRule;
import org.neo4j.test.rule.ImpermanentDbmsRule;

/**
 * @author mh
 * @since 22.05.16
 */
public class ExportGraphMLTest {
    private static final String exportQueryStartAndRel =
            "CALL apoc.export.graphml.query('MATCH (start:Start)-[rel:REL]->(end:End) RETURN start, rel', $file, $config)";
    private static final String exportQueryRelOnly =
            "CALL apoc.export.graphml.query('MATCH (start:Start)-[rel:REL]->(end:End) RETURN rel', $file, $config)";

    private static final String exportDataWithRelOnly =
            """
                MATCH (start:Start)-[rel:REL]->(end:End)
                CALL apoc.export.graphml.data([], [rel], $file, {})
                YIELD nodes, relationships, properties RETURN *""";
    private static final String exportDataWithStartAndRel =
            """
                MATCH (start:Start)-[rel:REL]->(end:End)
                CALL apoc.export.graphml.data([start], [rel], $file, $config)
                YIELD nodes, relationships, properties RETURN *""";

    @Rule
    public TestName testName = new TestName();

    @Rule
    public DbmsRule db = new ImpermanentDbmsRule()
            .withSetting(GraphDatabaseSettings.memory_tracking, true)
            .withSetting(
                    GraphDatabaseSettings.load_csv_file_url_root,
                    directory.toPath().toAbsolutePath());

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

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

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

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

    private String testWithUnknownDefault =
            """
            <?xml version="1.0" encoding="UTF-8" standalone="no"?>
            <graphml xmlns="http://graphml.graphdrawing.org/xmlns" xmlns:java="http://www.yworks.com/xml/yfiles-common/1.0/java" xmlns:sys="http://www.yworks.com/xml/yfiles-common/markup/primitives/2.0" xmlns:x="http://www.yworks.com/xml/yfiles-common/markup/2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:y="http://www.yworks.com/xml/graphml" xmlns:yed="http://www.yworks.com/xml/yed/3" xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://www.yworks.com/xml/schema/graphml/1.1/ygraphml.xsd">
              <key attr.name="restBaseUrl" attr.type="string" for="node" id="d3">
                <default xml:space="preserve"/>
              </key>
              <key id="age" for="node" attr.name="age" attr.type="long"></key>
              <graph edgedefault="directed" id="G">
                <node id="n0" labels=":Gem"><data key="labels">:Gem</data><data key="age">42</data><data key="likes">"food"</data><data key="height">173</data></node>
                <node id="n1" labels=":Gem"><data key="labels">:Gem</data><data key="age">42</data><data key="likes">"food"</data><data key="height">173</data></node>
                <node id="n2" labels=":Gem"><data key="labels">:Gem</data><data key="age">42</data><data key="likes">"food"</data></node>
                <edge id="e0" source="n0" target="n1" label="KNOWS"><data key="label">KNOWS</data></edge>
                <graphml:edge source="n0" target="n1" label="CREATED"></graphml:edge>
              </graph>
              <data key="d7">
                <y:Resources/>
              </data>
            </graphml>
            """;

    // The element: <default xml:space="preserve"/> should be skipped
    @Test
    public void testImportGraphMLWithUnknownDefaultKey() throws Exception {
        db.executeTransactionally("MATCH (n) DETACH DELETE n");

        File output = new File(directory, "test.graphml");
        FileWriter fw = new FileWriter(output);
        fw.write(testWithUnknownDefault);
        fw.close();
        TestUtil.testCall(
                db,
                "CALL apoc.import.graphml($file, {readLabels:true})",
                map("file", output.getAbsolutePath()),
                (r) -> {
                    assertResults(output, r, "statement");
                });

        TestUtil.testCall(
                db, "MATCH  (c:Gem {age: 42}) RETURN COUNT(c) AS c", null, (r) -> assertEquals(3L, r.get("c")));
    }

    @Test
    public void testImportGraphMLIssue() throws Exception {
        db.executeTransactionally("MATCH (n) DETACH DELETE n");

        File output = new File(directory, "import.graphml");
        FileWriter fw = new FileWriter(output);
        fw.write(EXPECTED_TYPES);
        fw.close();
        TestUtil.testCall(
                db, "CALL apoc.import.graphml($file,{readLabels:true})", map("file", output.getAbsolutePath()), (r) -> {
                    assertResults(output, r, "statement");
                });

        TestUtil.testCall(
                db,
                "MATCH  (c:Bar {age: 12, values: [1,2,3]}) RETURN COUNT(c) AS c",
                null,
                (r) -> assertEquals(1L, r.get("c")));
    }

    @Test
    public void testRelDefinedBeforeNodes() throws Exception {
        db.executeTransactionally("MATCH (n) DETACH DELETE n");

        String xml =
                """
                <?xml version="1.0" encoding="UTF-8"?>
                <graphml xmlns="http://graphml.graphdrawing.org/xmlns" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns      http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">
                  <edge id="e183" source="n27" target="n72">
                    <data key="d8">structure</data>
                    <data key="d23">183</data>
                    <data key="d25">60574</data>
                  </edge>
                  <node id="n27">
                    <data key="d22">HTML element</data>
                    <data key="d24">69</data>
                    <data key="d26">540</data>
                    <data key="d19">14</data>
                    <data key="d15">false</data>
                    <data key="d20">TITLE</data>
                  </node>
                  <node id="n72">
                    <data key="d22">text node</data>
                    <data key="d24">72</data>
                    <data key="d26">540</data>
                    <data key="d19">15</data>
                    <data key="d15">false</data>
                    <data key="d21">Example Domain</data>
                  </node>
                  <node id="n75">
                    <data key="d22">text node</data>
                    <data key="d24">75</data>
                    <data key="d26">540</data>
                    <data key="d19">16</data>
                    <data key="d15">false</data>
                    <data key="d21"></data>
                  </node>
                </graphml>
                """;

        File output = new File(directory, "import.graphml");
        FileWriter fw = new FileWriter(output);
        fw.write(xml);
        fw.close();

        RuntimeException e = assertThrows(
                RuntimeException.class,
                () -> TestUtil.testCall(
                        db, "CALL apoc.import.graphml($file,{})", map("file", output.getAbsolutePath()), r -> {}));

        String expectedMessage =
                "The node with the given id: n27 was not found. Check that it is defined before the referencing relationship.";
        Assertions.assertThat(e.getMessage()).contains(expectedMessage);
    }

    @Test
    public void testRoundtripInvalidUnicode() {
        String fileName = new File(directory, "allUnicode.graphml").getAbsolutePath();
        final String query = "CALL apoc.export.graphml.all($file, null)";

        db.executeTransactionally("MATCH (n) DETACH DELETE n");
        testRoundtripInvalidUnicodeCommon(query, fileName);
    }

    @Test
    public void testRoundtripInvalidUnicodeWithExportQuery() {
        String file = new File(directory, "allQuery.graphml").getAbsolutePath();
        // trello issue case: https://trello.com/c/6GboqUau/1070-s3cast-software-issue-with-apocimportgraphml
        final String query =
                "CALL apoc.export.graphml.query('MATCH (n:Unicode) RETURN n', $file, {useTypes: true, readLabels:true})";

        testRoundtripInvalidUnicodeCommon(query, file);
    }

    private void testRoundtripInvalidUnicodeCommon(String query, String file) {
        db.executeTransactionally(
                "CREATE (n:Unicode $props)",
                map(
                        "props",
                        map(
                                "propZero",
                                "\u007FnotEscaped'",
                                "propOne",
                                "\u00101628\u000eX",
                                "propTwo",
                                "abcde\u001ef",
                                "propThree",
                                "aj\u0000eje>",
                                "propFour",
                                "b\u0018ra\u000fz\u0001or'f",
                                "propFive",
                                "a\u0014noth&r\uffff \" one")));

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

        db.executeTransactionally("MATCH (n:Unicode) DETACH DELETE n");
        testImportInvalidUnicode(file);
    }

    @Test
    public void testImportInvalidUnicodeFile() {
        final String file =
                ClassLoader.getSystemResource("fileWithUnicode.graphml").toString();
        testImportInvalidUnicode(file);
    }

    private void testImportInvalidUnicode(String file) {
        TestUtil.testCall(db, "CALL apoc.import.graphml($file, {readLabels:true})", map("file", file), r -> {
            assertEquals(true, r.get("done"));
            assertEquals(1L, r.get("nodes"));
        });

        TestUtil.testCall(db, "MATCH (n:Unicode) RETURN n", r -> {
            final Node node = (Node) r.get("n");
            // valid unicode chars remain as they are
            assertEquals("\u007FnotEscaped'", node.getProperty("propZero"));
            assertEquals("1628X", node.getProperty("propOne"));
            assertEquals("abcdef", node.getProperty("propTwo"));
            assertEquals("ajeje>", node.getProperty("propThree"));
            assertEquals("brazor'f", node.getProperty("propFour"));
            assertEquals("anoth&r \" one", node.getProperty("propFive"));
        });

        db.executeTransactionally("MATCH (n:Unicode) DETACH DELETE n");
    }

    @Test
    public void testRoundTripWithSeparatedImport() {
        Map<String, Object> exportConfig = map("useTypes", true);

        Map<String, Object> importConfig = map(
                "readLabels", true, "storeNodeIds", true, "source", map("label", "Foo"), "target", map("label", "Bar"));

        // we didn't specified a source/target in export config
        // so we have to store the nodeIds and looking for them during relationship import
        separatedFileCommons(exportConfig, importConfig);
    }

    @Test
    public void testImportSeparatedFilesWithCustomId() {
        Map<String, Object> exportConfig =
                map("useTypes", true, "source", map("id", "name"), "target", map("id", "age"));

        Map<String, Object> importConfig = map(
                "readLabels",
                true,
                "source",
                map("label", "Foo", "id", "name"),
                "target",
                map("label", "Bar", "id", "age"));

        // we specified a source/target in export config
        // so storeNodeIds config is unnecessary and we search nodes by properties Foo.name and Bar.age
        separatedFileCommons(exportConfig, importConfig);
    }

    private void separatedFileCommons(Map<String, Object> exportConfig, Map<String, Object> importConfig) {
        db.executeTransactionally(
                "CREATE (:Foo {name: 'zzz'})-[:KNOWS]->(:Bar {age: 0}), (:Foo {name: 'aaa'})-[:KNOWS {id: 1}]->(:Bar {age: 666})");

        // we export 3 files: 1 for source nodes, 1 for end nodes, 1 for relationships
        String outputNodesFoo = new File(directory, "queryNodesFoo.graphml").getAbsolutePath();
        String outputNodesBar = new File(directory, "queryNodesBar.graphml").getAbsolutePath();
        String outputRelationships = new File(directory, "queryRelationship.graphml").getAbsolutePath();

        TestUtil.testCall(
                db,
                "CALL apoc.export.graphml.query('MATCH (start:Foo)-[:KNOWS]->(:Bar) RETURN start',$file, $config)",
                map("file", outputNodesFoo, "config", exportConfig),
                (r) -> assertEquals(3L, r.get("nodes")));

        TestUtil.testCall(
                db,
                "CALL apoc.export.graphml.query('MATCH (:Foo)-[:KNOWS]->(end:Bar) RETURN end', $file, $config) ",
                map("file", outputNodesBar, "config", exportConfig),
                (r) -> assertEquals(3L, r.get("nodes")));

        TestUtil.testCall(
                db,
                "MATCH (:Foo)-[rel:KNOWS]->(:Bar) WITH collect(rel) as rels \n"
                        + "call apoc.export.graphml.data([], rels, $file, $config) "
                        + "YIELD nodes, relationships RETURN nodes, relationships",
                map("file", outputRelationships, "config", exportConfig),
                (r) -> assertEquals(3L, r.get("relationships")));

        // delete current entities and re-import
        db.executeTransactionally("MATCH (n) DETACH DELETE n");

        TestUtil.testCall(
                db,
                "CALL apoc.import.graphml($file, $config)",
                map("file", outputNodesFoo, "config", importConfig),
                (r) -> assertEquals(3L, r.get("nodes")));

        TestUtil.testCall(
                db,
                "CALL apoc.import.graphml($file, $config)",
                map("file", outputNodesBar, "config", importConfig),
                (r) -> assertEquals(3L, r.get("nodes")));

        TestUtil.testCall(
                db,
                "CALL apoc.import.graphml($file, $config)",
                map("file", outputRelationships, "config", importConfig),
                (r) -> assertEquals(3L, r.get("relationships")));

        TestUtil.testResult(
                db,
                "MATCH (start:Foo)-[rel:KNOWS]->(end:Bar) \n"
                        + "RETURN start.name AS startName, rel.id AS relId, end.age AS endAge \n"
                        + "ORDER BY start.name",
                (r) -> {
                    Map<String, Object> row = r.next();
                    assertions(row, "aaa", 1L, 666L);
                    row = r.next();
                    assertions(row, "foo", null, 42L);
                    row = r.next();
                    assertions(row, "zzz", null, 0L);
                    assertFalse(r.hasNext());
                });
    }

    private void assertions(Map<String, Object> row, String expectedSource, Long expectedRel, Long expectedTarget) {
        assertEquals(expectedSource, row.get("startName"));
        assertEquals(expectedRel, row.get("relId"));
        assertEquals(expectedTarget, row.get("endAge"));
    }

    @Test
    public void testImportGraphMLLargeFile() {
        db.executeTransactionally("MATCH (n) DETACH DELETE n");

        final String file = ClassLoader.getSystemResource("largeFile.graphml").toString();
        TestUtil.testCall(db, "CALL apoc.import.graphml($file,{readLabels:true})", map("file", file), (r) -> {
            assertEquals(335160L, r.get("nodes"));
            assertEquals(5666L, r.get("relationships"));
            assertEquals(737297L, r.get("properties"));
            assertEquals(file, r.get("file"));
            assertEquals("graphml", r.get("format"));
            assertEquals(true, r.get("done"));
        });
    }

    @Test
    public void testImportGraphMLTerminate() {
        final String file = ClassLoader.getSystemResource("largeFile.graphml").toString();
        checkTerminationGuard(db, 5L, "CALL apoc.import.graphml($file,{readLabels:true})", map("file", file));
    }

    @Test
    public void testImportGraphMLWithEdgeWithoutDataKeys() throws Exception {
        db.executeTransactionally("MATCH (n) DETACH DELETE n");

        File output = new File(directory, "import.graphml");
        FileWriter fw = new FileWriter(output);
        fw.write(EXPECTED_TYPES_NO_DATA_KEY);
        fw.close();
        TestUtil.testCall(
                db,
                "CALL apoc.import.graphml($file,{readLabels:true})",
                map("file", output.getAbsolutePath()),
                (r) -> assertEquals(2L, r.get("nodes")));

        TestUtil.testCall(
                db, "MATCH  ()-[c:RELATED]->() RETURN COUNT(c) AS c", null, (r) -> assertEquals(1L, r.get("c")));
    }

    @Test
    public void issue2797WithImportGraphMl() {
        db.executeTransactionally("CREATE (n:FOO {name: 'foo'})");
        db.executeTransactionally("CREATE CONSTRAINT unique_foo FOR (n:FOO) REQUIRE n.name IS UNIQUE");
        try {
            TestUtil.testCall(
                    db,
                    "CALL apoc.import.graphml($file, {readLabels:true})",
                    map("file", new File(directory, "importNodeEdges.graphml").getAbsolutePath()),
                    (r) -> fail());
        } catch (Exception e) {
            String expected = "Failed to invoke procedure `apoc.import.graphml`: "
                    + "Caused by: IndexEntryConflictException{propertyValues=( String(\"foo\") ), addedEntityId=-1, existingEntityId=3}";
            assertEquals(expected, e.getMessage());
        }

        // should return only 1 node due to constraint exception
        TestUtil.testCall(
                db,
                "MATCH (n:FOO) RETURN properties(n) AS props",
                r -> assertEquals(Map.of("name", "foo"), r.get("props")));

        db.executeTransactionally("DROP CONSTRAINT unique_foo");
    }

    @Test
    public void testImportGraphMLWithoutCharactersDataKeys() throws Exception {
        db.executeTransactionally("MATCH (n) DETACH DELETE n");

        File output = new File(directory, "import.graphml");
        FileWriter fw = new FileWriter(output);
        fw.write(EXPECTED_TYPES_WITHOUT_CHAR_DATA_KEYS);
        fw.close();
        TestUtil.testCall(
                db, "CALL apoc.import.graphml($file,{readLabels:true})", map("file", output.getAbsolutePath()), (r) -> {
                    assertResults(output, r, "statement");
                });

        TestUtil.testCall(
                db, "MATCH  ()-[c:RELATED]->() RETURN COUNT(c) AS c", null, (r) -> assertEquals(1L, r.get("c")));
    }

    private String NO_REL_TYPES = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
            + "<graphml xmlns=\"http://graphml.graphdrawing.org/xmlns\"\n"
            + "         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n"
            + "         xsi:schemaLocation=\"http://graphml.graphdrawing.org/xmlns\n"
            + "         http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd\">\n"
            + "<!-- Created by igraph -->\n"
            + "  <key id=\"username\" for=\"node\" attr.name=\"username\" attr.type=\"string\"/>\n"
            + "  <key id=\"weight\" for=\"edge\" attr.name=\"weight\" attr.type=\"double\"/>\n"
            + "  <graph id=\"G\" edgedefault=\"directed\">\n"
            + "    <node id=\"n0\">\n"
            + "      <data key=\"username\">Dodo von den Bergen</data>\n"
            + "    </node>\n"
            + "    <node id=\"n1\">\n"
            + "      <data key=\"username\">Semolo75</data>\n"
            + "    </node>  "
            + "<edge source=\"n1\" target=\"n0\">\n"
            + "      <data key=\"weight\">1</data>\n"
            + "    </edge>  </graph>\n"
            + "</graphml>";

    @Test
    public void testImportDefaultRelationship() throws Exception {
        db.executeTransactionally("MATCH (n) DETACH DELETE n");

        File output = new File(directory, "import_no_rel_types.graphml");
        FileWriter fw = new FileWriter(output);
        fw.write(NO_REL_TYPES);
        fw.close();
        db.executeTransactionally(
                "CALL apoc.import.graphml( $file, { batchSize: 5000, readLabels: true, defaultRelationshipType:'DEFAULT_TYPE' })",
                map("file", output.getAbsolutePath()));
        TestUtil.testCall(db, "MATCH ()-[r]-() RETURN Distinct type(r) as type", (r) -> {
            String label = (String) r.get("type");
            assertEquals("DEFAULT_TYPE", label);
        });
    }

    @Test
    public void testImportGraphMLWithNoImportConfig() {
        File output = new File(directory, "all.graphml");
        QueryExecutionException e = assertThrows(
                QueryExecutionException.class,
                () -> TestUtil.testCall(
                        db,
                        "CALL apoc.import.graphml($file,{readLabels:true})",
                        map("file", output.getAbsolutePath()),
                        (r) -> assertResults(output, r, "database")));
        Throwable except = ExceptionUtils.getRootCause(e);
        TestCase.assertTrue(except instanceof RuntimeException);
        assertEquals(
                "Import from files not enabled, please set apoc.import.file.enabled=true in your apoc.conf",
                except.getMessage());
    }

    @Test
    public void testImportGraphMLNodeEdge() throws Exception {
        db.executeTransactionally("MATCH (n) DETACH DELETE n");

        File output = new File(directory, "importNodeEdges.graphml");
        FileWriter fw = new FileWriter(output);
        fw.write(EXPECTED_READ_NODE_EDGE);
        fw.close();
        final String query = "CALL apoc.import.graphml($file,{readLabels:true})";
        final String absolutePath = output.getAbsolutePath();
        commonAssertionImportNodeEdge(absolutePath, query, map("file", absolutePath));
    }

    // There was a bug that some programs export valid graphml files with NaN strings, we do support
    // NAN in Cypher, so no reason to error badly on them.
    @Test
    public void testNumbersThatAreNaN() throws Exception {
        db.executeTransactionally("MATCH (n) DETACH DELETE n");

        File output = new File(directory, "gemTest.graphml");
        FileWriter fw = new FileWriter(output);
        String NAN_NUMBERS =
                """
                <?xml version="1.0" encoding="UTF-8"?>
                <graphml xmlns="http://graphml.graphdrawing.org/xmlns"
                         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                         xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns
                         http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">
                  <key id="age" for="node" attr.name="age" attr.type="long"/>
                  <key id="height" for="node" attr.name="height" attr.type="int"/>
                  <key id="weight" for="node" attr.name="weight" attr.type="double"/>
                  <key id="shoeSize" for="node" attr.name="shoeSize" attr.type="float"/>
                  <graph id="G" edgedefault="directed">
                    <node id="n0">
                      <data key="age">25</data>
                      <data key="height">189</data>
                      <data key="weight">93</data>
                      <data key="shoeSize">44</data>
                    </node>
                    <node id="n1">
                      <data key="age">nan</data>
                      <data key="height">nan</data>
                      <data key="weight">nan</data>
                      <data key="shoeSize">nan</data>
                    </node>
                <edge source="n0" target="n1">
                    </edge>
                </graph>
                </graphml>""";
        fw.write(NAN_NUMBERS);
        fw.close();
        final String query = "CALL apoc.import.graphml($file,{readLabels: true, storeNodeIds:true})";
        final String absolutePath = output.getAbsolutePath();

        TestUtil.testCall(db, query, map("file", absolutePath), (r) -> {
            assertEquals(2L, r.get("nodes"));
            assertEquals(1L, r.get("relationships"));
            assertEquals(8L, r.get("properties"));
            assertEquals(absolutePath, r.get("file"));
            assertEquals("file", r.get("source"));
            assertEquals("graphml", r.get("format"));
            assertTrue("Should get time greater than 0", ((long) r.get("time")) > 0);
        });

        TestUtil.testCall(db, "MATCH (a)-[rel:RELATED]->(b) RETURN a, rel, b", null, (r) -> {
            Node a = (Node) r.get("a");
            var mapA = Util.map("id", "n0", "age", 25L, "weight", 93.0, "shoeSize", 44.0f, "height", 189);
            assertEquals(mapA, a.getAllProperties());

            Node b = (Node) r.get("b");
            var mapB = Util.map(
                    "id", "n1", "age", Float.NaN, "weight", Double.NaN, "shoeSize", Float.NaN, "height", Float.NaN);
            assertEquals(mapB, b.getAllProperties());
        });
    }

    @Test
    public void testImportGraphMLNodeEdgeWithBinary() {
        db.executeTransactionally("MATCH (n) DETACH DELETE n");

        commonAssertionImportNodeEdge(
                null,
                "CALL apoc.import.graphml($file,{readLabels:true, compression: 'DEFLATE'})",
                map("file", fileToBinary(new File(directory, "importNodeEdges.graphml"), "DEFLATE")));
    }

    private void commonAssertionImportNodeEdge(String isBinary, String query, Map<String, Object> config) {
        TestUtil.testCall(db, query, config, (r) -> {
            assertEquals(3L, r.get("nodes"));
            assertEquals(3L, r.get("relationships"));
            assertEquals(5L, r.get("properties"));
            assertEquals(isBinary, r.get("file"));
            assertEquals(isBinary == null ? "binary" : "file", r.get("source"));
            assertEquals("graphml", r.get("format"));
            assertTrue("Should get time greater than 0", ((long) r.get("time")) > 0);
        });

        TestUtil.testCall(db, "MATCH (foo:FOO)-[rel:EDGE_LABEL]->(bar:BAR) RETURN foo, rel, bar", null, (r) -> {
            assertFoo(((Node) r.get("foo")));
            assertBar(((Node) r.get("bar")));
            assertEquals("EDGE_LABEL", ((Relationship) r.get("rel")).getType().name());
            assertEquals(Util.map("name", "foo"), ((Relationship) r.get("rel")).getAllProperties());
        });
        TestUtil.testCall(db, "MATCH (bar:BAR)-[test:TEST]->(foo:FOO) RETURN bar, test, foo", null, (r) -> {
            assertFoo(((Node) r.get("foo")));
            assertBar(((Node) r.get("bar")));
            assertEquals("TEST", ((Relationship) r.get("test")).getType().name());
        });
        TestUtil.testCall(db, "MATCH (bar:BAR)-[knows:KNOWS]->(qwerty:QWERTY) RETURN bar, knows, qwerty", null, (r) -> {
            assertBar(((Node) r.get("bar")));
            assertEquals(Arrays.asList(label("QWERTY")), ((Node) r.get("qwerty")).getLabels());
            assertEquals(Util.map("name", "qwerty"), ((Node) r.get("qwerty")).getAllProperties());
            assertEquals("KNOWS", ((Relationship) r.get("knows")).getType().name());
        });
    }

    private void assertBar(Node node) {
        assertEquals(Arrays.asList(label("BAR")), node.getLabels());
        assertEquals(Util.map("name", "bar", "kids", "[a,b,c]"), node.getAllProperties());
    }

    private void assertFoo(Node node) {
        assertEquals(Arrays.asList(label("FOO")), node.getLabels());
        assertEquals(Util.map("name", "foo"), node.getAllProperties());
    }

    @Test
    public void testExportDataGraphML() {
        db.executeTransactionally("MATCH (n) DETACH DELETE n");
        db.executeTransactionally("CREATE (p:Person {name: 'Foo'})");
        db.executeTransactionally("CREATE (p:Person {name: 'Bar'})");
        db.executeTransactionally("CREATE (p:Person {name: 'Baz'})");
        db.executeTransactionally("CREATE (p:Person {name: 'Foo0'})");

        File output = new File(directory, "data.graphml");
        TestUtil.testCall(
                db,
                "MATCH (person:Person) \n" + "WHERE person.name STARTS WITH \"F\"\n"
                        + "WITH collect(person) as people\n"
                        + "CALL apoc.export.graphml.data(people, [], $file, {})\n"
                        + "YIELD file, source, format, nodes, relationships, properties, time, rows, batchSize, batches, done, data\n"
                        + "RETURN *",
                map("file", output.getAbsolutePath()),
                (r) -> assertEquals(2L, r.get("nodes")));
        assertXMLEquals(output, EXPECTED_DATA);
    }

    @Test
    public void testExportAllGraphML() {
        File output = new File(directory, "all.graphml");
        TestUtil.testCall(
                db,
                "CALL apoc.export.graphml.all($file,null)",
                map("file", output.getAbsolutePath()),
                (r) -> assertResults(output, r, "database"));
        assertXMLEquals(output, EXPECTED_FALSE);
    }

    @Test
    public void testExportAllGraphMLWithCompression() {
        final CompressionAlgo algo = CompressionAlgo.DEFLATE;
        File output = new File(directory, "all.graphml.zz");
        TestUtil.testCall(
                db,
                "CALL apoc.export.graphml.all($file, $config)",
                map("file", output.getAbsolutePath(), "config", map("compression", algo.name())),
                (r) -> assertResults(output, r, "database"));
        assertXMLEquals(BinaryTestUtil.readFileToString(output, StandardCharsets.UTF_8, algo), EXPECTED_FALSE);
    }

    @Test
    public void testGraphMlRoundtrip() {
        final CompressionAlgo algo = CompressionAlgo.NONE;
        File output = new File(directory, "all.graphml.zz");
        final Map<String, Object> params = map(
                "file",
                output.getAbsolutePath(),
                "config",
                map(CompressionConfig.COMPRESSION, algo.name(), "readLabels", true, "useTypes", true));
        TestUtil.testCall(
                db,
                "CALL apoc.export.graphml.all($file, $config)",
                params,
