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

import static apoc.ApocConfig.APOC_EXPORT_FILE_ENABLED;
import static apoc.ApocConfig.APOC_IMPORT_FILE_ENABLED;
import static apoc.ApocConfig.apocConfig;
import static apoc.util.BinaryTestUtil.getDecompressedData;
import static apoc.util.CompressionAlgo.DEFLATE;
import static apoc.util.CompressionAlgo.GZIP;
import static apoc.util.CompressionAlgo.NONE;
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.Util.INVALID_QUERY_MODE_ERROR;
import static java.nio.charset.StandardCharsets.UTF_8;
import static junit.framework.TestCase.assertTrue;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;

import apoc.HelperProcedures;
import apoc.csv.CsvTestUtil;
import apoc.graph.Graphs;
import apoc.meta.Meta;
import apoc.meta.MetaRestricted;
import apoc.util.CompressionAlgo;
import apoc.util.CompressionConfig;
import apoc.util.TestUtil;
import apoc.util.Util;
import apoc.util.collection.Iterators;
import com.neo4j.test.extension.EnterpriseDbmsExtension;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.neo4j.graphdb.GraphDatabaseService;
import org.neo4j.graphdb.QueryExecutionException;
import org.neo4j.graphdb.Result;
import org.neo4j.test.TestDatabaseManagementServiceBuilder;
import org.neo4j.test.extension.ExtensionCallback;
import org.neo4j.test.extension.Inject;
import org.neo4j.test.utils.TestDirectory;

/**
 * @author mh
 * @since 22.05.16
 */
@EnterpriseDbmsExtension(configurationCallback = "configure", createDatabasePerTest = false)
public class ExportCsvTest {

