/*
 * Capsule
 * Copyright (c) 2014-2015, Parallel Universe Software Co. and Contributors. All rights reserved.
 * 
 * This program and the accompanying materials are licensed under the terms 
 * of the Eclipse Public License v1.0, available at
 * http://www.eclipse.org/legal/epl-v10.html
 */
package co.paralleluniverse.capsule;

import co.paralleluniverse.common.JarClassLoader;
import co.paralleluniverse.common.ZipFS;
import com.google.common.jimfs.Jimfs;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.nio.charset.Charset;
import static java.nio.charset.StandardCharsets.UTF_8;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarInputStream;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import org.junit.Test;
import static org.junit.Assert.*;

/**
 *
 * @author pron
 */
public class JarTest {
    @Test
    public void testCreateJar() throws Exception {
        ByteArrayOutputStream res = new Jar()
                .setAttribute("Foo", "1234")
                .setAttribute("Bar", "5678")
                .addEntry(Paths.get("foo.txt"), Jar.toInputStream("I am foo!\n", UTF_8))
                .addEntry(Paths.get("dir", "bar.txt"), Jar.toInputStream("I am bar!\n", UTF_8))
                .write(new ByteArrayOutputStream());

        // printEntries(toInput(res));
        assertEquals("I am foo!\n", getEntryAsString(toInput(res), Paths.get("foo.txt"), UTF_8));
        assertEquals("I am bar!\n", getEntryAsString(toInput(res), Paths.get("dir", "bar.txt"), UTF_8));
        Manifest man2 = toInput(res).getManifest();
        assertEquals("1234", man2.getMainAttributes().getValue("Foo"));
        assertEquals("5678", man2.getMainAttributes().getValue("Bar"));
    }

    @Test
    public void testUpdateJar() throws Exception {
        FileSystem fs = Jimfs.newFileSystem();
        Path jarPath = fs.getPath("test.jar");
        try {
            // create
            new Jar()
                    .setAttribute("Foo", "1234")
                    .setAttribute("Bar", "5678")
                    .setListAttribute("List", Arrays.asList("a", "b"))
                    .setMapAttribute("Map", new HashMap<String, String>() {
                        {
                            put("x", "1");
                            put("y", "2");
                        }
                    })
                    .addEntry(Paths.get("foo.txt"), Jar.toInputStream("I am foo!\n", UTF_8))
                    .addEntry(Paths.get("dir", "bar.txt"), Jar.toInputStream("I am bar!\n", UTF_8))
                    .write(jarPath);

            // update
            Jar jar = new Jar(jarPath);
            ByteArrayOutputStream res = jar
                    .setAttribute("Baz", "hi!")
                    .setAttribute("Bar", "8765")
                    .setListAttribute("List", addLast(addFirst(jar.getListAttribute("List"), "0"), "c"))
                    .setMapAttribute("Map", put(put(jar.getMapAttribute("Map", null), "z", "3"), "x", "0"))
                    .addEntry(Paths.get("dir", "baz.txt"), Jar.toInputStream("And I am baz!\n", UTF_8))
                    .write(new ByteArrayOutputStream());

            // test
            // printEntries(toInput(res));
            assertEquals("I am foo!\n", getEntryAsString(toInput(res), Paths.get("foo.txt"), UTF_8));
            assertEquals("I am bar!\n", getEntryAsString(toInput(res), Paths.get("dir", "bar.txt"), UTF_8));
            assertEquals("And I am baz!\n", getEntryAsString(toInput(res), Paths.get("dir", "baz.txt"), UTF_8));
            Manifest man2 = toInput(res).getManifest();
            assertEquals("1234", man2.getMainAttributes().getValue("Foo"));
            assertEquals("8765", man2.getMainAttributes().getValue("Bar"));
            assertEquals("hi!", man2.getMainAttributes().getValue("Baz"));
            assertEquals(Arrays.asList("0", "a", "b", "c"), new Jar(toInput(res)).getListAttribute("List"));
            assertEquals(new HashMap<String, String>() {
                {
                    put("x", "0");
                    put("y", "2");
                    put("z", "3");
                }
            }, new Jar(toInput(res)).getMapAttribute("Map", null));
        } finally {
            Files.delete(jarPath);
        }
    }

