/* ====================================================================
   Licensed to the Apache Software Foundation (ASF) under one or more
   contributor license agreements.  See the NOTICE file distributed with
   this work for additional information regarding copyright ownership.
   The ASF licenses this file to You 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 org.apache.poi.ss.tests;

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.stream.Stream;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.poi.EmptyFileException;
import org.apache.poi.EncryptedDocumentException;
import org.apache.poi.POIDataSamples;
import org.apache.poi.hssf.HSSFTestDataSamples;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.openxml4j.opc.PackageAccess;
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.apache.poi.util.SuppressForbidden;
import org.apache.poi.util.TempFile;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbookFactory;
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;

public final class TestWorkbookFactory {
    private static final String xls = "SampleSS.xls";
    private static final String xlsx = "SampleSS.xlsx";
    private static final String[] xls_protected = new String[]{"password.xls", "password"};
    private static final String[] xlsx_protected = new String[]{"protected_passtika.xlsx", "tika"};
    private static final String txt = "SampleSS.txt";

    private static final Logger LOGGER = LogManager.getLogger(TestWorkbookFactory.class);

    /**
     * Closes the sample workbook read in from filename.
     * Throws an exception if closing the workbook results in the file on disk getting modified.
     *
     * @param filename the sample workbook to read in
     * @param wb       the workbook to close
     */
    private static void assertCloseDoesNotModifyFile(String filename, Workbook wb) throws IOException {
        final byte[] before = HSSFTestDataSamples.getTestDataFileContent(filename);
        // FIXME: replace with wb.close() when bug 58779 is resolved
        closeOrRevert(wb);
        final byte[] after = HSSFTestDataSamples.getTestDataFileContent(filename);
        assertArrayEquals(before, after, filename + " sample file was modified as a result of closing the workbook");
    }

    /**
     * bug 58779: Closing an XSSFWorkbook that was created with WorkbookFactory modifies the file
     * FIXME: replace this method with wb.close() when bug 58779 is resolved.
     *
     * @param wb the workbook to close or revert
     */
    private static void closeOrRevert(Workbook wb) throws IOException {
        if (wb instanceof HSSFWorkbook) {
            wb.close();
        } else if (wb instanceof XSSFWorkbook) {
            final XSSFWorkbook xwb = (XSSFWorkbook) wb;
            if (PackageAccess.READ == xwb.getPackage().getPackageAccess()) {
                xwb.close();
            } else {
                // TODO: close() re-writes the sample-file?! Resort to revert() for now to close file handle...
                LOGGER.atWarn().log("reverting XSSFWorkbook rather than closing it to avoid close() modifying the file on disk. Refer to bug 58779.");
                xwb.getPackage().revert();
            }
        } else {
            throw new RuntimeException("Unsupported workbook type");
        }
    }

    @Test
    void testCreateNative() throws Exception {
        // POIFS -> hssf
        try (Workbook wb = WorkbookFactory.create(
            new POIFSFileSystem(HSSFTestDataSamples.openSampleFileStream(xls))
        )) {
            assertNotNull(wb);
            assertTrue(wb instanceof HSSFWorkbook);
        }

        try (Workbook wb = WorkbookFactory.create(
            new POIFSFileSystem(HSSFTestDataSamples.openSampleFileStream(xls)).getRoot()
        )) {
            assertNotNull(wb);
            assertTrue(wb instanceof HSSFWorkbook);
        }

        // Package -> xssf
        try (Workbook wb = XSSFWorkbookFactory.createWorkbook(
            OPCPackage.open(HSSFTestDataSamples.openSampleFileStream(xlsx))
        )) {
            assertNotNull(wb);
            //noinspection ConstantConditions
            assertTrue(wb instanceof XSSFWorkbook);
        }
    }

    @Test
    void testCreateReadOnly() throws Exception {
        // POIFS -> hssf
        try (Workbook wb = WorkbookFactory.create(HSSFTestDataSamples.getSampleFile(xls), null, true)) {
            assertNotNull(wb);
            assertTrue(wb instanceof HSSFWorkbook);
            assertCloseDoesNotModifyFile(xls, wb);
        }

        // Package -> xssf
        try (Workbook wb = WorkbookFactory.create(HSSFTestDataSamples.getSampleFile(xlsx), null, true)) {
            assertNotNull(wb);
            assertTrue(wb instanceof XSSFWorkbook);
            assertCloseDoesNotModifyFile(xlsx, wb);
        }
    }

    /**
     * Creates the appropriate kind of Workbook, but
     * checking the mime magic at the start of the
     * InputStream, then creating what's required.
     */
    @Test
    void testCreateGeneric() throws Exception {
        // InputStream -> either
        try (Workbook wb = WorkbookFactory.create(HSSFTestDataSamples.openSampleFileStream(xls))) {
            assertNotNull(wb);
            assertTrue(wb instanceof HSSFWorkbook);
        }

        try (Workbook wb = WorkbookFactory.create(HSSFTestDataSamples.openSampleFileStream(xlsx))) {
            assertNotNull(wb);
            assertTrue(wb instanceof XSSFWorkbook);
        }

        // File -> either
        try (Workbook wb = WorkbookFactory.create(HSSFTestDataSamples.getSampleFile(xls))) {
            assertNotNull(wb);
            assertTrue(wb instanceof HSSFWorkbook);
            assertCloseDoesNotModifyFile(xls, wb);
        }

        try (Workbook wb = WorkbookFactory.create(HSSFTestDataSamples.getSampleFile(xlsx))) {
            assertNotNull(wb);
            assertTrue(wb instanceof XSSFWorkbook);
            assertCloseDoesNotModifyFile(xlsx, wb);
        }

        // Invalid type -> exception
        final byte[] before = HSSFTestDataSamples.getTestDataFileContent(txt);
        assertThrows(IOException.class, () -> WorkbookFactory.create(new File(txt)));
        final byte[] after = HSSFTestDataSamples.getTestDataFileContent(txt);
        assertArrayEquals(before, after, "Invalid type file was modified after trying to open the file as a spreadsheet");
    }

    public static Stream<Arguments> workbookPass() {
        return Stream.of(
            // Unprotected, no password given, opens normally
            Arguments.of(xls, null, false, HSSFWorkbook.class),
            Arguments.of(xlsx, null, false, XSSFWorkbook.class),
            // Unprotected, wrong password, opens normally
            Arguments.of(xls, "wrong", false, HSSFWorkbook.class),
            Arguments.of(xlsx, "wrong", false, XSSFWorkbook.class),
            // Protected, correct password, opens fine
            Arguments.of(xls_protected[0], xls_protected[1], false, HSSFWorkbook.class),
            Arguments.of(xlsx_protected[0], xlsx_protected[1], false, XSSFWorkbook.class),
            // Protected, wrong password, throws Exception
            Arguments.of(xls_protected[0], "wrong", true, HSSFWorkbook.class),
            Arguments.of(xlsx_protected[0], "wrong", true, XSSFWorkbook.class)
        );
    }

    /**
     * Check that the overloaded stream methods which take passwords work properly
     */
    @ParameterizedTest
    @MethodSource("workbookPass")
    void testCreateWithPasswordFromStream(String file, String pass, boolean fails, Class<? extends Workbook> clazz) throws Exception {
        try (InputStream is = HSSFTestDataSamples.openSampleFileStream(file)) {
            if (fails) {
                assertThrows(EncryptedDocumentException.class, () -> WorkbookFactory.create(is, pass),
                    "Shouldn't be able to open with the wrong password");
            } else {
                try (Workbook wb = WorkbookFactory.create(is, pass)) {
                    assertNotNull(wb);
                    assertTrue(clazz.isInstance(wb));
                }
            }
        }
    }

    /**
     * Check that the overloaded file methods which take passwords work properly
     */
    @ParameterizedTest
    @MethodSource("workbookPass")
    void testCreateWithPasswordFromFile(String fileName, String pass, boolean fails, Class<? extends Workbook> clazz) throws Exception {
        File file = HSSFTestDataSamples.getSampleFile(fileName);
        if (fails) {
            assertThrows(EncryptedDocumentException.class, () -> WorkbookFactory.create(file, pass),
                "Shouldn't be able to open with the wrong password");
        } else {
            try (Workbook wb = WorkbookFactory.create(file, pass)) {
                assertNotNull(wb);
                assertTrue(clazz.isInstance(wb));
                assertCloseDoesNotModifyFile(fileName, wb);
            }
        }
    }

    /**
     * Check that a helpful exception is given on an empty input stream
     */
    @Test
    void testEmptyInputStream() {
        InputStream emptyStream = new ByteArrayInputStream(new byte[0]);
        assertThrows(EmptyFileException.class, () -> WorkbookFactory.create(emptyStream));
    }

    /**
     * Check that a helpful exception is given on an empty file
     */
    @Test
    void testEmptyFile() throws Exception {
        File emptyFile = TempFile.createTempFile("empty", ".poi");
        assertThrows(EmptyFileException.class, () -> WorkbookFactory.create(emptyFile),
            "Shouldn't be able to create for an empty file");
        assertTrue(emptyFile.delete());
    }

    /**
     * Check that a helpful exception is raised on a non-existing file
     */
    @Test
    void testNonExistingFile() {
        File nonExistingFile = new File("notExistingFile");
        assertFalse(nonExistingFile.exists());
        assertThrows(FileNotFoundException.class, () -> WorkbookFactory.create(nonExistingFile, "password", true));
    }

    /**
     * See Bugzilla bug #62831 - #WorkbookFactory.create(File) needs
     * to work for sub-classes of File too, eg JFileChooser
     */
    @Test
    void testFileSubclass() throws Exception {
        File normalXLS = HSSFTestDataSamples.getSampleFile(xls);
        File normalXLSX = HSSFTestDataSamples.getSampleFile(xlsx);
        File altXLS = new TestFile(normalXLS.getAbsolutePath());
        File altXLSX = new TestFile(normalXLSX.getAbsolutePath());
        assertTrue(altXLS.exists());
        assertTrue(altXLSX.exists());

        try (Workbook wb = WorkbookFactory.create(altXLS)) {
            assertNotNull(wb);
            assertTrue(wb instanceof HSSFWorkbook);
            closeOrRevert(wb);
        }

        try (Workbook wb = WorkbookFactory.create(altXLSX)) {
            assertNotNull(wb);
            assertTrue(wb instanceof XSSFWorkbook);
            closeOrRevert(wb);
        }
    }

    private static class TestFile extends File {
        public TestFile(String file) {
            super(file);
        }
    }

    /**
     * Check that the overloaded file methods which take passwords work properly
     */
    @Test
    void testCreateEmpty() throws Exception {
        Workbook wb = WorkbookFactory.create(false);
        assertTrue(wb instanceof HSSFWorkbook);
        closeOrRevert(wb);

        wb = WorkbookFactory.create(true);
        assertTrue(wb instanceof XSSFWorkbook);
        closeOrRevert(wb);
    }

    @Test
    @SuppressForbidden("test code")
    void testOpenManyHSSF() throws Exception {
        final int size = 1000;
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        ArrayList<Future<Boolean>> futures = new ArrayList<>(size);
        for (int i = 0; i < size; i++) {
            futures.add(executorService.submit(this::openHSSFFile));
        }
        for (Future<Boolean> future : futures) {
            assertTrue(future.get());
        }
    }

    @Test
    void testInvalidFormatException() throws IOException {
        String filename = "OPCCompliance_DerivedPartNameFAIL.docx";
        try (InputStream is = POIDataSamples.getOpenXML4JInstance().openResourceAsStream(filename)) {
            assertThrows(IOException.class, () -> WorkbookFactory.create(is));
        }
    }

    private boolean openHSSFFile() {
        try {
            // POIFS -> hssf
            try (InputStream is = HSSFTestDataSamples.openSampleFileStream(xls)) {
                Workbook wb = WorkbookFactory.create(new POIFSFileSystem(is));
                assertNotNull(wb);
                assertTrue(wb instanceof HSSFWorkbook);
                assertCloseDoesNotModifyFile(xls, wb);
            }
            return true;
        } catch (Exception e) {
            return false;
        }
    }
}
