/*
 * Copyright (c) 2014, 2020 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0, which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the
 * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
 * version 2 with the GNU Classpath Exception, which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 */

package org.glassfish.tyrus.container.jdk.client;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import jakarta.websocket.ClientEndpoint;
import jakarta.websocket.OnClose;
import jakarta.websocket.Session;
import jakarta.websocket.server.ServerEndpoint;

import org.glassfish.tyrus.client.ClientManager;
import org.glassfish.tyrus.client.ClientProperties;
import org.glassfish.tyrus.client.ThreadPoolConfig;
import org.glassfish.tyrus.server.Server;
import org.glassfish.tyrus.test.tools.TestContainer;

import org.junit.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

/**
 * Test thread pool timeout and situation when the thread pool is idle, timeout is running and a client with different
 * thread pool config or timeout setting is initiated. In such cases, the idle thread pool should be terminated
 * and a new one created with the new settings.
 *
 * @author Petr Janouch
 */
public class ThreadPoolTimeoutTest extends TestContainer {

    /**
     * Test that client thread pool is destroyed after close timeout elapses. The opposite - thread pool gets reused
     * if a new client is initiated before the timeout elapses is tested by {@link #testSameConfig()}.
     */
    @Test
    public void testThreadPoolTimeout() {
        Server server = null;
        try {
            server = startServer(AnnotatedServerEndpoint.class);

            ClientManager client1 = ClientManager.createClient(JdkClientContainer.class.getName());
            final AtomicBoolean factory1Used = new AtomicBoolean(false);
            ThreadPoolConfig threadPoolConfig1 =
                    ThreadPoolConfig.defaultConfig()
                                    .setPoolName("testThreadPoolTimeout")
                                    .setThreadFactory(new ThreadFactory() {
                                        @Override
                                        public Thread newThread(Runnable r) {
                                            factory1Used.set(true);
                                            return new Thread(r);
                                        }

                                        // this will decide if the two ThreadPoolConfigs
                                        // equal or not
                                        @Override
                                        public boolean equals(Object obj) {
                                            return true;
                                        }
                                    });
            client1.getProperties().put(ClientProperties.WORKER_THREAD_POOL_CONFIG, threadPoolConfig1);
            client1.getProperties().put(ClientProperties.SHARED_CONTAINER_IDLE_TIMEOUT, 1);

            CountDownLatch sessionCloseLatch = new CountDownLatch(1);
            client1.connectToServer(new AnnotatedClientEndpoint(sessionCloseLatch),
                                    getURI(AnnotatedServerEndpoint.class));
            assertTrue(factory1Used.get());
            terminateServer(server, sessionCloseLatch);

            // wait for the thread pool to timeout and be closed
            Thread.sleep(3000);

            server = startServer(AnnotatedServerEndpoint.class);

            ClientManager client2 = ClientManager.createClient(JdkClientContainer.class.getName());
            final AtomicBoolean factory2Used = new AtomicBoolean(false);
            ThreadPoolConfig threadPoolConfig2 = ThreadPoolConfig.defaultConfig()
                                                                 .setPoolName("testThreadPoolTimeout")
                                                                 .setThreadFactory(new ThreadFactory() {
                                                                     @Override
                                                                     public Thread newThread(Runnable r) {
                                                                         factory2Used.set(true);
                                                                         return new Thread(r);
                                                                     }

                                                                     // this will decide if the two ThreadPoolConfigs
                                                                     // equal or not
                                                                     @Override
                                                                     public boolean equals(Object obj) {
                                                                         return true;
                                                                     }
                                                                 });
            client2.getProperties().put(ClientProperties.WORKER_THREAD_POOL_CONFIG, threadPoolConfig2);
            client2.getProperties().put(ClientProperties.SHARED_CONTAINER_IDLE_TIMEOUT, 1);

            sessionCloseLatch = new CountDownLatch(1);
            client2.connectToServer(new AnnotatedClientEndpoint(sessionCloseLatch),
                                    getURI(AnnotatedServerEndpoint.class));
            assertTrue(factory2Used.get());
            terminateServer(server, sessionCloseLatch);

        } catch (Exception e) {
            e.printStackTrace();
            fail();
        } finally {
            stopServer(server);
        }
    }

