/*
 * 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.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayDeque;
import java.util.Collections;
import java.util.jar.Manifest;
import java.util.zip.Deflater;

import org.apache.maven.plugin.surefire.StartupReportConfiguration;
import org.apache.maven.plugin.surefire.SurefireProperties;
import org.apache.maven.plugin.surefire.booterclient.lazytestprovider.AbstractCommandReader;
import org.apache.maven.plugin.surefire.booterclient.lazytestprovider.Commandline;
import org.apache.maven.plugin.surefire.booterclient.lazytestprovider.TestLessInputStream;
import org.apache.maven.plugin.surefire.booterclient.lazytestprovider.TestLessInputStream.TestLessInputStreamBuilder;
import org.apache.maven.plugin.surefire.booterclient.lazytestprovider.TestProvidingInputStream;
import org.apache.maven.plugin.surefire.booterclient.output.ForkClient;
import org.apache.maven.plugin.surefire.extensions.LegacyForkNodeFactory;
import org.apache.maven.plugin.surefire.extensions.SurefireConsoleOutputReporter;
import org.apache.maven.plugin.surefire.extensions.SurefireStatelessReporter;
import org.apache.maven.plugin.surefire.extensions.SurefireStatelessTestsetInfoReporter;
import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
import org.apache.maven.plugin.surefire.report.DefaultReporterFactory;
import org.apache.maven.surefire.api.booter.Shutdown;
import org.apache.maven.surefire.api.report.ReporterConfiguration;
import org.apache.maven.surefire.api.report.ReporterFactoryOptions;
import org.apache.maven.surefire.booter.AbstractPathConfiguration;
import org.apache.maven.surefire.booter.ClassLoaderConfiguration;
import org.apache.maven.surefire.booter.Classpath;
import org.apache.maven.surefire.booter.PropertiesWrapper;
import org.apache.maven.surefire.booter.ProviderConfiguration;
import org.apache.maven.surefire.booter.StartupConfiguration;
import org.apache.maven.surefire.booter.SurefireBooterForkException;
import org.apache.maven.surefire.extensions.ForkNodeFactory;
import org.apache.maven.surefire.shared.compress.archivers.zip.Zip64Mode;
import org.apache.maven.surefire.shared.compress.archivers.zip.ZipArchiveEntry;
import org.apache.maven.surefire.shared.compress.archivers.zip.ZipArchiveOutputStream;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

import static org.apache.commons.io.FileUtils.deleteQuietly;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

/**
 *
 */
public class ForkStarterTest {
    private static String baseDir = System.getProperty("user.dir");
    private static File tmp;

    @BeforeAll
    public static void prepareFiles() throws IOException {
        File target = new File(baseDir, "target");
        tmp = new File(target, "tmp");
        tmp.mkdirs();
        File booter = new File(tmp, "surefirebooter.jar");
        booter.createNewFile();

        try (ZipArchiveOutputStream zos = new ZipArchiveOutputStream(new FileOutputStream(booter))) {
            zos.setUseZip64(Zip64Mode.Never);
            zos.setLevel(Deflater.NO_COMPRESSION);

            ZipArchiveEntry ze = new ZipArchiveEntry("META-INF/MANIFEST.MF");
            zos.putArchiveEntry(ze);

            Manifest man = new Manifest();

            man.getMainAttributes().putValue("Manifest-Version", "1.0");
            man.getMainAttributes().putValue("Main-Class", MainClass.class.getName());

            man.write(zos);

            zos.closeArchiveEntry();

            ze = new ZipArchiveEntry("org/apache/maven/plugin/surefire/booterclient/MainClass.class");
            zos.putArchiveEntry(ze);
            String classesDir = Paths.get(target.getPath(), "test-classes").toString();
            Path cls = Paths.get(
                    classesDir, "org", "apache", "maven", "plugin", "surefire", "booterclient", "MainClass.class");
            zos.write(Files.readAllBytes(cls));
            zos.closeArchiveEntry();
        }
    }

    @AfterAll
    public static void deleteTmp() {
        deleteQuietly(tmp);
    }

