/*
 * JBoss, Home of Professional Open Source
 * Copyright 2010, Red Hat Middleware LLC, and individual contributors
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * Licensed 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.jboss.shrinkwrap.impl.base.exporter;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.logging.Logger;

import org.jboss.shrinkwrap.api.Archive;
import org.jboss.shrinkwrap.api.ArchivePath;
import org.jboss.shrinkwrap.api.ArchivePaths;
import org.jboss.shrinkwrap.api.GenericArchive;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.Asset;
import org.jboss.shrinkwrap.api.exporter.ArchiveExportException;
import org.jboss.shrinkwrap.api.exporter.FileExistsException;
import org.jboss.shrinkwrap.api.exporter.StreamExporter;
import org.jboss.shrinkwrap.api.importer.StreamImporter;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.jboss.shrinkwrap.impl.base.io.IOUtil;
import org.junit.Assert;
import org.junit.Test;

/**
 * Base support for testing stream-based exporters
 *
 * @author <a href="mailto:baileyje@gmail.com">John Bailey</a>
 * @author <a href="mailto:aslak@conduct.no">Aslak Knutsen</a>
 * @author <a href="mailto:andrew.rubinger@jboss.org">ALR</a>
 * @version $Revision: $
 */
public abstract class StreamExporterTestBase<T extends StreamImporter<T>> extends ExportTestBase {
    // -------------------------------------------------------------------------------------||
    // Class Members ----------------------------------------------------------------------||
    // -------------------------------------------------------------------------------------||

    /**
     * Logger
     */
    private static final Logger log = Logger.getLogger(StreamExporterTestBase.class.getName());

    // -------------------------------------------------------------------------------------||
    // Contracts --------------------------------------------------------------------------||
    // -------------------------------------------------------------------------------------||

    /**
     * Ensures the contents of the specified {@link File} are as expected
     *
     * @param file
     * @throws IOException
     *             If an I/O error occurred
     */
    protected abstract void ensureInExpectedForm(File file) throws IOException;

    /**
     * Obtains the {@link Asset} located at the specified {@link ArchivePath} in the specified {@link File}, or null if
     * nothing is found at the specified path
     *
     * @param file
     * @param path
     * @return
     */
    protected abstract InputStream getContentsFromExportedFile(File file, ArchivePath path) throws IOException;

    /**
     * Obtains the type of {@link StreamImporter} used for this test
     *
     * @return
     */
    protected abstract Class<T> getImporterClass();

    // -------------------------------------------------------------------------------------||
    // Tests ------------------------------------------------------------------------------||
    // -------------------------------------------------------------------------------------||

    /**
     * Test to make sue an archive can be exported and all contents are correctly located.
     *
     * @throws Exception
     */
    @Test
    public void testExport() throws Exception {
        log.info("testExport");

        // Get an archive instance
        Archive<?> archive = createArchiveWithAssets();

        // Export as InputStream
        final InputStream exportStream = this.exportAsInputStream(archive);

        // Validate
        final File tempDirectory = createTempDirectory("testExport");
        final File serialized = new File(tempDirectory, archive.getName());
        final FileOutputStream out = new FileOutputStream(serialized);
        IOUtil.copyWithClose(exportStream, out);
        ensureInExpectedForm(serialized);
    }

    /**
     * Test to ensure that the export process accepts an archive with only directories, no assets.
     *
     * @throws Exception
     */
    @Test
    public void testExportArchiveWithOnlyDirectories() throws IOException {
        // Create an archive with directories
        final ArchivePath path = ArchivePaths.create("/test/game");
        final Archive<?> archive = ShrinkWrap.create(JavaArchive.class, NAME_ARCHIVE).addAsDirectories(path);

        // Fully export by reading all content (export is on-demand)
        final InputStream content = this.exportAsInputStream(archive);
        final ByteArrayOutputStream exportedContents = new ByteArrayOutputStream();
        IOUtil.copyWithClose(content, exportedContents);

        final GenericArchive roundtrip = ShrinkWrap.create(this.getImporterClass(), "roundtrip.zip")
            .importFrom(new ByteArrayInputStream(exportedContents.toByteArray())).as(GenericArchive.class);
        log.info(roundtrip.toString(true));
        Assert.assertTrue(roundtrip.contains(path));
    }

