/*
 * 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.maven.plugin.surefire.booterclient;

import java.io.File;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.nio.charset.Charset;
import java.nio.file.Path;

import org.apache.maven.plugin.surefire.booterclient.JarManifestForkConfiguration.ClasspathElementUri;
import org.apache.maven.plugin.surefire.booterclient.output.InPluginProcessDumpSingleton;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.powermock.core.classloader.annotations.PowerMockIgnore;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.powermock.modules.junit4.PowerMockRunnerDelegate;

import static java.nio.charset.StandardCharsets.UTF_8;
import static org.apache.maven.plugin.surefire.booterclient.JarManifestForkConfiguration.escapeUri;
import static org.apache.maven.plugin.surefire.booterclient.JarManifestForkConfiguration.relativize;
import static org.apache.maven.plugin.surefire.booterclient.JarManifestForkConfiguration.toAbsoluteUri;
import static org.apache.maven.plugin.surefire.booterclient.JarManifestForkConfiguration.toClasspathElementUri;
import static org.apache.maven.surefire.shared.lang3.SystemUtils.IS_OS_WINDOWS;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.util.Files.delete;
import static org.assertj.core.util.Files.newTemporaryFolder;
import static org.junit.Assume.assumeTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.same;
import static org.powermock.api.mockito.PowerMockito.mock;
import static org.powermock.api.mockito.PowerMockito.mockStatic;
import static org.powermock.api.mockito.PowerMockito.when;

/**
 * Unit tests for {@link JarManifestForkConfiguration}.
 */
@RunWith(PowerMockRunner.class)
@PowerMockRunnerDelegate(JUnit4.class)
@PrepareForTest({JarManifestForkConfiguration.class, InPluginProcessDumpSingleton.class})
@PowerMockIgnore({"org.jacoco.agent.rt.*", "com.vladium.emma.rt.*"})
public class JarManifestForkConfigurationTest {
    private static final File TMP = newTemporaryFolder();

    private static File dumpDirectory;

    @BeforeClass
    public static void createSystemTemporaryDir() {
        dumpDirectory = new File(TMP, "dump");
        assertThat(dumpDirectory.mkdir()).isTrue();
    }

    @AfterClass
    public static void deleteSystemTemporaryDir() {
        delete(TMP);
    }

    @Test
    public void relativeClasspathUnixSimple() throws Exception {
        mockStatic(JarManifestForkConfiguration.class);
        Path parent = mock(Path.class);
        when(parent.toString()).thenReturn("/home/me/prj/target/surefire");
        Path classPathElement = mock(Path.class);
        when(classPathElement.toString()).thenReturn("/home/me/.m2/repository/grp/art/1.0/art-1.0.jar");
        when(relativize(parent, classPathElement)).thenReturn("../../../.m2/repository/grp/art/1.0/art-1.0.jar");
        when(toClasspathElementUri(same(parent), same(classPathElement), same(dumpDirectory), anyBoolean()))
                .thenCallRealMethod();
        when(escapeUri(anyString(), any(Charset.class))).thenCallRealMethod();
        assertThat(toClasspathElementUri(parent, classPathElement, dumpDirectory, true).uri)
                .isEqualTo("../../../.m2/repository/grp/art/1.0/art-1.0.jar");
    }

    @Test
    public void relativeClasspathUnixTricky() throws Exception {
        mockStatic(JarManifestForkConfiguration.class);
        Path parent = mock(Path.class);
        when(parent.toString()).thenReturn("/home/me/prj/target/surefire");
        Path classPathElement = mock(Path.class);
        when(classPathElement.toString()).thenReturn("/the Maven repo/grp/art/1.0/art-1.0.jar");
        when(relativize(parent, classPathElement)).thenReturn("../../../../../the Maven repo/grp/art/1.0/art-1.0.jar");
        when(toClasspathElementUri(same(parent), same(classPathElement), same(dumpDirectory), anyBoolean()))
                .thenCallRealMethod();
        when(escapeUri(anyString(), any(Charset.class))).thenCallRealMethod();
        assertThat(toClasspathElementUri(parent, classPathElement, dumpDirectory, true).uri)
                .isEqualTo("../../../../../the%20Maven%20repo/grp/art/1.0/art-1.0.jar");
    }

