/*
 * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

/*
 * @test id=default
 * @bug 8312498
 * @summary Basic test for JVMTI GetThreadState with virtual threads
 * @modules jdk.management
 * @library /test/lib
 * @run junit/othervm/native --enable-native-access=ALL-UNNAMED GetThreadStateTest
 */

/*
 * @test id=no-vmcontinuations
 * @requires vm.continuations
 * @modules jdk.management
 * @library /test/lib
 * @run junit/othervm/native -XX:+UnlockExperimentalVMOptions -XX:-VMContinuations --enable-native-access=ALL-UNNAMED GetThreadStateTest
 */

import java.util.StringJoiner;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.LockSupport;

import jdk.test.lib.thread.VThreadRunner;   // ensureParallelism requires jdk.management
import jdk.test.lib.thread.VThreadPinner;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.*;

class GetThreadStateTest {

    @BeforeAll
    static void setup() {
        System.loadLibrary("GetThreadStateTest");
        init();

        // need >=2 carriers for testing pinning when main thread is a virtual thread
        if (Thread.currentThread().isVirtual()) {
            VThreadRunner.ensureParallelism(2);
        }
    }

    /**
     * Test state of new/unstarted thread.
     */
    @Test
    void testUnstarted() {
        var thread = Thread.ofVirtual().unstarted(() -> { });
        check(thread, /*new*/ 0);
    }

    /**
     * Test state of terminated thread.
     */
    @Test
    void testTerminated() throws Exception {
        var thread = Thread.ofVirtual().start(() -> { });
        thread.join();
        check(thread, JVMTI_THREAD_STATE_TERMINATED);
    }

    /**
     * Test state of runnable thread.
     */
    @Test
    void testRunnable() throws Exception {
        var started = new AtomicBoolean();
        var done = new AtomicBoolean();
        var thread = Thread.ofVirtual().start(() -> {
            started.set(true);

            // spin until done
            while (!done.get()) {
                Thread.onSpinWait();
            }
        });
        try {
            // wait for thread to start execution
            awaitTrue(started);

            // thread should be runnable
            int expected = JVMTI_THREAD_STATE_ALIVE | JVMTI_THREAD_STATE_RUNNABLE;
            check(thread, expected);

            // re-test with interrupt status set
            thread.interrupt();
            check(thread, expected | JVMTI_THREAD_STATE_INTERRUPTED);
        } finally {
            done.set(true);
            thread.join();
        }
    }

    /**
     * Test state of thread waiting to enter a monitor when pinned and not pinned.
     */
    @ParameterizedTest
    @ValueSource(booleans = { true, false })
    void testMonitorEnter(boolean pinned) throws Exception {
        var ready = new AtomicBoolean();
        Object lock = new Object();
        var thread = Thread.ofVirtual().unstarted(() -> {
            if (pinned) {
                VThreadPinner.runPinned(() -> {
                    ready.set(true);
                    synchronized (lock) { }
                });
            } else {
                ready.set(true);
                synchronized (lock) { }
            }
        });
        try {
            synchronized (lock) {
                // start thread and wait for it to start execution
                thread.start();
                awaitTrue(ready);

                // thread should block on monitor enter
                int expected = JVMTI_THREAD_STATE_ALIVE | JVMTI_THREAD_STATE_BLOCKED_ON_MONITOR_ENTER;
                await(thread, expected);

                // re-test with interrupt status set
                thread.interrupt();
                check(thread, expected | JVMTI_THREAD_STATE_INTERRUPTED);
            }
        } finally {
            thread.join();
        }
    }

    /**
     * Test state of thread waiting in Object.wait() when pinned and not pinned.
     */
    @ParameterizedTest
    @ValueSource(booleans = { true, false })
    void testObjectWait(boolean pinned) throws Exception {
        var ready = new AtomicBoolean();
        Object lock = new Object();
        var thread = Thread.ofVirtual().start(() -> {
            synchronized (lock) {
                try {
                    if (pinned) {
                        VThreadPinner.runPinned(() -> {
                            ready.set(true);
                            lock.wait();
                        });
                    } else {
                        ready.set(true);
                        lock.wait();
                    }
                } catch (InterruptedException e) { }
            }
        });
        try {
            // wait for thread to start execution
            awaitTrue(ready);

            // thread should wait
            int expected = JVMTI_THREAD_STATE_ALIVE |
                    JVMTI_THREAD_STATE_WAITING |
                    JVMTI_THREAD_STATE_WAITING_INDEFINITELY |
                    JVMTI_THREAD_STATE_IN_OBJECT_WAIT;
            await(thread, expected);

            // notify so thread waits to re-enter monitor
            synchronized (lock) {
                lock.notifyAll();
                expected = JVMTI_THREAD_STATE_ALIVE | JVMTI_THREAD_STATE_BLOCKED_ON_MONITOR_ENTER;
                check(thread, expected);

                // re-test with interrupt status set
                thread.interrupt();
                check(thread, expected | JVMTI_THREAD_STATE_INTERRUPTED);
            }
        } finally {
            thread.interrupt();
            thread.join();
        }
    }