    private static final String EXPECTED_QUERY_NODES = String.format("\"u\"%n"
            + "\"{\"\"id\"\":0,\"\"labels\"\":[\"\"User\"\",\"\"User1\"\"],\"\"properties\"\":{\"\"name\"\":\"\"foo\"\",\"\"age\"\":42,\"\"male\"\":true,\"\"kids\"\":[\"\"a\"\",\"\"b\"\",\"\"c\"\"]}}\"%n"
            + "\"{\"\"id\"\":1,\"\"labels\"\":[\"\"User\"\"],\"\"properties\"\":{\"\"name\"\":\"\"bar\"\",\"\"age\"\":42}}\"%n"
            + "\"{\"\"id\"\":2,\"\"labels\"\":[\"\"User\"\"],\"\"properties\"\":{\"\"age\"\":12}}\"%n");
    private static final String EXPECTED_QUERY =
            String.format("\"u.age\",\"u.name\",\"u.male\",\"u.kids\",\"labels(u)\"%n"
                    + "\"42\",\"foo\",\"true\",\"[\"\"a\"\",\"\"b\"\",\"\"c\"\"]\",\"[\"\"User1\"\",\"\"User\"\"]\"%n"
                    + "\"42\",\"bar\",\"\",\"\",\"[\"\"User\"\"]\"%n"
                    + "\"12\",\"\",\"\",\"\",\"[\"\"User\"\"]\"%n");
    private static final String EXPECTED_QUERY_WITHOUT_QUOTES = String.format(
            "u.age,u.name,u.male,u.kids,labels(u)%n" + "42,foo,true,[\"a\",\"b\",\"c\"],[\"User1\",\"User\"]%n"
                    + "42,bar,,,[\"User\"]%n"
                    + "12,,,,[\"User\"]%n");
    private static final String EXPECTED_QUERY_QUOTES_NONE = String.format(
            "a.name,a.city,a.street,labels(a)%n" + "Andrea,Milano,Via Garibaldi, 7,[\"Address1\",\"Address\"]%n"
                    + "Bar Sport,,,[\"Address\"]%n"
                    + ",,via Benni,[\"Address\"]%n");
    private static final String EXPECTED_QUERY_QUOTES_ALWAYS =
            String.format("\"a.name\",\"a.city\",\"a.street\",\"labels(a)\"%n"
                    + "\"Andrea\",\"Milano\",\"Via Garibaldi, 7\",\"[\"\"Address1\"\",\"\"Address\"\"]\"%n"
                    + "\"Bar Sport\",\"\",\"\",\"[\"\"Address\"\"]\"%n"
                    + "\"\",\"\",\"via Benni\",\"[\"\"Address\"\"]\"%n");
    private static final String EXPECTED_QUERY_QUOTES_NEEDED = String.format("a.name,a.city,a.street,labels(a)%n"
            + "Andrea,Milano,\"Via Garibaldi, 7\",\"[\"\"Address1\"\",\"\"Address\"\"]\"%n"
            + "Bar Sport,,,\"[\"\"Address\"\"]\"%n"
            + ",,via Benni,\"[\"\"Address\"\"]\"%n");
    private static final String EXPECTED_ALL_ALWAYS =
            """
            "_id","_labels","age","city","kids","male","name","street","_start","_end","_type","id","value1","value2"
            "0",":User:User1","42","","[""a"",""b"",""c""]","true","foo","",,,,,,
            "1",":User","42","","","","bar","",,,,,,
            "2",":User","12","","","","","",,,,,,
            "3",":Address:Address1","","Milano","","","Andrea","Via Garibaldi, 7",,,,,,
            "4",":Address","","","","","Bar Sport","",,,,,,
            "5",":Address","","","","","","via Benni",,,,,,
            "6",":ESCAPING","1","","","","I am ""groot"", and more ","",,,,,,
            "7",":ESCAPING","2","","",""," ","",,,,,,
            "8",":ESCAPING","3","","","","","",,,,,,
            "9",":ESCAPING","4","","","","","",,,,,,
            ,,,,,,,,"0","1","KNOWS","","",""
            ,,,,,,,,"3","4","NEXT_DELIVERY","","",""
            ,,,,,,,,"8","9","REL","0"," ",""
            """;
    private static final String EXPECTED_ALL_ALWAYS_2 =
            """
            "_id","_labels","age","city","kids","male","name","street","_start","_end","_type","id","value1","value2"
            "0",":User:User1","42","","[""a"",""b"",""c""]","true","foo","",,,,,,
            "1",":User","42","","","","bar","",,,,,,
            "2",":User","12","","","","","",,,,,,
            "3",":Address:Address1","","Milano","","","Andrea","Via Garibaldi, 7",,,,,,
            "4",":Address","","","","","Bar Sport","",,,,,,
            "5",":Address","","","","","","via Benni",,,,,,
            "6",":ESCAPING","1","","","","I am ""groot"", and more ","",,,,,,
            "7",":ESCAPING","2","","",""," ","",,,,,,
            "8",":ESCAPING","3","","","","","",,,,,,
            "9",":ESCAPING","4","","","","","",,,,,,
            ,,,,,,,,"0","1","KNOWS","","",""
            ,,,,,,,,"8","9","REL","0"," ",""
            ,,,,,,,,"3","4","NEXT_DELIVERY","","",""
            """;
    private static final List<String> EXPECTED_ALL_ALWAYS_USER = List.of(
            """
            ":ID","name","age:long",":LABEL"
            "1","bar","42","User"
            "2","","12","User"
            """,
            """
            ":ID","age:long","name",":LABEL"
            "1","42","bar","User"
            "2","12","","User"
            """);
    private static final List<String> EXPECTED_ALL_ALWAYS_USER1 = List.of(
            """
            ":ID","name","age:long","male:boolean","kids",":LABEL"
            "0","foo","42","true","[""a"",""b"",""c""]","User1;User"
            """,
            """
            ":ID","age:long","kids","male:boolean","name",":LABEL"
            "0","42","[""a"",""b"",""c""]","true","foo","User1;User"
            """);
    private static final String EXPECTED_ALL_ALWAYS_ADDRESS =
            """
            ":ID","name","street",":LABEL"
            "4","Bar Sport","","Address"
            "5","","via Benni","Address"
            """;
    private static final String EXPECTED_ALL_ALWAYS_ADDRESS1 =
            """
            ":ID","name","city","street",":LABEL"
            "3","Andrea","Milano","Via Garibaldi, 7","Address1;Address"
            """;
    private static final List<String> EXPECTED_ALL_ALWAYS_ESCAPING = List.of(
            """
            ":ID","name","age:long",":LABEL"
            "6","I am ""groot"", and more ","1","ESCAPING"
            "7"," ","2","ESCAPING"
            "8","","3","ESCAPING"
            "9","","4","ESCAPING"
            """,
            """
            ":ID","age:long","name",":LABEL"
            "6","1","I am ""groot"", and more ","ESCAPING"
            "7","2"," ","ESCAPING"
            "8","3","","ESCAPING"
            "9","4","","ESCAPING"
            """);
    private static final String EXPECTED_ALL_ALWAYS_KNOWS =
            """
            ":START_ID",":END_ID",":TYPE"
            "0","1","KNOWS"
            """;
    private static final String EXPECTED_ALL_ALWAYS_NEXT_DELIVERY =
            """
            ":START_ID",":END_ID",":TYPE"
            "3","4","NEXT_DELIVERY"
            """;
    private static final List<String> EXPECTED_ALL_ALWAYS_REL = List.of(
            """
            ":START_ID",":END_ID",":TYPE","value2","value1","id:long"
            "8","9","REL",""," ","0"
            """,
            """
            ":START_ID",":END_ID",":TYPE","id:long","value1","value2"
            "8","9","REL","0"," ",""
            """);

