/*
 * 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.commons.vfs2.filter;

import static org.junit.Assert.fail;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import org.apache.commons.vfs2.FileObject;
import org.apache.commons.vfs2.FileSelectInfo;
import org.apache.commons.vfs2.FileSystemException;
import org.apache.commons.vfs2.FileSystemManager;
import org.apache.commons.vfs2.VFS;
import org.junit.Assert;

/**
 * Base class for test cases.
 */
public abstract class BaseFilterTest {

    /**
     * Verifies at least all given objects are in the list.
     *
     * @param list    List to use.
     * @param objects Objects to find.
     */
    protected static void assertContains(final List<?> list, final Object... objects) {
        for (final Object obj : objects) {
            Assert.assertTrue("Couldn't find " + obj + " in " + Arrays.asList(objects), list.indexOf(obj) > -1);
        }
    }

    /**
     * Verifies only the given objects are in the list.
     *
     * @param list    List to scan.
     * @param objects Objects to find.
     */
    protected static void assertContainsOnly(final List<?> list, final Object... objects) {
        for (final Object obj : objects) {
            Assert.assertTrue("Couldn't find " + obj + " in " + Arrays.asList(objects), list.indexOf(obj) > -1);
        }
        Assert.assertEquals(objects.length, list.size());
    }

    /**
     * Concatenate a path and a file name taking {@code null} and empty string
     * values into account.
     *
     * @param path      Path - Can be {@code null} or an empty string.
     * @param fileName  Filename - Cannot be {@code null}.
     * @param separator Separator for directories - Can be {@code null} or an
     *                  empty string.
     *
     * @return Path and file name divided by the separator.
     */
    public static String concatPathAndFilename(final String path, final String fileName, final String separator) {

        if (fileName == null) {
            throw new IllegalArgumentException("file name cannot be null");
        }
        if (fileName.trim().isEmpty()) {
            throw new IllegalArgumentException("file name cannot be empty");
        }
        if (separator == null) {
            throw new IllegalArgumentException("separator cannot be null");
        }
        if (separator.trim().isEmpty()) {
            throw new IllegalArgumentException("separator cannot be empty");
        }

        if (path == null) {
            return fileName;
        }
        final String trimmedPath = path.trim();
        if (trimmedPath.isEmpty()) {
            return fileName;
        }
        final String trimmedFilename = fileName.trim();
        if (trimmedPath.endsWith(separator)) {
            return trimmedPath + trimmedFilename;
        }
        return trimmedPath + separator + trimmedFilename;

    }

    /**
     * Creates a file select info object for the given file.
     *
     * @param file File to create an info for.
     * @return File select info.
     */
    protected static FileSelectInfo createFileSelectInfo(final File file) {
        try {
            final FileSystemManager fsManager = VFS.getManager();
            final FileObject fileObject = fsManager.toFileObject(file);
            return new FileSelectInfo() {
                @Override
                public FileObject getBaseFolder() {
                    try {
                        return fileObject.getParent();
                    } catch (final FileSystemException ex) {
                        throw new RuntimeException(ex);
                    }
                }

                @Override
                public int getDepth() {
                    return 0;
                }

                @Override
                public FileObject getFile() {
                    return fileObject;
                }

                @Override
                public String toString() {
                    return Objects.toString(fileObject);
                }
            };
        } catch (final FileSystemException ex) {
            throw new RuntimeException(ex);
        }
    }

    /**
     * Returns the temporary directory.
     *
     * @return java.io.tmpdir
     */
    protected static File getTempDir() {
        return new File(System.getProperty("java.io.tmpdir"));
    }

    /**
     * Returns a sub directory of the temporary directory.
     *
     * @param name Name of the sub directory.
     *
     * @return Sub directory of java.io.tmpdir.
     */
    protected static File getTestDir(final String name) {
        return new File(getTempDir(), name);
    }

    /**
     * Returns a ZIP file object.
     *
     * @param file File to resolve.
     *
     * @return File object.
     *
     * @throws FileSystemException Error resolving the file.
     */
    protected static FileObject getZipFileObject(final File file) throws FileSystemException {
        final FileSystemManager fsManager = VFS.getManager();
        return fsManager.resolveFile("zip:" + file.toURI());
    }

    /**
     * List all files for a directory.
     *
     * @param srcDir Directory to list the files for - Cannot be {@code null}
     *               and must be a valid directory.
     * @param filter Filter or {@code null} for all files.
     *
     * @return List of child entries of the directory.
     */
    private static File[] listFiles(final File srcDir, final FileFilter filter) {

        final File[] files;
        if (filter == null) {
            files = srcDir.listFiles();
        } else {
            files = srcDir.listFiles(filter);
        }
        return files;

    }