    @Test
    public void relativeClasspathWindowsSimple() throws Exception {
        mockStatic(JarManifestForkConfiguration.class);
        Path parent = mock(Path.class);
        when(parent.toString()).thenReturn("C:\\Windows\\Temp\\surefire");
        Path classPathElement = mock(Path.class);
        when(classPathElement.toString()).thenReturn("C:\\Users\\me\\.m2\\repository\\grp\\art\\1.0\\art-1.0.jar");
        when(relativize(parent, classPathElement))
                .thenReturn("..\\..\\..\\Users\\me\\.m2\\repository\\grp\\art\\1.0\\art-1.0.jar");
        when(toClasspathElementUri(same(parent), same(classPathElement), same(dumpDirectory), anyBoolean()))
                .thenCallRealMethod();
        when(escapeUri(anyString(), any(Charset.class))).thenCallRealMethod();
        assertThat(toClasspathElementUri(parent, classPathElement, dumpDirectory, true).uri)
                .isEqualTo("../../../Users/me/.m2/repository/grp/art/1.0/art-1.0.jar");
    }

    @Test
    public void relativeClasspathWindowsTricky() throws Exception {
        mockStatic(JarManifestForkConfiguration.class);
        Path parent = mock(Path.class);
        when(parent.toString()).thenReturn("C:\\Windows\\Temp\\surefire");
        Path classPathElement = mock(Path.class);
        when(classPathElement.toString()).thenReturn("C:\\Test User\\me\\.m2\\repository\\grp\\art\\1.0\\art-1.0.jar");
        when(relativize(parent, classPathElement))
                .thenReturn("..\\..\\..\\Test User\\me\\.m2\\repository\\grp\\art\\1.0\\art-1.0.jar");
        when(toClasspathElementUri(same(parent), same(classPathElement), same(dumpDirectory), anyBoolean()))
                .thenCallRealMethod();
        when(escapeUri(anyString(), any(Charset.class))).thenCallRealMethod();
        assertThat(toClasspathElementUri(parent, classPathElement, dumpDirectory, true).uri)
                .isEqualTo("../../../Test%20User/me/.m2/repository/grp/art/1.0/art-1.0.jar");
    }

    @Test
    public void crossDriveWindows() throws Exception {
        mockStatic(JarManifestForkConfiguration.class);
        mockStatic(InPluginProcessDumpSingleton.class);
        when(InPluginProcessDumpSingleton.getSingleton()).thenReturn(mock(InPluginProcessDumpSingleton.class));
        Path parent = mock(Path.class);
        when(parent.toString()).thenReturn("C:\\Windows\\Temp\\surefire");
        Path classPathElement = mock(Path.class);
        when(classPathElement.toString()).thenReturn("X:\\Users\\me\\.m2\\repository\\grp\\art\\1.0\\art-1.0.jar");
        when(classPathElement.toUri()).thenAnswer(new Answer<URI>() {
            @Override
            public URI answer(InvocationOnMock invocation) throws URISyntaxException {
                String path = invocation.getMock().toString();
                return new URI("file", "", "/" + path.replace('\\', '/'), null);
            }
        });
        when(relativize(same(parent), same(classPathElement)))
                .thenThrow(new IllegalArgumentException("'other' has different root"));
        when(toClasspathElementUri(same(parent), same(classPathElement), same(dumpDirectory), anyBoolean()))
                .thenCallRealMethod();
        when(escapeUri(anyString(), any(Charset.class))).thenCallRealMethod();
        when(toAbsoluteUri(same(classPathElement))).thenCallRealMethod();
        assertThat(toClasspathElementUri(parent, classPathElement, dumpDirectory, true).uri)
                .isEqualTo("file:///X:/Users/me/.m2/repository/grp/art/1.0/art-1.0.jar");
    }

    @Test
    public void uncWindows() throws Exception {
        assumeTrue(IS_OS_WINDOWS);
        mockStatic(JarManifestForkConfiguration.class);
        mockStatic(InPluginProcessDumpSingleton.class);
        when(InPluginProcessDumpSingleton.getSingleton()).thenReturn(mock(InPluginProcessDumpSingleton.class));
        Path parent = mock(Path.class);
        when(parent.toString()).thenReturn("C:\\Windows\\Temp\\surefire");
        Path classPathElement = mock(Path.class);
        when(classPathElement.toString()).thenReturn("\\\\server\\grp\\art\\1.0\\art-1.0.jar");
        when(classPathElement.toFile()).thenAnswer(new Answer<File>() {
            @Override
            public File answer(InvocationOnMock invocation) {
                String path = invocation.getMock().toString();
                return new File(path);
            }
        });
        when(relativize(same(parent), same(classPathElement)))
                .thenThrow(new IllegalArgumentException("'other' has different root"));
        when(toClasspathElementUri(same(parent), same(classPathElement), same(dumpDirectory), anyBoolean()))
                .thenCallRealMethod();
        when(escapeUri(anyString(), any(Charset.class))).thenCallRealMethod();
        when(toAbsoluteUri(same(classPathElement))).thenCallRealMethod();
        assertThat(toClasspathElementUri(parent, classPathElement, dumpDirectory, true).uri)
                .isEqualTo("file:////server/grp/art/1.0/art-1.0.jar");
    }