    private static final String EXP_SAMPLE =
            """
            "_id","_labels","address","age","baz","city","foo","kids","last:Name","male","name","street","_start","_end","_type","id","one","three","value1","value2"
            "0",":User:User1","","42","","","","[""a"",""b"",""c""]","","true","foo","",,,,,,,,
            "1",":User","","42","","","","","","","bar","",,,,,,,,
            "2",":User","","12","","","","","","","","",,,,,,,,
            "3",":Address:Address1","","","","Milano","","","","","Andrea","Via Garibaldi, 7",,,,,,,,
            "4",":Address","","","","","","","","","Bar Sport","",,,,,,,,
            "5",":Address","","","","","","","","","","via Benni",,,,,,,,
            "6",":ESCAPING","","1","","","","","","","I am ""groot"", and more ","",,,,,,,,
            "7",":ESCAPING","","2","","","","","",""," ","",,,,,,,,
            "8",":ESCAPING","","3","","","","","","","","",,,,,,,,
            "9",":ESCAPING","","4","","","","","","","","",,,,,,,,
            "10",":Sample:User","","","","","","","Galilei","","","",,,,,,,,
            "11",":Sample:User","Universe","","","","","","","","","",,,,,,,,
            "12",":Sample:User","","","","","bar","","","","","",,,,,,,,
            "13",":Sample:User","","","baa","","true","","","","","",,,,,,,,
            ,,,,,,,,,,,,"0","1","KNOWS","","","","",""
            ,,,,,,,,,,,,"3","4","NEXT_DELIVERY","","","","",""
            ,,,,,,,,,,,,"8","9","REL","0","",""," ",""
            ,,,,,,,,,,,,"12","13","KNOWS","","two","four","",""
            """;
    private static final String EXPECTED_ALL_NONE =
            """
            _id,_labels,age,city,kids,male,name,street,_start,_end,_type,id,value1,value2
            0,:User:User1,42,,["a","b","c"],true,foo,,,,,,,
            1,:User,42,,,,bar,,,,,,,
            2,:User,12,,,,,,,,,,,
            3,:Address:Address1,,Milano,,,Andrea,Via Garibaldi, 7,,,,,,
            4,:Address,,,,,Bar Sport,,,,,,,
            5,:Address,,,,,,via Benni,,,,,,
            6,:ESCAPING,1,,,,I am "groot", and more ,,,,,,,
            7,:ESCAPING,2,,,, ,,,,,,,
            8,:ESCAPING,3,,,,,,,,,,,
            9,:ESCAPING,4,,,,,,,,,,,
            ,,,,,,,,0,1,KNOWS,,,
            ,,,,,,,,8,9,REL,0, ,
            ,,,,,,,,3,4,NEXT_DELIVERY,,,
            """;

    private static final String EXPECTED_ALL_NONE_2 =
            """
            _id,_labels,age,city,kids,male,name,street,_start,_end,_type,id,value1,value2
            0,:User:User1,42,,["a","b","c"],true,foo,,,,,,,
            1,:User,42,,,,bar,,,,,,,
            2,:User,12,,,,,,,,,,,
            3,:Address:Address1,,Milano,,,Andrea,Via Garibaldi, 7,,,,,,
            4,:Address,,,,,Bar Sport,,,,,,,
            5,:Address,,,,,,via Benni,,,,,,
            6,:ESCAPING,1,,,,I am "groot", and more ,,,,,,,
            7,:ESCAPING,2,,,, ,,,,,,,
            8,:ESCAPING,3,,,,,,,,,,,
            9,:ESCAPING,4,,,,,,,,,,,
            ,,,,,,,,0,1,KNOWS,,,
            ,,,,,,,,3,4,NEXT_DELIVERY,,,
            ,,,,,,,,8,9,REL,0, ,
            """;