    @Test
    public void testUpdateJar2() throws Exception {
        // create
        ByteArrayOutputStream baos = new Jar()
                .setAttribute("Foo", "1234")
                .setAttribute("Bar", "5678")
                .addEntry(Paths.get("foo.txt"), Jar.toInputStream("I am foo!\n", UTF_8))
                .addEntry(Paths.get("dir", "bar.txt"), Jar.toInputStream("I am bar!\n", UTF_8))
                .write(new ByteArrayOutputStream());

        // update
        ByteArrayOutputStream res = new Jar(toInput(baos))
                .setAttribute("Baz", "hi!")
                .setAttribute("Bar", "8765")
                .addEntry(Paths.get("dir", "baz.txt"), Jar.toInputStream("And I am baz!\n", UTF_8))
                .write(new ByteArrayOutputStream());

        // test
        // printEntries(toInput(res));
        assertEquals("I am foo!\n", getEntryAsString(toInput(res), Paths.get("foo.txt"), UTF_8));
        assertEquals("I am bar!\n", getEntryAsString(toInput(res), Paths.get("dir", "bar.txt"), UTF_8));
        assertEquals("And I am baz!\n", getEntryAsString(toInput(res), Paths.get("dir", "baz.txt"), UTF_8));
        Manifest man2 = toInput(res).getManifest();
        assertEquals("1234", man2.getMainAttributes().getValue("Foo"));
        assertEquals("8765", man2.getMainAttributes().getValue("Bar"));
        assertEquals("hi!", man2.getMainAttributes().getValue("Baz"));
    }

    @Test
    public void testUpdateJar3() throws Exception {
        // create
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        new Jar()
                .setOutputStream(baos)
                .setAttribute("Foo", "1234")
                .setAttribute("Bar", "5678")
                .addEntry(Paths.get("foo.txt"), Jar.toInputStream("I am foo!\n", UTF_8))
                .addEntry(Paths.get("dir", "bar.txt"), Jar.toInputStream("I am bar!\n", UTF_8))
                .close();

        // update
        ByteArrayOutputStream res = new Jar(toInput(baos))
                .setAttribute("Baz", "hi!")
                .setAttribute("Bar", "8765")
                .addEntry(Paths.get("dir", "baz.txt"), Jar.toInputStream("And I am baz!\n", UTF_8))
                .write(new ByteArrayOutputStream());

        // test
        // printEntries(toInput(res));
        assertEquals("I am foo!\n", getEntryAsString(toInput(res), Paths.get("foo.txt"), UTF_8));
        assertEquals("I am bar!\n", getEntryAsString(toInput(res), Paths.get("dir", "bar.txt"), UTF_8));
        assertEquals("And I am baz!\n", getEntryAsString(toInput(res), Paths.get("dir", "baz.txt"), UTF_8));
        Manifest man2 = toInput(res).getManifest();
        assertEquals("1234", man2.getMainAttributes().getValue("Foo"));
        assertEquals("8765", man2.getMainAttributes().getValue("Bar"));
        assertEquals("hi!", man2.getMainAttributes().getValue("Baz"));
    }

