package org.jabref.logic.cleanup;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Optional;

import javax.xml.transform.TransformerException;

import org.jabref.logic.FilePreferences;
import org.jabref.logic.bibtex.FileFieldWriter;
import org.jabref.logic.formatter.bibtexfields.HtmlToLatexFormatter;
import org.jabref.logic.formatter.bibtexfields.LatexCleanupFormatter;
import org.jabref.logic.formatter.bibtexfields.NormalizeDateFormatter;
import org.jabref.logic.formatter.bibtexfields.NormalizeMonthFormatter;
import org.jabref.logic.formatter.bibtexfields.NormalizePagesFormatter;
import org.jabref.logic.formatter.bibtexfields.UnitsToLatexFormatter;
import org.jabref.logic.formatter.casechanger.ProtectTermsFormatter;
import org.jabref.logic.journals.JournalAbbreviationRepository;
import org.jabref.logic.preferences.TimestampPreferences;
import org.jabref.logic.protectedterms.ProtectedTermsLoader;
import org.jabref.logic.protectedterms.ProtectedTermsPreferences;
import org.jabref.logic.xmp.XmpPreferences;
import org.jabref.logic.xmp.XmpUtilReader;
import org.jabref.logic.xmp.XmpUtilWriter;
import org.jabref.model.FieldChange;
import org.jabref.model.database.BibDatabase;
import org.jabref.model.database.BibDatabaseContext;
import org.jabref.model.entry.BibEntry;
import org.jabref.model.entry.LinkedFile;
import org.jabref.model.entry.Month;
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.metadata.MetaData;

import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.Answers;

import static org.jabref.logic.xmp.DublinCoreExtractor.DC_SOURCE;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

class CleanupWorkerTest {

    private final CleanupPreferences emptyPreset = new CleanupPreferences(EnumSet.noneOf(CleanupPreferences.CleanupStep.class));
    private CleanupWorker worker;
    private XmpPreferences xmpPreferences;
    private BibDatabase bibDatabase;

    // Ensure that the folder stays the same for all tests.
    // By default, @TempDir creates a new folder for each usage
    private Path bibFolder;

    // Currently, this directory is created below the bibFolder
    private Path pdfPath;

    @BeforeEach
    void setUp(@TempDir Path bibFolder) throws IOException {
        this.bibFolder = bibFolder;
        pdfPath = bibFolder.resolve("ARandomlyNamedFolder");
        Files.createDirectory(pdfPath);

        MetaData metaData = new MetaData();
        metaData.setLibrarySpecificFileDirectory(pdfPath.toAbsolutePath().toString());
        BibDatabaseContext context = new BibDatabaseContext(new BibDatabase(), metaData);
        Files.createFile(bibFolder.resolve("test.bib"));
        context.setDatabasePath(bibFolder.resolve("test.bib"));

        FilePreferences fileDirPrefs = mock(FilePreferences.class, Answers.RETURNS_SMART_NULLS);
        // Search and store files relative to bib file overwrites all other dirs
        when(fileDirPrefs.shouldStoreFilesRelativeToBibFile()).thenReturn(true);

        bibDatabase = mock(BibDatabase.class);
        when(bibDatabase.resolveForStrings(anyList(), eq(false)))
                .thenAnswer(invocation -> invocation.getArgument(0));

        xmpPreferences = mock(XmpPreferences.class);
        when(xmpPreferences.getKeywordSeparator()).thenReturn(',');

        worker = new CleanupWorker(context, fileDirPrefs, mock(TimestampPreferences.class), false, mock(JournalAbbreviationRepository.class));
    }