    /**
     * Creates a ZIP file and adds all files in a directory and all it's sub
     * directories to the archive. Only entries are added that comply to the file
     * filter.
     *
     * @param srcDir   Directory to add - Cannot be {@code null} and must be a
     *                 valid directory.
     * @param filter   Filter or {@code null} for all files/directories.
     * @param destPath Path to use for the ZIP archive - May be {@code null} or
     *                 an empyt string.
     * @param destFile Target ZIP file - Cannot be {@code null}.
     *
     * @throws IOException Error writing to the output stream.
     */
    public static void zipDir(final File srcDir, final FileFilter filter, final String destPath, final File destFile)
            throws IOException {

        if (srcDir == null) {
            throw new IllegalArgumentException("srcDir cannot be null");
        }
        if (!srcDir.exists()) {
            throw new IllegalArgumentException("srcDir does not exist");
        }
        if (!srcDir.isDirectory()) {
            throw new IllegalArgumentException("srcDir is not a directory");
        }
        if (destFile == null) {
            throw new IllegalArgumentException("destFile cannot be null");
        }

        try (final ZipOutputStream out = new ZipOutputStream(new BufferedOutputStream(Files.newOutputStream(destFile.toPath())))) {
            zipDir(srcDir, filter, destPath, out);
        }

    }

    /**
     * Add a directory to a ZIP output stream.
     *
     * @param srcDir   Directory to add - Cannot be {@code null} and must be a
     *                 valid directory.
     * @param filter   Filter or {@code null} for all files.
     * @param destPath Path to use for the ZIP archive - May be {@code null} or
     *                 an empyt string.
     * @param out      Destination stream - Cannot be {@code null}.
     *
     * @throws IOException Error writing to the output stream.
     */
    private static void zipDir(final File srcDir, final FileFilter filter, final String destPath,
            final ZipOutputStream out) throws IOException {

        final File[] files = listFiles(srcDir, filter);
        for (final File file : files) {
            if (file.isDirectory()) {
                zipDir(file, filter, concatPathAndFilename(destPath, file.getName(), File.separator), out);
            } else {
                zipFile(file, destPath, out);
            }
        }

    }

    /**
     * Creates a ZIP file and adds all files in a directory and all it's sub
     * directories to the archive.
     *
     * @param srcDir   Directory to add - Cannot be {@code null} and must be a
     *                 valid directory.
     * @param destPath Path to use for the ZIP archive - May be {@code null} or
     *                 an empyt string.
     * @param destFile Target ZIP file - Cannot be {@code null}.
     *
     * @throws IOException Error writing to the output stream.
     */
    public static void zipDir(final File srcDir, final String destPath, final File destFile) throws IOException {

        zipDir(srcDir, null, destPath, destFile);

    }

    /**
     * Adds a file to a ZIP output stream.
     *
     * @param srcFile  File to add - Cannot be {@code null}.
     * @param destPath Path to use for the file - May be {@code null} or empty.
     * @param out      Destination stream - Cannot be {@code null}.
     *
     * @throws IOException Error writing to the output stream.
     */
    private static void zipFile(final File srcFile, final String destPath, final ZipOutputStream out)
            throws IOException {

        final byte[] buf = new byte[1024];
        try (final InputStream in = new BufferedInputStream(Files.newInputStream(srcFile.toPath()))) {
            final ZipEntry zipEntry = new ZipEntry(concatPathAndFilename(destPath, srcFile.getName(), File.separator));
            zipEntry.setTime(srcFile.lastModified());
            out.putNextEntry(zipEntry);
            int len;
            while ((len = in.read(buf)) > 0) {
                out.write(buf, 0, len);
            }
            out.closeEntry();
        }
    }

    /**
     * Asserts that the array contains the given file names.
     *
     * @param files     Array to check.
     * @param fileNames names File names to find.
     */
    protected void assertContains(final FileObject[] files, final String... fileNames) {
        for (final String fileName : fileNames) {
            if (!find(files, fileName)) {
                fail("File '" + fileName + "' not found in: " + Arrays.asList(files));
            }
        }
    }

    private boolean find(final FileObject[] files, final String fileName) {
        for (final FileObject file : files) {
            final String name = file.getName().getBaseName();
            if (name.equals(fileName)) {
                return true;
            }
        }
        return false;
    }

}
