/**
 * Copyright (c) 2016, 2017, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

/*
 * @test
 * @bug 8157068 8177844
 * @summary Patch java.base and user module with ModuleHashes attribute
 * @library /lib/testlibrary /test/lib
 * @modules jdk.compiler
 * @build jdk.test.lib.compiler.CompilerUtils
 *        jdk.test.lib.util.FileUtils
 *        jdk.test.lib.Platform
 * @run testng PatchSystemModules
 */

import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;

import jdk.test.lib.compiler.CompilerUtils;
import jdk.test.lib.util.FileUtils;
import jdk.testlibrary.JDKToolFinder;
import org.testng.annotations.BeforeTest;
import org.testng.annotations.Test;

import static jdk.testlibrary.ProcessTools.executeCommand;
import static org.testng.Assert.*;

public class PatchSystemModules {
    private static final String JAVA_HOME = System.getProperty("java.home");

    private static final Path TEST_SRC = Paths.get(System.getProperty("test.src"));

    private static final Path JMODS = Paths.get(JAVA_HOME, "jmods");
    private static final Path MODS_DIR = Paths.get("mods");
    private static final Path JARS_DIR = Paths.get("jars");
    private static final Path PATCH_DIR = Paths.get("patches");
    private static final Path IMAGE = Paths.get("image");
    private static final Path NEW_M1_JAR = JARS_DIR.resolve("new_m1.jar");

    private static final String JAVA_BASE = "java.base";
    private final String[] modules = new String[] { "m1", "m2" };

    @BeforeTest
    private void setup() throws Throwable {
        Path src = TEST_SRC.resolve("src");
        Path src1 = TEST_SRC.resolve("src1");

        for (String name : modules) {
            assertTrue(CompilerUtils.compile(src.resolve(name),
                                             MODS_DIR,
                                             "--module-source-path", src.toString()));
        }

        // compile patched source
        String patchDir = src1.resolve(JAVA_BASE).toString();
        assertTrue(CompilerUtils.compile(src1.resolve(JAVA_BASE),
                                         PATCH_DIR.resolve(JAVA_BASE),
                                         "--patch-module", "java.base=" + patchDir));
        assertTrue(CompilerUtils.compile(src1.resolve("m2"),
                                         PATCH_DIR.resolve("m2")));

        createJars();

        // create an image with only m1 and m2
        if (Files.exists(JMODS)) {
            // create an image with m1,m2
            createImage();
        }

        // compile a different version of m1
        Path tmp = Paths.get("tmp");
        assertTrue(CompilerUtils.compile(src1.resolve("m1"), tmp,
                                         "--module-path", MODS_DIR.toString(),
                                         "--module-source-path", src1.toString()));

        // package new_m1.jar
        jar("--create",
            "--file=" + NEW_M1_JAR.toString(),
            "-C", tmp.resolve("m1").toString(), ".");
    }

    /*
     * Test patching system module and user module on module path
     */
    @Test
    public void test() throws Throwable {
        Path patchedJavaBase = PATCH_DIR.resolve(JAVA_BASE);
        Path patchedM2 = PATCH_DIR.resolve("m2");

        Path home = Paths.get(JAVA_HOME);
        runTest(home,
                "--module-path", MODS_DIR.toString(),
                "-m", "m1/p1.Main", "1");
        runTest(home,
                "--patch-module", "java.base=" + patchedJavaBase,
                "--module-path", MODS_DIR.toString(),
                "-m", "m1/p1.Main", "1");

        runTest(home,
                "--patch-module", "m2=" + patchedM2.toString(),
                "--module-path", MODS_DIR.toString(),
                "-m", "m1/p1.Main", "2");
    }

    /*
     * Test --patch-module on a custom image
     */
    @Test
    public void testImage() throws Throwable {
        if (Files.notExists(JMODS))
            return;

        Path patchedJavaBase = PATCH_DIR.resolve(JAVA_BASE);
        Path patchedM2 = PATCH_DIR.resolve("m2");

        runTest(IMAGE,
                "-m", "m1/p1.Main", "1");
        runTest(IMAGE,
                "--patch-module", "java.base=" + patchedJavaBase,
                "-m", "m1/p1.Main", "1");
        runTest(IMAGE,
                "--patch-module", "m2=" + patchedM2.toString(),
                "-m", "m1/p1.Main", "2");
    }