    @Test
    void cleanupXmpMetadataRemovesMetadata() throws IOException, TransformerException {
        Path pdfFile = pdfPath.resolve("test.pdf");
        try (PDDocument doc = new PDDocument()) {
            doc.addPage(new PDPage());
            doc.save(pdfFile.toFile());
        }

        BibEntry metadataEntry = new BibEntry(StandardEntryType.Article);
        metadataEntry.setCitationKey("olly2018");
        metadataEntry.withField(StandardField.AUTHOR, "Olly and Johannes")
                     .withField(StandardField.TITLE, "Stefan's palace")
                     .withField(StandardField.JOURNAL, "Test Journal")
                     .withField(StandardField.VOLUME, "1")
                     .withField(StandardField.NUMBER, "1")
                     .withField(StandardField.PAGES, "1-2")
                     .withField(StandardField.ISSN, "978-123-123")
                     .withField(StandardField.NOTE, "NOTE")
                     .withField(StandardField.ABSTRACT, "ABSTRACT")
                     .withField(StandardField.COMMENT, "COMMENT")
                     .withField(StandardField.DOI, "10/3212.3123")
                     .withField(StandardField.FILE, ":article_dublinCore.pdf:PDF")
                     .withField(StandardField.GROUPS, "NO")
                     .withField(StandardField.HOWPUBLISHED, "online")
                     .withField(StandardField.KEYWORDS, "k1, k2")
                     .withField(StandardField.OWNER, "me")
                     .withField(StandardField.REVIEW, "review")
                     .withField(StandardField.URL, "https://www.olly2018.edu")
                     .withField(new UnknownField((DC_SOURCE)), "JabRef")
                     .setMonth(Month.MARCH);
        new XmpUtilWriter(xmpPreferences).writeXmp(pdfFile, metadataEntry, bibDatabase);

        BibEntry entry = new BibEntry();
        LinkedFile fileField = new LinkedFile("Test PDF", pdfFile, "PDF");
        entry.setField(StandardField.FILE, FileFieldWriter.getStringRepresentation(fileField));

        CleanupPreferences preset = new CleanupPreferences(CleanupPreferences.CleanupStep.REMOVE_XMP_METADATA);
        List<FieldChange> changes = worker.cleanup(preset, entry);
        assertNotEquals(List.of(), changes);
        assertEquals(List.of(), new XmpUtilReader().readRawXmp(pdfFile));
    }

    @Test
    void cleanupDoesNothingByDefault(@TempDir Path bibFolder) throws IOException {
        BibEntry entry = new BibEntry();
        entry.setCitationKey("Toot");
        entry.setField(StandardField.PDF, "aPdfFile");
        entry.setField(new UnknownField("some"), "1st");
        entry.setField(StandardField.DOI, "http://dx.doi.org/10.1016/0001-8708(80)90035-3");
        entry.setField(StandardField.MONTH, "01");
        entry.setField(StandardField.PAGES, "1-2");
        entry.setField(StandardField.DATE, "01/1999");
        entry.setField(StandardField.PDF, "aPdfFile");
        entry.setField(StandardField.ISSN, "aPsFile");
        entry.setField(StandardField.FILE, "link::");
        entry.setField(StandardField.JOURNAL, "test");
        entry.setField(StandardField.TITLE, "<b>hallo</b> units 1 A case AlGaAs and latex $\\alpha$$\\beta$");
        entry.setField(StandardField.ABSTRACT, "Réflexions");
        Path path = bibFolder.resolve("ARandomlyNamedFile");
        Files.createFile(path);
        LinkedFile fileField = new LinkedFile("", path.toAbsolutePath(), "");
        entry.setField(StandardField.FILE, FileFieldWriter.getStringRepresentation(fileField));

        List<FieldChange> changes = worker.cleanup(emptyPreset, entry);
        assertEquals(List.of(), changes);
    }

    @Test
    void upgradeExternalLinksMoveFromPdfToFile() {
        CleanupPreferences preset = new CleanupPreferences(CleanupPreferences.CleanupStep.CLEAN_UP_UPGRADE_EXTERNAL_LINKS);
        BibEntry entry = new BibEntry();
        entry.setField(StandardField.PDF, "aPdfFile");

        worker.cleanup(preset, entry);
        assertEquals(Optional.empty(), entry.getField(StandardField.PDF));
        assertEquals(Optional.of("aPdfFile:aPdfFile:PDF"), entry.getField(StandardField.FILE));
    }

