/*
 * 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.maven.plugins.war;

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;

import org.apache.maven.plugin.testing.stubs.ArtifactStub;
import org.apache.maven.plugins.war.stub.MavenProjectArtifactsStub;
import org.codehaus.plexus.util.FileUtils;

/**
 * @author Stephane Nicoll
 */
public abstract class AbstractWarExplodedMojoTest extends AbstractWarMojoTest {

    protected WarExplodedMojo mojo;

    public void setUp() throws Exception {
        super.setUp();
        mojo = (WarExplodedMojo) lookupMojo("exploded", getPomFile());
    }

    /**
     * Returns the pom configuration to use.
     *
     * @return the pom configuration
     */
    protected abstract File getPomFile();

    /**
     * Returns the test directory to use.
     *
     * @return the test directory
     */
    protected abstract File getTestDirectory();

    /**
     * Configures the exploded mojo for the specified test.
     *
     * If the {@code sourceFiles} parameter is {@code null}, sample JSPs are created by default.
     *
     * @param testId the id of the test
     * @param artifactStubs the dependencies (may be null)
     * @param sourceFiles the source files to create (may be null)
     * @return the webapp directory
     * @throws Exception if an error occurs while configuring the mojo
     */
    protected File setUpMojo(final String testId, ArtifactStub[] artifactStubs, String[] sourceFiles) throws Exception {
        final MavenProjectArtifactsStub project = new MavenProjectArtifactsStub();
        final File webAppDirectory = new File(getTestDirectory(), testId);

        // Create the webapp sources
        File webAppSource;
        if (sourceFiles == null) {
            webAppSource = createWebAppSource(testId);
        } else {
            webAppSource = createWebAppSource(testId, false);
            for (String sourceFile : sourceFiles) {
                File sample = new File(webAppSource, sourceFile);
                createFile(sample);
            }
        }

        final File classesDir = createClassesDir(testId, true);
        final File workDirectory = new File(getTestDirectory(), "/war/work-" + testId);
        createDir(workDirectory);

        if (artifactStubs != null) {
            for (ArtifactStub artifactStub : artifactStubs) {
                project.addArtifact(artifactStub);
            }
        }

        configureMojo(mojo, new LinkedList<>(), classesDir, webAppSource, webAppDirectory, project);
        setVariableValueToObject(mojo, "workDirectory", workDirectory);

        return webAppDirectory;
    }

    /**
     * Configures the exploded mojo for the specified test.
     *
     * @param testId the id of the test
     * @param artifactStubs the dependencies (may be null)
     * @return the webapp directory
     * @throws Exception if an error occurs while configuring the mojo
     */
    protected File setUpMojo(final String testId, ArtifactStub[] artifactStubs) throws Exception {
        return setUpMojo(testId, artifactStubs, null);
    }

    /**
     * Cleans up a directory.
     *
     * @param directory the directory to remove
     * @throws IOException if an error occurred while removing the directory
     */
    protected void cleanDirectory(File directory) throws IOException {
        if (directory != null && directory.isDirectory() && directory.exists()) {
            FileUtils.deleteDirectory(directory);
        }
    }

    /**
     * Asserts the default content of the war based on the specified webapp directory.
     *
     * @param webAppDirectory the webapp directory
     * @return a list of File objects that have been asserted
     */
    protected List<File> assertDefaultContent(File webAppDirectory) {
        // Validate content of the webapp
        File expectedWebSourceFile = new File(webAppDirectory, "pansit.jsp");
        File expectedWebSource2File = new File(webAppDirectory, "org/web/app/last-exile.jsp");

        assertTrue("source file not found: " + expectedWebSourceFile.toString(), expectedWebSourceFile.exists());
        assertTrue("source file not found: " + expectedWebSource2File.toString(), expectedWebSource2File.exists());

        final List<File> content = new ArrayList<>();
        content.add(expectedWebSourceFile);
        content.add(expectedWebSource2File);

        return content;
    }