    private static final List<String> EXPECTED_ALL_NONE_USER1 = List.of(
            """
            :ID,name,age:long,male:boolean,kids,:LABEL
            0,foo,42,true,["a","b","c"],User1;User
            """,
            """
            :ID,age:long,kids,male:boolean,name,:LABEL
            0,42,["a","b","c"],true,foo,User1;User
            """);
    private static final String EXPECTED_ALL_NONE_ADDRESS1 =
            """
            :ID,name,city,street,:LABEL
            3,Andrea,Milano,Via Garibaldi, 7,Address1;Address
            """;
    private static final List<String> EXPECTED_ALL_NONE_ESCAPING = List.of(
            """
            :ID,name,age:long,:LABEL
            6,I am "groot", and more ,1,ESCAPING
            7, ,2,ESCAPING
            8,,3,ESCAPING
            9,,4,ESCAPING
            """,
            """
            :ID,age:long,name,:LABEL
            6,1,I am "groot", and more ,ESCAPING
            7,2, ,ESCAPING
            8,3,,ESCAPING
            9,4,,ESCAPING
            """);

    private static final String EXPECTED_ALL_IF_NEEDED_RELS =
            """
            ,,,,,,,,0,1,KNOWS,,,
            ,,,,,,,,3,4,NEXT_DELIVERY,,,
            ,,,,,,,,8,9,REL,0, ,""
            """;
    private static final String EXPECTED_ALL_IF_NEEDED =
            """
            _id,_labels,age,city,kids,male,name,street,_start,_end,_type,id,value1,value2
            0,:User:User1,42,,"[""a"",""b"",""c""]",true,foo,,,,,,,
            1,:User,42,,,,bar,,,,,,,
            2,:User,12,,,,,,,,,,,
            3,:Address:Address1,,Milano,,,Andrea,"Via Garibaldi, 7",,,,,,
            4,:Address,,,,,Bar Sport,,,,,,,
            5,:Address,,,,,,via Benni,,,,,,
            6,:ESCAPING,1,,,,"I am ""groot"", and more ",,,,,,,
            7,:ESCAPING,2,,,, ,,,,,,,
            8,:ESCAPING,3,,,,,,,,,,,
            9,:ESCAPING,4,,,,,,,,,,,
            ,,,,,,,,0,1,KNOWS,,,
            ,,,,,,,,3,4,NEXT_DELIVERY,,,
            ,,,,,,,,8,9,REL,0, ,
            """;