    /**
     * Test to make sure an archive can be exported to file and all contents are correctly located.
     *
     * @throws Exception
     */
    @Test
    public void testExportToFile() throws IOException {
        log.info("testExportToFile");

        // Get a temp directory for the test
        File tempDirectory = createTempDirectory("testExportToFile");

        // Get an archive instance
        Archive<?> archive = createArchiveWithAssets();

        // Export as File
        final File exported = new File(tempDirectory, archive.getName());
        this.exportAsFile(archive, exported, true);

        // Roundtrip assertion
        this.ensureInExpectedForm(exported);
    }

    /**
     * Ensures that we get an {@link IllegalArgumentException} if we attempt to export to a directory
     *
     * @throws IOException
     */
    @Test
    public void testExportToDirectoryFails() throws IOException {
        log.info("testExportToDirectoryFails");

        // Get a temp directory for the test
        File tempDirectory = createTempDirectory("testExportToDirectoryFails");

        // Get an archive instance
        Archive<?> archive = createArchiveWithAssets();

        // Export as File to a directory
        try {
            this.exportAsFile(archive, tempDirectory, true);
        }
        // Expected
        catch (final IllegalArgumentException iae) {
            // Good
            return;
        }

        // Fail
        Assert
            .fail("Should have encountered " + IllegalArgumentException.class.getSimpleName() + " exporting to a dir");

    }

    /**
     * Test to make sure an archive can be exported to a {@link OutputStream} and all contents are correctly located.
     *
     * @throws Exception
     */
    @Test
    public void testExportToOutStream() throws IOException {
        log.info("testExportToOutStream");

        // Get a temp directory for the test
        final File tempDirectory = createTempDirectory("testExportToOutStream");

        // Get an archive instance
        final Archive<?> archive = createArchiveWithAssets();

        // Export as OutStream and flush to a file manually
        final File serializedArchive = new File(tempDirectory, archive.getName());
        final OutputStream out = new FileOutputStream(serializedArchive);
        this.exportToOutputStream(archive, out);

        // Validate
        this.ensureInExpectedForm(serializedArchive);
    }

    /**
     * Test to make sure an archive can be exported to file and all contents are correctly located.
     *
     * @throws Exception
     */
    @Test
    public void testExportToExistingFileFails() throws IOException {
        log.info("testExportToExistingFileFails");

        // Get a temp directory for the test
        final File tempDirectory = createTempDirectory("testExportToExistingFileFails");

        // Get an archive instance
        final Archive<?> archive = createArchiveWithAssets();

        // Export as File
        final File alreadyExists = new File(tempDirectory, archive.getName());
        final OutputStream alreadyExistsOutputStream = new FileOutputStream(alreadyExists);
        alreadyExistsOutputStream.write(new byte[] {});
        alreadyExistsOutputStream.close();
        Assert.assertTrue("The test setup is incorrect; an empty file should exist before writing the archive",
            alreadyExists.exists());

        // Should fail, as we're not overwriting
        boolean gotExpectedException = false;
        try {
            this.exportAsFile(archive, alreadyExists, false);
        } catch (final FileExistsException fee) {
            gotExpectedException = true;
        }
        Assert.assertTrue("Should get " + FileExistsException.class.getSimpleName()
            + " when exporting to an existing file when overwrite is false", gotExpectedException);
    }