    @Test
    public void testAddDirectory1() throws Exception {
        FileSystem fs = Jimfs.newFileSystem();
        Path myDir = fs.getPath("dir1", "dir2");

        Files.createDirectories(myDir.resolve("da"));
        Files.createDirectories(myDir.resolve("db"));
        Files.createFile(myDir.resolve("da").resolve("x"));
        Files.createFile(myDir.resolve("da").resolve("y"));
        Files.createFile(myDir.resolve("db").resolve("w"));
        Files.createFile(myDir.resolve("db").resolve("z"));

        ByteArrayOutputStream res = new Jar()
                .setAttribute("Foo", "1234")
                .addEntries((Path) null, myDir, Jar.notMatches("db/w"))
                .write(new ByteArrayOutputStream());

        // printEntries(toInput(res));
        assertTrue(getEntry(toInput(res), Paths.get("da", "x")) != null);
        assertTrue(getEntry(toInput(res), Paths.get("da", "y")) != null);
        assertTrue(getEntry(toInput(res), Paths.get("db", "w")) == null);
        assertTrue(getEntry(toInput(res), Paths.get("db", "z")) != null);
        Manifest man2 = toInput(res).getManifest();
        assertEquals("1234", man2.getMainAttributes().getValue("Foo"));
    }

    @Test
    public void testAddDirectory2() throws Exception {
        FileSystem fs = Jimfs.newFileSystem();
        Path myDir = fs.getPath("dir1", "dir2");

        Files.createDirectories(myDir.resolve("da"));
        Files.createDirectories(myDir.resolve("db"));
        Files.createFile(myDir.resolve("da").resolve("x"));
        Files.createFile(myDir.resolve("da").resolve("y"));
        Files.createFile(myDir.resolve("db").resolve("w"));
        Files.createFile(myDir.resolve("db").resolve("z"));

        ByteArrayOutputStream res = new Jar()
                .setAttribute("Foo", "1234")
                .addEntries(Paths.get("d1", "d2"), myDir, Jar.notMatches("d1/d2/db/w"))
                .write(new ByteArrayOutputStream());

        // printEntries(toInput(res));
        assertTrue(getEntry(toInput(res), Paths.get("d1", "d2", "da", "x")) != null);
        assertTrue(getEntry(toInput(res), Paths.get("d1", "d2", "da", "y")) != null);
        assertTrue(getEntry(toInput(res), Paths.get("d1", "d2", "db", "w")) == null);
        assertTrue(getEntry(toInput(res), Paths.get("d1", "d2", "db", "z")) != null);
        Manifest man2 = toInput(res).getManifest();
        assertEquals("1234", man2.getMainAttributes().getValue("Foo"));
    }

    @Test
    public void testAddZip1() throws Exception {
        FileSystem fs = Jimfs.newFileSystem();
        Path myZip = fs.getPath("zip1");

        new Jar().addEntry(Paths.get("foo.txt"), Jar.toInputStream("I am foo!\n", UTF_8))
                .addEntry(Paths.get("dir", "bar.txt"), Jar.toInputStream("I am bar!\n", UTF_8))
                .write(myZip);

        ByteArrayOutputStream res = new Jar()
                .setAttribute("Foo", "1234")
                .addEntries((String) null, myZip)
                .write(new ByteArrayOutputStream());

        // printEntries(toInput(res));
        assertEquals("I am foo!\n", getEntryAsString(toInput(res), Paths.get("foo.txt"), UTF_8));
        assertEquals("I am bar!\n", getEntryAsString(toInput(res), Paths.get("dir", "bar.txt"), UTF_8));
        Manifest man2 = toInput(res).getManifest();
        assertEquals("1234", man2.getMainAttributes().getValue("Foo"));
    }

    @Test
    public void testAddZip2() throws Exception {
        FileSystem fs = Jimfs.newFileSystem();
        Path myZip = fs.getPath("zip1");

        new Jar().addEntry(Paths.get("foo.txt"), Jar.toInputStream("I am foo!\n", UTF_8))
                .addEntry(Paths.get("dir", "bar.txt"), Jar.toInputStream("I am bar!\n", UTF_8))
                .write(myZip);

        ByteArrayOutputStream res = new Jar()
                .setAttribute("Foo", "1234")
                .addEntries(Paths.get("d1", "d2"), myZip)
                .write(new ByteArrayOutputStream());

        // printEntries(toInput(res));
        assertEquals("I am foo!\n", getEntryAsString(toInput(res), Paths.get("d1", "d2", "foo.txt"), UTF_8));
        assertEquals("I am bar!\n", getEntryAsString(toInput(res), Paths.get("d1", "d2", "dir", "bar.txt"), UTF_8));
        Manifest man2 = toInput(res).getManifest();
        assertEquals("1234", man2.getMainAttributes().getValue("Foo"));
    }

