package org.jabref.logic.importer.fileformat;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;

import org.jabref.logic.importer.ImportFormatPreferences;
import org.jabref.logic.importer.ParserResult;
import org.jabref.logic.util.StandardFileType;
import org.jabref.model.entry.BibEntry;
import org.jabref.model.entry.BibEntryPreferences;
import org.jabref.model.entry.KeywordList;
import org.jabref.model.entry.field.StandardField;
import org.jabref.model.entry.field.UnknownField;
import org.jabref.model.entry.types.StandardEntryType;
import org.jabref.model.util.DummyFileUpdateMonitor;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.Answers;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

/// This class tests the BibtexImporter.
///
/// Tests for writing can be found at {@link org.jabref.logic.exporter.BibDatabaseWriterTest}.
/// Tests for parsing single entry BibTeX can be found at {@link BibtexParserTest}
class BibtexImporterTest {

    private BibtexImporter importer;

    @BeforeEach
    void setUp() {
        importer = createImporter(new BibEntryPreferences(','));
    }

    private BibtexImporter createImporter(BibEntryPreferences bibEntryPreferences) {
        ImportFormatPreferences importFormatPreferences = mock(ImportFormatPreferences.class, Answers.RETURNS_DEEP_STUBS);
        when(importFormatPreferences.bibEntryPreferences()).thenReturn(bibEntryPreferences);
        return new BibtexImporter(importFormatPreferences, new DummyFileUpdateMonitor());
    }

    @Test
    void isRecognizedFormat() throws IOException, URISyntaxException {
        Path file = Path.of(BibtexImporterTest.class.getResource("BibtexImporter.examples.bib").toURI());
        assertTrue(importer.isRecognizedFormat(file));
    }

    @Test
    void importEntries() throws IOException, URISyntaxException {
        Path file = Path.of(BibtexImporterTest.class.getResource("BibtexImporter.examples.bib").toURI());
        List<BibEntry> bibEntries = importer.importDatabase(file).getDatabase().getEntries();

        assertEquals(4, bibEntries.size());

        for (BibEntry entry : bibEntries) {
            if ("aksin".equals(entry.getCitationKey().get())) {
                assertEquals(
                        Optional.of(
                                """
                                        Aks{\\i}n, {\\"O}zge and T{\\"u}rkmen, Hayati and Artok, Levent
                                                          and {\\c{C}}etinkaya, Bekir and Ni, Chaoying and
                                                          B{\\"u}y{\\"u}kg{\\"u}ng{\\"o}r, Orhan and {\\"O}zkal, Erhan"""),
                        entry.getField(StandardField.AUTHOR));
                assertEquals(Optional.of("aksin"), entry.getCitationKey());
                assertEquals(Optional.of("2006"), entry.getField(StandardField.DATE));
                assertEquals(Optional.of("Effect of immobilization on catalytic characteristics"), entry.getField(new UnknownField("indextitle")));
                assertEquals(Optional.of("#jomch#"), entry.getField(StandardField.JOURNAL));
                assertEquals(Optional.of("13"), entry.getField(StandardField.NUMBER));
                assertEquals(Optional.of("3027-3036"), entry.getField(StandardField.PAGES));
                assertEquals(Optional
                                .of("""
                                        Effect of immobilization on catalytic characteristics of
                                                          saturated {Pd-N}-heterocyclic carbenes in {Mizoroki-Heck}
                                                          reactions"""),
                        entry.getField(StandardField.TITLE));
                assertEquals(Optional.of("691"), entry.getField(StandardField.VOLUME));
            } else if ("stdmodel".equals(entry.getCitationKey().get())) {
                assertEquals(Optional
                                .of("""
                                        A \\texttt{set} with three members discussing the standard
                                                          model of particle physics. The \\texttt{crossref} field
                                                          in the \\texttt{@set} entry and the \\texttt{entryset} field in
                                                          each set member entry is needed only when using BibTeX as the
                                                          backend"""),
                        entry.getField(StandardField.ANNOTATION));
                assertEquals(Optional.of("stdmodel"), entry.getCitationKey());
                assertEquals(Optional.of("glashow,weinberg,salam"), entry.getField(StandardField.ENTRYSET));
            } else if ("set".equals(entry.getCitationKey().get())) {
                assertEquals(Optional
                                .of("""
                                        A \\texttt{set} with three members. The \\texttt{crossref} field
                                                          in the \\texttt{@set} entry and the \\texttt{entryset} field in
                                                          each set member entry is needed only when using BibTeX as the
                                                          backend"""),
                        entry.getField(StandardField.ANNOTATION));
                assertEquals(Optional.of("set"), entry.getCitationKey());
                assertEquals(Optional.of("herrmann,aksin,yoon"), entry.getField(StandardField.ENTRYSET));
            } else if ("Preissel2016".equals(entry.getCitationKey().get())) {
                assertEquals(Optional.of("Heidelberg"), entry.getField(StandardField.ADDRESS));
                assertEquals(Optional.of("Preißel, René"), entry.getField(StandardField.AUTHOR));
                assertEquals(Optional.of("Preissel2016"), entry.getCitationKey());
                assertEquals(Optional.of("3., aktualisierte und erweiterte Auflage"),
                        entry.getField(StandardField.EDITION));
                assertEquals(Optional.of("978-3-86490-311-3"), entry.getField(StandardField.ISBN));
                assertEquals(Optional.of("Versionsverwaltung"), entry.getField(StandardField.KEYWORDS));
                assertEquals(Optional.of("XX, 327 Seiten"), entry.getField(StandardField.PAGES));
                assertEquals(Optional.of("dpunkt.verlag"), entry.getField(StandardField.PUBLISHER));
                assertEquals(Optional.of("Git: dezentrale Versionsverwaltung im Team : Grundlagen und Workflows"),
                        entry.getField(StandardField.TITLE));
                assertEquals(Optional.of("http://d-nb.info/107601965X"), entry.getField(StandardField.URL));
                assertEquals(Optional.of("2016"), entry.getField(StandardField.YEAR));
            }
        }
    }