    private static final List<String> EXPECTED_ALL_IF_NEEDED_USER = List.of(
            """
            :ID,name,age:long,:LABEL
            1,bar,42,User
            2,,12,User
            """,
            """
            :ID,age:long,name,:LABEL
            1,42,bar,User
            2,12,,User
            """);
    private static final List<String> EXPECTED_ALL_IF_NEEDED_USER1 = List.of(
            """
            :ID,name,age:long,male:boolean,kids,:LABEL
            0,foo,42,true,"[""a"",""b"",""c""]",User1;User
            """,
            """
            :ID,age:long,kids,male:boolean,name,:LABEL
            0,42,"[""a"",""b"",""c""]",true,foo,User1;User
            """);
    private static final String EXPECTED_ALL_IF_NEEDED_ADDRESS =
            """
            :ID,name,street,:LABEL
            4,Bar Sport,,Address
            5,,via Benni,Address
            """;
    private static final String EXPECTED_ALL_IF_NEEDED_ADDRESS1 =
            """
            :ID,name,city,street,:LABEL
            3,Andrea,Milano,"Via Garibaldi, 7",Address1;Address
            """;
    private static final List<String> EXPECTED_ALL_IF_NEEDED_ESCAPING = List.of(
            """
            :ID,name,age:long,:LABEL
            6,"I am ""groot"", and more ",1,ESCAPING
            7, ,2,ESCAPING
            8,,3,ESCAPING
            9,,4,ESCAPING
            """,
            """
            :ID,age:long,name,:LABEL
            6,1,"I am ""groot"", and more ",ESCAPING
            7,2, ,ESCAPING
            8,3,,ESCAPING
            9,4,,ESCAPING
            """);
    private static final String EXPECTED_ALL_IF_NEEDED_KNOWS =
            """
            :START_ID,:END_ID,:TYPE
            0,1,KNOWS
            """;
    private static final String EXPECTED_ALL_IF_NEEDED_NEXT_DELIVERY =
            """
            :START_ID,:END_ID,:TYPE
            3,4,NEXT_DELIVERY
            """;
    private static final List<String> EXPECTED_ALL_IF_NEEDED_REL = List.of(
            """
            :START_ID,:END_ID,:TYPE,value2,value1,id:long
            8,9,REL,, ,0
            """,
            """
            :START_ID,:END_ID,:TYPE,id:long,value1,value2
            8,9,REL,0, ,
            """);
    private static final String EXPECTED_QUERY_DIFFERENTIATE_NULLS_NONE =
            """
            age,name
            1,I am "groot", and more\s
            2,\s
            3,
            4,
            """;
    private static final String EXPECTED_QUERY_DIFFERENTIATE_NULLS_IF_NEEDED =
            """
            age,name
            1,"I am ""groot"", and more "
            2,\s
            3,""
            4,
            """;
    private static final String EXPECTED_QUERY_DONT_DIFFERENTIATE_NULLS_IF_NEEDED =
            """
            age,name
            1,"I am ""groot"", and more "
            2,\s
            3,
            4,
            """;
    private static final String EXPECTED_QUERY_DIFFERENTIATE_NULLS_ALWAYS =
            """
            "age","name"
            "1","I am ""groot"", and more "
            "2"," "
            "3",""
            "4",
            """;
    private static final String EXPECTED_QUERY_DONT_DIFFERENTIATE_NULLS_ALWAYS =
            """
            "age","name"
            "1","I am ""groot"", and more "
            "2"," "
            "3",""
            "4",""
            """;
    private static final String EXPECTED_DATA_DIFFERENTIATE_NULLS_NONE =
            """
            _id,_labels,age,name,_start,_end,_type
            6,:ESCAPING,1,I am "groot", and more ,,,
            7,:ESCAPING,2, ,,,
            8,:ESCAPING,3,,,,
            9,:ESCAPING,4,,,,
            """;
    private static final String EXPECTED_DATA_DIFFERENTIATE_NULLS_IF_NEEDED =
            """
            _id,_labels,age,name,_start,_end,_type
            6,:ESCAPING,1,"I am ""groot"", and more ",,,
            7,:ESCAPING,2, ,,,
            8,:ESCAPING,3,"",,,
            9,:ESCAPING,4,,,,
            """;
    private static final String EXPECTED_DATA_DONT_DIFFERENTIATE_NULLS_IF_NEEDED =
            """
            _id,_labels,age,name,_start,_end,_type
            6,:ESCAPING,1,"I am ""groot"", and more ",,,
            7,:ESCAPING,2, ,,,
            8,:ESCAPING,3,,,,
            9,:ESCAPING,4,,,,
            """;
    private static final String EXPECTED_DATA_DIFFERENTIATE_NULLS_ALWAYS =
            """
            "_id","_labels","age","name","_start","_end","_type"
            "6",":ESCAPING","1","I am ""groot"", and more ",,,
            "7",":ESCAPING","2"," ",,,
            "8",":ESCAPING","3","",,,
            "9",":ESCAPING","4",,,,
            """;
    private static final String EXPECTED_DATA_DONT_DIFFERENTIATE_NULLS_ALWAYS =
            """
            "_id","_labels","age","name","_start","_end","_type"
            "6",":ESCAPING","1","I am ""groot"", and more ",,,
            "7",":ESCAPING","2"," ",,,
            "8",":ESCAPING","3","",,,
            "9",":ESCAPING","4","",,,
            """;
    private static final String EXPECTED_ALL_DIFFERENTIATE_NULLS_IF_NEEDED =
            """
                    _id,_labels,age,city,kids,male,name,street,_start,_end,_type,id,value1,value2
                    0,:User:User1,42,,"[""a"",""b"",""c""]",true,foo,,,,,,,
                    1,:User,42,,,,bar,,,,,,,
                    2,:User,12,,,,,,,,,,,
                    3,:Address:Address1,,Milano,,,Andrea,"Via Garibaldi, 7",,,,,,
                    4,:Address,,,,,Bar Sport,,,,,,,
                    5,:Address,,,,,,via Benni,,,,,,
                    6,:ESCAPING,1,,,,"I am ""groot"", and more ",,,,,,,
                    7,:ESCAPING,2,,,, ,,,,,,,
                    8,:ESCAPING,3,,,,"",,,,,,,
                    9,:ESCAPING,4,,,,,,,,,,,
                    """
                    + EXPECTED_ALL_IF_NEEDED_RELS;