    @Test
    public void testAddZip3() throws Exception {
        ByteArrayOutputStream myZip = new Jar()
                .addEntry(Paths.get("foo.txt"), Jar.toInputStream("I am foo!\n", UTF_8))
                .addEntry(Paths.get("dir", "bar.txt"), Jar.toInputStream("I am bar!\n", UTF_8))
                .write(new ByteArrayOutputStream());

        ByteArrayOutputStream res = new Jar()
                .setAttribute("Foo", "1234")
                .addEntries((Path) null, toInput(myZip))
                .write(new ByteArrayOutputStream());

        // printEntries(toInput(res));
        assertEquals("I am foo!\n", getEntryAsString(toInput(res), Paths.get("foo.txt"), UTF_8));
        assertEquals("I am bar!\n", getEntryAsString(toInput(res), Paths.get("dir", "bar.txt"), UTF_8));
        Manifest man2 = toInput(res).getManifest();
        assertEquals("1234", man2.getMainAttributes().getValue("Foo"));
    }

    @Test
    public void testAddZip4() throws Exception {
        ByteArrayOutputStream myZip = new Jar()
                .addEntry(Paths.get("foo.txt"), Jar.toInputStream("I am foo!\n", UTF_8))
                .addEntry(Paths.get("dir", "bar.txt"), Jar.toInputStream("I am bar!\n", UTF_8))
                .write(new ByteArrayOutputStream());

        ByteArrayOutputStream res = new Jar()
                .setAttribute("Foo", "1234")
                .addEntries(Paths.get("d1", "d2"), toInput(myZip))
                .write(new ByteArrayOutputStream());

        // printEntries(toInput(res));
        assertEquals("I am foo!\n", getEntryAsString(toInput(res), Paths.get("d1", "d2", "foo.txt"), UTF_8));
        assertEquals("I am bar!\n", getEntryAsString(toInput(res), Paths.get("d1", "d2", "dir", "bar.txt"), UTF_8));
        Manifest man2 = toInput(res).getManifest();
        assertEquals("1234", man2.getMainAttributes().getValue("Foo"));
    }

    @Test
    public void testAddPackage() throws Exception {
        final Class clazz = JarClassLoader.class;

        ByteArrayOutputStream res = new Jar()
                .setAttribute("Foo", "1234")
                .addPackageOf(clazz, new Jar.Filter() {

                    @Override
                    public boolean filter(String entryName) {
                        return !"co/paralleluniverse/common/FlexibleClassLoader.class".equals(entryName);
                    }
                })
                .write(new ByteArrayOutputStream());

        // printEntries(toInput(res));
        final Path pp = Paths.get(clazz.getPackage().getName().replace('.', '/'));

        assertTrue(getEntry(toInput(res), pp.resolve(clazz.getSimpleName() + ".class")) != null);
        assertTrue(getEntry(toInput(res), pp.resolve("ProcessUtil.class")) != null);
        assertTrue(getEntry(toInput(res), pp.resolve("FlexibleClassLoader.class")) == null);

        Manifest man2 = toInput(res).getManifest();
        assertEquals("1234", man2.getMainAttributes().getValue("Foo"));
    }