    @Test
    void importSemicolonSeparatedKeywordsUsesConfiguredSeparator() throws IOException {
        // [utest->req~import.bibtex.keywords.normalize-delimiters~1]
        List<BibEntry> importedEntries = importer.importDatabase(new BufferedReader(Reader.of("""
                @Article{,
                  keywords = {keywordOne; keywordTwo; keywordThree},
                }
                """))).getDatabase().getEntries();

        assertEquals(1, importedEntries.size());
        BibEntry importedEntry = importedEntries.getFirst();
        assertEquals(Optional.of("keywordOne, keywordTwo, keywordThree"), importedEntry.getField(StandardField.KEYWORDS));
        assertEquals(new KeywordList("keywordOne", "keywordTwo", "keywordThree"), importedEntry.getKeywords(','));
    }

    @Test
    void importSplitsOnAllConfiguredInputDelimiters() throws IOException {
        // [utest->req~import.bibtex.keywords.normalize-delimiters~1]
        List<BibEntry> importedEntries = importer.importDatabase(new BufferedReader(Reader.of("""
                @Article{,
                  keywords = {keywordOne, keywordTwo; keywordThree},
                }
                """))).getDatabase().getEntries();

        assertEquals(1, importedEntries.size());
        BibEntry importedEntry = importedEntries.getFirst();
        assertEquals(Optional.of("keywordOne, keywordTwo, keywordThree"), importedEntry.getField(StandardField.KEYWORDS));
        assertEquals(new KeywordList("keywordOne", "keywordTwo", "keywordThree"), importedEntry.getKeywords(','));
    }

    @Test
    void importConfiguredInputDelimitersStillEscapeEmbeddedConfiguredSeparatorWhenItIsNotAcceptedOnImport() throws IOException {
        // [utest->req~import.bibtex.keywords.normalize-delimiters~1]
        BibtexImporter importerWithSemicolonOnly = createImporter(new BibEntryPreferences(',', ";#"));
        List<BibEntry> importedEntries = importerWithSemicolonOnly.importDatabase(new BufferedReader(Reader.of("""
                @Article{,
                  keywords = {keywordOne, keywordTwo; keywordThree},
                }
                """))).getDatabase().getEntries();

        assertEquals(1, importedEntries.size());
        BibEntry importedEntry = importedEntries.getFirst();
        assertEquals(Optional.of("keywordOne\\, keywordTwo, keywordThree"), importedEntry.getField(StandardField.KEYWORDS));
        assertEquals(new KeywordList("keywordOne, keywordTwo", "keywordThree"), importedEntry.getKeywords(','));
    }