    /**
     * Test that if there is an idle thread pool and a client with the same thread pool config is initiated, the idle
     * thread poll will be reused.
     */
    @Test
    public void testSameConfig() {
        Server server = null;
        try {
            server = startServer(AnnotatedServerEndpoint.class);

            ClientManager client1 = ClientManager.createClient(JdkClientContainer.class.getName());
            final AtomicBoolean factory1Used = new AtomicBoolean(false);
            ThreadPoolConfig threadPoolConfig1 = ThreadPoolConfig.defaultConfig()
                                                                 .setPoolName("testSameConfig")
                                                                 .setThreadFactory(new ThreadFactory() {
                                                                     @Override
                                                                     public Thread newThread(Runnable r) {
                                                                         factory1Used.set(true);
                                                                         return new Thread(r);
                                                                     }

                                                                     // this will decide if the two ThreadPoolConfigs
                                                                     // equal or not
                                                                     @Override
                                                                     public boolean equals(Object obj) {
                                                                         return true;
                                                                     }
                                                                 });
            client1.getProperties().put(ClientProperties.WORKER_THREAD_POOL_CONFIG, threadPoolConfig1);

            CountDownLatch sessionCloseLatch = new CountDownLatch(1);
            client1.connectToServer(new AnnotatedClientEndpoint(sessionCloseLatch),
                                    getURI(AnnotatedServerEndpoint.class));
            assertTrue(factory1Used.get());
            terminateServer(server, sessionCloseLatch);

            server = startServer(AnnotatedServerEndpoint.class);

            ClientManager client2 = ClientManager.createClient(JdkClientContainer.class.getName());
            final AtomicBoolean factory2Used = new AtomicBoolean(false);
            ThreadPoolConfig threadPoolConfig2 = ThreadPoolConfig.defaultConfig()
                                                                 .setPoolName("testSameConfig")
                                                                 .setThreadFactory(new ThreadFactory() {
                                                                     @Override
                                                                     public Thread newThread(Runnable r) {
                                                                         factory2Used.set(true);
                                                                         return new Thread(r);
                                                                     }

                                                                     // this will decide if the two ThreadPoolConfigs
                                                                     // equal or not
                                                                     @Override
                                                                     public boolean equals(Object obj) {
                                                                         return true;
                                                                     }
                                                                 });
            client2.getProperties().put(ClientProperties.WORKER_THREAD_POOL_CONFIG, threadPoolConfig2);

            sessionCloseLatch = new CountDownLatch(1);
            client2.connectToServer(new AnnotatedClientEndpoint(sessionCloseLatch),
                                    getURI(AnnotatedServerEndpoint.class));
            assertFalse(factory2Used.get());
            terminateServer(server, sessionCloseLatch);

        } catch (Exception e) {
            e.printStackTrace();
            fail();
        } finally {
            stopServer(server);
        }
    }

    /**
     * Test that if there is an idle thread pool and a client with a different thread pool config is initiated, a new
     * thread pool with the new settings will be crated.
     */
    @Test
    public void testDifferentConfig() {
        Server server = null;
        try {
            server = startServer(AnnotatedServerEndpoint.class);

            ClientManager client1 = ClientManager.createClient(JdkClientContainer.class.getName());
            final AtomicBoolean factory1Used = new AtomicBoolean(false);
            ThreadPoolConfig threadPoolConfig1 = ThreadPoolConfig.defaultConfig().setThreadFactory(new ThreadFactory() {
                @Override
                public Thread newThread(Runnable r) {
                    factory1Used.set(true);
                    return new Thread(r);
                }

                // this will decide if the two ThreadPoolConfigs equal or not
                @Override
                public boolean equals(Object obj) {
                    return false;
                }
            });
            client1.getProperties().put(ClientProperties.WORKER_THREAD_POOL_CONFIG, threadPoolConfig1);

            CountDownLatch sessionCloseLatch = new CountDownLatch(1);
            client1.connectToServer(new AnnotatedClientEndpoint(sessionCloseLatch),
                                    getURI(AnnotatedServerEndpoint.class));
            assertTrue(factory1Used.get());
            terminateServer(server, sessionCloseLatch);

            server = startServer(AnnotatedServerEndpoint.class);

            ClientManager client2 = ClientManager.createClient(JdkClientContainer.class.getName());
            final AtomicBoolean factory2Used = new AtomicBoolean(false);
            ThreadPoolConfig threadPoolConfig2 = ThreadPoolConfig.defaultConfig().setThreadFactory(new ThreadFactory() {
                @Override
                public Thread newThread(Runnable r) {
                    factory2Used.set(true);
                    return new Thread(r);
                }

                // this will decide if the two ThreadPoolConfigs equal or not
                @Override
                public boolean equals(Object obj) {
                    return false;
                }
            });
            client2.getProperties().put(ClientProperties.WORKER_THREAD_POOL_CONFIG, threadPoolConfig2);

            sessionCloseLatch = new CountDownLatch(1);
            client2.connectToServer(new AnnotatedClientEndpoint(sessionCloseLatch),
                                    getURI(AnnotatedServerEndpoint.class));
            assertTrue(factory2Used.get());
            terminateServer(server, sessionCloseLatch);

        } catch (Exception e) {
            e.printStackTrace();
            fail();
        } finally {
            stopServer(server);
        }
    }