    /**
     * Test to make sue an archive can be exported and nested archives are also in exported.
     *
     * @throws Exception
     */
    @Test
    public void testExportNested() throws Exception {
        log.info("testExportNested");

        // Get a temp directory for the test
        final File tempDirectory = createTempDirectory("testExportNested");

        // Get an archive instance
        final Archive<?> archive = createArchiveWithNestedArchives();

        // Export as InputStream
        final InputStream exportStream = this.exportAsInputStream(archive);

        // Write out and retrieve as exported file
        final File exported = new File(tempDirectory, NAME_ARCHIVE + this.getArchiveExtension());
        final OutputStream exportedOut = new FileOutputStream(exported);
        IOUtil.copyWithClose(exportStream, exportedOut);

        // Validate entries were written out
        this.ensureAssetInExportedFile(exported, PATH_ONE, ASSET_ONE);
        this.ensureAssetInExportedFile(exported, PATH_TWO, ASSET_TWO);

        // Validate nested archive entries were written out
        final ArchivePath nestedArchivePath = ArchivePaths.create(NAME_NESTED_ARCHIVE + this.getArchiveExtension());

        // Get inputstream for entry
        final InputStream nestedArchiveStream = this.getContentsFromExportedFile(exported, nestedArchivePath);

        // Write out and retrieve nested contents
        final File nestedFile = new File(tempDirectory, NAME_NESTED_ARCHIVE + this.getArchiveExtension());
        final OutputStream nestedOut = new FileOutputStream(nestedFile);
        IOUtil.copyWithClose(nestedArchiveStream, nestedOut);

        // Ensure contents are in the nested
        this.ensureAssetInExportedFile(nestedFile, PATH_ONE, ASSET_ONE);
        this.ensureAssetInExportedFile(nestedFile, PATH_TWO, ASSET_TWO);

        // Validate nested archive entries were written out
        final ArchivePath nestedArchiveTwoPath = ArchivePaths.create(NESTED_PATH,
            NAME_NESTED_ARCHIVE_2 + this.getArchiveExtension());
        this.getContentsFromExportedFile(exported, nestedArchiveTwoPath);
        final InputStream nestedArchiveTwoStream = this.getContentsFromExportedFile(exported, nestedArchiveTwoPath);

        // Write out and retrieve secondnested contents
        final File nestedTwoFile = new File(tempDirectory, NAME_NESTED_ARCHIVE_2 + this.getArchiveExtension());
        final OutputStream nestedTwoOut = new FileOutputStream(nestedTwoFile);
        IOUtil.copyWithClose(nestedArchiveTwoStream, nestedTwoOut);

        // Ensure contents are in the second nested
        this.ensureAssetInExportedFile(nestedTwoFile, PATH_ONE, ASSET_ONE);
        this.ensureAssetInExportedFile(nestedTwoFile, PATH_TWO, ASSET_TWO);
    }

    @Test(expected = ArchiveExportException.class)
    public void testExportThrowsArchiveExceptionOnAssetWriteFailure() throws IOException {
        log.info("testExportThrowsArchiveExceptionOnAssetWriteFailure");
        Archive<?> archive = createArchiveWithAssets();

        // Check if a the path already contains a node so we remove it from the parent's children
        if (archive.contains(PATH_ONE)) {
            archive.delete(PATH_ONE);
        }

        archive.add(new Asset() {
            @Override
            public InputStream openStream() {
                throw new RuntimeException("Mock Exception from an Asset write");
            }

        }, PATH_ONE);

        // Export
        final InputStream in = this.exportAsInputStream(archive);

        // Read in the full content (to in turn empty the underlying buffer and ensure we complete)
        final OutputStream sink = new OutputStream() {
            @Override
            public void write(int b) throws IOException {
            }
        };
        IOUtil.copyWithClose(in, sink);

    }

    // -------------------------------------------------------------------------------------||
    // Helper Methods ---------------------------------------------------------------------||
    // -------------------------------------------------------------------------------------||

    /**
     * Exports the specified archive as an {@link InputStream}
     */
    private InputStream exportAsInputStream(final Archive<?> archive) {
        assert archive != null : "archive must be specified";
        final Class<? extends StreamExporter> exporter = this.getExporterClass();
        assert exporter != null : "Exporter class must be specified";
        return archive.as(this.getExporterClass()).exportAsInputStream();
    }

    /**
     * Exports the specified archive as a {@link File}, overwriting an existing one is specified
     *
     * @param archive
     * @param file
     * @param overwrite
     */
    private void exportAsFile(final Archive<?> archive, final File file, final boolean overwrite) {
        // Precondition checks
        assert file != null : "file must be specified";
        assert archive != null : "archive must be specified";

        // Export
        final Class<? extends StreamExporter> exporter = this.getExporterClass();
        assert exporter != null : "Exporter class must be specified";
        archive.as(exporter).exportTo(file, overwrite);
    }

