/*
 * 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.test.standard_config;

import java.io.IOException;
import java.net.URI;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

import jakarta.websocket.ClientEndpoint;
import jakarta.websocket.CloseReason;
import jakarta.websocket.ContainerProvider;
import jakarta.websocket.DeploymentException;
import jakarta.websocket.Endpoint;
import jakarta.websocket.EndpointConfig;
import jakarta.websocket.OnMessage;
import jakarta.websocket.OnOpen;
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.server.Server;
import org.glassfish.tyrus.test.tools.TestContainer;

import org.glassfish.grizzly.http.server.HttpHandler;
import org.glassfish.grizzly.http.server.HttpServer;
import org.glassfish.grizzly.http.server.Request;
import org.glassfish.grizzly.http.server.Response;

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

/**
 * Tests life cycle of executor services managed by the client manager.
 *
 * @author Petr Janouch
 */
public class ClientExecutorsManagementTest extends TestContainer {

    /**
     * Test basic executor services life cycle.
     */
    @Test
    public void testBasicLifecycle() {
        Server server = null;
        try {
            server = startServer(AnnotatedServerEndpoint.class);
            ClientManager clientManager1 = createClient();
            Session session1 = clientManager1
                    .connectToServer(AnnotatedClientEndpoint.class, getURI(AnnotatedServerEndpoint.class));
            ExecutorService executorService1 = clientManager1.getExecutorService();
            ScheduledExecutorService scheduledExecutorService1 = clientManager1.getScheduledExecutorService();

            ClientManager clientManager2 = createClient();
            Session session2 = clientManager2
                    .connectToServer(AnnotatedClientEndpoint.class, getURI(AnnotatedServerEndpoint.class));
            ExecutorService executorService2 = clientManager2.getExecutorService();
            ScheduledExecutorService scheduledExecutorService2 = clientManager2.getScheduledExecutorService();

            Session session3 = clientManager1
                    .connectToServer(AnnotatedClientEndpoint.class, getURI(AnnotatedServerEndpoint.class));
            ExecutorService executorService3 = clientManager1.getExecutorService();
            ScheduledExecutorService scheduledExecutorService3 = clientManager1.getScheduledExecutorService();

            // executors from the same container should be the same
            assertTrue(executorService1 == executorService3);
            assertTrue(scheduledExecutorService1 == scheduledExecutorService3);

            assertTrue(executorService1 != executorService2);
            assertTrue(scheduledExecutorService1 != scheduledExecutorService2);

            assertFalse(executorService1.isShutdown());
            assertFalse(scheduledExecutorService1.isShutdown());

            assertFalse(executorService2.isShutdown());
            assertFalse(scheduledExecutorService2.isShutdown());

            session1.close();
            session2.close();

            assertTrue(executorService2.awaitTermination(5, TimeUnit.SECONDS));
            assertTrue(scheduledExecutorService2.awaitTermination(5, TimeUnit.SECONDS));

            // closing session1 should not close executorService1 and scheduledExecutorService1 as it is still used
            // by session3
            assertFalse(executorService1.isShutdown());
            assertFalse(scheduledExecutorService1.isShutdown());

            session3.close();

            assertTrue(executorService1.awaitTermination(5, TimeUnit.SECONDS));
            assertTrue(scheduledExecutorService1.awaitTermination(5, TimeUnit.SECONDS));
        } catch (Exception e) {
            e.printStackTrace();
            fail();
        } finally {
            stopServer(server);
        }
    }