    /**
     * Test state of thread waiting in Object.wait(millis) when pinned and not pinned.
     */
    @ParameterizedTest
    @ValueSource(booleans = { true, false })
    void testObjectWaitMillis(boolean pinned) throws Exception {
        var ready = new AtomicBoolean();
        Object lock = new Object();
        var thread = Thread.ofVirtual().start(() -> {
            synchronized (lock) {
                synchronized (lock) {
                    try {
                        if (pinned) {
                            VThreadPinner.runPinned(() -> {
                                ready.set(true);
                                lock.wait(Long.MAX_VALUE);
                            });
                        } else {
                            ready.set(true);
                            lock.wait(Long.MAX_VALUE);
                        }
                    } catch (InterruptedException e) { }
                }
            }
        });
        try {
            // wait for thread to start execution
            awaitTrue(ready);

            // thread should wait
            int expected = JVMTI_THREAD_STATE_ALIVE |
                    JVMTI_THREAD_STATE_WAITING |
                    JVMTI_THREAD_STATE_WAITING_WITH_TIMEOUT |
                    JVMTI_THREAD_STATE_IN_OBJECT_WAIT;
            await(thread, expected);

            // notify so thread waits to re-enter monitor
            synchronized (lock) {
                lock.notifyAll();
                expected = JVMTI_THREAD_STATE_ALIVE | JVMTI_THREAD_STATE_BLOCKED_ON_MONITOR_ENTER;
                check(thread, expected);

                // re-test with interrupt status set
                thread.interrupt();
                check(thread, expected | JVMTI_THREAD_STATE_INTERRUPTED);
            }
        } finally {
            thread.interrupt();
            thread.join();
        }
    }

    /**
     * Test state of thread parked with LockSupport.park when pinned and not pinned.
     */
    @ParameterizedTest
    @ValueSource(booleans = { true, false })
    void testPark(boolean pinned) throws Exception {
        var ready = new AtomicBoolean();
        var done = new AtomicBoolean();
        var thread = Thread.ofVirtual().start(() -> {
            if (pinned) {
                VThreadPinner.runPinned(() -> {
                    ready.set(true);
                    while (!done.get()) {
                        LockSupport.park();
                    }
                });
            } else {
                ready.set(true);
                while (!done.get()) {
                    LockSupport.park();
                }
            }
        });
        try {
            // wait for thread to start execution
            awaitTrue(ready);

            // thread should park
            int expected = JVMTI_THREAD_STATE_ALIVE |
                    JVMTI_THREAD_STATE_WAITING |
                    JVMTI_THREAD_STATE_WAITING_INDEFINITELY |
                    JVMTI_THREAD_STATE_PARKED;
            await(thread, expected);
        } finally {
            done.set(true);
            LockSupport.unpark(thread);
            thread.join();
        }
    }

    /**
     * Test state of thread parked with LockSupport.parkNanos when pinned and not pinned.
     */
    @ParameterizedTest
    @ValueSource(booleans = { true, false })
    void testParkNanos(boolean pinned) throws Exception {
        var ready = new AtomicBoolean();
        var done = new AtomicBoolean();
        var thread = Thread.ofVirtual().start(() -> {
            if (pinned) {
                VThreadPinner.runPinned(() -> {
                    ready.set(true);
                    while (!done.get()) {
                        LockSupport.parkNanos(Long.MAX_VALUE);
                    }
                });
            } else {
                ready.set(true);
                while (!done.get()) {
                    LockSupport.parkNanos(Long.MAX_VALUE);
                }
            }
        });
        try {
            // wait for thread to start execution
            awaitTrue(ready);

            // thread should park
            int expected = JVMTI_THREAD_STATE_ALIVE |
                    JVMTI_THREAD_STATE_WAITING |
                    JVMTI_THREAD_STATE_WAITING_WITH_TIMEOUT |
                    JVMTI_THREAD_STATE_PARKED;
            await(thread, expected);
        } finally {
            done.set(true);
            LockSupport.unpark(thread);
            thread.join();
        }
    }

    /**
     * Waits for the boolean value to become true.
     */
    private static void awaitTrue(AtomicBoolean ref) throws Exception {
        while (!ref.get()) {
            Thread.sleep(20);
        }
    }

