/*
 * Copyright (c) 2016, 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
 * @bug 8159377
 * @library /test/lib
 * @summary Tests ObjectFilter on default agent
 * @author Harsha Wardhana B
 * @modules java.management
 * @build DefaultAgentFilterTest
 * @run main/othervm/timeout=600 -XX:+UsePerfData DefaultAgentFilterTest
 */
import java.io.EOFException;
import java.io.File;
import java.io.IOException;
import java.io.InvalidClassException;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.net.BindException;
import java.rmi.UnmarshalException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.management.MBeanServerConnection;
import javax.management.ObjectName;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;

import jdk.test.lib.process.ProcessTools;
import jdk.test.lib.Utils;

public class DefaultAgentFilterTest {

    public static class MyTestObject implements Serializable {

        String a;
        int id;
    }

    public interface TestMBean {

        public void op1(HashSet<Object> params);

        public void op2(String s, HashSet<String> params);

        public void op3(MyTestObject obj, String s, HashMap<String, String> param);
    }

    public static class Test implements TestMBean {

        @Override
        public void op1(HashSet<Object> params) {
            System.out.println("Invoked op1");
        }

        @Override
        public void op2(String s, HashSet<String> params) {
            System.out.println("Invoked op2");
        }

        @Override
        public void op3(MyTestObject obj, String s, HashMap<String, String> param) {
            System.out.println("Invoked op3");
        }
    }

    private static class TestAppRun implements AutoCloseable {

        private Process p;
        private final ProcessBuilder pb;
        private final String name;
        private final AtomicBoolean started = new AtomicBoolean(false);
        private volatile long pid = -1;

        public TestAppRun(ProcessBuilder pb, String name) {
            this.pb = pb;
            this.name = name;
        }

        public synchronized void start() throws Exception {
            if (started.compareAndSet(false, true)) {
                try {
                    AtomicBoolean error = new AtomicBoolean(false);
                    AtomicBoolean bindError = new AtomicBoolean(false);
                    // The predicate below tries to recognise failures.  On a port clash, it sees e.g.
                    // Error: Exception thrown by the agent: java.rmi.server.ExportException: Port already in use: 46481; nested exception is:
                    // ...and will never see "main enter" from TestApp.
                    p = ProcessTools.startProcess(
                            TEST_APP_NAME + "{" + name + "}",
                            pb,
                            (line) -> {
                                if (line.contains("Exception")) {
                                    error.set(true);
                                    bindError.set(line.toLowerCase().contains("port already in use"));
                                    return true; // On Exception, app will never start
                                }
                                return line.contains("main enter");
                            },
                            60, TimeUnit.SECONDS);
                    if (bindError.get()) {
                        throw new BindException("Process could not be started");
                    } else if (error.get()) {
                        throw new RuntimeException();
                    }
                    pid = p.pid();
                } catch (Exception ex) {
                    if (p != null) {
                        p.destroy();
                        p.waitFor();
                    }
                    throw ex;
                }
            }
        }

        public long getPid() {
            return pid;
        }

        public synchronized void stop()
                throws IOException, InterruptedException {
            if (started.compareAndSet(true, false)) {
                p.getOutputStream().write(0);
                p.getOutputStream().flush();
                int ec = p.waitFor();
                if (ec != 0) {
                    StringBuilder msg = new StringBuilder();
                    msg.append("Test application '").append(name);
                    msg.append("' failed with exit code: ");
                    msg.append(ec);
                    System.err.println(msg);
                }
            }
        }

        @Override
        public void close() throws Exception {
            stop();
        }
    }

    private static final String TEST_APP_NAME = "TestApp";
    private static final int FREE_PORT_ATTEMPTS = 10;

    private static void testDefaultAgent(String propertyFile) throws Exception {
        for (int i = 1; i <= FREE_PORT_ATTEMPTS; i++) {
            int port = Utils.getFreePort();
            System.out.println("Attempting testDefaultAgent(" + propertyFile + ") with port: " + port);
            try {
                testDefaultAgent(propertyFile, port);
                break;  // return succesfully
            } catch (BindException b) {
                // Retry with new port.  Throw if last iteration:
                if (i == FREE_PORT_ATTEMPTS) {
                    throw(b);
                }
            }
        }
    }

