/*
 *  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
 *
 *      https://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.tools.ant.taskdefs.modules;

import java.io.ByteArrayOutputStream;
import java.io.PrintStream;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.FileReader;

import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;

import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;

import java.time.Instant;
import java.time.temporal.ChronoUnit;

import java.util.Collection;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.spi.ToolProvider;

import org.junit.Assert;
import org.junit.Assume;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.BuildFileRule;

/**
 * Tests the {@link Link} task.
 */
public class LinkTest {
    /*
     * TODO:
     * Test --order-resources (how?)
     * Test --exclude-files (what does this actually do?)
     * Test --endian (how?)
     * Test --vm (how?)
     */

    @Rule
    public final BuildFileRule buildRule = new BuildFileRule();

    @Rule
    public final ExpectedException expected = ExpectedException.none();

    @Before
    public void setUp() {
        buildRule.configureProject("src/etc/testcases/taskdefs/link.xml");
        buildRule.executeTarget("setUp");
    }

    private static boolean isWindows() {
        return System.getProperty("os.name").contains("Windows");
    }

    private static boolean isEarlierThan(final Instant time,
                                         final Path path) {
        try {
            return Files.getLastModifiedTime(path).toInstant().isBefore(time);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    private static class ImageStructure {
        final File root;
        final File bin;
        final File java;

        ImageStructure(final File root) {
            this.root = root;

            bin = new File(root, "bin");
            java = new File(bin, isWindows() ? "java.exe" : "java");
        }
    }

    private ImageStructure verifyImageBuiltNormally() {
        ImageStructure image = new ImageStructure(
            new File(buildRule.getProject().getProperty("image")));

        Assert.assertTrue("Checking that image was successfully created.",
            image.root.exists());

        Assert.assertTrue("Checking that image has java executable.",
            image.java.exists());

        return image;
    }

    @Test
    public void testModulepath() {
        buildRule.executeTarget("modulepath");
        verifyImageBuiltNormally();
    }

    @Test
    public void testImageNotRecreatedFromStaleJmods()
    throws IOException {
        buildRule.executeTarget("imageNewerThanJmods");
        ImageStructure image = verifyImageBuiltNormally();

        Instant future = Instant.now().plus(30, ChronoUnit.MINUTES);
        try (Stream<Path> imageFiles = Files.walk(image.root.toPath())) {

            Assert.assertTrue("Checking that newer image was not written "
                + "when source files are older.",
                imageFiles.noneMatch(i -> isEarlierThan(future, i)));
        }
    }

    @Test
    public void testNoModulePath() {
        expected.expect(BuildException.class);
        buildRule.executeTarget("nomodulepath");
    }

    @Test
    public void testNoModules() {
        expected.expect(BuildException.class);
        buildRule.executeTarget("nomodules");
    }

    @Test
    public void testModulePathRef() {
        buildRule.executeTarget("modulepathref");
        verifyImageBuiltNormally();
    }

    @Test
    public void testNestedModulePath() {
        buildRule.executeTarget("modulepath-nested");
        verifyImageBuiltNormally();
    }

    @Test
    public void testModulePathInAttributeAndNested() {
        buildRule.executeTarget("modulepath-both");
        verifyImageBuiltNormally();
    }

    @Test
    public void testNestedModules()
    throws IOException,
           InterruptedException {

        buildRule.executeTarget("modules-nested");

        ImageStructure image = verifyImageBuiltNormally();

        ProcessBuilder builder = new ProcessBuilder(
            image.java.toString(),
            buildRule.getProject().getProperty("hello.main-class"));
        builder.inheritIO();
        int exitCode = builder.start().waitFor();
        Assert.assertEquals(
            "Checking that execution of first module succeeded.", 0, exitCode);

        builder.command(
            image.java.toString(),
            buildRule.getProject().getProperty("smile.main-class"));
        exitCode = builder.start().waitFor();
        Assert.assertEquals(
            "Checking that execution of second module succeeded.", 0, exitCode);
    }

    @Test
    public void testNestedModuleMissingName() {
        expected.expect(BuildException.class);
        buildRule.executeTarget("modules-nested-missing-name");
    }

    @Test
    public void testModulesInAttributeAndNested() {
        buildRule.executeTarget("modules-both");
        verifyImageBuiltNormally();
    }

    @Test
    public void testObservableModules() {
        expected.expect(BuildException.class);
        buildRule.executeTarget("observable");
    }

    @Test
    public void testNestedObservableModules() {
        expected.expect(BuildException.class);
        buildRule.executeTarget("observable-nested");
    }

    @Test
    public void testNestedObservableModuleMissingName() {
        expected.expect(BuildException.class);
        buildRule.executeTarget("observable-nested-missing-name");
    }

    @Test
    public void testObservableModulesInAttributeAndNested() {
        buildRule.executeTarget("observable-both");
        verifyImageBuiltNormally();
    }

    private void verifyLaunchersExist() {
        ImageStructure image = verifyImageBuiltNormally();

        File launcher1 =
            new File(image.bin, isWindows() ? "Hello.bat" : "Hello");
        Assert.assertTrue("Checking that image has 'Hello' launcher.",
            launcher1.exists());

        File launcher2 =
            new File(image.bin, isWindows() ? "Smile.bat" : "Smile");
        Assert.assertTrue("Checking that image has 'Smile' launcher.",
            launcher2.exists());
    }

    @Test
    public void testLaunchers() {
        buildRule.executeTarget("launchers");
        verifyLaunchersExist();
    }

    @Test
    public void testNestedLaunchers() {
        buildRule.executeTarget("launchers-nested");
        verifyLaunchersExist();
    }

    @Test
    public void testNestedLauncherMissingName() {
        expected.expect(BuildException.class);
        buildRule.executeTarget("launchers-nested-missing-name");
    }

    @Test
    public void testNestedLauncherMissingModule() {
        expected.expect(BuildException.class);
        buildRule.executeTarget("launchers-nested-missing-module");
    }

    @Test
    public void testLaunchersInAttributeAndNested() {
        buildRule.executeTarget("launchers-both");
        verifyLaunchersExist();
    }

    private void verifyLocales()
    throws IOException,
           InterruptedException {

        ImageStructure image = verifyImageBuiltNormally();

        String mainClass =
            buildRule.getProject().getProperty("localefinder.main-class");
        Assert.assertNotNull("Checking that main-class property exists",
            mainClass);

        ProcessBuilder builder =
            new ProcessBuilder(image.java.toString(), mainClass, "zh", "in");
        builder.inheritIO();
        int exitCode = builder.start().waitFor();

        Assert.assertEquals("Verifying that image has access to locales "
            + "specified during linking.", 0, exitCode);

        builder.command(image.java.toString(), mainClass, "ja");
        exitCode = builder.start().waitFor();

        Assert.assertNotEquals(
            "Verifying that image does not have access to locales "
            + "not specified during linking.", 0, exitCode);
    }

    @Test
    public void testLocales()
    throws IOException,
           InterruptedException {

        buildRule.executeTarget("locales");
        verifyLocales();
    }

    @Test
    public void testNestedLocales()
    throws IOException,
           InterruptedException {

        buildRule.executeTarget("locales-nested");
        verifyLocales();
    }

    @Test
    public void testNestedLocaleMissingName() {
        expected.expect(BuildException.class);
        buildRule.executeTarget("locales-nested-missing-name");
    }

    @Test
    public void testLocalesInAttributeAndNested()
    throws IOException,
           InterruptedException {

        buildRule.executeTarget("locales-both");
        verifyLocales();
    }

    @Test
    public void testExcludeResources()
    throws IOException {
        buildRule.executeTarget("excluderesources");
        ImageStructure image = verifyImageBuiltNormally();

        String mainClass =
            buildRule.getProject().getProperty("hello.main-class");
        Assert.assertNotNull("Checking that main-class property exists",
            mainClass);

        ProcessBuilder builder =
            new ProcessBuilder(image.java.toString(), mainClass,
                "resource1.txt", "resource2.txt");
        builder.redirectInput(ProcessBuilder.Redirect.INHERIT);
        builder.redirectErrorStream(true);

        Collection<String> outputLines;
        Process process = builder.start();
        try (BufferedReader reader = new BufferedReader(
            new InputStreamReader(process.getInputStream()))) {

            outputLines = reader.lines().collect(Collectors.toList());
        }

        Assert.assertTrue(
            "Checking that excluded resource is actually excluded.",
            outputLines.stream().anyMatch(
                l -> l.endsWith("resource1.txt absent")));

        Assert.assertTrue(
            "Checking that resource not excluded is present.",
            outputLines.stream().anyMatch(
                l -> l.endsWith("resource2.txt present")));
    }

    @Test
    public void testNestedExcludeResources()
    throws IOException {
        buildRule.executeTarget("excluderesources-nested");
        ImageStructure image = verifyImageBuiltNormally();

        String mainClass =
            buildRule.getProject().getProperty("hello.main-class");
        Assert.assertNotNull("Checking that main-class property exists",
            mainClass);

        ProcessBuilder builder =
            new ProcessBuilder(image.java.toString(), mainClass,
                "resource1.txt", "resource2.txt");
        builder.redirectInput(ProcessBuilder.Redirect.INHERIT);
        builder.redirectErrorStream(true);

        Collection<String> outputLines;
        Process process = builder.start();
        try (BufferedReader reader = new BufferedReader(
            new InputStreamReader(process.getInputStream()))) {

            outputLines = reader.lines().collect(Collectors.toList());
        }

        Assert.assertTrue(
            "Checking that excluded resource is actually excluded.",
            outputLines.stream().anyMatch(
                l -> l.endsWith("resource1.txt absent")));

        Assert.assertTrue(
            "Checking that resource not excluded is present.",
            outputLines.stream().anyMatch(
                l -> l.endsWith("resource2.txt present")));
    }

    @Test
    public void testNestedExcludeResourcesFile()
    throws IOException {
        buildRule.executeTarget("excluderesources-nested-file");
        ImageStructure image = verifyImageBuiltNormally();

        String mainClass =
            buildRule.getProject().getProperty("hello.main-class");
        Assert.assertNotNull("Checking that main-class property exists",
            mainClass);

        ProcessBuilder builder =
            new ProcessBuilder(image.java.toString(), mainClass,
                "resource1.txt", "resource2.txt");
        builder.redirectInput(ProcessBuilder.Redirect.INHERIT);
        builder.redirectErrorStream(true);

        Collection<String> outputLines;
        Process process = builder.start();
        try (BufferedReader reader = new BufferedReader(
            new InputStreamReader(process.getInputStream()))) {

            outputLines = reader.lines().collect(Collectors.toList());
        }

        Assert.assertTrue(
            "Checking that excluded resource is actually excluded.",
            outputLines.stream().anyMatch(
                l -> l.endsWith("resource1.txt absent")));

        Assert.assertTrue(
            "Checking that resource not excluded is present.",
            outputLines.stream().anyMatch(
                l -> l.endsWith("resource2.txt present")));
    }

    @Test
    public void testNestedExcludeResourcesNoAttributes() {
        expected.expect(BuildException.class);
        buildRule.executeTarget("excluderesources-nested-no-attr");
    }

    @Test
    public void testNestedExcludeResourcesFileAndPattern() {
        expected.expect(BuildException.class);
        buildRule.executeTarget("excluderesources-nested-both");
    }

    @Test
    public void testExcludeResourcesAttributeAndNested()
    throws IOException {
        buildRule.executeTarget("excluderesources-both");
        ImageStructure image = verifyImageBuiltNormally();

        String mainClass =
            buildRule.getProject().getProperty("hello.main-class");
        Assert.assertNotNull("Checking that main-class property exists",
            mainClass);

        ProcessBuilder builder =
            new ProcessBuilder(image.java.toString(), mainClass,
                "resource1.txt", "resource2.txt");
        builder.redirectInput(ProcessBuilder.Redirect.INHERIT);
        builder.redirectErrorStream(true);

        Collection<String> outputLines;
        Process process = builder.start();
        try (BufferedReader reader = new BufferedReader(
            new InputStreamReader(process.getInputStream()))) {

            outputLines = reader.lines().collect(Collectors.toList());
        }

        Assert.assertTrue(
            "Checking that first excluded resource is actually excluded.",
            outputLines.stream().anyMatch(
                l -> l.endsWith("resource1.txt absent")));

        Assert.assertTrue(
            "Checking that second excluded resource is actually excluded.",
            outputLines.stream().anyMatch(
                l -> l.endsWith("resource2.txt absent")));
    }

    @Test
    public void testExcludeFiles()
    throws IOException {
        buildRule.executeTarget("excludefiles");
        verifyImageBuiltNormally();
        // TODO: Test created image (what does --exclude-files actually do?)
    }

    @Test
    public void testNestedExcludeFiles()
    throws IOException {
        buildRule.executeTarget("excludefiles-nested");
        verifyImageBuiltNormally();
        // TODO: Test created image (what does --exclude-files actually do?)
    }

    @Test
    public void testNestedExcludeFilesFile()
    throws IOException {
        buildRule.executeTarget("excludefiles-nested-file");
        ImageStructure image = verifyImageBuiltNormally();
        // TODO: Test created image (what does --exclude-files actually do?)
    }

    @Test
    public void testNestedExcludeFilesNoAttributes() {
        expected.expect(BuildException.class);
        buildRule.executeTarget("excludefiles-nested-no-attr");
    }

    @Test
    public void testNestedExcludeFilesFileAndPattern() {
        expected.expect(BuildException.class);
        buildRule.executeTarget("excludefiles-nested-both");
    }

    @Test
    public void testExcludeFilesAttributeAndNested()
    throws IOException {
        buildRule.executeTarget("excludefiles-both");
        verifyImageBuiltNormally();
        // TODO: Test created image (what does --exclude-files actually do?)
    }

    @Test
    public void testOrdering()
    throws IOException {
        buildRule.executeTarget("ordering");
        verifyImageBuiltNormally();
        // TODO: Test resource order in created image (how?)
    }

    @Test
    public void testNestedOrdering()
    throws IOException {
        buildRule.executeTarget("ordering-nested");
        verifyImageBuiltNormally();
        // TODO: Test resource order in created image (how?)
    }

    @Test
    public void testNestedOrderingListFile()
    throws IOException {
        buildRule.executeTarget("ordering-nested-file");
        ImageStructure image = verifyImageBuiltNormally();
        // TODO: Test resource order in created image (how?)
    }

    @Test
    public void testNestedOrderingNoAttributes() {
        expected.expect(BuildException.class);
        buildRule.executeTarget("ordering-nested-no-attr");
    }

    @Test
    public void testNestedOrderingFileAndPattern() {
        expected.expect(BuildException.class);
        buildRule.executeTarget("ordering-nested-both");
    }

    @Test
    public void testOrderingAttributeAndNested()
    throws IOException {
        buildRule.executeTarget("ordering-both");
        verifyImageBuiltNormally();
        // TODO: Test resource order in created image (how?)
    }

    @Test
    public void testIncludeHeaders() {
        buildRule.executeTarget("includeheaders");
        ImageStructure image = verifyImageBuiltNormally();

        File[] headers = new File(image.root, "include").listFiles();
        Assert.assertTrue("Checking that include files were omitted.",
            headers == null || headers.length == 0);
    }

    @Test
    public void testIncludeManPages() {
        buildRule.executeTarget("includemanpages");
        ImageStructure image = verifyImageBuiltNormally();

        File[] manPages = new File(image.root, "man").listFiles();
        Assert.assertTrue("Checking that man pages were omitted.",
            manPages == null || manPages.length == 0);
    }

    @Test
    public void testIncludeNativeCommands() {
        buildRule.executeTarget("includenativecommands");
        ImageStructure image = new ImageStructure(
            new File(buildRule.getProject().getProperty("image")));

        Assert.assertTrue("Checking that image was successfully created.",
            image.root.exists());

        Assert.assertFalse(
            "Checking that image was stripped of java executable.",
            image.java.exists());
    }

    private long totalSizeOf(final Path path)
    throws IOException {
        if (Files.isDirectory(path)) {
            long size = 0;
            try (DirectoryStream<Path> children = Files.newDirectoryStream(path)) {
                for (Path child : children) {
                    size += totalSizeOf(child);
                }
            }
            return size;
        }

        if (Files.isRegularFile(path)) {
            return Files.size(path);
        }

        return 0;
    }

    @Test
    public void testCompression()
    throws IOException {
        buildRule.executeTarget("compression");
        ImageStructure image = verifyImageBuiltNormally();

        File compressedImageRoot =
            new File(buildRule.getProject().getProperty("compressed-image"));

        long size = totalSizeOf(image.root.toPath());
        long compressedSize = totalSizeOf(compressedImageRoot.toPath());

        Assert.assertTrue("Checking that compression resulted in smaller image.",
            compressedSize < size);
    }

    @Test
    public void testNestedCompression()
    throws IOException {
        buildRule.executeTarget("compression-nested");
        ImageStructure image = verifyImageBuiltNormally();

        File compressedImageRoot =
            new File(buildRule.getProject().getProperty("compressed-image"));

        long size = totalSizeOf(image.root.toPath());
        long compressedSize = totalSizeOf(compressedImageRoot.toPath());

        Assert.assertTrue("Checking that compression resulted in smaller image.",
            compressedSize < size);
    }

    @Test
    public void testNestedCompressionNoAttributes() {
        expected.expect(BuildException.class);
        buildRule.executeTarget("compression-nested-no-attr");
    }

    @Test
    public void testNestedCompressionAttributeAndNested() {
        expected.expect(BuildException.class);
        buildRule.executeTarget("compression-both");
    }

    @Test
    public void testEndian() {
        buildRule.executeTarget("endian");
        verifyImageBuiltNormally();
        // TODO: How can we test the created image?  Which files does --endian
        // affect?
    }

    @Test
    public void testVMType() {
        buildRule.executeTarget("vm");
        verifyImageBuiltNormally();
        // TODO: How can we test the created image?  Which files does --vm
        // affect?
    }

    @Test
    public void testReleaseInfoFile()
    throws IOException {
        buildRule.executeTarget("releaseinfo-file");
        ImageStructure image = verifyImageBuiltNormally();

        File release = new File(image.root, "release");
        try (BufferedReader reader =
            Files.newBufferedReader(release.toPath())) {

            Assert.assertTrue("Checking for 'test=true' in image release info.",
                reader.lines().anyMatch(l -> l.equals("test=true")));
        }
    }

    @Test
    public void testReleaseInfoDelete()
    throws IOException {
        buildRule.executeTarget("releaseinfo-delete");
        ImageStructure image = verifyImageBuiltNormally();

        File release = new File(image.root, "release");
        try (BufferedReader reader =
            Files.newBufferedReader(release.toPath())) {

            Assert.assertFalse("Checking that 'test' was deleted "
                + "from image release info.",
                reader.lines().anyMatch(l -> l.startsWith("test=")));
        }
    }

    @Test
    public void testReleaseInfoNestedDelete()
    throws IOException {
        buildRule.executeTarget("releaseinfo-nested-delete");
        ImageStructure image = verifyImageBuiltNormally();

        File release = new File(image.root, "release");
        try (BufferedReader reader =
            Files.newBufferedReader(release.toPath())) {

            Assert.assertFalse("Checking that 'test' was deleted "
                + "from image release info.",
                reader.lines().anyMatch(l -> l.startsWith("test=")));
        }
    }

    @Test
    public void testReleaseInfoNestedDeleteNoKey() {
        expected.expect(BuildException.class);
        buildRule.executeTarget("releaseinfo-nested-delete-no-key");
    }

    @Test
    public void testReleaseInfoDeleteAttributeAndNested()
    throws IOException {
        buildRule.executeTarget("releaseinfo-nested-delete-both");
        ImageStructure image = verifyImageBuiltNormally();

        File release = new File(image.root, "release");
        try (BufferedReader reader =
            Files.newBufferedReader(release.toPath())) {

            Assert.assertTrue(
                "Checking that 'test' and 'foo' were deleted "
                + "from image release info.",
                reader.lines().noneMatch(l ->
                    l.startsWith("test=") || l.startsWith("foo=")));
        }
    }

    @Test
    public void testReleaseInfoAddFile()
    throws IOException {
        buildRule.executeTarget("releaseinfo-add-file");
        ImageStructure image = verifyImageBuiltNormally();

        File release = new File(image.root, "release");
        try (BufferedReader reader = new BufferedReader(
            new FileReader(release))) {

            Assert.assertTrue("Checking that 'test=s\u00ed' was added "
                + "to image release info.",
                reader.lines().anyMatch(l -> l.equals("test=s\u00ed")));
        }
    }

    @Test
    public void testReleaseInfoAddFileWithCharset()
    throws IOException {
        buildRule.executeTarget("releaseinfo-add-file-charset");
        ImageStructure image = verifyImageBuiltNormally();

        File release = new File(image.root, "release");
        // Using FileReader here since 'release' file is in platform's charset.
        try (BufferedReader reader = new BufferedReader(
            new FileReader(release))) {

            Assert.assertTrue("Checking that 'test=s\u00ed' was added "
                + "to image release info.",
                reader.lines().anyMatch(l -> l.equals("test=s\u00ed")));
        }
    }

    @Test
    public void testReleaseInfoAddKeyAndValue()
    throws IOException {
        buildRule.executeTarget("releaseinfo-add-key");
        ImageStructure image = verifyImageBuiltNormally();

        File release = new File(image.root, "release");
        try (BufferedReader reader =
            Files.newBufferedReader(release.toPath())) {

            Assert.assertTrue("Checking that 'test=true' was added "
                + "to image release info.",
                reader.lines().anyMatch(l -> l.equals("test=true")));
        }
    }

    @Test
    public void testReleaseInfoAddNoValue() {
        expected.expect(BuildException.class);
        buildRule.executeTarget("releaseinfo-add-no-value");
    }

    @Test
    public void testReleaseInfoAddNoKey() {
        expected.expect(BuildException.class);
        buildRule.executeTarget("releaseinfo-add-no-key");
    }

    @Test
    public void testReleaseInfoAddFileAndKey() {
        expected.expect(BuildException.class);
        buildRule.executeTarget("releaseinfo-add-file-and-key");
    }

    @Test
    public void testReleaseInfoAddFileAndValue() {
        expected.expect(BuildException.class);
        buildRule.executeTarget("releaseinfo-add-file-and-value");
    }

    @Test
    public void testDebugStripping()
    throws IOException,
           InterruptedException {

        buildRule.executeTarget("debug");
        ImageStructure image = verifyImageBuiltNormally();

        ProcessBuilder builder = new ProcessBuilder(
            image.java.toString(),
            buildRule.getProject().getProperty("thrower.main-class"));
        builder.redirectInput(ProcessBuilder.Redirect.INHERIT);
        builder.redirectErrorStream(true);

        Process process = builder.start();
        try (BufferedReader linesReader = new BufferedReader(
            new InputStreamReader(process.getInputStream()))) {

            Assert.assertTrue(
                "Checking that stack trace contains no debug information.",
                linesReader.lines().noneMatch(
                    l -> l.matches(".*\\([^)]*:[0-9]+\\)")));
        }
        process.waitFor();
    }

    @Test
    public void testDeduplicationOfLicenses() {
        buildRule.executeTarget("dedup");
        ImageStructure image = verifyImageBuiltNormally();

        String helloModuleName =
            buildRule.getProject().getProperty("hello.mod");
        String smileModuleName =
            buildRule.getProject().getProperty("smile.mod");

        Assert.assertNotNull("Checking that 'hello.mod' property was set.",
            helloModuleName);
        Assert.assertNotNull("Checking that 'smile.mod' property was set.",
            smileModuleName);

        Assume.assumeFalse("Checking that this operating system"
            + " supports symbolic links as a means of license de-duplication.",
            System.getProperty("os.name").contains("Windows"));

        Path legal = image.root.toPath().resolve("legal");

        Path[] licenses = {
            legal.resolve(helloModuleName).resolve("USELESSLICENSE"),
            legal.resolve(smileModuleName).resolve("USELESSLICENSE"),
        };

        int nonLinkCount = 0;
        for (Path license : licenses) {
            if (!Files.isSymbolicLink(license)) {
                nonLinkCount++;
            }
        }

        Assert.assertEquals(
            "Checking that USELESSLICENSE only exists once in image "
            + "and all other instances are links to it.",
            1, nonLinkCount);
    }

    @Test
    public void testIgnoreSigning() {
        buildRule.executeTarget("ignoresigning");
        verifyImageBuiltNormally();
    }

    /**
     * Should fail due to jlink rejecting identically named files whose
     * contents are different.
     */
    @Test
    public void testDeduplicationOfInconsistentLicenses() {
        expected.expect(BuildException.class);
        buildRule.executeTarget("dedup-identical");
    }

    @Test
    public void testBindingOfServices()
    throws IOException,
           InterruptedException {
        buildRule.executeTarget("bindservices");
        ImageStructure image = verifyImageBuiltNormally();

        String mainClass = buildRule.getProject().getProperty("inc.main-class");

        ProcessBuilder builder = new ProcessBuilder(
            image.java.toString(), mainClass);
        builder.redirectInput(ProcessBuilder.Redirect.INHERIT);
        builder.redirectError(ProcessBuilder.Redirect.INHERIT);

        Process process = builder.start();
        try (BufferedReader linesReader = new BufferedReader(
            new InputStreamReader(process.getInputStream()))) {

            Assert.assertEquals(
                "Checking that bindServices=false results in no providers in image.",
                0, linesReader.lines().count());
        }
        process.waitFor();

        image = new ImageStructure(
            new File(buildRule.getProject().getProperty("image2")));

        Assert.assertTrue("Checking that image2 was successfully created.",
            image.root.exists());
        Assert.assertTrue("Checking that image2 has java executable.",
            image.java.exists());

        builder = new ProcessBuilder(image.java.toString(), mainClass);
        builder.redirectInput(ProcessBuilder.Redirect.INHERIT);
        builder.redirectError(ProcessBuilder.Redirect.INHERIT);

        process = builder.start();
        try (BufferedReader linesReader = new BufferedReader(
            new InputStreamReader(process.getInputStream()))) {

            Assert.assertEquals(
                "Checking that bindServices=true results in image with provider.",
                5, linesReader.lines().count());
        }
        process.waitFor();
    }

    private String runJlink(final String... args) {
        ToolProvider jlink = ToolProvider.findFirst("jlink").orElseThrow(
            () -> new RuntimeException("jlink tool not found in JDK."));

        ByteArrayOutputStream stdout = new ByteArrayOutputStream();
        ByteArrayOutputStream stderr = new ByteArrayOutputStream();

        int exitCode;
        try (PrintStream out = new PrintStream(stdout);
             PrintStream err = new PrintStream(stderr)) {

            exitCode = jlink.run(out, err, args);
        }

        if (exitCode != 0) {
            throw new RuntimeException(
                "jlink failed, output is: " + stdout + ", error is: " + stderr);
        }

        return stdout.toString();
    }
}