    @Test
    void upgradeExternalLinksMoveFromPsToFile() {
        CleanupPreferences preset = new CleanupPreferences(CleanupPreferences.CleanupStep.CLEAN_UP_UPGRADE_EXTERNAL_LINKS);
        BibEntry entry = new BibEntry();
        entry.setField(StandardField.PS, "aPsFile");

        worker.cleanup(preset, entry);
        assertEquals(Optional.empty(), entry.getField(StandardField.PDF));
        assertEquals(Optional.of("aPsFile:aPsFile:PostScript"), entry.getField(StandardField.FILE));
    }

    @Test
    void cleanupDoiRemovesLeadingHttp() {
        CleanupPreferences preset = new CleanupPreferences(CleanupPreferences.CleanupStep.CLEAN_UP_DOI);
        BibEntry entry = new BibEntry();
        entry.setField(StandardField.DOI, "http://dx.doi.org/10.1016/0001-8708(80)90035-3");

        worker.cleanup(preset, entry);
        assertEquals(Optional.of("10.1016/0001-8708(80)90035-3"), entry.getField(StandardField.DOI));
    }

    @Test
    void cleanupDoiReturnsChanges() {
        CleanupPreferences preset = new CleanupPreferences(CleanupPreferences.CleanupStep.CLEAN_UP_DOI);
        BibEntry entry = new BibEntry();
        entry.setField(StandardField.DOI, "http://dx.doi.org/10.1016/0001-8708(80)90035-3");

        List<FieldChange> changes = worker.cleanup(preset, entry);

        FieldChange expectedChange = new FieldChange(entry, StandardField.DOI, "http://dx.doi.org/10.1016/0001-8708(80)90035-3", "10.1016/0001-8708(80)90035-3");
        assertEquals(List.of(expectedChange), changes);
    }

    @Test
    void cleanupDoiFindsDoiInURLFieldAndMoveItToDOIField() {
        CleanupPreferences preset = new CleanupPreferences(CleanupPreferences.CleanupStep.CLEAN_UP_DOI);
        BibEntry entry = new BibEntry();
        entry.setField(StandardField.URL, "http://dx.doi.org/10.1016/0001-8708(80)90035-3");

        worker.cleanup(preset, entry);
        assertEquals(Optional.of("10.1016/0001-8708(80)90035-3"), entry.getField(StandardField.DOI));
        assertEquals(Optional.empty(), entry.getField(StandardField.URL));
    }

    @Test
    void cleanupDoiReturnsChangeWhenDoiInURLField() {
        CleanupPreferences preset = new CleanupPreferences(CleanupPreferences.CleanupStep.CLEAN_UP_DOI);
        BibEntry entry = new BibEntry();
        entry.setField(StandardField.URL, "http://dx.doi.org/10.1016/0001-8708(80)90035-3");

        List<FieldChange> changes = worker.cleanup(preset, entry);
        List<FieldChange> changeList = new ArrayList<>();
        changeList.add(new FieldChange(entry, StandardField.DOI, null, "10.1016/0001-8708(80)90035-3"));
        changeList.add(new FieldChange(entry, StandardField.URL, "http://dx.doi.org/10.1016/0001-8708(80)90035-3", null));
        assertEquals(changeList, changes);
    }

    @Test
    void cleanupMonthChangesNumberToBibtex() {
        CleanupPreferences preset = new CleanupPreferences(new FieldFormatterCleanupActions(true,
                List.of(new FieldFormatterCleanup(StandardField.MONTH, new NormalizeMonthFormatter()))));
        BibEntry entry = new BibEntry();
        entry.setField(StandardField.MONTH, "01");

        worker.cleanup(preset, entry);
        assertEquals(Optional.of("#jan#"), entry.getField(StandardField.MONTH));
    }

    @Test
    void cleanupPageNumbersConvertsSingleDashToDouble() {
        CleanupPreferences preset = new CleanupPreferences(new FieldFormatterCleanupActions(true,
                List.of(new FieldFormatterCleanup(StandardField.PAGES, new NormalizePagesFormatter()))));
        BibEntry entry = new BibEntry();
        entry.setField(StandardField.PAGES, "1-2");

        worker.cleanup(preset, entry);
        assertEquals(Optional.of("1--2"), entry.getField(StandardField.PAGES));
    }

