/*
 * 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.felix.framework.cache;

import junit.framework.TestCase;
import org.apache.felix.framework.Logger;
import org.osgi.framework.Constants;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;

public class BundleCacheTest extends TestCase
{
    private File tempDir;
    private File cacheDir;
    private File filesDir;
    private BundleCache cache;
    private File archiveFile;
    private File jarFile;

    @Override
    protected void setUp() throws Exception
    {
        super.setUp();
        tempDir = File.createTempFile("felix-temp", ".dir");
        assertTrue("precondition", tempDir.delete());
        assertTrue("precondition", tempDir.mkdirs());

        cacheDir = new File(tempDir, "felix-cache");
        assertTrue("precondition", cacheDir.mkdir());

        filesDir = new File(tempDir, "files");
        String cacheDirPath = cacheDir.getPath();

        Map<String, String> params = new HashMap<String, String>();
        params.put("felix.cache.profiledir", cacheDirPath);
        params.put("felix.cache.dir", cacheDirPath);
        params.put(Constants.FRAMEWORK_STORAGE, cacheDirPath);

        cache = new BundleCache(new Logger(){
            @Override
            protected void doLog(int level, String msg, Throwable throwable) {
            }
        }, params);

        archiveFile = new File(filesDir, "bundle1");

        createTestArchive(archiveFile);

        File innerArchiveFile = new File(archiveFile, "inner");
        createTestArchive(innerArchiveFile);

        new File(innerArchiveFile, "empty").mkdirs();

        createJar(archiveFile, new File(archiveFile, "inner/i+?äö \\§$%nner.jar"));

        Manifest manifest = new Manifest();
        manifest.getMainAttributes().putValue("foo", "bar");
        manifest.getMainAttributes().putValue("Manifest-Version", "v1");

        File mf = new File(archiveFile, "META-INF/MANIFEST.MF");
        mf.getParentFile().mkdirs();
        FileOutputStream output = new FileOutputStream(mf);
        manifest.write(output);
        output.close();
        jarFile = new File(filesDir, "bundle1.jar");
        createJar(archiveFile, jarFile);
    }

    public void testNoZipSlip() throws Exception
    {
        File bundle = new File(filesDir, "slip");
        Manifest manifest = new Manifest();
        manifest.getMainAttributes().putValue("Manifest-Version", "v1");
        manifest.getMainAttributes().putValue(Constants.BUNDLE_MANIFESTVERSION, "2");
        manifest.getMainAttributes().putValue(Constants.BUNDLE_SYMBOLICNAME, "slip");

        JarOutputStream output = new JarOutputStream(new FileOutputStream(bundle),manifest);

        output.putNextEntry(new ZipEntry("../../bar.jar"));

        output.write(BundleCache.read(new FileInputStream(jarFile), jarFile.length()));

        output.closeEntry();

        output.close();

        BundleArchive archive = cache.create(1, 1, "slip", new FileInputStream(bundle), null);

        testNoZipSlip(archive);

        archive = cache.create(1, 1, bundle.toURI().toURL().toString(), null, null);

        testNoZipSlip(archive);

        archive = cache.create(1, 1, "reference:" + bundle.toURI().toURL().toString(), null, null);

        testNoZipSlip(archive);

        File dir = new File(filesDir, "exploded");

        dir.mkdirs();

        File test = new File(dir, "../../bar.jar");
        test.createNewFile();
        test.deleteOnExit();

        archive = cache.create(1, 1, "reference:" + dir.toURI().toURL().toString(), null, null);

        testNoZipSlip(archive);
    }

    public void testNoZipSlip(BundleArchive archive) throws Exception
    {
        Content content = archive.getCurrentRevision().getContent().getEntryAsContent("../../bar.jar");

        assertNull(content);

        String lib = archive.getCurrentRevision().getContent().getEntryAsNativeLibrary("../../bar.jar");

        assertNull(lib);
    }

    public void testDirectoryReference() throws Exception
    {
        BundleArchive archive = testBundle("reference:" + archiveFile.toURI().toURL(), null);
        String path = "../../../../../../../../../../../../../../../../..".replace("/", File.separator);
        assertFalse(archive.getCurrentRevision().getContent().hasEntry(path));
        assertFalse(archive.getCurrentRevision().getContent().isDirectory(path));
        assertNull(archive.getCurrentRevision().getContent().getEntryAsURL(path));
        assertNull(archive.getCurrentRevision().getContent().getEntryAsBytes(path + jarFile.getAbsolutePath()));
        assertNull(archive.getCurrentRevision().getContent().getEntryAsNativeLibrary(path + jarFile.getAbsolutePath()));
        assertEquals(0L, archive.getCurrentRevision().getContent().getContentTime(path + jarFile.getAbsolutePath()));
    }

    public void testJarReference() throws Exception
    {
       testBundle("reference:" + jarFile.toURI().toURL().toString(), null);
    }

    public void testJar() throws Exception
    {
       testBundle(jarFile.toURI().toURL().toString(), null);
    }

    public void testInputStream() throws Exception
    {
        testBundle("bla", jarFile);
    }

    private BundleArchive testBundle(String location, File file) throws Exception
    {
        BundleArchive archive = cache.create(1, 1, location, file != null ? new FileInputStream(file) : null, null);

        assertNotNull(archive);

        assertEquals(Long.valueOf(0), archive.getCurrentRevisionNumber());

        testRevision(archive);

        String nativeLib = archive.getCurrentRevision().getContent().getEntryAsNativeLibrary("file1");

        assertNotNull(nativeLib);
        assertTrue(new File(nativeLib).isFile());

        archive.revise(location, file != null ? new FileInputStream(file) : null);

        assertEquals(Long.valueOf(1), archive.getCurrentRevisionNumber());

        testRevision(archive);

        String nativeLib2 = archive.getCurrentRevision().getContent().getEntryAsNativeLibrary("file1");

        assertNotNull(nativeLib2);
        assertTrue(new File(nativeLib).isFile());
        assertTrue(new File(nativeLib2).isFile());

        archive.purge();

        assertEquals(Long.valueOf(1), archive.getCurrentRevisionNumber());

        testRevision(archive);

        String nativeLib3 = archive.getCurrentRevision().getContent().getEntryAsNativeLibrary("file1");

        assertNotNull(nativeLib3);
        assertNotSame(nativeLib, nativeLib2);
        assertNotSame(nativeLib2, nativeLib3);
        assertFalse(new File(nativeLib).isFile());
        assertTrue(new File(nativeLib2).isFile());
        assertTrue(new File(nativeLib3).isFile());

        return archive;
    }

    private void testRevision(BundleArchive archive) throws Exception
    {
        BundleArchiveRevision revision = archive.getCurrentRevision();
        assertNotNull(revision);
        assertNotNull(revision.getManifestHeader());
        assertEquals("bar", revision.getManifestHeader().get("foo"));
        perRevision(revision.getContent(),  new TreeSet<String>(Arrays.asList("META-INF/", "META-INF/MANIFEST.MF", "file1", "inner/", "inner/empty/", "inner/file1", "inner/i+?äö \\§$%nner.jar")));
        perRevision(revision.getContent().getEntryAsContent("inner"),  new TreeSet<String>(Arrays.asList("file1", "empty/", "i+?äö \\§$%nner.jar")));
        assertNull(revision.getContent().getEntryAsContent("inner/inner"));
        assertNotNull(revision.getContent().getEntryAsContent("inner/empty/"));
        assertNull(revision.getContent().getEntryAsContent("inner/empty").getEntries());
        perRevision(revision.getContent().getEntryAsContent("inner/").getEntryAsContent("i+?äö \\§$%nner.jar"), new TreeSet<String>(Arrays.asList("file1", "inner/", "inner/empty/", "inner/file1")));
    }

    private void perRevision(Content content, Set<String> expectedEntries) throws Exception
    {
        assertNotNull(content);
        Enumeration<String> entries =  content.getEntries();
        Set<String> foundEntries = new TreeSet<String>();
        while (entries.hasMoreElements())
        {
            foundEntries.add(entries.nextElement());
        }
        assertEquals(expectedEntries, foundEntries);

        assertTrue(content.hasEntry("file1"));
        assertFalse(content.hasEntry("foo"));
        assertFalse(content.hasEntry("foo/bar"));

        byte[] entry = content.getEntryAsBytes("file1");
        assertNotNull(entry);
        assertEquals("file1", new String(entry, "UTF-8"));
        assertNull(content.getEntryAsBytes("foo"));
        assertNull(content.getEntryAsBytes("foo/bar"));


        InputStream input = content.getEntryAsStream("file1");
        assertNotNull(input);
        entry = new byte[1014];
        int j = 0;
        for (int i = input.read();i != -1; i = input.read())
        {
            entry[j++] = (byte) i;
        }
        assertEquals("file1", new String(entry,  0 , j, "UTF-8"));
        assertNull(content.getEntryAsStream("foo"));
        assertNull(content.getEntryAsStream("foo/bar"));

        URL url = content.getEntryAsURL("file1");
        assertNotNull(url);
        input = url.openStream();
        assertNotNull(input);
        entry = new byte[1014];
        j = 0;
        for (int i = input.read();i != -1; i = input.read())
        {
            entry[j++] = (byte) i;
        }
        assertEquals("file1", new String(entry,  0 , j, "UTF-8"));
        assertNull(content.getEntryAsURL("foo"));
        assertNull(content.getEntryAsURL("foo/bar"));

        assertNull(content.getEntryAsNativeLibrary("blub"));
        String nativeLib = content.getEntryAsNativeLibrary("file1");
        assertNotNull(nativeLib);
        assertTrue(new File(nativeLib).isFile());
        content.close();
    }

    @Override
    protected void tearDown() throws Exception {
        super.tearDown();
        cache.delete();
        assertTrue(!cacheDir.exists());
        assertTrue(BundleCache.deleteDirectoryTree(tempDir));
    }

    private void createTestArchive(File archiveFile) throws Exception
    {
        createFile(archiveFile, "file1", "file1".getBytes("UTF-8"));
    }

    private void createFile(File parent, String path, byte[] content) throws IOException
    {
        File target = new File(parent, path);

        target.getParentFile().mkdirs();

        assertTrue(target.getParentFile().isDirectory());

        FileOutputStream output = new FileOutputStream(target);
        try
        {
            output.write(content);
        }
        finally
        {
            output.close();
        }
    }

    private void createJar(File source, File target) throws Exception
    {
        File tmp = File.createTempFile("bundle", ".jar", filesDir);
        JarOutputStream output;
        if (new File(source, "META-INF/MANIFEST.MF").isFile())
        {
           output = new JarOutputStream(new FileOutputStream(tmp),new Manifest(new FileInputStream(new File(source, "META-INF/MANIFEST.MF"))));
        }
        else
        {
            output = new JarOutputStream(new FileOutputStream(tmp));
        }

        writeRecursive(source, "", output);

        output.close();
        target.delete();
        tmp.renameTo(target);
    }

    private void writeRecursive(File current, String path, JarOutputStream output) throws Exception
    {
        if (current.isDirectory())
        {
            File[] children = current.listFiles();
            if (children != null)
            {
                for (File file : children)
                {
                    if (!file.getName().equals("MANIFEST.MF"))
                    {
                        String next = path + file.getName();
                        if (file.isDirectory())
                        {
                            next += "/";
                        }
                        output.putNextEntry(new ZipEntry(next));
                        if (file.isDirectory())
                        {
                            output.closeEntry();
                        }
                        writeRecursive(file, next, output);
                    }
                }
            }
        }
        else if (current.isFile())
        {
            output.write(BundleCache.read(new FileInputStream(current), current.length()));
            output.closeEntry();
        }
    }
}