    /**
     * Test that different situations that can cause connect to fail will not cause container resources not to be
     * freed.
     * <p/>
     * (Client manager counts active connections. This test tests, that connection failures caused by different
     * situations are registered by the connection counter.)
     */
    @Test
    public void testConnectionFail() {
        Server server = null;
        try {
            server = startServer(AnnotatedServerEndpoint.class);
            ClientManager clientManager = createClient();

            Session session =
                    clientManager.connectToServer(AnnotatedClientEndpoint.class, getURI(AnnotatedServerEndpoint.class));
            try {
                clientManager.connectToServer(FaultyEndpoint.class, getURI(AnnotatedServerEndpoint.class));
                fail();
            } catch (Exception e) {
                // exception is expected
            }

            try {
                clientManager.connectToServer(AnnotatedClientEndpoint.class, URI.create("ws://nonExistentServer.com"));
                fail();
            } catch (Exception e) {
                // exception is expected
            }

            try {
                clientManager.connectToServer(AnnotatedClientEndpoint.class, getURI("/nonExistentEndpoint"));
                fail();
            } catch (Exception e) {
                // exception is expected
            }

            CountDownLatch blockResponseLatch = new CountDownLatch(1);
            HttpServer lazyServer = getLazyServer(blockResponseLatch);
            clientManager.getProperties().put(ClientProperties.HANDSHAKE_TIMEOUT, 2000);
            try {
                clientManager.connectToServer(
                        AnnotatedClientEndpoint.class, URI.create("ws://localhost:8026/lazyServer"));
                fail();
            } catch (Exception e) {
                // exception is expected
            } finally {
                blockResponseLatch.countDown();
                lazyServer.shutdown();
            }

            ExecutorService executorService = clientManager.getExecutorService();
            ScheduledExecutorService scheduledExecutorService = clientManager.getScheduledExecutorService();

            assertFalse(executorService.isShutdown());
            assertFalse(scheduledExecutorService.isShutdown());

            // closing the only successfully established connection should cause the executors to be released
            session.close();

            assertTrue(executorService.awaitTermination(5, TimeUnit.SECONDS));
            assertTrue(scheduledExecutorService.awaitTermination(5, TimeUnit.SECONDS));
        } catch (Exception e) {
            e.printStackTrace();
            fail();
        } finally {
            stopServer(server);
        }
    }

    /**
     * Test that if executor services have been destroyed, new ones will be created if the client manager creates new
     * connections.
     */
    @Test
    public void testConnectAfterClose() {
        Server server = null;
        try {
            server = startServer(AnnotatedServerEndpoint.class);
            ClientManager clientManager = createClient();

            Session session =
                    clientManager.connectToServer(AnnotatedClientEndpoint.class, getURI(AnnotatedServerEndpoint.class));
            session.close();

            Session session2 =
                    clientManager.connectToServer(AnnotatedClientEndpoint.class, getURI(AnnotatedServerEndpoint.class));

            ExecutorService executorService = clientManager.getExecutorService();
            ScheduledExecutorService scheduledExecutorService = clientManager.getScheduledExecutorService();

            assertFalse(executorService.isShutdown());
            assertFalse(scheduledExecutorService.isShutdown());

            session2.close();

            assertTrue(executorService.awaitTermination(5, TimeUnit.SECONDS));
            assertTrue(scheduledExecutorService.awaitTermination(5, TimeUnit.SECONDS));
        } catch (Exception e) {
            e.printStackTrace();
            fail();
        } finally {
            stopServer(server);
        }
    }

    /**
     * Test that executor services get destroyed if reconnect is used.
     */
    @Test
    public void testReconnect() {
        Server server = null;
        try {
            server = startServer(ReconnectServerEndpoint.class);
            ClientManager clientManager = createClient();
            clientManager.getProperties().put(ClientProperties.RECONNECT_HANDLER, new ClientManager.ReconnectHandler() {
                private int counter = 0;

                @Override
                public boolean onDisconnect(CloseReason closeReason) {
                    counter++;

                    // reconnect once
                    if (counter < 2) {
                        return true;
                    }

                    return false;
                }

                @Override
                public long getDelay() {
                    return 0;
                }
            });

            final AtomicReference<Session> session = new AtomicReference<Session>();
            // connect once and reconnect once
            final CountDownLatch onOpenLatch = new CountDownLatch(2);
            clientManager.connectToServer(new Endpoint() {
                @Override
                public void onOpen(Session s, EndpointConfig config) {
                    session.set(s);
                    onOpenLatch.countDown();
                }
            }, getURI(ReconnectServerEndpoint.class));

            ExecutorService executorService = clientManager.getExecutorService();
            ScheduledExecutorService scheduledExecutorService = clientManager.getScheduledExecutorService();

            // force reconnect
            session.get().getBasicRemote().sendText("Close");
            assertTrue(onOpenLatch.await(5, TimeUnit.SECONDS));

            ExecutorService executorService2 = clientManager.getExecutorService();
            ScheduledExecutorService scheduledExecutorService2 = clientManager.getScheduledExecutorService();

            assertFalse(executorService2.isShutdown());
            assertFalse(scheduledExecutorService2.isShutdown());

            session.get().close();

            assertTrue(executorService.awaitTermination(5, TimeUnit.SECONDS));
            assertTrue(scheduledExecutorService.awaitTermination(5, TimeUnit.SECONDS));
            assertTrue(executorService2.awaitTermination(5, TimeUnit.SECONDS));
            assertTrue(scheduledExecutorService2.awaitTermination(5, TimeUnit.SECONDS));
        } catch (Exception e) {
            e.printStackTrace();
            fail();
        } finally {
            stopServer(server);
        }
    }