    @Test
    void cleanupDatesConvertsToCorrectFormat() {
        CleanupPreferences preset = new CleanupPreferences(new FieldFormatterCleanupActions(true,
                List.of(new FieldFormatterCleanup(StandardField.DATE, new NormalizeDateFormatter()))));
        BibEntry entry = new BibEntry();
        entry.setField(StandardField.DATE, "01/1999");

        worker.cleanup(preset, entry);
        assertEquals(Optional.of("1999-01"), entry.getField(StandardField.DATE));
    }

    @Test
    void cleanupFixFileLinksMovesSingleDescriptionToLink() {
        CleanupPreferences preset = new CleanupPreferences(CleanupPreferences.CleanupStep.FIX_FILE_LINKS);
        BibEntry entry = new BibEntry();
        entry.setField(StandardField.FILE, "link::");

        worker.cleanup(preset, entry);
        assertEquals(Optional.of(":link:"), entry.getField(StandardField.FILE));
    }

    @Test
    void cleanupMoveFilesMovesFileFromSubfolder(@TempDir Path bibFolder) throws IOException {
        CleanupPreferences preset = new CleanupPreferences(CleanupPreferences.CleanupStep.MOVE_PDF);

        Path path = bibFolder.resolve("AnotherRandomlyNamedFolder");
        Files.createDirectory(path);
        Path tempFile = Files.createFile(path.resolve("test.pdf"));
        BibEntry entry = new BibEntry();
        LinkedFile fileField = new LinkedFile("", tempFile.toAbsolutePath(), "");
        entry.setField(StandardField.FILE, FileFieldWriter.getStringRepresentation(fileField));

        worker.cleanup(preset, entry);
        LinkedFile newFileField = new LinkedFile("", tempFile.getFileName(), "");
        assertEquals(Optional.of(FileFieldWriter.getStringRepresentation(newFileField)), entry.getField(StandardField.FILE));
    }

    @Test
    void cleanupRelativePathsConvertAbsoluteToRelativePath() throws IOException {
        CleanupPreferences preset = new CleanupPreferences(CleanupPreferences.CleanupStep.MAKE_PATHS_RELATIVE);

        Path path = pdfPath.resolve("AnotherRandomlyNamedFile");
        Files.createFile(path);
        BibEntry entry = new BibEntry();
        LinkedFile fileField = new LinkedFile("", path.toAbsolutePath(), "");
        entry.setField(StandardField.FILE, FileFieldWriter.getStringRepresentation(fileField));

        worker.cleanup(preset, entry);
        LinkedFile newFileField = new LinkedFile("", path.getFileName(), "");
        assertEquals(Optional.of(FileFieldWriter.getStringRepresentation(newFileField)), entry.getField(StandardField.FILE));
    }

    @Test
    void cleanupRenamePdfRenamesRelativeFile() throws IOException {
        CleanupPreferences preset = new CleanupPreferences(CleanupPreferences.CleanupStep.RENAME_PDF);

        Path path = pdfPath.resolve("AnotherRandomlyNamedFile.tmp");
        Files.createFile(path);
        BibEntry entry = new BibEntry()
                .withCitationKey("Toot");
        LinkedFile fileField = new LinkedFile("", path.toAbsolutePath(), "");
        entry.setField(StandardField.FILE, FileFieldWriter.getStringRepresentation(fileField));

        worker.cleanup(preset, entry);
        LinkedFile newFileField = new LinkedFile("", Path.of("Toot.tmp"), "");
        assertEquals(Optional.of(FileFieldWriter.getStringRepresentation(newFileField)), entry.getField(StandardField.FILE));
    }

