/*
 * 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.shared.release.exec;

import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Writer;

import org.apache.maven.settings.Proxy;
import org.apache.maven.settings.Server;
import org.apache.maven.settings.Settings;
import org.apache.maven.settings.io.xpp3.SettingsXpp3Writer;
import org.apache.maven.shared.release.PlexusJUnit4TestCase;
import org.apache.maven.shared.release.ReleaseResult;
import org.apache.maven.shared.release.env.DefaultReleaseEnvironment;
import org.apache.maven.shared.release.util.MavenCrypto;
import org.codehaus.plexus.util.cli.Arg;
import org.codehaus.plexus.util.cli.CommandLineException;
import org.codehaus.plexus.util.cli.Commandline;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.sonatype.plexus.components.sec.dispatcher.SecDispatcher;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.endsWith;
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

/**
 * Test the forked Maven executor.
 *
 * @author <a href="mailto:brett@apache.org">Brett Porter</a>
 */
public class ForkedMavenExecutorTest extends PlexusJUnit4TestCase {
    private MavenCrypto mavenCrypto;
    private SecDispatcher secDispatcher;

    @Override
    public void setUp() throws Exception {
        super.setUp();

        mavenCrypto = lookup(MavenCrypto.class);
        secDispatcher = lookup(SecDispatcher.class);
    }

    @Test
    public void testExecution() throws Exception {
        // prepare
        File workingDirectory = getTestFile("target/working-directory");
        Process mockProcess = mock(Process.class);
        when(mockProcess.getInputStream()).thenReturn(mock(InputStream.class));
        when(mockProcess.getErrorStream()).thenReturn(mock(InputStream.class));
        when(mockProcess.getOutputStream()).thenReturn(mock(OutputStream.class));
        when(mockProcess.waitFor()).thenReturn(0);

        Commandline commandLineMock = mock(Commandline.class);
        when(commandLineMock.execute()).thenReturn(mockProcess);

        Arg valueArgument = mock(Arg.class);
        when(commandLineMock.createArg()).thenReturn(valueArgument);

        CommandLineFactory commandLineFactoryMock = mock(CommandLineFactory.class);
        when(commandLineFactoryMock.createCommandLine(isA(String.class) /*"mvn"*/))
                .thenReturn(commandLineMock);

        ForkedMavenExecutor executor = new ForkedMavenExecutor(mavenCrypto, commandLineFactoryMock);

        // execute
        executor.executeGoals(
                workingDirectory,
                "clean integration-test",
                new DefaultReleaseEnvironment(),
                false,
                null,
                null,
                new ReleaseResult());

        // verify
        verify(mockProcess).getInputStream();
        verify(mockProcess).getErrorStream();
        verify(mockProcess).getOutputStream();
        verify(mockProcess).waitFor();
        verify(commandLineMock).setWorkingDirectory(workingDirectory.getAbsolutePath());
        verify(commandLineMock).addEnvironment("MAVEN_TERMINATE_CMD", "on");
        verify(commandLineMock).execute();
        verify(commandLineMock, times(3)).createArg();
        verify(valueArgument).setValue("clean");
        verify(valueArgument).setValue("integration-test");
        verify(valueArgument).setValue("--batch-mode");
        verify(commandLineFactoryMock).createCommandLine(endsWith("mvn"));

        verifyNoMoreInteractions(mockProcess, commandLineFactoryMock, commandLineMock, valueArgument);
    }

    @Test
    public void testExecutionWithCustomPomFile() throws Exception {
        File workingDirectory = getTestFile("target/working-directory");
        Process mockProcess = mock(Process.class);
        when(mockProcess.getInputStream()).thenReturn(mock(InputStream.class));
        when(mockProcess.getErrorStream()).thenReturn(mock(InputStream.class));
        when(mockProcess.getOutputStream()).thenReturn(mock(OutputStream.class));
        when(mockProcess.waitFor()).thenReturn(0);

        Commandline commandLineMock = mock(Commandline.class);
        when(commandLineMock.execute()).thenReturn(mockProcess);

        Arg argMock = mock(Arg.class);
        when(commandLineMock.createArg()).thenReturn(argMock);

        CommandLineFactory commandLineFactoryMock = mock(CommandLineFactory.class);
        when(commandLineFactoryMock.createCommandLine(isA(String.class) /* "mvn" */))
                .thenReturn(commandLineMock);

        ForkedMavenExecutor executor = new ForkedMavenExecutor(mavenCrypto, commandLineFactoryMock);

        // execute
        executor.executeGoals(
                workingDirectory,
                "clean integration-test",
                new DefaultReleaseEnvironment(),
                false,
                null,
                "my-pom.xml",
                new ReleaseResult());
        // verify
        verify(mockProcess).getInputStream();
        verify(mockProcess).getErrorStream();
        verify(mockProcess).getOutputStream();
        verify(mockProcess).waitFor();
        verify(commandLineMock).setWorkingDirectory(workingDirectory.getAbsolutePath());
        verify(commandLineMock).addEnvironment("MAVEN_TERMINATE_CMD", "on");
        verify(commandLineMock).execute();
        verify(commandLineMock, times(5)).createArg();
        verify(argMock).setValue("clean");
        verify(argMock).setValue("integration-test");
        verify(argMock).setValue("-f");
        verify(argMock).setValue("my-pom.xml");
        verify(argMock).setValue("--batch-mode");
        verify(commandLineFactoryMock).createCommandLine(endsWith("mvn"));

        verifyNoMoreInteractions(mockProcess, commandLineMock, argMock, commandLineFactoryMock);
    }

