/*
 * Created on Jul 1, 2011
 * Copyright 2011 by Eduard Weissmann (edi.weissmann@gmail.com).
 * 
 * This file is part of the Sejda source code
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.sejda.cli;

import static org.apache.commons.io.FilenameUtils.separatorsToWindows;
import static org.hamcrest.CoreMatchers.endsWith;
import static org.hamcrest.CoreMatchers.hasItem;
import static org.hamcrest.core.CombinableMatcher.either;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;

import org.apache.commons.lang3.StringUtils;
import org.hamcrest.Matcher;
import org.junit.Before;
import org.junit.Test;
import org.sejda.cli.command.StandardTestableTask;
import org.sejda.model.input.PdfFileSource;
import org.sejda.model.input.PdfMergeInput;
import org.sejda.model.outline.OutlinePolicy;
import org.sejda.model.parameter.MergeParameters;
import org.sejda.model.pdf.form.AcroFormPolicy;
import org.sejda.model.pdf.page.PageRange;
import org.sejda.model.toc.ToCPolicy;

/**
 * Tests for the MergeTask command line interface
 * 
 * @author Eduard Weissmann
 * 
 */
public class MergeTaskTest extends AbstractTaskTest {

    public MergeTaskTest() {
        super(StandardTestableTask.MERGE);
    }

    @Override
    @Before
    public void setUp() {
        super.setUp();

        createTestPdfFile("/tmp/merge/file1.pdf");
        createTestPdfFile("/tmp/merge/file2.pdf");
        createTestPdfFile("/tmp/merge/file3.pdf");
        createTestPdfFile("/tmp/merge/file4.pdf");
        createTestPdfFile("/tmp/merge/file5.txt");
        createTestPdfFile("/tmp/merge/subdir/file5.pdf");

        createTestTextFile("./location/filenames.csv",
                "/tmp/merge/file3.pdf, /tmp/merge/file1.pdf, /tmp/merge/file2.pdf");
        createTestTextFile("./location/empty_filenames.csv", "");
        createTestTextFile("./location/filenames_invalidPaths.csv",
                "/tmp/merge/fileDoesntExist.pdf,/tmp/merge/file1.pdf");

        createTestTextFile("/tmp/filenames.xml", getClass().getResourceAsStream("/merge-filelist-config.xml"));
        // files inside
        createTestPdfFile("/tmp/pdf/inputFile.pdf");
        createTestPdfFile("/tmp/pdf/inputFile2.pdf");
        createTestPdfFile("/tmp/inputFile1.pdf");
        createTestPdfFile("/tmp/inputFile2.pdf");
        createTestPdfFile("/tmp/subdir/inputFile1.pdf");
        createTestPdfFile("/tmp/subdir3/inputFile2.pdf");
        createTestPdfFile("/tmp/subdir2/inputFile1.pdf");
        createTestPdfFile("/tmp/subdir2/inputFile2.pdf");
        createTestPdfFile("/tmp/subdir2/inputFile3.pdf");

        createTestTextFile("./location/filenames_invalidXml.xml", "<filelist><file value=\"/tmp/merge/file1.pdf \">");
        createTestFolder("/tmp/emptyFolder");
        createTestPdfFile("./location/filenames.xls");
    }

    @Test
    public void onAddBlanks() {
        MergeParameters parameters = defaultCommandLine().withFlag("--addBlanks").invokeSejdaConsole();
        assertTrue(parameters.isBlankPageIfOdd());
    }

    @Test
    public void offAddBlankss() {
        MergeParameters parameters = defaultCommandLine().invokeSejdaConsole();
        assertFalse(parameters.isBlankPageIfOdd());
    }

    @Test
    public void onDiscardBookmarks() {
        MergeParameters parameters = defaultCommandLine().with("-b", "discard").invokeSejdaConsole();
        assertEquals(OutlinePolicy.DISCARD, parameters.getOutlinePolicy());
    }

    @Test
    public void onOneEachDoc() {
        MergeParameters parameters = defaultCommandLine().with("-b", "one_entry_each_doc").invokeSejdaConsole();
        assertEquals(OutlinePolicy.ONE_ENTRY_EACH_DOC, parameters.getOutlinePolicy());
    }

    @Test
    public void addFooter() {
        MergeParameters parameters = defaultCommandLine().withFlag("--footer").invokeSejdaConsole();
        assertTrue(parameters.isFilenameFooter());
    }

