/*
 * 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.application;

import static com.google.common.truth.Truth.assertThat;

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.ApkSubject;
import com.android.build.gradle.integration.common.truth.DexFileSubject;
import com.android.build.gradle.integration.common.utils.SdkHelper;
import com.android.build.gradle.internal.incremental.ColdswapMode;
import com.android.build.gradle.internal.incremental.InstantRunBuildContext;
import com.android.build.gradle.internal.incremental.InstantRunVerifierStatus;
import com.android.ide.common.process.ProcessException;
import com.android.sdklib.BuildToolInfo;
import com.android.sdklib.IAndroidTarget;
import com.google.common.io.ByteStreams;
import com.google.common.truth.Expect;
import com.google.devtools.build.lib.rules.android.apkmanifest.ExternalBuildApkManifest;
import com.google.devtools.build.lib.rules.android.apkmanifest.ExternalBuildApkManifest.ApkManifest;
import com.google.protobuf.ByteString;
import com.google.protobuf.CodedOutputStream;

import org.apache.commons.io.FileUtils;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.xml.sax.SAXException;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.zip.ZipEntry;

import javax.xml.parsers.ParserConfigurationException;

import groovy.util.FileNameFinder;

/**
 * Integration test for the ExternalBuildPlugin.
 */
@RunWith(FilterableParameterized.class)
public class ExternalBuildPluginTest {

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

    @Parameterized.Parameter
    public Packaging mPackaging;

    private File manifestFile;

    @Rule
    public Expect expect = Expect.createAndEnableStackTrace();

    @Rule
    public GradleTestProject mProject = GradleTestProject.builder()
            .fromTestProject("externalBuildPlugin")
            .create();

    @Before
    public void setUp() throws IOException {
        IAndroidTarget target = SdkHelper.getTarget(23);
        assertThat(target).isNotNull();

        ApkManifest.Builder apkManifest =
                ApkManifest.newBuilder()
                        .setAndroidSdk(ExternalBuildApkManifest.AndroidSdk.newBuilder()
                                .setAndroidJar(
                                    target.getFile(IAndroidTarget.ANDROID_JAR).getAbsolutePath())
                                // TODO: Start putting dx.jar in the proto
                                .setDx(SdkHelper.getDxJar().getAbsolutePath())
                                .setAapt(target.getBuildToolInfo().getPath(
                                        BuildToolInfo.PathId.AAPT)))
                        .setResourceApk(ExternalBuildApkManifest.Artifact.newBuilder()
                                .setExecRootPath("resources.ap_"))
                        .setAndroidManifest(ExternalBuildApkManifest.Artifact.newBuilder()
                                .setExecRootPath("AndroidManifest.xml"));

        List<String> jarFiles = new FileNameFinder().getFileNames(
                mProject.getTestDir().getAbsolutePath(), "**/*.jar");
        Path projectPath = mProject.getTestDir().toPath();
        for (String jarFile : jarFiles) {
            Path jarFilePath = new File(jarFile).toPath();
            Path relativePath = projectPath.relativize(jarFilePath);
            apkManifest.addJars(ExternalBuildApkManifest.Artifact.newBuilder()
                    .setExecRootPath(relativePath.toString())
                    .setHash(ByteString.copyFromUtf8(String.valueOf(jarFile.hashCode()))));
        }

        manifestFile = mProject.file("apk_manifest.tmp");
        try (OutputStream os = new BufferedOutputStream(new FileOutputStream(manifestFile))) {
            CodedOutputStream cos =
                    CodedOutputStream.newInstance(os);
            apkManifest.build().writeTo(cos);
            cos.flush();
        }
    }