    @Test
    public void processShouldExitWithoutSayingGoodBye() throws Exception {
        ReporterConfiguration reporterConfiguration = new ReporterConfiguration(tmp, true);

        ProviderConfiguration providerConfiguration = mock(ProviderConfiguration.class);
        when(providerConfiguration.getReporterConfiguration()).thenReturn(reporterConfiguration);
        when(providerConfiguration.getShutdown()).thenReturn(Shutdown.EXIT);

        StartupConfiguration startupConfiguration = mock(StartupConfiguration.class);
        when(startupConfiguration.getClasspathConfiguration()).thenReturn(new ClasspathConfig());
        when(startupConfiguration.getClassLoaderConfiguration()).thenReturn(new ClassLoaderConfiguration(false, false));
        when(startupConfiguration.getProviderClassName()).thenReturn(MainClass.class.getName());

        ForkConfiguration forkConfiguration = mock(ForkConfiguration.class);
        when(forkConfiguration.getWorkingDirectory()).thenReturn(tmp);
        when(forkConfiguration.getTempDirectory()).thenReturn(tmp);
        when(forkConfiguration.getPluginPlatform()).thenReturn(new Platform());
        Commandline cli = new Commandline();
        cli.setWorkingDirectory(tmp);
        cli.setExecutable(System.getProperty("java.home") + "/bin/java");
        cli.createArg().setLine("-jar");
        cli.createArg().setLine("surefirebooter.jar");
        cli.createArg().setLine("fail");
        when(forkConfiguration.createCommandLine(eq(startupConfiguration), eq(1), eq(tmp)))
                .thenReturn(cli);

        SurefireStatelessTestsetInfoReporter statelessTestsetInfoReporter = new SurefireStatelessTestsetInfoReporter();
        SurefireConsoleOutputReporter outputReporter = new SurefireConsoleOutputReporter();
        SurefireStatelessReporter xmlReporter = new SurefireStatelessReporter(true, "3");

        StartupReportConfiguration startupReportConfiguration = new StartupReportConfiguration(
                true,
                true,
                null,
                false,
                tmp,
                true,
                "",
                null,
                false,
                0,
                null,
                null,
                true,
                true,
                true,
                false,
                xmlReporter,
                outputReporter,
                statelessTestsetInfoReporter,
                new ReporterFactoryOptions());

        ConsoleLogger logger = mock(ConsoleLogger.class);

        ForkStarter forkStarter = new ForkStarter(
                providerConfiguration, startupConfiguration, forkConfiguration, 0, startupReportConfiguration, logger);

        DefaultReporterFactory reporterFactory = new DefaultReporterFactory(startupReportConfiguration, logger, 1);

        Class<?>[] types = {
            Object.class,
            PropertiesWrapper.class,
            ForkClient.class,
            SurefireProperties.class,
            int.class,
            AbstractCommandReader.class,
            ForkNodeFactory.class,
            boolean.class
        };
        TestProvidingInputStream testProvidingInputStream = new TestProvidingInputStream(new ArrayDeque<String>());
        SurefireBooterForkException ex = assertThrows(
                SurefireBooterForkException.class,
                () -> invokeMethod(
                        forkStarter,
                        "fork",
                        types,
                        null,
                        new PropertiesWrapper(Collections.<String, String>emptyMap()),
                        new ForkClient(reporterFactory, null, 1),
                        new SurefireProperties(),
                        1,
                        testProvidingInputStream,
                        new LegacyForkNodeFactory(),
                        true));
        assertThat(ex.getMessage())
                .contains("Process Exit Code: 1")
                .contains("The forked VM terminated without properly saying goodbye.")
                .contains("VM crash or System.exit called?");
        testProvidingInputStream.close();
    }