    /**
     * Asserts that the given thread has the expected JVMTI state.
     */
    private static void check(Thread thread, int expected) {
        System.err.format("  expect state=0x%x (%s) ...%n", expected, jvmtiStateToString(expected));
        int state = jvmtiState(thread);
        System.err.format("  thread state=0x%x (%s)%n", state, jvmtiStateToString(state));
        assertEquals(expected, state);
    }

    /**
     * Waits indefinitely for the given thread to get to the target JVMTI state.
     */
    private static void await(Thread thread, int targetState) throws Exception {
        System.err.format("  await state=0x%x (%s) ...%n", targetState, jvmtiStateToString(targetState));
        int state = jvmtiState(thread);
        System.err.format("  thread state=0x%x (%s)%n", state, jvmtiStateToString(state));
        while (state != targetState) {
            assertTrue(thread.isAlive(), "Thread has terminated");
            Thread.sleep(20);
            state = jvmtiState(thread);
            System.err.format("  thread state=0x%x (%s)%n", state, jvmtiStateToString(state));
        }
    }

    private static final int JVMTI_THREAD_STATE_ALIVE = 0x0001;
    private static final int JVMTI_THREAD_STATE_TERMINATED = 0x0002;
    private static final int JVMTI_THREAD_STATE_RUNNABLE = 0x0004;
    private static final int JVMTI_THREAD_STATE_BLOCKED_ON_MONITOR_ENTER = 0x0400;
    private static final int JVMTI_THREAD_STATE_WAITING = 0x0080;
    private static final int JVMTI_THREAD_STATE_WAITING_INDEFINITELY = 0x0010;
    private static final int JVMTI_THREAD_STATE_WAITING_WITH_TIMEOUT = 0x0020;
    private static final int JVMTI_THREAD_STATE_SLEEPING = 0x0040;
    private static final int JVMTI_THREAD_STATE_IN_OBJECT_WAIT = 0x0100;
    private static final int JVMTI_THREAD_STATE_PARKED = 0x0200;
    private static final int JVMTI_THREAD_STATE_SUSPENDED = 0x100000;
    private static final int JVMTI_THREAD_STATE_INTERRUPTED = 0x200000;
    private static final int JVMTI_THREAD_STATE_IN_NATIVE = 0x400000;

    private static native void init();
    private static native int jvmtiState(Thread thread);

    private static String jvmtiStateToString(int state) {
        StringJoiner sj = new StringJoiner(" | ");
        if ((state & JVMTI_THREAD_STATE_ALIVE) != 0)
            sj.add("JVMTI_THREAD_STATE_ALIVE");
        if ((state & JVMTI_THREAD_STATE_TERMINATED) != 0)
            sj.add("JVMTI_THREAD_STATE_TERMINATED");
        if ((state & JVMTI_THREAD_STATE_RUNNABLE) != 0)
            sj.add("JVMTI_THREAD_STATE_RUNNABLE");
        if ((state & JVMTI_THREAD_STATE_BLOCKED_ON_MONITOR_ENTER) != 0)
            sj.add("JVMTI_THREAD_STATE_BLOCKED_ON_MONITOR_ENTER");
        if ((state & JVMTI_THREAD_STATE_WAITING) != 0)
            sj.add("JVMTI_THREAD_STATE_WAITING");
        if ((state & JVMTI_THREAD_STATE_WAITING_INDEFINITELY) != 0)
            sj.add("JVMTI_THREAD_STATE_WAITING_INDEFINITELY");
        if ((state & JVMTI_THREAD_STATE_WAITING_WITH_TIMEOUT) != 0)
            sj.add("JVMTI_THREAD_STATE_WAITING_WITH_TIMEOUT");
        if ((state & JVMTI_THREAD_STATE_IN_OBJECT_WAIT) != 0)
            sj.add("JVMTI_THREAD_STATE_IN_OBJECT_WAIT");
        if ((state & JVMTI_THREAD_STATE_PARKED) != 0)
            sj.add("JVMTI_THREAD_STATE_PARKED");
        if ((state & JVMTI_THREAD_STATE_SUSPENDED) != 0)
            sj.add("JVMTI_THREAD_STATE_SUSPENDED");
        if ((state & JVMTI_THREAD_STATE_INTERRUPTED) != 0)
            sj.add("JVMTI_THREAD_STATE_INTERRUPTED");
        if ((state & JVMTI_THREAD_STATE_IN_NATIVE) != 0)
            sj.add("JVMTI_THREAD_STATE_IN_NATIVE");
        String s = sj.toString();
        return s.isEmpty() ? "<empty>" : s;
    }
}