    private static final List<String> EXPECTED_ALL_DIFFERENTIATE_NULLS_IF_NEEDED_USER = List.of(
            """
            :ID,name,age:long,:LABEL
            1,bar,42,User
            2,,12,User
            """,
            """
            :ID,age:long,name,:LABEL
            1,42,bar,User
            2,12,,User
            """);

    private static final String EXPECTED_ALL_DIFFERENTIATE_NULLS_IF_NEEDED_ADDRESS =
            """
            :ID,name,street,:LABEL
            4,Bar Sport,,Address
            5,,via Benni,Address
            """;
    private static final List<String> EXPECTED_ALL_DIFFERENTIATE_NULLS_IF_NEEDED_ESCAPING = List.of(
            """
            :ID,name,age:long,:LABEL
            6,"I am ""groot"", and more ",1,ESCAPING
            7, ,2,ESCAPING
            8,"",3,ESCAPING
            9,,4,ESCAPING
            """,
            """
            :ID,age:long,name,:LABEL
            6,1,"I am ""groot"", and more ",ESCAPING
            7,2, ,ESCAPING
            8,3,"",ESCAPING
            9,4,,ESCAPING
            """);
    private static final List<String> EXPECTED_ALL_DIFFERENTIATE_NULLS_IF_NEEDED_REL = List.of(
            """
            :START_ID,:END_ID,:TYPE,value2,value1,id:long
            8,9,REL,"", ,0
            """,
            """
            :START_ID,:END_ID,:TYPE,id:long,value1,value2
            8,9,REL,0, ,""
            """);

    private static final String EXPECTED_ALL_DIFFERENTIATE_NULLS_ALWAYS =
            """
            "_id","_labels","age","city","kids","male","name","street","_start","_end","_type","id","value1","value2"
            "0",":User:User1","42",,"[""a"",""b"",""c""]","true","foo",,,,,,,
            "1",":User","42",,,,"bar",,,,,,,
            "2",":User","12",,,,,,,,,,,
            "3",":Address:Address1",,"Milano",,,"Andrea","Via Garibaldi, 7",,,,,,
            "4",":Address",,,,,"Bar Sport",,,,,,,
            "5",":Address",,,,,,"via Benni",,,,,,
            "6",":ESCAPING","1",,,,"I am ""groot"", and more ",,,,,,,
            "7",":ESCAPING","2",,,," ",,,,,,,
            "8",":ESCAPING","3",,,,"",,,,,,,
            "9",":ESCAPING","4",,,,,,,,,,,
            ,,,,,,,,"0","1","KNOWS",,,
            ,,,,,,,,"3","4","NEXT_DELIVERY",,,
            ,,,,,,,,"8","9","REL","0"," ",""
                    """;

    @Inject
    TestDirectory testDirectory;

    @Inject
    GraphDatabaseService db;

    private Path directory;

    @ExtensionCallback
    void configure(TestDatabaseManagementServiceBuilder builder) {
        this.directory = testDirectory.directory("import");
        builder.setConfigRaw(Map.of(
                "server.directories.import",
                directory.toAbsolutePath().toString(),
                "internal.dbms.cypher.enable_experimental_versions",
                "true"));
    }

    @BeforeAll
    void setUp() {
        TestUtil.registerProcedure(
                db,
                ExportCSV.class,
                Graphs.class,
                Meta.class,
                MetaRestricted.class,
                ImportCsv.class,
                HelperProcedures.class);
        apocConfig().setProperty(APOC_IMPORT_FILE_ENABLED, true);
        apocConfig().setProperty(APOC_EXPORT_FILE_ENABLED, true);

        db.executeTransactionally(
                "CREATE (f:User1:User {name:'foo',age:42,male:true,kids:['a','b','c']})-[:KNOWS]->(b:User {name:'bar',age:42}),(c:User {age:12})");
        db.executeTransactionally(
                "CREATE (f:Address1:Address {name:'Andrea', city: 'Milano', street:'Via Garibaldi, 7'})-[:NEXT_DELIVERY]->(a:Address {name: 'Bar Sport'}), (b:Address {street: 'via Benni'})");
        db.executeTransactionally(
                """
        CREATE
            (:ESCAPING {age: 1, name: 'I am "groot", and more '}),
            (:ESCAPING {age: 2, name: ' '}),
            (:ESCAPING {age: 3, name: ''})-[:REL {id: 0, value1: ' ', value2: ""}]->(:ESCAPING {age: 4})
        """);
    }

    private String readFile(String fileName) throws Exception {
        return readFile(fileName, UTF_8, CompressionAlgo.NONE);
    }