    @Test
    void importConfiguredInputKeywordDelimiters() throws IOException {
        // [utest->req~import.bibtex.keywords.normalize-delimiters~1]
        BibtexImporter importerWithCustomInputSeparators = createImporter(new BibEntryPreferences(',', ";#"));
        List<BibEntry> importedEntries = importerWithCustomInputSeparators.importDatabase(new BufferedReader(Reader.of("""
                @Article{,
                  keywords = {keywordOne; keywordTwo# keywordThree},
                }
                """))).getDatabase().getEntries();

        assertEquals(1, importedEntries.size());
        assertEquals(Optional.of("keywordOne, keywordTwo, keywordThree"), importedEntries.getFirst().getField(StandardField.KEYWORDS));
    }

    @Test
    void importCanInferOneDelimiterByPriority() throws IOException {
        // [utest->req~import.bibtex.keywords.normalize-delimiters~1]
        BibtexImporter importerWithPriorityInference = createImporter(
                new BibEntryPreferences(',', ";,", BibEntryPreferences.ImportDelimiterParsingStrategy.INFER_DELIMITER_BY_PRIORITY));
        List<BibEntry> importedEntries = importerWithPriorityInference.importDatabase(new BufferedReader(Reader.of("""
                @Article{,
                  keywords = {keywordOne, keywordTwo; keywordThree},
                }
                """))).getDatabase().getEntries();

        assertEquals(1, importedEntries.size());
        assertEquals(Optional.of("keywordOne\\, keywordTwo, keywordThree"), importedEntries.getFirst().getField(StandardField.KEYWORDS));
    }

    @Test
    void importSemicolonSeparatedKeywordsFallsBackToDefaultSeparatorWhenPreferenceIsMissing() throws IOException {
        // [utest->req~import.bibtex.keywords.normalize-delimiters~1]
        BibEntryPreferences bibEntryPreferences = mock(BibEntryPreferences.class);
        when(bibEntryPreferences.getKeywordSeparator()).thenReturn(null);
        when(bibEntryPreferences.getImportKeywordDelimiters()).thenReturn(BibEntryPreferences.getDefault().getImportKeywordDelimiters());

        BibtexImporter importerWithoutKeywordSeparator = createImporter(bibEntryPreferences);
        List<BibEntry> importedEntries = importerWithoutKeywordSeparator.importDatabase(new BufferedReader(Reader.of("""
                @Article{,
                  keywords = {keywordOne; keywordTwo},
                }
                """))).getDatabase().getEntries();

        assertEquals(1, importedEntries.size());
        assertEquals(Optional.of("keywordOne, keywordTwo"), importedEntries.getFirst().getField(StandardField.KEYWORDS));
    }

    @Test
    void getFormatName() {
        assertEquals("BibTeX", importer.getName());
    }

    @Test
    void getExtensions() {
        assertEquals(StandardFileType.BIBTEX_DB, importer.getFileType());
    }

    @Test
    void recognizesDatabaseID() throws IOException, URISyntaxException {
        Path file = Path.of(BibtexImporterTest.class.getResource("AutosavedSharedDatabase.bib").toURI());
        String sharedDatabaseID = importer.importDatabase(file).getDatabase().getSharedDatabaseID().get();
        assertEquals("13ceoc8dm42f5g1iitao3dj2ap", sharedDatabaseID);
    }

    static Stream<Arguments> parsingOfEncodedFileWithHeader() {
        return Stream.of(
                Arguments.of(StandardCharsets.US_ASCII, "encoding-us-ascii-with-header.bib"),
                Arguments.of(StandardCharsets.UTF_8, "encoding-utf-8-with-header.bib"),
                Arguments.of(Charset.forName("Windows-1252"), "encoding-windows-1252-with-header.bib"),
                Arguments.of(StandardCharsets.UTF_16BE, "encoding-utf-16BE-with-header.bib"),
                Arguments.of(StandardCharsets.UTF_16BE, "encoding-utf-16BE-without-header.bib")
        );
    }

    @ParameterizedTest
    @MethodSource
    void parsingOfEncodedFileWithHeader(Charset charset, String fileName) throws URISyntaxException, IOException {
        ParserResult parserResult = importer.importDatabase(
                Path.of(BibtexImporterTest.class.getResource(fileName).toURI()));
        assertEquals(Optional.of(charset), parserResult.getMetaData().getEncoding());
    }