    @Test
    void cleanupHtmlToLatexConvertsEpsilonToLatex() {
        CleanupPreferences preset = new CleanupPreferences(new FieldFormatterCleanupActions(true,
                List.of(new FieldFormatterCleanup(StandardField.TITLE, new HtmlToLatexFormatter()))));
        BibEntry entry = new BibEntry();
        entry.setField(StandardField.TITLE, "&Epsilon;");

        worker.cleanup(preset, entry);
        assertEquals(Optional.of("{{$\\Epsilon$}}"), entry.getField(StandardField.TITLE));
    }

    @Test
    void cleanupUnitsConvertsOneAmpereToLatex() {
        CleanupPreferences preset = new CleanupPreferences(new FieldFormatterCleanupActions(true,
                List.of(new FieldFormatterCleanup(StandardField.TITLE, new UnitsToLatexFormatter()))));
        BibEntry entry = new BibEntry();
        entry.setField(StandardField.TITLE, "1 A");

        worker.cleanup(preset, entry);
        assertEquals(Optional.of("1~{A}"), entry.getField(StandardField.TITLE));
    }

    @Test
    void cleanupCasesAddsBracketAroundAluminiumGalliumArsenid() {
        ProtectedTermsLoader protectedTermsLoader = new ProtectedTermsLoader(
                new ProtectedTermsPreferences(ProtectedTermsLoader.getInternalLists(), List.of(),
                        List.of(), List.of()));
        assertNotEquals(List.of(), protectedTermsLoader.getProtectedTerms());
        CleanupPreferences preset = new CleanupPreferences(new FieldFormatterCleanupActions(true, List.of(new FieldFormatterCleanup(StandardField.TITLE, new ProtectTermsFormatter(protectedTermsLoader)))));
        BibEntry entry = new BibEntry();
        entry.setField(StandardField.TITLE, "AlGaAs");

        worker.cleanup(preset, entry);
        assertEquals(Optional.of("{AlGaAs}"), entry.getField(StandardField.TITLE));
    }

    @Test
    void cleanupLatexMergesTwoLatexMathEnvironments() {
        CleanupPreferences preset = new CleanupPreferences(new FieldFormatterCleanupActions(true,
                List.of(new FieldFormatterCleanup(StandardField.TITLE, new LatexCleanupFormatter()))));
        BibEntry entry = new BibEntry();
        entry.setField(StandardField.TITLE, "$\\alpha$$\\beta$");

        worker.cleanup(preset, entry);
        assertEquals(Optional.of("$\\alpha\\beta$"), entry.getField(StandardField.TITLE));
    }

    @Test
    void convertToBiblatexMovesAddressToLocation() {
        CleanupPreferences preset = new CleanupPreferences(CleanupPreferences.CleanupStep.CONVERT_TO_BIBLATEX);
        BibEntry entry = new BibEntry();
        entry.setField(StandardField.ADDRESS, "test");

        worker.cleanup(preset, entry);
        assertEquals(Optional.empty(), entry.getField(StandardField.ADDRESS));
        assertEquals(Optional.of("test"), entry.getField(StandardField.LOCATION));
    }

    @Test
    void convertToBiblatexMovesJournalToJournalTitle() {
        CleanupPreferences preset = new CleanupPreferences(CleanupPreferences.CleanupStep.CONVERT_TO_BIBLATEX);
        BibEntry entry = new BibEntry();
        entry.setField(StandardField.JOURNAL, "test");

        worker.cleanup(preset, entry);
        assertEquals(Optional.empty(), entry.getField(StandardField.JOURNAL));
        assertEquals(Optional.of("test"), entry.getField(StandardField.JOURNALTITLE));
    }

    @Test
    void cleanupWithDisabledFieldFormatterChangesNothing() {
        CleanupPreferences preset = new CleanupPreferences(new FieldFormatterCleanupActions(false,
                List.of(new FieldFormatterCleanup(StandardField.MONTH, new NormalizeMonthFormatter()))));
        BibEntry entry = new BibEntry();
        entry.setField(StandardField.MONTH, "01");

        worker.cleanup(preset, entry);
        assertEquals(Optional.of("01"), entry.getField(StandardField.MONTH));
    }
}