    @Test
    @SuppressWarnings("checkstyle:magicnumber")
    public void shouldEscapeUri() throws Exception {
        assertThat(escapeUri("a", UTF_8)).isEqualTo("a");
        assertThat(escapeUri(" ", UTF_8)).isEqualTo("%20");
        assertThat(escapeUri("%", UTF_8)).isEqualTo("%25");
        assertThat(escapeUri("+", UTF_8)).isEqualTo("%2B");
        assertThat(escapeUri(",", UTF_8)).isEqualTo("%2C");
        assertThat(escapeUri("/", UTF_8)).isEqualTo("/");
        assertThat(escapeUri("7", UTF_8)).isEqualTo("7");
        assertThat(escapeUri(":", UTF_8)).isEqualTo("%3A");
        assertThat(escapeUri("@", UTF_8)).isEqualTo("%40");
        assertThat(escapeUri("A", UTF_8)).isEqualTo("A");
        assertThat(escapeUri("[", UTF_8)).isEqualTo("%5B");
        assertThat(escapeUri("\\", UTF_8)).isEqualTo("/");
        assertThat(escapeUri("]", UTF_8)).isEqualTo("%5D");
        assertThat(escapeUri("`", UTF_8)).isEqualTo("%60");
        assertThat(escapeUri("a", UTF_8)).isEqualTo("a");
        assertThat(escapeUri("{", UTF_8)).isEqualTo("%7B");
        assertThat(escapeUri("" + (char) 0xFF, UTF_8)).isEqualTo("%C3%BF");

        assertThat(escapeUri("..\\..\\..\\Test : User\\me\\.m2\\repository\\grp\\art\\1.0\\art-1.0.jar", UTF_8))
                .isEqualTo("../../../Test%20%3A%20User/me/.m2/repository/grp/art/1.0/art-1.0.jar");

        assertThat(escapeUri(
                        "..\\..\\..\\Test : User\\me\\.m2\\repository\\grp\\art\\1.0\\art-1.0.jar",
                        Charset.defaultCharset()))
                .isEqualTo("../../../Test%20%3A%20User/me/.m2/repository/grp/art/1.0/art-1.0.jar");

        assertThat(escapeUri("../../surefire-its/target/junit-pathWithÜmlaut_1/target/surefire", UTF_8))
                .isEqualTo("../../surefire-its/target/junit-pathWith%C3%9Cmlaut_1/target/surefire");

        String source = "../../surefire-its/target/junit-pathWithÜmlaut_1/target/surefire";
        String encoded = escapeUri("../../surefire-its/target/junit-pathWithÜmlaut_1/target/surefire", UTF_8);
        String decoded = URLDecoder.decode(encoded, UTF_8.name());
        assertThat(decoded).isEqualTo(source);
    }

    @Test
    public void shouldRelativizeOnRealPlatform() {
        Path parentDir = new File(TMP, "test-parent-1").toPath();

        Path testDir = new File(TMP, "@1 test with white spaces").toPath();

        String relativeTestDir = relativize(parentDir, testDir);

        assertThat(relativeTestDir).isEqualTo(".." + File.separator + "@1 test with white spaces");
    }

    @Test
    public void shouldMakeAbsoluteUriOnRealPlatform() throws Exception {
        Path testDir = new File(TMP, "@2 test with white spaces").toPath();

        URI testDirUri = new URI(toAbsoluteUri(testDir));

        assertThat(testDirUri.getScheme()).isEqualTo("file");

        assertThat(testDirUri.getRawPath()).isEqualTo(testDir.toUri().getRawPath());
    }

    @Test
    public void shouldMakeRelativeUriOnRealPlatform() throws Exception {
        Path parentDir = new File(TMP, "test-parent-2").toPath();

        Path testDir = new File(TMP, "@3 test with white spaces").toPath();

        ClasspathElementUri testDirUriPath = toClasspathElementUri(parentDir, testDir, dumpDirectory, true);

        assertThat(testDirUriPath.uri).isEqualTo("../%403%20test%20with%20white%20spaces");
    }
}