    /*
     * Test a module linked in a system hashed in ModuleHashes attribute
     * cannot be upgraded
     */
    @Test
    public void upgradeHashedModule() throws Throwable {
        if (Files.notExists(JMODS))
            return;

        // Fail to upgrade m1.jar with mismatched hash
        runTestWithExitCode(getJava(IMAGE),
                "--upgrade-module-path", NEW_M1_JAR.toString(),
                "-m", "m1/p1.Main");

        // test when SystemModules fast path is not enabled, i.e. exploded image
        runTestWithExitCode(getJava(IMAGE),
                "--patch-module", "java.base=" + PATCH_DIR.resolve(JAVA_BASE),
                "--upgrade-module-path", NEW_M1_JAR.toString(),
                "-m", "m1/p1.Main");
    }

    /*
     * Test a module linked in a system hashed in ModuleHashes attribute
     * cannot be upgraded combining with --patch-module and --upgrade-module-path
     */
    @Test
    public void patchHashedModule() throws Throwable {
        if (Files.notExists(JMODS))
            return;

        // --patch-module does not disable hash check.
        // Test that a hashed module cannot be upgraded.
        runTestWithExitCode(getJava(IMAGE),
                "--patch-module", "m1=.jar",
                "--upgrade-module-path", NEW_M1_JAR.toString(),
                "-m", "m1/p1.Main");

        // test when SystemModules fast path is not enabled, i.e. exploded image
        runTestWithExitCode(getJava(IMAGE),
                "--patch-module", "java.base=" + PATCH_DIR.resolve(JAVA_BASE),
                "--patch-module", "m1=.jar",
                "--upgrade-module-path", NEW_M1_JAR.toString(),
                "-m", "m1/p1.Main");
    }

    private void runTestWithExitCode(String... options) throws Throwable {
        assertTrue(executeCommand(options)
                        .outputTo(System.out)
                        .errorTo(System.out)
                        .shouldContain("differs to expected hash")
                        .getExitValue() != 0);
    }

    private void runTest(Path image, String... opts) throws Throwable {
        String[] options =
            Stream.concat(Stream.of(getJava(image)),
                          Stream.of(opts))
                  .toArray(String[]::new);

        ProcessBuilder pb = new ProcessBuilder(options);
        int exitValue =  executeCommand(pb)
                            .outputTo(System.out)
                            .errorTo(System.out)
                            .getExitValue();

        assertTrue(exitValue == 0);
    }

    static void createJars() throws Throwable {
        FileUtils.deleteFileTreeUnchecked(JARS_DIR);

        Files.createDirectories(JARS_DIR);
        Path m1 = JARS_DIR.resolve("m1.jar");
        Path m2 = JARS_DIR.resolve("m2.jar");

        // hash m1 in m2's Hashes attribute
        jar("--create",
            "--file=" + m1.toString(),
            "-C", MODS_DIR.resolve("m1").toString(), ".");

        jar("--create",
            "--file=" + m2.toString(),
            "--module-path", JARS_DIR.toString(),
            "--hash-modules", "m1",
            "-C", MODS_DIR.resolve("m2").toString(), ".");
    }

    static void createImage() throws Throwable {
        FileUtils.deleteFileTreeUnchecked(IMAGE);

        String mpath = JARS_DIR.toString() + File.pathSeparator + JMODS.toString();
        execTool("jlink", "--module-path", mpath,
                 "--add-modules", "m1",
                 "--output", IMAGE.toString());
    }

    static void jar(String... args) throws Throwable {
        execTool("jar", args);
    }

    static void execTool(String tool, String... args) throws Throwable {
        String path = JDKToolFinder.getJDKTool(tool);
        List<String> commands = new ArrayList<>();
        commands.add(path);
        Stream.of(args).forEach(commands::add);
        ProcessBuilder pb = new ProcessBuilder(commands);
        int exitValue =  executeCommand(pb)
            .outputTo(System.out)
            .errorTo(System.out)
            .shouldNotContain("no module is recorded in hash")
            .getExitValue();

        assertTrue(exitValue == 0);
    }

    static String getJava(Path image) {
        boolean isWindows = System.getProperty("os.name").startsWith("Windows");
        Path java = image.resolve("bin").resolve(isWindows ? "java.exe" : "java");
        if (Files.notExists(java))
            throw new RuntimeException(java + " not found");
        return java.toAbsolutePath().toString();
    }
}