    @Test
    public void testBuild()
            throws ProcessException, IOException, ParserConfigurationException, SAXException {
        FileUtils.write(mProject.getBuildFile(), ""
+ "apply from: \"../commonHeader.gradle\"\n"
+ "buildscript {\n "
+ "  apply from: \"../commonBuildScript.gradle\"\n"
+ "}\n"
+ "\n"
+ "apply plugin: 'base'\n"
+ "apply plugin: 'com.android.external.build'\n"
+ "\n"
+ "externalBuild {\n"
+ "  executionRoot = $/" + mProject.getTestDir().getAbsolutePath() +"/$\n"
+ "  buildManifestPath = $/" + manifestFile.getAbsolutePath() + "/$\n"
+ "}\n");

        mProject.executor()
            .withInstantRun(23, ColdswapMode.AUTO)
            .withPackaging(mPackaging)
            .run("clean", "process");

        InstantRunBuildContext instantRunBuildContext = loadFromBuildInfo();
        assertThat(instantRunBuildContext.getPreviousBuilds()).hasSize(1);
        assertThat(instantRunBuildContext.getLastBuild()).isNotNull();
        assertThat(instantRunBuildContext.getLastBuild().getArtifacts()).hasSize(1);
        InstantRunBuildContext.Build fullBuild  = instantRunBuildContext.getLastBuild();
        assertThat(fullBuild.getVerifierStatus().get()).isEqualTo(InstantRunVerifierStatus.INITIAL_BUILD);
        assertThat(fullBuild.getArtifacts()).hasSize(1);
        InstantRunBuildContext.Artifact artifact = fullBuild.getArtifacts().get(0);
        assertThat(artifact.getType()).isEqualTo(InstantRunBuildContext.FileType.MAIN);
        assertThat(artifact.getLocation().exists()).isTrue();

        ApkSubject apkSubject = expect.about(ApkSubject.FACTORY).that(artifact.getLocation());
        apkSubject.contains("instant-run.zip");
        assertThat(apkSubject.hasMainDexFile());

        // now perform a hot swap test.
        File mainClasses = new File(mProject.getTestDir(), "jars/main/classes.jar");
        assertThat(mainClasses.exists()).isTrue();

        File originalFile = new File(mainClasses.getParentFile(), "original_classes.jar");
        assertThat(mainClasses.renameTo(originalFile)).isTrue();

        try (JarFile inputJar = new JarFile(originalFile);
             JarOutputStream jarOutputFile = new JarOutputStream(new BufferedOutputStream(
                new FileOutputStream(new File(mainClasses.getParentFile(), "classes.jar"))))) {
            Enumeration<JarEntry> entries = inputJar.entries();
            while (entries.hasMoreElements()) {
                JarEntry element = entries.nextElement();
                try (InputStream inputStream = new BufferedInputStream(
                        inputJar.getInputStream(element))) {
                    if (!element.isDirectory()) {
                        jarOutputFile.putNextEntry(new ZipEntry(element.getName()));
                        try {
                            if (element.getName().contains("MainActivity.class")) {
                                // perform hot swap change
                                byte[] classBytes = new byte[(int) element.getSize()];
                                ByteStreams.readFully(inputStream, classBytes);
                                classBytes = hotswapChange(classBytes);
                                jarOutputFile.write(classBytes);
                            } else {
                                ByteStreams.copy(inputStream, jarOutputFile);
                            }
                        } finally {
                            jarOutputFile.closeEntry();
                        }
                    }
                }
            }
        }

        mProject.executor()
                .withInstantRun(23, ColdswapMode.AUTO)
                .withPackaging(mPackaging)
                .run("process");

        instantRunBuildContext = loadFromBuildInfo();
        assertThat(instantRunBuildContext.getPreviousBuilds()).hasSize(2);
        InstantRunBuildContext.Build lastBuild = instantRunBuildContext.getLastBuild();
        assertThat(lastBuild).isNotNull();
        assertThat(lastBuild.getVerifierStatus().isPresent());
        assertThat(lastBuild.getVerifierStatus().get()).isEqualTo(InstantRunVerifierStatus.COMPATIBLE);
        assertThat(lastBuild.getArtifacts()).hasSize(1);
        artifact = lastBuild.getArtifacts().get(0);
        assertThat(artifact.getType()).isEqualTo(InstantRunBuildContext.FileType.RELOAD_DEX);
        assertThat(artifact.getLocation()).isNotNull();
        File dexFile = artifact.getLocation();
        assertThat(dexFile.exists()).isTrue();
        DexFileSubject reloadDex = expect.about(DexFileSubject.FACTORY).that(dexFile);
        reloadDex.hasClass("Lcom/android/tools/fd/runtime/AppPatchesLoaderImpl;").that();
        reloadDex.hasClass("Lcom/example/jedo/blazeapp/MainActivity$override;").that();
    }

    private static byte[] hotswapChange(byte[] inputClass) {
        ClassReader cr = new ClassReader(inputClass);
        ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);
        ClassVisitor cv = new ClassVisitor(Opcodes.ASM5, cw) {
            @Override
            public MethodVisitor visitMethod(int access, String name, String desc, String signature,
                    String[] exceptions) {
                MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
                return new MethodVisitor(Opcodes.ASM5, mv) {
                    @Override
                    public void visitCode() {
                        // add a useless logging to the method.
                        mv.visitFieldInsn(Opcodes.GETSTATIC,
                                "java/lang/System",
                                "out",
                                "Ljava/io/PrintStream;");
                        mv.visitLdcInsn("test changed !");
                        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
                                "java/io/PrintStream",
                                "println",
                                "(Ljava/lang/String;)V",
                                false);
                        super.visitCode();
                    }
                };
            }
        };
        cr.accept(cv, ClassReader.EXPAND_FRAMES);
        return cw.toByteArray();
    }

    private InstantRunBuildContext loadFromBuildInfo()
            throws ParserConfigurationException, SAXException, IOException {
        // assert build-info.xml presence.
        File buildInfo = new File(mProject.getTestDir(), "build/reload-dex/debug/build-info.xml");
        assertThat(buildInfo.exists()).isTrue();
        InstantRunBuildContext instantRunBuildContext = new InstantRunBuildContext();
        instantRunBuildContext.setApiLevel(23, ColdswapMode.AUTO.toString(), "arm");
        instantRunBuildContext.loadFromXmlFile(buildInfo);
        return instantRunBuildContext;
    }
}