    @Test
    public void testExecutionWithArguments() throws Exception {
        File workingDirectory = getTestFile("target/working-directory");
        Process mockProcess = mock(Process.class);
        when(mockProcess.getInputStream()).thenReturn(mock(InputStream.class));
        when(mockProcess.getErrorStream()).thenReturn(mock(InputStream.class));
        when(mockProcess.getOutputStream()).thenReturn(mock(OutputStream.class));
        when(mockProcess.waitFor()).thenReturn(0);

        Commandline commandLineMock = mock(Commandline.class);
        when(commandLineMock.execute()).thenReturn(mockProcess);

        Arg argMock = mock(Arg.class);
        when(commandLineMock.createArg()).thenReturn(argMock);

        CommandLineFactory commandLineFactoryMock = mock(CommandLineFactory.class);
        when(commandLineFactoryMock.createCommandLine(endsWith("mvn"))).thenReturn(commandLineMock);

        ForkedMavenExecutor executor = new ForkedMavenExecutor(mavenCrypto, commandLineFactoryMock);

        // execute
        String arguments = "-DperformRelease=true -Dmaven.test.skip=true";
        executor.executeGoals(
                workingDirectory,
                "clean integration-test",
                new DefaultReleaseEnvironment(),
                false,
                arguments,
                null,
                new ReleaseResult());

        // verify
        verify(mockProcess).getInputStream();
        verify(mockProcess).getErrorStream();
        verify(mockProcess).getOutputStream();
        verify(mockProcess).waitFor();
        verify(commandLineMock).setWorkingDirectory(workingDirectory.getAbsolutePath());
        verify(commandLineMock).addEnvironment("MAVEN_TERMINATE_CMD", "on");
        verify(commandLineMock).execute();
        verify(commandLineMock, times(4)).createArg();
        verify(argMock).setValue("clean");
        verify(argMock).setValue("integration-test");
        verify(argMock).setValue("--batch-mode");
        verify(argMock).setLine("-DperformRelease=true -Dmaven.test.skip=true");
        verify(commandLineFactoryMock).createCommandLine(endsWith("mvn"));

        verifyNoMoreInteractions(mockProcess, commandLineMock, argMock, commandLineFactoryMock);
    }

    @Test
    public void testExecutionWithNonZeroExitCode() throws Exception {
        // prepare
        File workingDirectory = getTestFile("target/working-directory");
        Process mockProcess = mock(Process.class);
        when(mockProcess.getInputStream()).thenReturn(mock(InputStream.class));
        when(mockProcess.getErrorStream()).thenReturn(mock(InputStream.class));
        when(mockProcess.getOutputStream()).thenReturn(mock(OutputStream.class));
        when(mockProcess.waitFor()).thenReturn(1);
        when(mockProcess.exitValue()).thenReturn(1); // why was this here in the original test?

        Commandline commandLineMock = mock(Commandline.class);
        when(commandLineMock.execute()).thenReturn(mockProcess);

        Arg argMock = mock(Arg.class);
        when(commandLineMock.createArg()).thenReturn(argMock);

        CommandLineFactory commandLineFactoryMock = mock(CommandLineFactory.class);
        when(commandLineFactoryMock.createCommandLine(endsWith("mvn"))).thenReturn(commandLineMock);

        ForkedMavenExecutor executor = new ForkedMavenExecutor(mavenCrypto, commandLineFactoryMock);

        // execute
        try {
            executor.executeGoals(
                    workingDirectory,
                    "clean integration-test",
                    new DefaultReleaseEnvironment(),
                    false,
                    null,
                    null,
                    new ReleaseResult());

            fail("Should have thrown an exception");
        } catch (MavenExecutorException e) {
            assertEquals("Check exit code", 1, e.getExitCode());
        }

        // verify
        verify(mockProcess).getInputStream();
        verify(mockProcess).getErrorStream();
        verify(mockProcess).getOutputStream();
        verify(mockProcess).waitFor();
        //        verify( mockProcess ).exitValue();
        verify(commandLineMock).setWorkingDirectory(workingDirectory.getAbsolutePath());
        verify(commandLineMock).addEnvironment("MAVEN_TERMINATE_CMD", "on");
        verify(commandLineMock).execute();
        verify(commandLineMock, times(3)).createArg();
        verify(argMock).setValue("clean");
        verify(argMock).setValue("integration-test");
        verify(argMock).setValue("--batch-mode");
        verify(commandLineFactoryMock).createCommandLine(endsWith("mvn"));

        verifyNoMoreInteractions(mockProcess, commandLineMock, argMock, commandLineFactoryMock);
    }