    /**
     * Test that if there is an idle thread pool and a client with the same thread pool config, but different thread
     * pool timeout is initiated, a new thread pool with the new timeout will be initiated.
     */
    @Test
    public void testDifferentThreadPoolTimeout() {
        Server server = null;
        try {
            server = startServer(AnnotatedServerEndpoint.class);

            ClientManager client1 = ClientManager.createClient(JdkClientContainer.class.getName());
            final AtomicBoolean factory1Used = new AtomicBoolean(false);
            ThreadPoolConfig threadPoolConfig1 =
                    ThreadPoolConfig.defaultConfig()
                                    .setPoolName("testDifferentThreadPoolTimeout")
                                    .setThreadFactory(new ThreadFactory() {
                                        @Override
                                        public Thread newThread(Runnable r) {
                                            factory1Used.set(true);
                                            return new Thread(r);
                                        }

                                        // this will decide if the two ThreadPoolConfigs
                                        // equal or not
                                        @Override
                                        public boolean equals(Object obj) {
                                            return true;
                                        }
                                    });
            client1.getProperties().put(ClientProperties.WORKER_THREAD_POOL_CONFIG, threadPoolConfig1);
            client1.getProperties().put(ClientProperties.SHARED_CONTAINER_IDLE_TIMEOUT, 1);

            CountDownLatch sessionCloseLatch = new CountDownLatch(1);
            client1.connectToServer(new AnnotatedClientEndpoint(sessionCloseLatch),
                                    getURI(AnnotatedServerEndpoint.class));
            assertTrue(factory1Used.get());
            terminateServer(server, sessionCloseLatch);

            server = startServer(AnnotatedServerEndpoint.class);
            ClientManager client2 = ClientManager.createClient(JdkClientContainer.class.getName());
            final AtomicBoolean factory2Used = new AtomicBoolean(false);
            ThreadPoolConfig threadPoolConfig2 =
                    ThreadPoolConfig.defaultConfig()
                                    .setPoolName("testDifferentThreadPoolTimeout")
                                    .setThreadFactory(new ThreadFactory() {
                                        @Override
                                        public Thread newThread(Runnable r) {
                                            factory2Used.set(true);
                                            return new Thread(r);
                                        }

                                        // this will decide if the two ThreadPoolConfigs
                                        // equal or not
                                        @Override
                                        public boolean equals(Object obj) {
                                            return true;
                                        }
                                    });
            client2.getProperties().put(ClientProperties.WORKER_THREAD_POOL_CONFIG, threadPoolConfig2);
            client2.getProperties().put(ClientProperties.SHARED_CONTAINER_IDLE_TIMEOUT, 2);

            sessionCloseLatch = new CountDownLatch(1);
            client2.connectToServer(new AnnotatedClientEndpoint(sessionCloseLatch),
                                    getURI(AnnotatedServerEndpoint.class));
            assertTrue(factory2Used.get());
            terminateServer(server, sessionCloseLatch);

        } catch (Exception e) {
            e.printStackTrace();
            fail();
        } finally {
            stopServer(server);
        }
    }

    /**
     * Stops server and waits for the connection on the client to be closed.
     */
    private void terminateServer(Server server, CountDownLatch sessionCloseLatch) {
        stopServer(server);
        try {
            /* Tests in the package are sensitive to freeing resources. Unclosed sessions might hinder the next test
                (if the next test requires a fresh client thread pool) */
            assertTrue(sessionCloseLatch.await(5, TimeUnit.SECONDS));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @ServerEndpoint("/threadPoolTimeoutTestEndpoint")
    public static class AnnotatedServerEndpoint {
    }

    @ClientEndpoint
    public static class AnnotatedClientEndpoint {

        private final CountDownLatch sessionCloseLatch;

        AnnotatedClientEndpoint(CountDownLatch sessionCloseLatch) {
            this.sessionCloseLatch = sessionCloseLatch;
        }

        @OnClose
        public void onClose(Session session) {
            sessionCloseLatch.countDown();
        }
    }
}
