/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * 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 com.android.build.gradle.integration.packaging;

import static com.android.build.gradle.integration.common.truth.TruthHelper.assertThatAar;
import static com.android.build.gradle.integration.common.truth.TruthHelper.assertThatApk;
import static com.android.build.gradle.integration.common.truth.TruthHelper.assertThatZip;

import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.build.gradle.integration.common.fixture.GradleTestProject;
import com.android.build.gradle.integration.common.fixture.Packaging;
import com.android.build.gradle.integration.common.runner.FilterableParameterized;
import com.android.build.gradle.integration.common.truth.AbstractAndroidSubject;
import com.android.build.gradle.integration.common.utils.TestFileUtils;
import com.android.build.gradle.integration.common.utils.ZipHelper;
import com.android.utils.FileUtils;
import com.google.common.base.Charsets;
import com.google.common.io.Files;

import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.util.Collection;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;

/**
 * test for packaging of asset files.
 */
@RunWith(FilterableParameterized.class)
public class NativeSoPackagingFromJarTest {
    private static final String LIB_X86_LIBHELLO_SO = "lib/x86/libhello.so";
    private static final String COM_FOO_FOO_CLASS = "com/foo/Foo.class";

    @Parameterized.Parameters(name = "{0}")
    public static Collection<Object[]> data() {
        return Packaging.getParameters();
    }

    @Parameterized.Parameter
    public Packaging mPackaging;

    @ClassRule
    public static GradleTestProject project = GradleTestProject.builder()
            .fromTestProject("projectWithModules")
            .create();

    private static GradleTestProject appProject;
    private static GradleTestProject libProject;


    @BeforeClass
    public static void setUp() throws Exception {
        appProject = project.getSubproject("app");

        // rewrite settings.gradle to remove un-needed modules
        Files.write("include \"app\"\ninclude \"library\"\n"
, new File(project.getTestDir(), "settings.gradle"), Charsets.UTF_8);

        // setup dependencies.
        TestFileUtils.appendToFile(appProject.getBuildFile(),
"\ndependencies {\n" +
"  compile files(\"libs/foo.jar\")\n" +
"}\n");

        libProject = project.getSubproject("library");

        TestFileUtils.appendToFile(libProject.getBuildFile(),
"\ndependencies {\n" +
"  compile files(\"libs/bar.jar\")\n" +
"}\n");


        File appDir = appProject.getTestDir();
        createJarWithNativeLib(new File(appDir, "libs"), "foo.jar", false);

        File libDir = libProject.getTestDir();
        createJarWithNativeLib(new File(libDir, "libs"), "bar.jar", true);
    }

    private static void createJarWithNativeLib(
            @NonNull File folder, @NonNull String fileName, boolean includeClass) throws Exception {
        FileUtils.mkdirs(folder);
        File jarFile = new File(folder, fileName);

        try (FileOutputStream fos = new FileOutputStream(jarFile);
                JarOutputStream jarOutputStream = new JarOutputStream(
                        new BufferedOutputStream(fos))) {
            jarOutputStream.putNextEntry(new JarEntry(LIB_X86_LIBHELLO_SO));
            jarOutputStream.write("hello".getBytes());
            jarOutputStream.closeEntry();

            if (includeClass) {
                jarOutputStream.putNextEntry(new JarEntry(COM_FOO_FOO_CLASS));
                jarOutputStream.write(getDummyClassByteCode());
                jarOutputStream.closeEntry();
            }
        }
    }

    @Test
    public void testAppPackaging() throws Exception {
        project.executor().withPackaging(mPackaging).run("app:assembleDebug");
        checkApk(appProject, "libhello.so", "hello");
    }

    @Test
    public void testLibraryPackaging() throws Exception {
        project.executor().withPackaging(mPackaging).run("library:assembleDebug");
        checkAar(libProject, "libhello.so", "hello");

        // also check that the bar.jar is also present as a local jar with a the class
        // but not the so file.
        // first extract bar.jar from the apk.
        File aar = libProject.getAar("debug");
        File barJar = ZipHelper.extractFile(aar, "libs/bar.jar");

        assertThatZip(barJar).contains(COM_FOO_FOO_CLASS);
        assertThatZip(barJar).doesNotContain(LIB_X86_LIBHELLO_SO);
    }

    /**
     * check an apk has (or not) the given asset file name.
     *
     * If the content is non-null the file is expected to be there with the same content. If the
     * content is null the file is not expected to be there.
     *
     * @param project the project
     * @param filename the filename
     * @param content the content
     */
    private static void checkApk(
            @NonNull GradleTestProject project, @NonNull String filename, @Nullable String content)
            throws Exception {
        File apk = project.getApk("debug");
        check(assertThatApk(apk), "lib", filename, content);
        PackagingTests.checkZipAlign(apk);
    }

    /**
     * check an aat has (or not) the given asset file name.
     *
     * If the content is non-null the file is expected to be there with the same content. If the
     * content is null the file is not expected to be there.
     *
     * @param project the project
     * @param filename the filename
     * @param content the content
     */
    private static void checkAar(
            @NonNull GradleTestProject project, @NonNull String filename, @Nullable String content)
            throws Exception {
        check(assertThatAar(project.getAar("debug")), "jni", filename, content);
    }

    private static void check(
            @NonNull AbstractAndroidSubject subject,
            @NonNull String folderName,
            @NonNull String filename,
            @Nullable String content)
            throws Exception {
        if (content != null) {
            subject.containsFileWithContent(folderName + "/x86/" + filename, content);
        } else {
            subject.doesNotContain(folderName + "/x86/" + filename);
        }
    }

    /**
     * Creates a class and returns the byte[] with the class
     * @return
     */
    private static byte[] getDummyClassByteCode() {
        ClassWriter cw = new ClassWriter(0);
        FieldVisitor fv;
        MethodVisitor mv;
        AnnotationVisitor av0;

        cw.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC + Opcodes.ACC_SUPER, "com/foo/Foo", null, "java/lang/Object", null);

        mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
        mv.visitCode();
        mv.visitVarInsn(Opcodes.ALOAD, 0);
        mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
        mv.visitInsn(Opcodes.RETURN);
        mv.visitMaxs(1, 1);
        mv.visitEnd();

        mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "aaa", "()V", null, null);
        mv.visitCode();
        mv.visitVarInsn(Opcodes.ALOAD, 0);
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "test/Aaa", "bbb", "()V",
                false);
        mv.visitInsn(Opcodes.RETURN);
        mv.visitMaxs(1, 1);
        mv.visitEnd();

        mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "bbb", "()V", null, null);
        mv.visitCode();
        mv.visitInsn(Opcodes.RETURN);
        mv.visitMaxs(0, 1);
        mv.visitEnd();

        mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "ccc", "()V", null, null);
        mv.visitCode();
        mv.visitInsn(Opcodes.RETURN);
        mv.visitMaxs(0, 1);
        mv.visitEnd();

        cw.visitEnd();

        return cw.toByteArray();
    }
}
