/*
 * 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.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Random;
import java.util.logging.Logger;

import junit.framework.TestCase;

import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.Asset;
import org.jboss.shrinkwrap.api.asset.ByteArrayAsset;
import org.jboss.shrinkwrap.api.exporter.ZipExporter;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.jboss.shrinkwrap.impl.base.io.IOUtil;
import org.junit.Test;

/**
 * Stress test to ensure that archives exported as ZIPs which have size larger than available RAM can be processed,
 * proving that we're buffering the encoding process.
 *
 * SHRINKWRAP-116
 *
 * @author <a href="mailto:andrew.rubinger@jboss.org">ALR</a>
 * @version $Revision: $
 */
public class ZipExporterStressTest {
    // -------------------------------------------------------------------------------------||
    // Class Members ----------------------------------------------------------------------||
    // -------------------------------------------------------------------------------------||

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

    /**
     * 2^20
     */
    private static BigDecimal MEGA = new BigDecimal(1024 * 1024);

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

    /**
     * Ensures that we can export archives of large sizes without leading to {@link OutOfMemoryError}
     *
     * SHRINKWRAP-116
     */
    @Test
    public void exportHugeArchive() throws IOException {
        // Log
        log.info("exportHugeArchive");
        log.info("This test may take awhile as it's intended to fill memory");

        // Get an archive instance
        final JavaArchive archive = ShrinkWrap.create(JavaArchive.class, "hugeArchive.jar");

        // Approximate the free memory to start
        final Runtime runtime = Runtime.getRuntime();
        final long startFreeMemBytes = totalFreeMemory(runtime);
        long beforeExportFreeMemBytes = startFreeMemBytes;
        int counter = 0;
        // Loop through and add a MB Asset
        final String pathPrefix = "path";

        // Fill up the archive until we've got only 30% of memory left
        while (beforeExportFreeMemBytes > (startFreeMemBytes * .3)) {
            archive.add(MegaByteAsset.newInstance(), pathPrefix + counter++);
            System.gc(); // Signal to the VM to try to clean up a bit, not the most reliable, but makes this OK on my
                         // machine
            beforeExportFreeMemBytes = totalFreeMemory(runtime);
            log.info("Current Free Memory (MB): " + this.megaBytesFromBytes(beforeExportFreeMemBytes));
        }
        log.info("Wrote: " + archive.toString());
        log.info("Started w/ free memory (MB): " + this.megaBytesFromBytes(startFreeMemBytes));
        log.info("Free memory before export (MB): " + this.megaBytesFromBytes(beforeExportFreeMemBytes));

        // Export; at this point we have less than 50% available memory so
        // we can't carry the whole archive in RAM twice; this
        // should ensure the ZIP impl uses an internal buffer
        final InputStream in = archive.as(ZipExporter.class).exportAsInputStream();
        final CountingOutputStream out = new CountingOutputStream();

        // Copy, counting the final size of the exported ZIP
        IOUtil.copyWithClose(in, out);

        // Ensure we've just exported a ZIP larger than our available memory (proving we've buffered the encoding
        // process)
        TestCase.assertTrue("Test setup failed; we should be writing out more bytes than we have free memory",
            out.bytesWritten > beforeExportFreeMemBytes);
        log.info("Final ZIP export was: " + this.megaBytesFromBytes(out.bytesWritten) + " MB");
        final long afterExportFreeMemBytes = totalFreeMemory(runtime);
        log.info("Free memory after export (MB): " + this.megaBytesFromBytes(afterExportFreeMemBytes));
    }

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

    /**
     * Returns the number of MB the specified number of bytes represents
     *
     * @param bytes
     * @return
     */
    private BigDecimal megaBytesFromBytes(final long bytes) {
        return new BigDecimal(bytes).divide(MEGA).setScale(2, RoundingMode.HALF_UP);
    }

    /**
     * Obtains an estimate of the total amount of free memory available to the JVM
     *
     * @param runtime
     * @return
     */
    private static long totalFreeMemory(final Runtime runtime) {
        return runtime.maxMemory() - runtime.totalMemory() + runtime.freeMemory();
    }

    /**
     * An {@link Asset} which contains a megabyte of dummy data
     *
     * @author <a href="mailto:andrew.rubinger@jboss.org">ALR</a>
     */
    private static class MegaByteAsset extends ByteArrayAsset implements Asset {
        /**
         * Dummy megabyte
         */
        private static int MEGA = 1024 * 1024;

        private static final Random random = new Random();

        private MegaByteAsset(final byte[] content) {
            super(content);
        }

        static MegaByteAsset newInstance() {
            /**
             * Bytes must be random/distributed so that compressing these in ZIP isn't too efficient
             */
            final byte[] content = new byte[MEGA];
            random.nextBytes(content);
            return new MegaByteAsset(content);
        }
    }

    /**
     * {@link OutputStream} which does nothing but count the bytes written
     *
     * @author <a href="mailto:andrew.rubinger@jboss.org">ALR</a>
     * @version $Revision: $
     */
    private static class CountingOutputStream extends OutputStream {
        long bytesWritten = 0;

        @Override
        public void write(int b) throws IOException {
            bytesWritten++;
        }
    }

}
