/*
 * Copyright (c) 2001, 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.
 */

package nsk.jdi.ClassPrepareEvent.thread;

import com.sun.jdi.*;
import com.sun.jdi.event.*;
import com.sun.jdi.request.*;

import java.io.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.Iterator;

import nsk.share.*;
import nsk.share.jpda.*;
import nsk.share.jdi.*;


// This class is the debugger in the test

public class thread001 {
    static final int PASSED = 0;
    static final int FAILED = 2;
    static final int JCK_STATUS_BASE = 95;

    static final int TIMEOUT_DELTA = 1000; // milliseconds

    static final String COMMAND_READY = "ready";
    static final String COMMAND_QUIT  = "quit";
    static final String COMMAND_RUN   = "run";
    static final String COMMAND_DONE  = "done";
    static final String COMMAND_ERROR = "error";

    static final String PACKAGE_NAME = "nsk.jdi.ClassPrepareEvent.thread";
    static final String DEBUGEE_NAME = PACKAGE_NAME + ".thread001a";

    static private Debugee debuggee;
    static private VirtualMachine vm;
    static private IOPipe pipe;
    static private Log log;
    static private ArgumentHandler argHandler;

    static private ClassPrepareRequest checkedRequest;
    static private ThreadReference eventThread;

    static private String checkedThreads[][] = {
                {"main",        "thread001a",           "0"},
                {"main",        "InnerThread",          "0"},
                {"innerThread", "OuterThread",          "0"},
                {"innerThread", "ClassForInnerThread",  "0"},
                {"outerThread", "ClassForOuterThread",  "0"}
           };

    static private int threadStatus;

    static private volatile boolean testFailed;
    static private CountDownLatch eventsReceivedLatch;
    static private int eventTimeout;

    public static void main (String argv[]) {
         int result = run(argv,System.out);
         if (result != 0) {
             throw new RuntimeException("TEST FAILED with result " + result);
         }
    }