    /**
     * Exports the specified archive to an {@link OutputStream}
     *
     * @param archive
     * @return
     */
    private void exportToOutputStream(final Archive<?> archive, final OutputStream out) {
        assert archive != null : "archive must be specified";
        assert out != null : "outstream must be specified";

        // Export
        final Class<? extends StreamExporter> exporter = this.getExporterClass();
        assert exporter != null : "Exporter class must be specified";
        try {
            archive.as(exporter).exportTo(out);
        } finally {
            try {
                out.close();
            } catch (final IOException ioe) {
                log.warning("Could not close " + out + ": " + ioe);
            }
        }
    }

    /**
     * Test implementation of an {@link ExecutorService} which counts all jobs submitted.
     *
     * @author <a href="mailto:andrew.rubinger@jboss.org">ALR</a>
     * @version $Revision: $
     */
    private static class CountingExecutorService implements ExecutorService {

        private final ExecutorService delegate;

        int counter = 0;

        public CountingExecutorService() {
            delegate = Executors.newSingleThreadExecutor();
        }

        @Override
        public boolean awaitTermination(final long timeout, final TimeUnit unit) throws InterruptedException {
            return delegate.awaitTermination(timeout, unit);
        }

        @Override
        public <T> List<Future<T>> invokeAll(final Collection<? extends Callable<T>> tasks) throws InterruptedException {
            return delegate.invokeAll(tasks);
        }

        @Override
        public <T> List<Future<T>> invokeAll(final Collection<? extends Callable<T>> tasks, final long timeout,
            final TimeUnit unit) throws InterruptedException {
            return delegate.invokeAll(tasks, timeout, unit);
        }

        @Override
        public <T> T invokeAny(final Collection<? extends Callable<T>> tasks) throws InterruptedException,
            ExecutionException {
            return delegate.invokeAny(tasks);
        }

        @Override
        public <T> T invokeAny(final Collection<? extends Callable<T>> tasks, final long timeout, final TimeUnit unit)
            throws InterruptedException, ExecutionException, TimeoutException {
            return delegate.invokeAny(tasks, timeout, unit);
        }

        @Override
        public boolean isShutdown() {
            return delegate.isShutdown();
        }

        @Override
        public boolean isTerminated() {
            return delegate.isTerminated();
        }

        @Override
        public void shutdown() {
            delegate.shutdown();
        }

        @Override
        public List<Runnable> shutdownNow() {
            return delegate.shutdownNow();
        }

        @Override
        public <T> Future<T> submit(final Callable<T> task) {
            counter++;
            return delegate.submit(task);
        }

        @Override
        public Future<?> submit(final Runnable task) {
            counter++;
            return delegate.submit(task);
        }

        @Override
        public <T> Future<T> submit(final Runnable task, final T result) {
            counter++;
            return delegate.submit(task, result);
        }

        @Override
        public void execute(final Runnable command) {
            counter++;
            delegate.execute(command);
        }

    }

    /**
     * {@inheritDoc}
     *
     * @see org.jboss.shrinkwrap.impl.base.exporter.StreamExporterTestBase#ensureAssetInExportedFile(java.io.File,
     *      org.jboss.shrinkwrap.api.ArchivePath, org.jboss.shrinkwrap.api.asset.Asset)
     */
    protected final void ensureAssetInExportedFile(final File file, final ArchivePath path, final Asset asset)
        throws IOException {
        // Precondition checks
        assert file != null : "file must be specified";
        assert path != null : "path must be specified";
        assert asset != null : "asset must be specified";

        // Get as Exported File
        final InputStream actualStream = this.getContentsFromExportedFile(file, path);
        assert actualStream != null : "No contents found at path " + path + " in " + file.getAbsolutePath();
        byte[] actualContents = IOUtil.asByteArray(actualStream);
        byte[] expectedContents = IOUtil.asByteArray(asset.openStream());
        Assert.assertArrayEquals(expectedContents, actualContents);
    }
}