    private static void testDefaultAgent(String propertyFile, int port) throws Exception {
        String propFile = System.getProperty("test.src") + File.separator + propertyFile;
        List<String> pbArgs = new ArrayList<>();
        pbArgs.add("-XX:+UsePerfData");

        String[] args = new String[]{
            "-Dcom.sun.management.jmxremote.port=" + port,
            "-Dcom.sun.management.jmxremote.authenticate=false",
            "-Dcom.sun.management.jmxremote.ssl=false",
            "-Dcom.sun.management.config.file=" + propFile
        };
        pbArgs.addAll(Arrays.asList(args));
        pbArgs.add(TEST_APP_NAME);

        ProcessBuilder pb = ProcessTools.createTestJavaProcessBuilder(
                pbArgs.toArray(new String[pbArgs.size()])
        );

        try (TestAppRun s = new TestAppRun(pb, DefaultAgentFilterTest.class.getSimpleName())) {
            s.start();
            JMXServiceURL url = testConnect(port);
            testMBeanOperations(url);
        }
    }

    private static JMXServiceURL testConnect(int port) throws Exception {
        EOFException lastException = null;
        JMXServiceURL url = null;
        // factor adjusted timeout (5 seconds) for the RMI to become available
        long timeout = System.currentTimeMillis() + Utils.adjustTimeout(5000);
        do {
            lastException = null;
            try {
                Registry registry = LocateRegistry.getRegistry(port);
                String[] relist = registry.list();
                for (int i = 0; i < relist.length; ++i) {
                    System.out.println("Got registry: " + relist[i]);
                }
                String jmxUrlStr = String.format(
                        "service:jmx:rmi:///jndi/rmi://localhost:%d/jmxrmi",
                        port);
                url = new JMXServiceURL(jmxUrlStr);

                try (JMXConnector c = JMXConnectorFactory.connect(url, null)) {
                    MBeanServerConnection conn = c.getMBeanServerConnection();
                    ObjectName name = new ObjectName("jtreg:type=Test");
                    conn.createMBean(Test.class.getName(), name);
                }
            } catch (Exception ex) {
                if (ex instanceof EOFException) {
                    lastException = (EOFException) ex;
                    System.out.println("Error establishing RMI connection. Retrying in 500ms.");
                    Thread.sleep(500);
                } else {
                    throw ex;
                }
            }
        } while (lastException != null && System.currentTimeMillis() < timeout);
        if (lastException != null) {
            throw lastException;
        }
        return url;
    }

    public static void main(String[] args) throws Exception {
        System.out.println("---" + DefaultAgentFilterTest.class.getName() + "-main: starting ...");

        try {
            // filter DefaultAgentFilterTest$MyTestObject
            testDefaultAgent("mgmt1.properties");
            System.out.println("----\tTest FAILED !!");
            throw new RuntimeException("---" + DefaultAgentFilterTest.class.getName() + " - No exception reported");
        } catch (Exception ex) {
            if (ex instanceof InvalidClassException) {
                System.out.println("----\tTest PASSED !!");
            } else if (ex instanceof UnmarshalException
                    && ((UnmarshalException) ex).getCause() instanceof InvalidClassException) {
                System.out.println("----\tTest PASSED !!");
            } else {
                System.out.println(ex);
                System.out.println("----\tTest FAILED !!");
                throw ex;
            }
        }
        try {
            // filter non-existent class
            testDefaultAgent("mgmt2.properties");
            System.out.println("----\tTest PASSED !!");
        } catch (Exception ex) {
            System.out.println(ex);
            System.out.println("----\tTest FAILED !!");
            throw ex;
        }
        System.out.println("---" + DefaultAgentFilterTest.class.getName() + "-main: finished ...");
    }

    private static void testMBeanOperations(JMXServiceURL serverUrl) throws Exception {
        Map<String, Object> clientEnv = new HashMap<>(1);
        ObjectName name = new ObjectName("jtreg:type=Test");
        try (JMXConnector client = JMXConnectorFactory.connect(serverUrl, clientEnv)) {
            MBeanServerConnection conn = client.getMBeanServerConnection();

            HashSet<String> set = new HashSet<>();
            set.add("test1");
            set.add("test2");

            String a = "A";

            Object[] params1 = {set};
            String[] sig1 = {HashSet.class.getName()};
            conn.invoke(name, "op1", params1, sig1);

            Object[] params2 = {a, set};
            String[] sig2 = {String.class.getName(), HashSet.class.getName()};
            conn.invoke(name, "op2", params2, sig2);

            HashMap<String, String> map = new HashMap<>();
            map.put("a", "A");
            map.put("b", "B");

            Object[] params3 = {new MyTestObject(), a, map};
            String[] sig3 = {MyTestObject.class.getName(), String.class.getName(),
                HashMap.class.getName()};
            conn.invoke(name, "op3", params3, sig3);
        }
    }
}

class TestApp {

    private static void doSomething() throws IOException {
        int r = System.in.read();
        System.out.println("read: " + r);
    }

    public static void main(String args[]) throws Exception {
        System.out.println("main enter");
        System.out.flush();
        doSomething();
        System.out.println("main exit");
    }
}