    @Test
    public void onDefault() {
        MergeParameters parameters = defaultCommandLine().invokeSejdaConsole();
        assertEquals(OutlinePolicy.RETAIN, parameters.getOutlinePolicy());
        assertEquals(AcroFormPolicy.DISCARD, parameters.getAcroFormPolicy());
        assertEquals(ToCPolicy.NONE, parameters.getTableOfContentsPolicy());
        assertFalse(parameters.isFilenameFooter());
    }

    @Test
    public void onRetainAsOneEntry() {
        MergeParameters parameters = defaultCommandLine().with("-b", "retain_as_one_entry").invokeSejdaConsole();
        assertEquals(OutlinePolicy.RETAIN_AS_ONE_ENTRY, parameters.getOutlinePolicy());
    }

    @Test
    public void onMergeAcroForms() {
        MergeParameters parameters = defaultCommandLine().with("-a", "merge").invokeSejdaConsole();
        assertEquals(AcroFormPolicy.MERGE, parameters.getAcroFormPolicy());
    }

    @Test
    public void onFlattenAcroForms() {
        MergeParameters parameters = defaultCommandLine().with("-a", "flatten").invokeSejdaConsole();
        assertEquals(AcroFormPolicy.FLATTEN, parameters.getAcroFormPolicy());
    }

    @Test
    public void onMergeRenamingAcroForms() {
        MergeParameters parameters = defaultCommandLine().with("-a", "merge_renaming").invokeSejdaConsole();
        assertEquals(AcroFormPolicy.MERGE_RENAMING_EXISTING_FIELDS, parameters.getAcroFormPolicy());
    }

    @Test
    public void onTextNamesToC() {
        MergeParameters parameters = defaultCommandLine().with("-t", "file_names").invokeSejdaConsole();
        assertEquals(ToCPolicy.FILE_NAMES, parameters.getTableOfContentsPolicy());
    }

    @Test
    public void onTextTitlesToC() {
        MergeParameters parameters = defaultCommandLine().with("-t", "doc_titles").invokeSejdaConsole();
        assertEquals(ToCPolicy.DOC_TITLES, parameters.getTableOfContentsPolicy());
    }

    @Test
    public void folderInput() {
        MergeParameters parameters = defaultCommandLine().without("-f").with("-d", "/tmp/merge").invokeSejdaConsole();

        assertPdfMergeInputsFilesList(parameters, filesList("/tmp/merge/file1.pdf", "/tmp/merge/file2.pdf",
                "/tmp/merge/file3.pdf", "/tmp/merge/file4.pdf"));
    }

    @Test
    public void folderInputWithRegexp() {
        MergeParameters parameters = defaultCommandLine().without("-f").with("-d", "/tmp/merge")
                .with("-e", "([^\\s]+(1|4)(?i)(\\.pdf)$)").invokeSejdaConsole();

        assertPdfMergeInputsFilesList(parameters, filesList("/tmp/merge/file1.pdf", "/tmp/merge/file4.pdf"));
    }

    @Test
    public void folderInputWithRegexpFilteringEverythig() {
        defaultCommandLine().without("-f").with("-d", "/tmp/merge").with("-e", "([^\\s]+(\\.(norris))$)")
                .assertConsoleOutputContains("No input files specified in");

    }

    @Test
    public void wildcardInput() {
        MergeParameters parameters = defaultCommandLine().without("-f")
                .with("-f", "/tmp/merge/*.pdf /tmp/inputFile1.pdf").invokeSejdaConsole();

        assertPdfMergeInputsFilesList(parameters, filesList("/tmp/merge/file1.pdf", "/tmp/merge/file2.pdf",
                "/tmp/merge/file3.pdf", "/tmp/merge/file4.pdf", "/tmp/inputFile1.pdf"));
    }

    @Test
    public void folderInput_emptyFolder() {
        defaultCommandLine().with("-d", "/tmp/emptyFolder").assertConsoleOutputContains("No input files specified in");
    }

    private static List<Matcher<Iterable<? super String>>> filesList(String... filenames) {
        List<Matcher<Iterable<? super String>>> result = new ArrayList<Matcher<Iterable<? super String>>>();
        for (String current : filenames) {
            String filename = current.toString();
            result.add(either(hasItem(filename)).or(hasItem(endsWith(separatorsToWindows(filename)))));
        }
        return result;
    }