    @Test
    public void testAddPackage2() throws Exception {
        FileSystem fs = Jimfs.newFileSystem();
        Path jarPath = fs.getPath("test.jar");

        final Class clazz = JarClassLoader.class;

        new Jar().addPackageOf(clazz).write(jarPath);

        ByteArrayOutputStream res = new Jar()
                .setAttribute("Foo", "1234")
                .addPackageOf(new JarClassLoader(jarPath, true).loadClass(clazz.getName()), new Jar.Filter() {

                    @Override
                    public boolean filter(String entryName) {
                        return !"co/paralleluniverse/common/FlexibleClassLoader.class".equals(entryName);
                    }
                })
                .write(new ByteArrayOutputStream());

        // printEntries(toInput(res));
        final Path pp = Paths.get(clazz.getPackage().getName().replace('.', '/'));

        assertTrue(getEntry(toInput(res), pp.resolve(clazz.getSimpleName() + ".class")) != null);
        assertTrue(getEntry(toInput(res), pp.resolve("ProcessUtil.class")) != null);
        assertTrue(getEntry(toInput(res), pp.resolve("FlexibleClassLoader.class")) == null);

        Manifest man2 = toInput(res).getManifest();
        assertEquals("1234", man2.getMainAttributes().getValue("Foo"));
    }

    @Test
    public void testReallyExecutableJar() throws Exception {
        FileSystem fs = Jimfs.newFileSystem();
        Path jarPath = fs.getPath("test.jar");

        new Jar()
                .setAttribute("Foo", "1234")
                .setAttribute("Bar", "5678")
                .setReallyExecutable(true)
                .addEntry(Paths.get("foo.txt"), Jar.toInputStream("I am foo!\n", UTF_8))
                .addEntry(Paths.get("dir", "bar.txt"), Jar.toInputStream("I am bar!\n", UTF_8))
                .write(jarPath);

        // printEntries(toInput(res));
        BufferedReader reader = new BufferedReader(new InputStreamReader(Files.newInputStream(jarPath), UTF_8), 10); // Files.newBufferedReader(jarPath, UTF_8);
        String firstLine = reader.readLine();
        assertEquals("#!/bin/sh", firstLine);
    }

    @Test
    public void testStringPrefix() throws Exception {
        FileSystem fs = Jimfs.newFileSystem();
        Path jarPath = fs.getPath("test.jar");

        new Jar()
                .setAttribute("Foo", "1234")
                .setAttribute("Bar", "5678")
                .setJarPrefix("I'm the prefix!")
                .addEntry(Paths.get("foo.txt"), Jar.toInputStream("I am foo!\n", UTF_8))
                .addEntry(Paths.get("dir", "bar.txt"), Jar.toInputStream("I am bar!\n", UTF_8))
                .write(jarPath);

        // printEntries(toInput(res));
        BufferedReader reader = new BufferedReader(new InputStreamReader(Files.newInputStream(jarPath), UTF_8), 10); // Files.newBufferedReader(jarPath, UTF_8);
        String firstLine = reader.readLine();
        assertEquals("I'm the prefix!", firstLine);
    }

    @Test
    public void testFilePrefix() throws Exception {
        FileSystem fs = Jimfs.newFileSystem();
        Path jarPath = fs.getPath("test.jar");

        Path prefixPath = fs.getPath("prefix.dat");
        Files.copy(toInputStream("I'm the prefix!", UTF_8), prefixPath);

        new Jar()
                .setAttribute("Foo", "1234")
                .setAttribute("Bar", "5678")
                .setJarPrefix(prefixPath)
                .addEntry(Paths.get("foo.txt"), Jar.toInputStream("I am foo!\n", UTF_8))
                .addEntry(Paths.get("dir", "bar.txt"), Jar.toInputStream("I am bar!\n", UTF_8))
                .write(jarPath);

        // printEntries(toInput(res));
        BufferedReader reader = new BufferedReader(new InputStreamReader(Files.newInputStream(jarPath), UTF_8), 10); // Files.newBufferedReader(jarPath, UTF_8);
        String firstLine = reader.readLine();
        assertEquals("I'm the prefix!", firstLine);
    }