    private String readFile(String fileName, Charset charset, CompressionAlgo compression) throws Exception {
        final var path = directory.resolve(fileName);
        if (compression.isNone()) return Files.readString(path, charset);
        else return compression.decompress(Files.readAllBytes(path), charset);
    }

    @Test
    public void testExportInvalidQuoteValue() {
        assertThatThrownBy(() -> {
                    String fileName = "all.csv";
                    testCall(
                            db,
                            "CALL apoc.export.csv.all($file,{quotes: 'Invalid'})",
                            map("file", fileName),
                            (r) -> assertResults(fileName, r, "database"));
                })
                .hasMessage(
                        "Failed to invoke procedure `apoc.export.csv.all`: Caused by: java.lang.RuntimeException: The string value of the field quote is not valid");
    }

    @Test
    public void textExportWithTypes() {
        db.executeTransactionally(
                "CREATE (n:TestNode) SET n = {valFloat:toFloat(123), name:'NodeName', valInt:5, dateVal: date('2024-11-01')};");
        testCall(
                db,
                """
                            CALL apoc.graph.fromCypher("MATCH (n:TestNode) RETURN n", {}, 'TestNode.csv',{}) YIELD graph
                            CALL apoc.export.csv.graph(graph, null, {stream:true, useTypes:true}) YIELD properties, rows, data
                            return properties, rows, data;
                            """,
                (r) -> {
                    String data = (String) r.get("data");
                    // FLOAT value
                    assertTrue(data.contains("valFloat:float"));
                    assertTrue(data.contains("123.0"));
                    // INT value
                    assertTrue(data.contains("valInt:int"));
                    assertTrue(data.contains("5"));
                    // STRING value and unknown types to csv export
                    assertTrue(data.contains("name"));
                    assertTrue(data.contains("NodeName"));
                    assertTrue(data.contains("dateVal"));
                    assertTrue(data.contains("2024-11-01"));
                });

        final String deleteQuery = "MATCH (n:TestNode) DETACH DELETE n";
        db.executeTransactionally(deleteQuery);
    }

    @Test
    public void testExportAllCsvCompressed() throws Exception {
        final CompressionAlgo compressionAlgo = DEFLATE;
        String fileName = "all.csv.zz";
        testCall(
                db,
                "CALL apoc.export.csv.all($file, $config)",
                map("file", fileName, "config", map("compression", compressionAlgo.name())),
                (r) -> assertResults(fileName, r, "database"));
        assertEquals(EXPECTED_ALL_ALWAYS, readFile(fileName, UTF_8, compressionAlgo));
    }

    @Test
    public void testConsistentQuotingAlways() throws Exception {
        // All in one file
        String fileName1 = "allOneFileAlways.csv";
        testCall(
                db,
                "CALL apoc.export.csv.all($file,{bulkImport: false})",
                map("file", fileName1),
                (r) -> assertResults(fileName1, r, "database"));
        assertEquals(EXPECTED_ALL_ALWAYS, readFile(fileName1));

        // In separate files
        String fileNameStart = "allBulkImportAlways";
        String fileName2 = fileNameStart + ".csv";
        testCall(
                db,
                "CALL apoc.export.csv.all($file,{bulkImport: true})",
                map("file", fileName2),
                (r) -> assertResults(fileName2, r, "database"));
        assertThat(readFile(fileNameStart + ".nodes.User.csv")).isIn(EXPECTED_ALL_ALWAYS_USER);
        assertThat(readFile(fileNameStart + ".nodes.User1.User.csv")).isIn(EXPECTED_ALL_ALWAYS_USER1);
        assertEquals(EXPECTED_ALL_ALWAYS_ADDRESS, readFile(fileNameStart + ".nodes.Address.csv"));
        assertEquals(EXPECTED_ALL_ALWAYS_ADDRESS1, readFile(fileNameStart + ".nodes.Address1.Address.csv"));
        assertThat(readFile(fileNameStart + ".nodes.ESCAPING.csv")).isIn(EXPECTED_ALL_ALWAYS_ESCAPING);
        assertEquals(EXPECTED_ALL_ALWAYS_KNOWS, readFile(fileNameStart + ".relationships.KNOWS.csv"));
        assertEquals(EXPECTED_ALL_ALWAYS_NEXT_DELIVERY, readFile(fileNameStart + ".relationships.NEXT_DELIVERY.csv"));
        assertThat(readFile(fileNameStart + ".relationships.REL.csv")).isIn(EXPECTED_ALL_ALWAYS_REL);

        // Streaming
        testCall(db, "CALL apoc.export.csv.all(null,{stream: true})", (r) -> {
            String data = (String) r.get("data");
            assertEquals(EXPECTED_ALL_ALWAYS, data);
        });
    }