    public static int run(final String args[], final PrintStream out) {
        String command;

        testFailed = false;
        eventsReceivedLatch = new CountDownLatch(1);

        argHandler = new ArgumentHandler(args);
        log = new Log(out, argHandler);
        eventTimeout = argHandler.getWaitTime() * 60 * 1000; // milliseconds

        // launch debugee
        Binder binder = new Binder(argHandler, log);
        log.display("Connecting to debuggee");
        debuggee = binder.bindToDebugee(DEBUGEE_NAME);
        debuggee.redirectStderr(log, "refType001a >");
        pipe = debuggee.createIOPipe();
        vm = debuggee.VM();

        // create request and wait for expected events
        try {

            // create request for ClassPrepareEvent
            log.display("Creating request for ClassPrepareEvent");
            EventRequestManager erManager = debuggee.VM().eventRequestManager();
            if ((checkedRequest = erManager.createClassPrepareRequest()) == null) {
                throw new Failure("TEST FAILED: unable to create ClassPrepareRequest");
            }
            log.display("ClassPrepareRequest is created");

            checkedRequest.setSuspendPolicy(EventRequest.SUSPEND_ALL);
//         checkedRequest.enable();

            switch (checkedRequest.suspendPolicy()) {
                 case EventRequest.SUSPEND_NONE:
                      log.display("Suspend policy is SUSPEND_NONE");
                      break;
                 case EventRequest.SUSPEND_ALL:
                      log.display("Suspend policy is SUSPEND_ALL");
                      break;
                 case EventRequest.SUSPEND_EVENT_THREAD:
                      log.display("Suspend policy is SUSPEND_EVENT_THREAD");
                      break;
                 default:
                      throw new Failure("TEST BUG: Unknown suspend policy: " + checkedRequest.suspendPolicy());
            }

            // define separate thread to handle received events
            class EventHandler extends Thread {

                public void run() {

                    boolean isConnected = true;
                    boolean allEventsReceived = false;
                    // handle events until debuggee is disconnected
                    while (isConnected) {
                        EventSet eventSet = null;
                        try {
                            eventSet = vm.eventQueue().remove(TIMEOUT_DELTA);
                        } catch (InterruptedException e) {
                            throw new Failure("Unexpected InterruptedException while receiving events: " + e);
                        }

                        if (eventSet == null) {
                            continue;
                        }

                        // handle each event
                        EventIterator eventIterator = eventSet.eventIterator();
                        while (eventIterator.hasNext()) {
                            Event event = eventIterator.nextEvent();
//                            log.display("\nEvent received:\n  " + event);

                            if (event instanceof VMDeathEvent || event instanceof VMDisconnectEvent) {
                                log.display("eventHandler got " + event);
                                isConnected = false;
                            } else  if (event instanceof ClassPrepareEvent) {
                                ClassPrepareEvent castedEvent = (ClassPrepareEvent)event;
                                log.display("\nClassPrepareEvent received:\n  " + event);

                                EventRequest eventRequest = castedEvent.request();
                                if (!(checkedRequest.equals(eventRequest))) {
                                    log.complain("FAILURE 1: eventRequest is not equal to checked request");
                                    testFailed = true;
                                }

                                VirtualMachine eventMachine = castedEvent.virtualMachine();
                                if (!(vm.equals(eventMachine))) {
                                    log.complain("FAILURE 2: eventVirtualMachine is not equal to checked vm");
                                    testFailed = true;
                                }

                                // test method ClassPrepareEvent.thread()
                                eventThread = castedEvent.thread();
                                String threadName = eventThread.name();
                                if (eventThread == null) {
                                    log.complain("FAILURE 3: ClassPrepareEvent.thread() returns null");
                                    testFailed = true;
                                } else if ((threadName == null) || (threadName.equals(""))) {
                                    log.complain("FAILURE 4: thread reference has invalid empty name");
                                    testFailed = true;
                                }

                                ReferenceType refType = castedEvent.referenceType();
                                if (refType == null) {
                                    log.complain("FAILURE 5: ClassPrepareEvent.referenceType() returns null");
                                    testFailed = true;
                                } else {

                                     String className = refType.name();
                                     if ( className.startsWith(PACKAGE_NAME)) {

                                          log.display("Class " + className + " prepared in thread " + threadName);

                                          // Check that thread is in VirtualMachine.allThreads() list
                                          boolean found = false;
                                          Iterator threadsList = vm.allThreads().iterator();
                                          while (!found && threadsList.hasNext()) {
                                               found = eventThread.equals((ThreadReference)threadsList.next());
                                          }
                                          if (!found) {
                                              log.complain("FAILURE 6: " + threadName + " is not in debuggee's allThreads() list");
                                              testFailed = true;
                                          }

                                          // Check that all expected debuggee's thread create ClassPrepareEvent only once
                                          for (int i = 0; i < checkedThreads.length; i++) {
                                               if (threadName.equals(checkedThreads[i][0]) && className.endsWith(checkedThreads[i][1])) {
                                                    if (checkedThreads[i][2] == "0") {
                                                         checkedThreads[i][2] = "1";
                                                    } else {
                                                         log.complain("FAILURE 7: ClassPrepareEvent for " + threadName + " is received more that once");
                                                         testFailed = true;
                                                    }
                                               }
                                          }

                                          // check that thread is correctly suspended
                                          if (checkedRequest.suspendPolicy() == EventRequest.SUSPEND_ALL ||
                                              checkedRequest.suspendPolicy() == EventRequest.SUSPEND_EVENT_THREAD) {

                                               if (eventThread.isSuspended()) {
                                                   log.display("Thread " + threadName + " is correctly suspended");
                                               } else {
                                                   log.complain("FAILURE 7: Thread " + threadName + " is not suspended");
                                                   testFailed = true;
                                               }
                                          }

                                          // Check that all expected ClassPrepareEvent(s) are received.
                                          if (!allEventsReceived) {
                                              allEventsReceived = true;
                                              for (int i = 0; i < checkedThreads.length; i++) {
                                                  // checkedTypes[i][2] is "0" initially,
                                                  // "1" after corresponding ClassPrepareEvent is received.
                                                  if (checkedThreads[i][2] == "0") {
                                                      allEventsReceived = false;
                                                      break;
                                                   }
                                              }
                                              if (allEventsReceived) {
                                                  eventsReceivedLatch.countDown();
                                              }
                                          }
                                     }
                                 }
                            }

                            // ignore each oter event

                        } // event handled

//                        log.display("Resuming event set");
                        if (isConnected) {
                            eventSet.resume();
                        }

                    } // event set handled

                    log.display("eventHandler completed");

                } // for run()

            } // for EventHandler

            EventHandler eventHandler = new EventHandler();
            log.display("Starting eventHandler");
            eventHandler.start();

            // enable event request and resume debugee
            log.display("Enabling ClassPrepareEvent request");
            checkedRequest.enable();

            log.display("Resuming debuggee");
            debuggee.resume();

            // wait for debugee started
            log.display("Waiting for command: " + COMMAND_READY);
            command = pipe.readln();
            if (!command.equals(COMMAND_READY)) {
                throw new Failure("TEST BUG: unexpected debuggee's command: " + command);
            }

            // force debugee to start another thread
            log.display("Sending a command: " + COMMAND_RUN);
            pipe.println(COMMAND_RUN);

            // wait for debugee started requested thread
            log.display("Waiting for command: " + COMMAND_DONE);
            command = pipe.readln();
            if (!command.equals(COMMAND_DONE)) {
                log.complain("TEST BUG: unexpected debuggee's command: " + command);
                testFailed = true;
            }

            // wait for all expected events received or timeout exceeds
            try {
                if (!eventsReceivedLatch.await(eventTimeout, TimeUnit.MILLISECONDS)) {
                    log.complain("FAILURE 20: Timeout waiting for all events was exceeded");
                    testFailed = true;
                }
            } catch (InterruptedException e) {
                  log.complain("TEST INCOMPLETE: InterruptedException caught while waiting for eventHandler's death");
                  testFailed = true;
            }

            // check that all expected debuggee's threads created ClassPrepareEvent
            for (int i = 0; i < checkedThreads.length; i++) {
                if (checkedThreads[i][2].equals("0")) {
                    log.complain("FAILURE 9: ClassPrepareEvent for " + checkedThreads[i][1] +
                         " in thread " + checkedThreads[i][0] + " is not received");
                    testFailed = true;
                }
            }

        } catch (Failure e) {
            log.complain("TEST FAILURE: " + e.getMessage());
            testFailed = true;
        } catch (Exception e) {
            log.complain("Unexpected exception: " + e);
            e.printStackTrace(out);
            testFailed = true;
        } finally {


            // disable event requests to prevent appearance of further events
            if (checkedRequest != null && checkedRequest.isEnabled()) {
                log.display("Disabling StepEvent request");
                checkedRequest.disable();
            }

            // force debugee to exit
            log.display("Sending command: " + COMMAND_QUIT);
            pipe.println(COMMAND_QUIT);

            // wait for debuggee exits and analize its exit code
            log.display("Waiting for debuggee terminating");
            int debuggeeStatus = debuggee.endDebugee();
            if (debuggeeStatus == PASSED + JCK_STATUS_BASE) {
                log.display("Debuggee PASSED with exit code: " + debuggeeStatus);
            } else {
                log.complain("Debuggee FAILED with exit code: " + debuggeeStatus);
                testFailed = true;
            }
        }

        // check test results
        if (testFailed) {
            log.complain("TEST FAILED");
            return FAILED;
        }

        log.display("TEST PASSED");
        return PASSED;
    }
}