    //<editor-fold defaultstate="collapsed" desc="Utilities">
    /////////// Utilities ///////////////////////////////////
    private static JarInputStream toInput(JarOutputStream jos) {
        try {
            Field outField = FilterOutputStream.class.getDeclaredField("out");
            outField.setAccessible(true);
            jos.close();
            return toInput((ByteArrayOutputStream) outField.get(jos));
        } catch (Exception e) {
            throw new AssertionError(e);
        }
    }

    private static JarInputStream toInput(ByteArrayOutputStream baos) {
        try {
            baos.close();
            byte[] bytes = baos.toByteArray();

            return new JarInputStream(new ByteArrayInputStream(bytes));
        } catch (Exception e) {
            throw new AssertionError(e);
        }
    }

    private static <T> List<T> addLast(List<T> list, T value) {
        list.add(value);
        return list;
    }

    private static <T> List<T> addFirst(List<T> list, T value) {
        list.add(0, value);
        return list;
    }

    private static <K, V> Map<K, V> put(Map<K, V> map, K key, V value) {
        map.put(key, value);
        return map;
    }

    private static String getEntryAsString(JarInputStream jar, Path entry, Charset charset) throws IOException {
        byte[] buffer = getEntry(jar, entry);
        return buffer != null ? new String(buffer, charset) : null;
    }

    private static String getEntryAsString(JarFile jar, Path entry, Charset charset) throws IOException {
        byte[] buffer = getEntry(jar, entry);
        return buffer != null ? new String(buffer, charset) : null;
    }

    private static String getEntryAsString(Path jar, Path entry, Charset charset) throws IOException {
        byte[] buffer = getEntry(jar, entry);
        return buffer != null ? new String(buffer, charset) : null;
    }

    private static byte[] getEntry(Path jar, Path entry) throws IOException {
        try (FileSystem zipfs = ZipFS.newZipFileSystem(jar)) {
            return Files.readAllBytes(zipfs.getPath(entry.toString()));
        }
    }

    private static InputStream toInputStream(String s, Charset charset) {
        return new ByteArrayInputStream(s.getBytes(charset));
    }

    private static Manifest getManifest(Path jar, boolean useZipfs) throws IOException {
        if (useZipfs) {
            try (FileSystem zipfs = ZipFS.newZipFileSystem(jar)) {
                return new Manifest(Files.newInputStream(zipfs.getPath(JarFile.MANIFEST_NAME)));
            }
        } else
            return new JarInputStream(Files.newInputStream(jar)).getManifest();
    }

    private static byte[] getEntry(JarFile jar, Path entry) throws IOException {
        final Enumeration<JarEntry> entries = jar.entries();
        while (entries.hasMoreElements()) {
            final JarEntry je = entries.nextElement();
            if (je.getName().equals(entry.toString())) {
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                copy(jar.getInputStream(je), baos);
                baos.close();
                return baos.toByteArray();
            }
        }
        return null;
    }

    private static byte[] getEntry(JarInputStream jar, Path entry) throws IOException {
        JarEntry je;
        while ((je = jar.getNextJarEntry()) != null) {
            if (je.getName().equals(entry.toString())) {
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                copy(jar, baos);
                baos.close();
                return baos.toByteArray();
            }
        }
        return null;
    }

    private static void printEntries(JarInputStream jar) throws IOException {
        JarEntry je;
        while ((je = jar.getNextJarEntry()) != null)
            System.out.println(je.getName());
        System.out.println();
    }

    private static void copy(InputStream is, OutputStream os) throws IOException {
        try {
            final byte[] buffer = new byte[1024];
            for (int bytesRead; (bytesRead = is.read(buffer)) != -1;)
                os.write(buffer, 0, bytesRead);
        } finally {
            is.close();
        }
    }
    //</editor-fold>
}
