/*
 * 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.surefire.booter;

import java.io.File;
import java.io.InputStream;
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collection;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.Semaphore;

import org.apache.commons.io.FileUtils;
import org.apache.maven.surefire.api.util.SureFireFileManager;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.junit.jupiter.api.parallel.Isolated;

import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.assertj.core.api.Assertions.assertThat;

/**
 * Tests for {@link ForkedBooter}.
 */
@SuppressWarnings("checkstyle:magicnumber")
@Isolated
public class ForkedBooterTest {

    private static Object invokeMethod(Class<?> clazz, String methodName, Object... args) throws Exception {
        Method[] methods = clazz.getDeclaredMethods();
        for (Method method : methods) {
            if (method.getName().equals(methodName) && method.getParameterCount() == args.length) {
                method.setAccessible(true);
                return method.invoke(null, args);
            }
        }
        throw new NoSuchMethodException(clazz.getName() + "." + methodName);
    }

    @Test
    public void shouldGenerateThreadDump() throws Exception {
        Collection<String> threadNames = new ArrayList<>();
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        for (ThreadInfo threadInfo : threadMXBean.getThreadInfo(threadMXBean.getAllThreadIds(), 100)) {
            threadNames.add(threadInfo.getThreadName());
        }

        String dump = (String) invokeMethod(ForkedBooter.class, "generateThreadDump");

        for (String threadName : threadNames) {
            assertThat(dump).contains("\"" + threadName + "\"");
        }

        assertThat(dump).contains("   java.lang.Thread.State: ").contains("        at ");
    }

    @Test
    public void shouldFindCurrentProcessName() throws Exception {
        String process = ManagementFactory.getRuntimeMXBean().getName();
        String expected = (String) invokeMethod(ForkedBooter.class, "getProcessName");
        assertThat(process).isEqualTo(expected);
    }

    @Test
    public void shouldNotBeDebugMode() throws Exception {
        boolean expected = (boolean) invokeMethod(ForkedBooter.class, "isDebugging");
        assertThat(expected).isFalse();
    }

    @Test
    public void shouldReadSurefireProperties() throws Exception {
        File tmpDir = Files.createTempDirectory("ForkedBooterTest.1.").toFile();

        try {
            try (InputStream is = (InputStream) invokeMethod(
                    ForkedBooter.class,
                    "createSurefirePropertiesIfFileExists",
                    tmpDir.getCanonicalPath(),
                    "surefire.properties")) {
                assertThat(is).isNull();
            }

            File props = new File(tmpDir, "surefire.properties");

            assertThat(props.createNewFile()).isTrue();

            FileUtils.write(props, "key=value", UTF_8);

            try (InputStream is2 = (InputStream) invokeMethod(
                    ForkedBooter.class,
                    "createSurefirePropertiesIfFileExists",
                    tmpDir.getCanonicalPath(),
                    "surefire.properties")) {
                byte[] propsContent = new byte[20];
                int length = is2.read(propsContent);

                assertThat(new String(propsContent, 0, length)).isEqualTo("key=value");
            }
        } finally {
            FileUtils.deleteDirectory(tmpDir);
        }
    }

    @Test
    public void shouldCreateScheduler() throws Exception {
        ScheduledExecutorService scheduler = null;
        try {
            scheduler = (ScheduledExecutorService) invokeMethod(ForkedBooter.class, "createScheduler", "thread name");
            assertThat(scheduler).isNotNull();
        } finally {
            if (scheduler != null) {
                scheduler.shutdownNow();
            }
        }
    }

    @Test
    @Timeout(value = 10_000, unit = MILLISECONDS)
    public void testBarrier1() throws Exception {
        Semaphore semaphore = new Semaphore(2);
        boolean acquiredOnePermit = (boolean) invokeMethod(ForkedBooter.class, "acquireOnePermit", semaphore);

        assertThat(acquiredOnePermit).isTrue();
        assertThat(semaphore.availablePermits()).isEqualTo(1);
    }

    @Test
    public void testBarrier2() throws Exception {
        Semaphore semaphore = new Semaphore(0);
        Thread.currentThread().interrupt();
        try {
            boolean acquiredOnePermit = (boolean) invokeMethod(ForkedBooter.class, "acquireOnePermit", semaphore);

            assertThat(acquiredOnePermit).isFalse();
            assertThat(semaphore.availablePermits()).isEqualTo(0);
        } finally {
            assertThat(Thread.interrupted()).isFalse();
        }
    }

    @Test
    public void testScheduler() throws Exception {
        ScheduledThreadPoolExecutor executor =
                (ScheduledThreadPoolExecutor) invokeMethod(ForkedBooter.class, "createScheduler", "thread name");
        executor.shutdown();
        assertThat(executor.getCorePoolSize()).isEqualTo(1);
        assertThat(executor.getMaximumPoolSize()).isEqualTo(1);
    }

    @Test
    public void testIsDebug() throws Exception {
        boolean isDebug = (boolean) invokeMethod(ForkedBooter.class, "isDebugging");
        assertThat(isDebug).isFalse();
    }

    @Test
    public void testPropsNotExist() throws Exception {
        String target = System.getProperty("user.dir");
        String file = "not exists";
        InputStream is =
                (InputStream) invokeMethod(ForkedBooter.class, "createSurefirePropertiesIfFileExists", target, file);
        assertThat(is).isNull();
    }

    @Test
    public void testPropsExist() throws Exception {
        File props = SureFireFileManager.createTempFile("surefire", ".properties");
        String target = props.getParent();
        String file = props.getName();
        FileUtils.write(props, "Hi", StandardCharsets.US_ASCII);
        try (InputStream is =
                (InputStream) invokeMethod(ForkedBooter.class, "createSurefirePropertiesIfFileExists", target, file)) {
            assertThat(is).isNotNull();
            byte[] data = new byte[5];
            int bytes = is.read(data);
            assertThat(bytes).isEqualTo(2);
            assertThat(data[0]).isEqualTo((byte) 'H');
            assertThat(data[1]).isEqualTo((byte) 'i');
        }
    }

    @Test
    public void testThreadDump() throws Exception {
        String threads = (String) invokeMethod(ForkedBooter.class, "generateThreadDump");
        assertThat(threads).isNotNull();
        assertThat(threads).contains("\"main\"").contains("java.lang.Thread.State: RUNNABLE");
    }
}
