/*
 * Copyright (c) 2014, 2021 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.tests.servlet.maxsessions;

import java.io.IOException;
import java.net.URI;
import java.util.HashSet;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import jakarta.websocket.ClientEndpointConfig;
import jakarta.websocket.CloseReason;
import jakarta.websocket.DeploymentException;
import jakarta.websocket.Endpoint;
import jakarta.websocket.EndpointConfig;
import jakarta.websocket.MessageHandler;
import jakarta.websocket.Session;
import jakarta.websocket.server.ServerEndpointConfig;

import org.glassfish.tyrus.client.ClientManager;
import org.glassfish.tyrus.core.TyrusServerEndpointConfig;
import org.glassfish.tyrus.core.TyrusWebSocketEngine;
import org.glassfish.tyrus.server.Server;
import org.glassfish.tyrus.server.TyrusServerConfiguration;
import org.glassfish.tyrus.test.tools.TestContainer;

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

import junit.framework.Assert;

/**
 * Tests configuration and implementation of maximal number of open sessions.
 *
 * @author Ondrej Kosatka
 */
public class MaxSessionsAppTest extends TestContainer {

    private static final String CONTEXT_PATH = "/max-sessions-per-app-test";

    private static final int NUMBER_OF_ENDPOINTS = 3;
    private static final int NUMBER_OF_CLIENTS_OVER_LIMIT = 3;
    private static final int NUMBER_OF_CLIENT_SESSIONS = MaxSessionPerAppApplicationConfig.MAX_SESSIONS_PER_APP
            + NUMBER_OF_CLIENTS_OVER_LIMIT;

    public MaxSessionsAppTest() {
        setContextPath(CONTEXT_PATH);
    }

    public static class ServerDeployApplicationConfig extends TyrusServerConfiguration {
        public ServerDeployApplicationConfig() {
            super(new HashSet<Class<?>>() {
                {
                    add(ServiceEndpoint.class);
                }
            }, new HashSet<ServerEndpointConfig>() {
                {
                    for (final String path : MaxSessionPerAppApplicationConfig.PATHS) {
                        add(TyrusServerEndpointConfig.Builder.create(EchoEndpoint.class, path).build());
                    }
                }
            });
        }
    }

    public void maxSessions() throws DeploymentException, InterruptedException, IOException {
        final ClientManager client = createClient();

        final CountDownLatch closeNormalLatch = new CountDownLatch(MaxSessionPerAppApplicationConfig
                                                                           .MAX_SESSIONS_PER_APP);
        final CountDownLatch closeOverLimitLatch = new CountDownLatch(NUMBER_OF_CLIENTS_OVER_LIMIT);

        final Session[] sessions = new Session[NUMBER_OF_CLIENT_SESSIONS];

        for (int i = 0; i < NUMBER_OF_CLIENT_SESSIONS; i++) {
            URI uri = getURI("/echo" + ((i % NUMBER_OF_ENDPOINTS) + 1));
            System.out.println(uri);
            sessions[i] = client.connectToServer(new Endpoint() {
                @Override
                public void onOpen(Session session, EndpointConfig EndpointConfig) {
                    session.addMessageHandler(new MessageHandler.Whole<String>() {
                        @Override
                        public void onMessage(String message) {
                            System.out.println(message);
                        }
                    });
                    try {
                        session.getBasicRemote().sendText("Do or do not, there is no try.");
                    } catch (IOException e) {
                        // session can be closed by server
                    }
                }

                @Override
                public void onClose(Session session, CloseReason closeReason) {
                    System.out.println("Client-side close reason: " + closeReason);
                    if (closeReason.getCloseCode().getCode() == CloseReason.CloseCodes.NORMAL_CLOSURE.getCode()) {
                        closeNormalLatch.countDown();
                    } else if (closeReason.getCloseCode().getCode()
                            == CloseReason.CloseCodes.TRY_AGAIN_LATER.getCode()) {
                        closeOverLimitLatch.countDown();
                    }
                }
            }, ClientEndpointConfig.Builder.create().build(), uri);
        }

        assertTrue(String.format("Session should be closed just %d times with close code 1013 - Try Again Later",
                                 NUMBER_OF_CLIENTS_OVER_LIMIT), closeOverLimitLatch.await(3, TimeUnit.SECONDS));

        for (int i = 0; i < NUMBER_OF_CLIENT_SESSIONS; i++) {
            System.out.printf("session[%d] is open? %s\n", i, sessions[i].isOpen());
            if (i < MaxSessionPerAppApplicationConfig.MAX_SESSIONS_PER_APP) {
                assertTrue("Session in limit is closed!", sessions[i].isOpen());
                sessions[i].close();
            } else {
                assertFalse("Session over limit should be closed!", sessions[i].isOpen());
            }
        }

        Assert.assertTrue(
                "Number of normal closures should be the same as the limit!",
                closeNormalLatch.await(1, TimeUnit.SECONDS));

        final CountDownLatch messageReceivedLatch = new CountDownLatch(1);
        Session session = client.connectToServer(new Endpoint() {
            @Override
            public void onOpen(Session session, EndpointConfig EndpointConfig) {
                session.addMessageHandler(new MessageHandler.Whole<String>() {
                    @Override
                    public void onMessage(String message) {
                        messageReceivedLatch.countDown();
                    }
                });
                try {
                    session.getBasicRemote().sendText("New session is available after close all of the opened "
                                                              + "sessions.");
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

        }, ClientEndpointConfig.Builder.create().build(), getURI("/echo1"));

        assertTrue(messageReceivedLatch.await(1, TimeUnit.SECONDS));

        session.close();

        testViaServiceEndpoint(client, ServiceEndpoint.class, POSITIVE, "/echo");
        testViaServiceEndpoint(client, ServiceEndpoint.class, POSITIVE, "reset");

        Thread.sleep(100);
    }

    @Test
    public void maxSessionsSingle() throws DeploymentException, InterruptedException, IOException {
        getServerProperties().put(TyrusWebSocketEngine.MAX_SESSIONS_PER_APP,
                                  MaxSessionPerAppApplicationConfig.MAX_SESSIONS_PER_APP);

        Server server = startServer(ServerDeployApplicationConfig.class);
        try {
            maxSessions();
            System.out.println("==================");
        } finally {
            stopServer(server);
        }
    }

    @Test
    public void maxSessionsDurable() throws DeploymentException, InterruptedException, IOException {
        getServerProperties().put(TyrusWebSocketEngine.MAX_SESSIONS_PER_APP,
                                  MaxSessionPerAppApplicationConfig.MAX_SESSIONS_PER_APP);

        Server server = startServer(ServerDeployApplicationConfig.class);
        try {
            for (int i = 0; i < 10; i++) {
                maxSessions();
                System.out.println("==================");
            }
        } finally {
            stopServer(server);
        }
    }
}