    @ParameterizedTest
    @ValueSource(strings = {"encoding-windows-1252-with-header.bib", "encoding-windows-1252-without-header.bib"})
    void parsingOfWindows1252EncodedFileReadsDegreeCharacterCorrectly(String filename) throws URISyntaxException, IOException {
        ParserResult parserResult = importer.importDatabase(
                Path.of(BibtexImporterTest.class.getResource(filename).toURI()));
        assertEquals(
                List.of(new BibEntry(StandardEntryType.Article).withField(StandardField.ABSTRACT, "25° C")),
                parserResult.getDatabase().getEntries());
    }

    @ParameterizedTest
    @ValueSource(strings = {"encoding-utf-8-with-header.bib", "encoding-utf-8-without-header.bib",
            "encoding-utf-16BE-with-header.bib", "encoding-utf-16BE-without-header.bib"})
    void parsingFilesReadsUmlautCharacterCorrectly(String filename) throws URISyntaxException, IOException {
        ParserResult parserResult = importer.importDatabase(
                Path.of(BibtexImporterTest.class.getResource(filename).toURI()));
        assertEquals(
                List.of(new BibEntry(StandardEntryType.Article).withField(StandardField.TITLE, "Ü ist ein Umlaut")),
                parserResult.getDatabase().getEntries());
    }

    private static Stream<Arguments> parsingUtf16FilesWithAndWithoutBom() {
        return Stream.of(
                Arguments.of("tiny-BE-withBOM.bib", StandardCharsets.UTF_16BE),
                Arguments.of("tiny-BE-noBOM.bib", StandardCharsets.UTF_16BE),
                Arguments.of("tiny-LE-withBOM.bib", StandardCharsets.UTF_16LE),
                Arguments.of("tiny-LE-noBOM.bib", StandardCharsets.UTF_16LE)
        );
    }

    @ParameterizedTest
    @MethodSource
    void parsingUtf16FilesWithAndWithoutBom(String filename, Charset expectedEncoding) throws URISyntaxException, IOException {
        ParserResult parserResult = importer.importDatabase(
                Path.of(BibtexImporterTest.class.getResource(filename).toURI()));

        assertEquals(expectedEncoding, parserResult.getMetaData().getEncoding().orElseThrow());
        assertTrue(parserResult.getMetaData().getEncodingExplicitlySupplied());
        assertEquals("Verwilghen+vanGalen+Wilke2011", parserResult.getDatabase().getEntries().getFirst().getCitationKey().orElseThrow());
    }

    private static Stream<Arguments> encodingExplicitlySuppliedCorrectlyDetermined() {
        return Stream.of(
                Arguments.of("encoding-utf-8-with-header.bib", true),
                Arguments.of("encoding-utf-8-without-header.bib", false),
                Arguments.of("encoding-utf-16BE-with-header.bib", true),
                Arguments.of("encoding-utf-16BE-without-header.bib", false)
        );
    }

    @ParameterizedTest
    @MethodSource
    void encodingExplicitlySuppliedCorrectlyDetermined(String filename, boolean encodingExplicitlySupplied) throws URISyntaxException, IOException {
        ParserResult parserResult = importer.importDatabase(
                Path.of(BibtexImporterTest.class.getResource(filename).toURI()));
        assertEquals(encodingExplicitlySupplied, parserResult.getMetaData().getEncodingExplicitlySupplied());
    }

    @Test
    void wrongEncodingSupplied() throws URISyntaxException, IOException {
        ParserResult parserResult = importer.importDatabase(
                Path.of(BibtexImporterTest.class.getResource("encoding-windows-1252-but-utf-8-declared--decoding-fails.bib").toURI()));

        // The test file contains "Test{NBSP}I. Last" where the character "{NBSP}" is encoded using Windows-1252 instead of UTF-8
        assertEquals(
                List.of(new BibEntry(StandardEntryType.Article).withField(StandardField.AUTHOR, "Test�I. Last")),
                parserResult.getDatabase().getEntries());
    }

    @Test
    void encodingNotSupplied() throws URISyntaxException, IOException {
        ParserResult parserResult = importer.importDatabase(
                Path.of(BibtexImporterTest.class.getResource("encoding-utf-8-without-header.bib").toURI()));
        assertFalse(parserResult.getMetaData().getEncodingExplicitlySupplied());
    }
}