    @Test
    public void testConsistentQuotingIfNeeded() throws Exception {
        // All in one file
        String fileName1 = "allOneFileIfNeeded.csv";
        testCall(
                db,
                "CALL apoc.export.csv.all($file,{bulkImport: false, quotes: 'ifNeeded'})",
                map("file", fileName1),
                (r) -> assertResults(fileName1, r, "database"));
        assertEquals(EXPECTED_ALL_IF_NEEDED, readFile(fileName1));

        // In separate files
        String fileNameStart = "allBulkImportIfNeeded";
        String fileName2 = fileNameStart + ".csv";
        testCall(
                db,
                "CALL apoc.export.csv.all($file,{bulkImport: true, quotes: 'ifNeeded'})",
                map("file", fileName2),
                (r) -> assertResults(fileName2, r, "database"));
        assertThat(readFile(fileNameStart + ".nodes.User.csv")).isIn(EXPECTED_ALL_IF_NEEDED_USER);
        assertThat(readFile(fileNameStart + ".nodes.User1.User.csv")).isIn(EXPECTED_ALL_IF_NEEDED_USER1);
        assertEquals(EXPECTED_ALL_IF_NEEDED_ADDRESS, readFile(fileNameStart + ".nodes.Address.csv"));
        assertEquals(EXPECTED_ALL_IF_NEEDED_ADDRESS1, readFile(fileNameStart + ".nodes.Address1.Address.csv"));
        assertThat(readFile(fileNameStart + ".nodes.ESCAPING.csv")).isIn(EXPECTED_ALL_IF_NEEDED_ESCAPING);
        assertEquals(EXPECTED_ALL_IF_NEEDED_KNOWS, readFile(fileNameStart + ".relationships.KNOWS.csv"));
        assertEquals(
                EXPECTED_ALL_IF_NEEDED_NEXT_DELIVERY, readFile(fileNameStart + ".relationships.NEXT_DELIVERY.csv"));
        assertThat(readFile(fileNameStart + ".relationships.REL.csv")).isIn(EXPECTED_ALL_IF_NEEDED_REL);

        // Streaming
        testCall(db, "CALL apoc.export.csv.all(null,{stream: true, quotes: 'ifNeeded'})", (r) -> {
            String data = (String) r.get("data");
            assertEquals(EXPECTED_ALL_IF_NEEDED, data);
        });
    }

    @Test
    public void testConsistentQuotingIfNeededDifferentiateNulls() throws Exception {
        // All in one file
        String fileName1 = "allOneFileIfNeeded.csv";
        testCall(
                db,
                "CALL apoc.export.csv.all($file,{bulkImport: false, quotes: 'ifNeeded', differentiateNulls: true})",
                map("file", fileName1),
                (r) -> assertResults(fileName1, r, "database"));
        assertEquals(EXPECTED_ALL_DIFFERENTIATE_NULLS_IF_NEEDED, readFile(fileName1));

        // In separate files
        String fileNameStart = "allBulkImportIfNeeded";
        String fileName2 = fileNameStart + ".csv";

        testCall(
                db,
                "CALL apoc.export.csv.all($file,{bulkImport: true, quotes: 'ifNeeded', differentiateNulls: true})",
                map("file", fileName2),
                (r) -> assertResults(fileName2, r, "database"));
        assertThat(readFile(fileNameStart + ".nodes.User.csv")).isIn(EXPECTED_ALL_DIFFERENTIATE_NULLS_IF_NEEDED_USER);
        assertThat(readFile(fileNameStart + ".nodes.User1.User.csv")).isIn(EXPECTED_ALL_IF_NEEDED_USER1);
        assertEquals(
                EXPECTED_ALL_DIFFERENTIATE_NULLS_IF_NEEDED_ADDRESS, readFile(fileNameStart + ".nodes.Address.csv"));
        assertEquals(EXPECTED_ALL_IF_NEEDED_ADDRESS1, readFile(fileNameStart + ".nodes.Address1.Address.csv"));
        assertThat(readFile(fileNameStart + ".nodes.ESCAPING.csv"))
                .isIn(EXPECTED_ALL_DIFFERENTIATE_NULLS_IF_NEEDED_ESCAPING);