    @Test
    public void fileListConfigInput_csv() {
        MergeParameters parameters = defaultCommandLine().without("-f").with("-l", "./location/filenames.csv")
                .invokeSejdaConsole();

        assertPdfMergeInputsFilesList(parameters,
                filesList("/tmp/merge/file3.pdf", "/tmp/merge/file1.pdf", "/tmp/merge/file2.pdf"));
    }

    @Test
    public void fileListConfigInput_csv_invalidPaths() {
        defaultCommandLine().without("-f").with("-l", "./location/filenames_invalidPaths.csv")
                .assertConsoleOutputContains("Invalid filename found");
    }

    @Test
    public void fileListConfigInput_csv_doesntExist() {
        defaultCommandLine().without("-f").with("-l", "./location/doesntExist.csv")
                .assertConsoleOutputContains("does not exist");
    }

    @Test
    public void fileListConfigInput_xml_invalidXml() {
        defaultCommandLine().without("-f").with("-l", "./location/filenames_invalidXml.xml")
                .assertConsoleOutputContains("Can't extract filenames from");
    }

    @Test
    public void fileListConfigInput_xml_invalidContents() {
        defaultCommandLine().without("-f").with("-l", "./location/filenames_invalidXml.xml")
                .assertConsoleOutputContains("Can't extract filenames from");
    }

    @Test
    public void inputFiles_csv_empty() {
        defaultCommandLine().without("-f").with("-l", "./location/empty_filenames.csv")
                .assertConsoleOutputContains("No input files specified in ");
    }

    @Test
    public void inputFiles_unsupportedFormatConfigList() {
        defaultCommandLine().without("-f").with("-l", "./location/filenames.xls")
                .assertConsoleOutputContains("Unsupported file format: xls");
    }

    @Test
    public void filesInput() {
        MergeParameters parameters = defaultCommandLine().with("-f", "/tmp/merge/file4.pdf /tmp/merge/file2.pdf")
                .invokeSejdaConsole();

        assertPdfMergeInputsFilesList(parameters, filesList("/tmp/merge/file4.pdf", "/tmp/merge/file2.pdf"));
    }

    @Test
    public void input_noOptionGiven() {
        defaultCommandLine().without("-f").without("-d").without("-l")
                .assertConsoleOutputContains("No option given for input");
    }

    @Test
    public void input_tooManyOptionsGiven() {
        defaultCommandLine().without("-f").with("-d", "/tmp/merge").with("-l", "./location/filenames.xls")
                .assertConsoleOutputContains("Too many options given");
    }

    private void assertPdfMergeInputsFilesList(MergeParameters parameters,
            Collection<Matcher<Iterable<? super String>>> expectedFilesMatchers) {
        assertPdfMergeInputsFilesList(parameters, expectedFilesMatchers,
                nullsFilledList(parameters.getInputList().size()));
    }

    private List<String> nullsFilledList(int size) {
        List<String> result = new ArrayList<String>();
        for (int i = 0; i < size; i++) {
            result.add(null);
        }

        return result;
    }

    private void assertPdfMergeInputsFilesList(MergeParameters parameters,
            Collection<Matcher<Iterable<? super String>>> expectedFilesMatchers, List<String> expectedFilesPasswords) {
        List<String> actualFileList = new ArrayList<String>();
        List<String> actualPasswords = new ArrayList<String>();

        for (int i = 0; i < parameters.getInputList().size(); i++) {
            PdfMergeInput each = parameters.getPdfInputList().get(i);
            PdfFileSource pdfFileSource = (PdfFileSource) each.getSource();
            actualFileList.add(pdfFileSource.getSource().getAbsolutePath());
            actualPasswords.add(pdfFileSource.getPassword());
        }

        for (Matcher<Iterable<? super String>> expectedFileMatcher : expectedFilesMatchers) {
            assertThat(actualFileList, expectedFileMatcher);
        }
        assertEquals(expectedFilesPasswords, actualPasswords);
    }