    @Test
    public void testExecutionWithCommandLineException() throws Exception {
        // prepare
        File workingDirectory = getTestFile("target/working-directory");

        Commandline commandLineMock = mock(Commandline.class);
        when(commandLineMock.execute()).thenThrow(new CommandLineException("..."));

        Arg argMock = mock(Arg.class);
        when(commandLineMock.createArg()).thenReturn(argMock);

        CommandLineFactory commandLineFactoryMock = mock(CommandLineFactory.class);
        when(commandLineFactoryMock.createCommandLine(endsWith("mvn"))).thenReturn(commandLineMock);

        ForkedMavenExecutor executor = new ForkedMavenExecutor(mavenCrypto, commandLineFactoryMock);

        // execute
        try {
            executor.executeGoals(
                    workingDirectory,
                    "clean integration-test",
                    new DefaultReleaseEnvironment(),
                    false,
                    null,
                    null,
                    new ReleaseResult());

            fail("Should have thrown an exception");
        } catch (MavenExecutorException e) {
            assertEquals("Check cause", CommandLineException.class, e.getCause().getClass());
        }

        // verify
        verify(commandLineMock).setWorkingDirectory(workingDirectory.getAbsolutePath());
        verify(commandLineMock).addEnvironment("MAVEN_TERMINATE_CMD", "on");
        verify(commandLineMock).execute();
        verify(commandLineMock, times(3)).createArg();
        verify(argMock).setValue("clean");
        verify(argMock).setValue("integration-test");
        verify(argMock).setValue("--batch-mode");
        verify(commandLineFactoryMock).createCommandLine(endsWith("mvn"));

        verifyNoMoreInteractions(commandLineMock, argMock, commandLineFactoryMock);
    }

    @Test
    public void testEncryptSettings() throws Exception {
        // prepare
        File workingDirectory = getTestFile("target/working-directory");
        Process mockProcess = mock(Process.class);
        when(mockProcess.getInputStream()).thenReturn(mock(InputStream.class));
        when(mockProcess.getErrorStream()).thenReturn(mock(InputStream.class));
        when(mockProcess.getOutputStream()).thenReturn(mock(OutputStream.class));
        when(mockProcess.waitFor()).thenReturn(0);

        Commandline commandLineMock = mock(Commandline.class);
        when(commandLineMock.execute()).thenReturn(mockProcess);

        Arg valueArgument = mock(Arg.class);
        when(commandLineMock.createArg()).thenReturn(valueArgument);

        CommandLineFactory commandLineFactoryMock = mock(CommandLineFactory.class);
        when(commandLineFactoryMock.createCommandLine(isA(String.class) /* "mvn" */))
                .thenReturn(commandLineMock);

        ForkedMavenExecutor executor = new ForkedMavenExecutor(mavenCrypto, commandLineFactoryMock);

        Settings settings = new Settings();
        Server server = new Server();
        server.setPassphrase("server_passphrase");
        server.setPassword("server_password");
        settings.addServer(server);
        Proxy proxy = new Proxy();
        proxy.setPassword("proxy_password");
        settings.addProxy(proxy);

        DefaultReleaseEnvironment releaseEnvironment = new DefaultReleaseEnvironment();
        releaseEnvironment.setSettings(settings);

        AbstractMavenExecutor executorSpy = spy(executor);
        SettingsXpp3Writer settingsWriter = mock(SettingsXpp3Writer.class);

        ArgumentCaptor<Settings> encryptedSettings = ArgumentCaptor.forClass(Settings.class);

        when(executorSpy.getSettingsWriter()).thenReturn(settingsWriter);

        executorSpy.executeGoals(
                workingDirectory, "validate", releaseEnvironment, false, null, null, new ReleaseResult());

        verify(settingsWriter).write(isA(Writer.class), encryptedSettings.capture());

        assertNotSame(settings, encryptedSettings.getValue());

        Server encryptedServer = encryptedSettings.getValue().getServers().get(0);
        assertEquals("server_passphrase", secDispatcher.decrypt(encryptedServer.getPassphrase()));
        assertEquals("server_password", secDispatcher.decrypt(encryptedServer.getPassword()));

        Proxy encryptedProxy = encryptedSettings.getValue().getProxies().get(0);
        assertEquals("proxy_password", secDispatcher.decrypt(encryptedProxy.getPassword()));

        File settingsSecurity = new File(System.getProperty("user.home"), ".m2/settings-security.xml");
        if (settingsSecurity.exists()) {
            assertNotEquals("server_passphrase", encryptedServer.getPassphrase());
            assertNotEquals("server_password", encryptedServer.getPassword());
            assertNotEquals("proxy_password", encryptedProxy.getPassword());
        }
    }
}