    /**
     * Test that calling shut down on client manager does not clash with automatic executors management and does not
     * cause an error when there are still some sessions open.
     */
    @Test
    public void explicitShutDownTest() {
        Server server = null;
        try {
            server = startServer(AnnotatedServerEndpoint.class);
            ClientManager clientManager = createClient();
            Session session =
                    clientManager.connectToServer(AnnotatedClientEndpoint.class, getURI(AnnotatedServerEndpoint.class));
            clientManager.shutdown();
            session.close();
        } catch (Exception e) {
            e.printStackTrace();
            fail();
        } finally {
            stopServer(server);
        }
    }

    /**
     * Test that managed executors do not get closed if the last connection is closed.
     */
    @Test
    public void managedExecutorsTest() {
        if (System.getProperty("tyrus.test.host") == null) {
            return;
        }

        Server server = null;
        try {
            server = startServer(ManagedContainerEndpoint.class, AnnotatedServerEndpoint.class);
            ClientManager clientManager = createClient();
            CountDownLatch messageLatch = new CountDownLatch(1);
            Session session = clientManager
                    .connectToServer(new AnnotatedClientEndpoint(messageLatch), getURI(ManagedContainerEndpoint.class));
            session.getBasicRemote().sendText(getURI(AnnotatedServerEndpoint.class).toString());

            assertTrue(messageLatch.await(5, TimeUnit.SECONDS));
        } catch (Exception e) {
            e.printStackTrace();
            fail();
        } finally {
            stopServer(server);
        }
    }

    private HttpServer getLazyServer(final CountDownLatch blockResponseLatch) throws IOException {
        HttpServer server = HttpServer.createSimpleServer("/lazyServer", "localhost", 8026);
        server.getServerConfiguration().addHttpHandler(
                new HttpHandler() {
                    public void service(Request request, Response response) throws Exception {
                        blockResponseLatch.await(1, TimeUnit.MINUTES);
                    }
                }
        );

        server.start();
        return server;
    }

    @ServerEndpoint("/clientExecutorsEchoEndpoint")
    public static class AnnotatedServerEndpoint {

        @OnOpen
        public void onOpen(Session session) {
            session.setMaxIdleTimeout(0);
        }

    }

    @ClientEndpoint
    public static class AnnotatedClientEndpoint {

        private final CountDownLatch messageLatch;

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

        @OnMessage
        public void onMessage(Session session, String message) {
            messageLatch.countDown();
        }
    }

    public static class FaultyEndpoint {
    }

    @ServerEndpoint("/clientExecutorsReconnectEndpoint")
    public static class ReconnectServerEndpoint {

        @OnMessage
        public void onMessage(String message, Session session) throws IOException {
            session.close();
        }
    }

    @ServerEndpoint("/managedContainerEndpoint")
    public static class ManagedContainerEndpoint {

        @OnMessage
        public void onMessage(Session session, String message) throws IOException, DeploymentException {
            // one option for obtaining a container
            Session s = ContainerProvider.getWebSocketContainer()
                                         .connectToServer(AnnotatedClientEndpoint.class, URI.create(message));
            s.close();

            // another option for obtaining a container
            s = session.getContainer().connectToServer(AnnotatedClientEndpoint.class, URI.create(message));
            s.close();

            /* An IllegalStateException is thrown if any of the lifecycle operations are invoked on a managed executor
            service, so if the test gets here it means Tyrus does not try to shut down the managed executor service */
            session.getBasicRemote().sendText("OK");
        }
    }
}