    @Test
    public void processShouldWaitForAck() throws Exception {
        ReporterConfiguration reporterConfiguration = new ReporterConfiguration(tmp, true);

        ProviderConfiguration providerConfiguration = mock(ProviderConfiguration.class);
        when(providerConfiguration.getReporterConfiguration()).thenReturn(reporterConfiguration);
        when(providerConfiguration.getShutdown()).thenReturn(Shutdown.EXIT);

        StartupConfiguration startupConfiguration = mock(StartupConfiguration.class);
        when(startupConfiguration.getClasspathConfiguration()).thenReturn(new ClasspathConfig());
        when(startupConfiguration.getClassLoaderConfiguration()).thenReturn(new ClassLoaderConfiguration(false, false));
        when(startupConfiguration.getProviderClassName()).thenReturn(MainClass.class.getName());

        ForkConfiguration forkConfiguration = mock(ForkConfiguration.class);
        when(forkConfiguration.getWorkingDirectory()).thenReturn(tmp);
        when(forkConfiguration.getTempDirectory()).thenReturn(tmp);
        when(forkConfiguration.getPluginPlatform()).thenReturn(new Platform());
        Commandline cli = new Commandline();
        cli.setWorkingDirectory(tmp);
        cli.setExecutable(System.getProperty("java.home") + "/bin/java");
        cli.createArg().setLine("-jar");
        cli.createArg().setLine("surefirebooter.jar");
        when(forkConfiguration.createCommandLine(eq(startupConfiguration), eq(1), eq(tmp)))
                .thenReturn(cli);

        SurefireStatelessTestsetInfoReporter statelessTestsetInfoReporter = new SurefireStatelessTestsetInfoReporter();
        SurefireConsoleOutputReporter outputReporter = new SurefireConsoleOutputReporter();
        SurefireStatelessReporter xmlReporter = new SurefireStatelessReporter(true, "3");

        StartupReportConfiguration startupReportConfiguration = new StartupReportConfiguration(
                true,
                true,
                null,
                false,
                tmp,
                true,
                "",
                null,
                false,
                0,
                null,
                null,
                true,
                true,
                true,
                false,
                xmlReporter,
                outputReporter,
                statelessTestsetInfoReporter,
                new ReporterFactoryOptions());

        ConsoleLogger logger = mock(ConsoleLogger.class);

        ForkStarter forkStarter = new ForkStarter(
                providerConfiguration, startupConfiguration, forkConfiguration, 0, startupReportConfiguration, logger);

        DefaultReporterFactory reporterFactory = new DefaultReporterFactory(startupReportConfiguration, logger, 1);

        Class<?>[] types = {
            Object.class,
            PropertiesWrapper.class,
            ForkClient.class,
            SurefireProperties.class,
            int.class,
            AbstractCommandReader.class,
            ForkNodeFactory.class,
            boolean.class
        };
        TestLessInputStream testLessInputStream = new TestLessInputStreamBuilder().build();
        invokeMethod(
                forkStarter,
                "fork",
                types,
                null,
                new PropertiesWrapper(Collections.<String, String>emptyMap()),
                new ForkClient(reporterFactory, testLessInputStream, 1),
                new SurefireProperties(),
                1,
                testLessInputStream,
                new LegacyForkNodeFactory(),
                true);
        testLessInputStream.close();
    }

    @SuppressWarnings("unchecked")
    private static <T> T invokeMethod(Object target, String methodName, Class<?>[] paramTypes, Object... args)
            throws Exception {
        Class<?> clazz = target.getClass();
        while (clazz != null) {
            try {
                Method method = clazz.getDeclaredMethod(methodName, paramTypes);
                method.setAccessible(true);
                try {
                    return (T) method.invoke(target, args);
                } catch (java.lang.reflect.InvocationTargetException e) {
                    Throwable cause = e.getCause();
                    if (cause instanceof Exception) {
                        throw (Exception) cause;
                    }
                    throw e;
                }
            } catch (NoSuchMethodException e) {
                clazz = clazz.getSuperclass();
            }
        }
        throw new NoSuchMethodException(methodName);
    }

    private static class ClasspathConfig extends AbstractPathConfiguration {
        ClasspathConfig() {
            this(Classpath.emptyClasspath(), false, false);
        }

        private ClasspathConfig(Classpath surefireClasspathUrls, boolean enableAssertions, boolean childDelegation) {
            super(surefireClasspathUrls, enableAssertions, childDelegation);
        }

        @Override
        public Classpath getTestClasspath() {
            return Classpath.emptyClasspath();
        }

        @Override
        public boolean isModularPathConfig() {
            return false;
        }

        @Override
        public boolean isClassPathConfig() {
            return true;
        }

        @Override
        protected Classpath getInprocClasspath() {
            return Classpath.emptyClasspath();
        }
    }
}