    @Test
    public void fileListConfigInput_xml() {
        MergeParameters parameters = defaultCommandLine().without("-f").with("-l", "/tmp/filenames.xml")
                .invokeSejdaConsole();

        assertPdfMergeInputsFilesList(parameters,
                filesList("/tmp/pdf/inputFile.pdf", "/tmp/pdf/inputFile2.pdf", "/tmp/inputFile1.pdf",
                        "/tmp/inputFile2.pdf", "/tmp/subdir/inputFile1.pdf", "/tmp/subdir3/inputFile2.pdf",
                        "/tmp/subdir2/inputFile1.pdf", "/tmp/subdir2/inputFile2.pdf", "/tmp/subdir2/inputFile3.pdf"),
                Arrays.asList(null, "test", null, null, null, null, null, "secret2", null));
    }

    private static final String NO_PASSWORD = null;
    private static final Set<PageRange> NO_PAGE_RANGE_SPECIFIED = Collections.emptySet();
    private static final Set<PageRange> ALL_PAGES = Collections.emptySet();

    @Test
    public void pageRange_empty() {
        MergeParameters parameters = defaultCommandLine().invokeSejdaConsole();

        assertHasPdfMergeInput(parameters, "inputs/input.pdf", NO_PAGE_RANGE_SPECIFIED);
    }

    @Test
    public void pageRange_simpleInterval() {
        MergeParameters parameters = defaultCommandLine().with("-s", "3003-3010").invokeSejdaConsole();

        assertHasPdfMergeInput(parameters, "inputs/input.pdf", Arrays.asList(new PageRange(3003, 3010)));
        assertHasPdfMergeInput(parameters, "inputs/second_input.pdf", NO_PAGE_RANGE_SPECIFIED);
    }

    @Test
    public void pageRange_openedInterval() {
        MergeParameters parameters = defaultCommandLine().with("-s", "2-").invokeSejdaConsole();

        assertHasPdfMergeInput(parameters, "inputs/input.pdf", Arrays.asList(new PageRange(2)));
        assertHasPdfMergeInput(parameters, "inputs/second_input.pdf", NO_PAGE_RANGE_SPECIFIED);
    }

    @Test
    public void pageRange_singlePage() {
        MergeParameters parameters = defaultCommandLine().with("-s", "3003").invokeSejdaConsole();

        assertHasPdfMergeInput(parameters, "inputs/input.pdf", Arrays.asList(new PageRange(3003, 3003)));
        assertHasPdfMergeInput(parameters, "inputs/second_input.pdf", NO_PAGE_RANGE_SPECIFIED);
    }

    @Test
    public void pageRange_allPages() {
        MergeParameters parameters = defaultCommandLine().with("-s", "all").invokeSejdaConsole();

        assertHasPdfMergeInput(parameters, "inputs/input.pdf", ALL_PAGES);
        assertHasPdfMergeInput(parameters, "inputs/second_input.pdf", NO_PAGE_RANGE_SPECIFIED);
    }

    @Test
    public void pageRange_combined() {
        MergeParameters parameters = defaultCommandLine().with("-s", "all:3,5,8-10,2,2,9-9,30-").invokeSejdaConsole();

        assertHasPdfMergeInput(parameters, "inputs/input.pdf", ALL_PAGES);
        assertHasPdfMergeInput(parameters, "inputs/second_input.pdf",
                Arrays.asList(new PageRange(3, 3), new PageRange(5, 5), new PageRange(8, 10), new PageRange(2, 2),
                        new PageRange(9, 9), new PageRange(30)));
    }

    private void assertHasPdfMergeInput(MergeParameters parameters, String filename,
            Collection<PageRange> expectedPageRanges) {
        assertHasPdfMergeInput(parameters, filename, NO_PASSWORD, expectedPageRanges);
    }

    private void assertHasPdfMergeInput(MergeParameters parameters, String filename, String password,
            Collection<PageRange> expectedPageRanges) {
        boolean found = false;
        File file = new File(filename);
        for (PdfMergeInput each : parameters.getPdfInputList()) {
            PdfFileSource pdfFileSource = (PdfFileSource) each.getSource();
            if (matchesPdfFileSource(file, password, pdfFileSource)) {
                assertContainsAll("For file " + pdfFileSource.getName(), expectedPageRanges, each.getPageSelection());
                found = true;
            }
        }

        assertTrue(
                "File '" + file + "'"
                        + (StringUtils.isEmpty(password) ? " and no password" : " and password '" + password + "'"),
                found);

    }
}