    /**
     * Asserts the web.xml file of the war based on the specified webapp directory.
     *
     * @param webAppDirectory the webapp directory
     * @return a list with the web.xml File object
     */
    protected List<File> assertWebXml(File webAppDirectory) {
        File expectedWEBXMLFile = new File(webAppDirectory, "WEB-INF/web.xml");
        assertTrue("web xml not found: " + expectedWEBXMLFile.toString(), expectedWEBXMLFile.exists());

        final List<File> content = new ArrayList<>();
        content.add(expectedWEBXMLFile);

        return content;
    }

    /**
     * Asserts custom content of the war based on the specified webapp directory.
     *
     * @param webAppDirectory the webapp directory
     * @param filePaths an array of file paths relative to the webapp directory
     * @param customMessage a custom message if an assertion fails
     * @return a list of File objects that have been inspected
     */
    protected List<File> assertCustomContent(File webAppDirectory, String[] filePaths, String customMessage) {
        final List<File> content = new ArrayList<>();
        for (String filePath : filePaths) {
            final File expectedFile = new File(webAppDirectory, filePath);
            if (customMessage != null) {
                assertTrue(customMessage + " - " + expectedFile.toString(), expectedFile.exists());
            } else {
                assertTrue("source file not found: " + expectedFile.toString(), expectedFile.exists());
            }
            content.add(expectedFile);
        }
        return content;
    }

    /**
     * Asserts that the webapp contains only the specified files.
     *
     * @param webAppDirectory the webapp directory
     * @param expectedFiles the expected files
     * @param filter an optional filter to ignore some resources
     */
    protected void assertWebAppContent(File webAppDirectory, List<File> expectedFiles, FileFilter filter) {
        final List<File> webAppContent = new ArrayList<>();
        if (filter != null) {
            buildFilesList(webAppDirectory, filter, webAppContent);
        } else {
            buildFilesList(webAppDirectory, new FileFilterImpl(webAppDirectory, null), webAppContent);
        }

        // Now we have the files, sort them.
        Collections.sort(expectedFiles);
        Collections.sort(webAppContent);
        assertEquals(
                "Invalid webapp content, expected " + expectedFiles.size() + "file(s) " + expectedFiles + " but got "
                        + webAppContent.size() + " file(s) " + webAppContent,
                expectedFiles,
                webAppContent);
    }

    /**
     * Builds the list of files and directories from the specified dir.
     *
     * Note that the filter is not used the usual way. If the filter does not accept the current file, it's not added
     * but yet the subdirectories are added if any.
     *
     * @param dir the base directory
     * @param filter the filter
     * @param content the current content, updated recursively
     */
    private void buildFilesList(final File dir, FileFilter filter, final List<File> content) {
        final File[] files = dir.listFiles();

        for (File file : files) {
            // Add the file if the filter is ok with it
            if (filter.accept(file)) {
                content.add(file);
            }

            // Even if the file is not accepted and is a directory, add it
            if (file.isDirectory()) {
                buildFilesList(file, filter, content);
            }
        }
    }

    class FileFilterImpl implements FileFilter {

        private final List<String> rejectedFilePaths;

        private final int webAppDirIndex;

        public FileFilterImpl(File webAppDirectory, String[] rejectedFilePaths) {
            if (rejectedFilePaths != null) {
                this.rejectedFilePaths = Arrays.asList(rejectedFilePaths);
            } else {
                this.rejectedFilePaths = new ArrayList<>();
            }
            this.webAppDirIndex = webAppDirectory.getAbsolutePath().length() + 1;
        }

        public boolean accept(File file) {
            String effectiveRelativePath = buildRelativePath(file);
            return !(rejectedFilePaths.contains(effectiveRelativePath) || file.isDirectory());
        }

        private String buildRelativePath(File f) {
            return f.getAbsolutePath().substring(webAppDirIndex);
        }
    }
}
