/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.apache.axis2.jaxws.sample;

import org.apache.axis2.jaxws.TestLogger;
import org.apache.axis2.jaxws.sample.parallelasync.common.CallbackHandler;
import org.apache.axis2.jaxws.sample.parallelasync.server.AsyncPort;
import org.apache.axis2.jaxws.sample.parallelasync.server.AsyncService;
import org.apache.axis2.testutils.Axis2Server;
import org.junit.ClassRule;
import org.junit.Test;
import org.test.parallelasync.CustomAsyncResponse;
import org.test.parallelasync.SleepResponse;

import jakarta.xml.ws.BindingProvider;
import jakarta.xml.ws.Response;

import static org.apache.axis2.jaxws.framework.TestUtils.await;
import static org.apache.axis2.jaxws.framework.TestUtils.withRetry;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.util.Map;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

/**
 * Tests for Asynchrony in JAX-WS. Most of the simple invokeAsync/async
 * exceptions have been covered under jaxws.dispatch and jaxws.proxy test suites
 * 
 * ExecutionException tests are covered in jaxws.dispatch and jaxws.proxy
 */
public class ParallelAsyncTests {
    @ClassRule
    public static final Axis2Server server = new Axis2Server("target/repo");

    // used for logging
    private String myClassName = "ParallelAsyncTests";

    @Test
    public void testNOOP () {}

    /**
     * @testStrategy Check that the web service is up and running 
     *               before running any other tests
     */
    @Test
    public void testService_isAlive() throws Exception {
        final String MESSAGE = "testServiceAlive";

        final AsyncPort port = getPort((Executor)null);

        String req1base = "sleepAsync";
        String req2base = "remappedAsync";

        for (int i = 0; i < 10; i++) {
            
            final String request1 = req1base + "_" + i;
            final String request2 = req2base + "_" + i;

            TestLogger.logger.debug("Iteration [" + i + "] using request1 [" + request1 +
                    "]  request2 [" + request2 + "]");

            // submit request #1 to the server-side web service that 
            // the web service will keep until we ask for it
            Response<SleepResponse> resp1 = port.sleepAsync(request1);

            // submit request #2 to the server that essentially processes
            // without delay
            Response<CustomAsyncResponse> resp2 = port.remappedAsync(request2);

            // wait until the response for request #2 is done 
            await(resp2);

            // check the waiting request #1
            // Requests are not necessarily processed in order, so we need to retry.
            withRetry(new Runnable() {
                @Override
                public void run() {
                    String asleep = port.isAsleep(request1);
                    //System.out.println(title+"iteration ["+i+"]   port.isAsleep(request1 ["+request1+"]) = ["+asleep+"]");
                    assertEquals("sleepAsync did not sleep as expected", request1, asleep);
                }
            });

            // wakeup the waiting request #1
            String wake = port.wakeUp(request1);
            //System.out.println(title+"iteration ["+i+"]   port.wakeUp(request1 ["+request1+"]) = ["+wake+"]");

            // wait until the response for request #1 is done
            await(resp1);
        
            // get the responses
            String req1_result = null;
            String req2_result = null;

            try {
                req1_result = resp1.get().getMessage();
                req2_result = resp2.get().getResponse();
            } catch (Exception e) {
                TestLogger.logger.debug(
                        "Iteration [" + i + "] using request1 [" + request1 +
                                "]  request2 [" + request2 + "] :  got exception [" +
                                e.getClass().getName() + "]  [" + e.getMessage() + "] ");
                e.printStackTrace();
                fail(e.toString());
            }

            // check status on request #1
            assertEquals("sleepAsync did not return expected response ", request1, req1_result);

            // check status on request #2
            assertEquals("remappedAsync did not return expected response", request2, req2_result);
            

            // Calling get() again should return the same object as the first call to get()
            assertEquals("sleepAsync did not return expected response ", request1, resp1.get().getMessage());
            assertEquals("remappedAsync did not return expected response", request2, resp2.get().getResponse());
            
        }
        
        // check the callback operation
		CallbackHandler<SleepResponse> sleepCallbackHandler = new CallbackHandler<SleepResponse>();

        String request1 = req1base + "_with_Callback";
        //System.out.println(title+" port.sleepAsync("+request1+", callbackHander)  being submitted....");
		Future<?> sr = port.sleepAsync(request1, sleepCallbackHandler);

        // wait a bit for the server to process the request ...
        Thread.sleep(500);

        // check the waiting request 
        String asleepWithCallback = port.isAsleep(request1);
        //System.out.println(title+" port.isAsleep("+request1+") = ["+asleepWithCallback+"]");

        // wakeup the waiting request
        String wake = port.wakeUp(request1);
        //System.out.println(title+" port.wakeUp("+request1+") = ["+wake+"]");

        // wait a bit..
        Thread.sleep(500);

        // get the response
        String req_cb_result = null;

        try {

            SleepResponse sleepResp = sleepCallbackHandler.get();

            if (sleepResp != null)
            {
                req_cb_result = sleepResp.getMessage();
                TestLogger.logger.debug(
                        "Request [" + request1 + "] :  result [" + req_cb_result + "] ");
            }

        } catch (Exception ex) {
            TestLogger.logger.debug("Request [" + request1 + "] :  got exception [" +
                    ex.getClass().getName() + "]  [" + ex.getMessage() + "] ");
            ex.printStackTrace();
            fail(ex.toString());
        }

        // check status on request
        assertEquals("sleepAsync with callback did not sleep as expected", request1, req_cb_result);

    }


    /**
     * @testStrategy Test for ordering an executor to shutdownNow while there
     *               is a request being processed.  Uses the default executor.
     *               
     */
    @Test
    public void testService_ExecutorShutdownNow() throws Exception {
        final String MESSAGE = "testExecutorShutdownNow";

        AsyncService service = getService(null);
        AsyncPort port = getPort(service);

		// get the default executor and check to make sure it is an executor service
        ExecutorService ex = null;
        Executor executor = service.getExecutor();
        if ((executor != null) && (executor instanceof ExecutorService))
        {
            ex = (ExecutorService) executor;
        }
        else
        {
            TestLogger.logger.debug("No executor service available. Nothing to test.");
            return;
        }


        // submit a request to the server that will wait until we ask for it
		CallbackHandler<SleepResponse> sleepCallbackHandler1 = new CallbackHandler<SleepResponse>();

        String request1 = "sleepAsync_with_Callback_1";

        TestLogger.logger.debug("port.sleepAsync(" + request1 +
                ", callbackHander1)  #1 being submitted....");
		Future<?> sr1 = port.sleepAsync(request1, sleepCallbackHandler1);
        TestLogger.logger.debug(
                "port.sleepAsync(" + request1 + ", callbackHander1)  #1 .....submitted.");

        // wait a bit to make sure that the server has the request
        Thread.sleep(1000);

		// tell the executor to shutdown immediately, which 
        // attempts to stop all actively executing tasks via Thread.interrupt()
        // and should prevent new tasks from being submitted
        TestLogger.logger
                .debug("Shutting down executor [" + ex.getClass().getName() + "]");
        ex.shutdownNow();

        // check the waiting request 
        TestLogger.logger.debug("port.isAsleep(" + request1 + ") #1 being submitted....");
        String asleepWithCallback1 = port.isAsleep(request1);
        TestLogger.logger.debug(
                "port.isAsleep(" + request1 + ") #1 = [" + asleepWithCallback1 + "]");

        // wakeup the waiting request
        TestLogger.logger.debug("port.wakeUp(request1) #1 being submitted....");
        String wake1 = port.wakeUp(request1);
        TestLogger.logger.debug("port.wakeUp(" + request1 + ") #1 = [" + wake1 + "]");

        await(sr1);

        // try to get the response
        boolean gotException = false;
        try {

            SleepResponse sleepResp1 = sleepCallbackHandler1.get();

            if (sleepResp1 != null)
            {
                TestLogger.logger.debug("request [" + request1 +
                        "] #1:  sleepResponse [NOT NULL] from callback handler");
                String result1 = sleepResp1.getMessage();
                TestLogger.logger.debug(
                        "request [" + request1 + "] #1:  result [" + result1 + "] ");
            }
            else
            {
                TestLogger.logger.debug("request [" + request1 +
                        "] #1:  sleepResponse [NULL] from callback handler");

                // see what the Future says
                TestLogger.logger.debug(
                        "request [" + request1 + "] #1:  ....check Future response...");
                Object futureResult = sr1.get();
                TestLogger.logger.debug(
                        "request [" + request1 + "] #1:  ....Future response [" +
                                futureResult + "]...");
            }

        } catch (Exception exc) {

            TestLogger.logger.debug("request [" + request1 + "] :  got exception [" +
                    exc.getClass().getName() + "]  [" + exc.getMessage() + "] ");
            gotException = true;
        }

        assertTrue("Did not receive an exception from trying to access the response when the executor service is shutdown.",gotException);
    }


    /**
     * @testStrategy Test for ordering an executor to shutdownNow while there
     *               is a request being processed.  Uses an application executor
     *               service.
     */
    @Test
    public void testService_ExecutorShutdownNow_2() throws Exception {
        final String MESSAGE = "testExecutorShutdownNow_2";

        AsyncService service = getService(null);
        AsyncPort port = getPort(service);

		// get the default executor and check to make sure it is an executor service
		ExecutorService ex = Executors.newSingleThreadExecutor();
		service.setExecutor(ex);


        // submit a request to the server that will wait until we ask for it
		CallbackHandler<SleepResponse> sleepCallbackHandler1 = new CallbackHandler<SleepResponse>();

        String request1 = "sleepAsync_with_Callback_1";

        TestLogger.logger.debug("port.sleepAsync(" + request1 +
                ", callbackHander1)  #1 being submitted....");
		Future<?> sr1 = port.sleepAsync(request1, sleepCallbackHandler1);
        TestLogger.logger.debug(
                "port.sleepAsync(" + request1 + ", callbackHander1)  #1 .....submitted.");

        // wait a bit to make sure that the server has the request
        Thread.sleep(1000);

		// tell the executor to shutdown immediately, which 
        // attempts to stop all actively executing tasks via Thread.interrupt()
        // and should prevent new tasks from being submitted
        TestLogger.logger
                .debug("Shutting down executor [" + ex.getClass().getName() + "]");
        ex.shutdownNow();

        // check the waiting request 
        TestLogger.logger.debug("port.isAsleep(" + request1 + ") #1 being submitted....");
        String asleepWithCallback1 = port.isAsleep(request1);
        TestLogger.logger.debug(
                "port.isAsleep(" + request1 + ") #1 = [" + asleepWithCallback1 + "]");

        // wakeup the waiting request
        TestLogger.logger.debug("port.wakeUp(request1) #1 being submitted....");
        String wake1 = port.wakeUp(request1);
        TestLogger.logger.debug("port.wakeUp(" + request1 + ") #1 = [" + wake1 + "]");

        await(sr1);

        // try to get the response
        boolean gotException = false;
        try {

            SleepResponse sleepResp1 = sleepCallbackHandler1.get();

            if (sleepResp1 != null)
            {
                TestLogger.logger.debug("Request [" + request1 +
                        "] #1:  sleepResponse [NOT NULL] from callback handler");
                String result1 = sleepResp1.getMessage();
                TestLogger.logger.debug(
                        "Request [" + request1 + "] #1:  result [" + result1 + "] ");
            }
            else
            {
                TestLogger.logger.debug("Request [" + request1 +
                        "] #1:  sleepResponse [NULL] from callback handler");

                // see what the Future says
                TestLogger.logger.debug(
                        "Request [" + request1 + "] #1:  ....check Future response...");
                Object futureResult = sr1.get();
                TestLogger.logger.debug(
                        "Request [" + request1 + "] #1:  ....Future response [" +
                                futureResult + "]...");
            }

        } catch (Exception exc) {

            TestLogger.logger.debug("Request [" + request1 + "] :  got exception [" +
                    exc.getClass().getName() + "]  [" + exc.getMessage() + "] ");
            gotException = true;
        }

        assertTrue("Did not receive an exception from trying to access the response when the executor service is shutdown.",gotException);
    }

    /**
     * @testStrategy Test for ordering an executor to shutdownNow before there
     *               is a request.  Uses the default executor.
     *               
     */
    @Test
    public void testService_ExecutorShutdownNow_3() throws Exception {
        final String MESSAGE = "testExecutorShutdownNow_3";

        AsyncService service = getService(null);
        AsyncPort port = getPort(service);

		// get the default executor and check to make sure it is an executor service
        ExecutorService ex = null;
        Executor executor = service.getExecutor();
        if ((executor != null) && (executor instanceof ExecutorService))
        {
            ex = (ExecutorService) executor;

            // tell the executor to shutdown immediately, which 
            // attempts to stop all actively executing tasks via Thread.interrupt()
            // and should prevent new tasks from being submitted
            TestLogger.logger
                    .debug("Shutting down executor [" + ex.getClass().getName() + "]");
            ex.shutdownNow();
        }
        else
        {
            TestLogger.logger.debug("No executor service available. Nothing to test.");
            return;
        }


        boolean gotRequestException = false;

        String request1 = "sleepAsync_with_Callback_1";
        CallbackHandler<SleepResponse> sleepCallbackHandler1 = new CallbackHandler<SleepResponse>();
        Future<?> sr1 = null;

        try
        {
            // submit a request to the server that will wait until we ask for it
            TestLogger.logger.debug("port.sleepAsync(" + request1 +
                    ", callbackHander1)  #1 being submitted....");
            sr1 = port.sleepAsync(request1, sleepCallbackHandler1);
            TestLogger.logger.debug("port.sleepAsync(" + request1 +
                    ", callbackHander1)  #1 .....submitted.");
        }
        catch (Exception exc)
        {
            TestLogger.logger.debug("Request [" + request1 + "] :  got exception [" +
                    exc.getClass().getName() + "]  [" + exc.getMessage() + "] ");
            gotRequestException = true;
        }

        // if the request went through, continue processing to see if the response is stopped
        // this makes sure that the server doesn't keep the request forever
        boolean gotResponseException = false;

        if (!gotRequestException)
        {
            // wakeup the waiting request
            TestLogger.logger.debug("port.wakeUp(request1) #1 being submitted....");
            String wake1 = port.wakeUp(request1);
            TestLogger.logger.debug("port.wakeUp(" + request1 + ") #1 = [" + wake1 + "]");

            // try to get the response
            try {

                SleepResponse sleepResp1 = sleepCallbackHandler1.get();

                if (sleepResp1 != null)
                {
                    TestLogger.logger.debug("Request [" + request1 +
                            "] #1:  sleepResponse [NOT NULL] from callback handler");
                    String result1 = sleepResp1.getMessage();
                    TestLogger.logger.debug(
                            "Request [" + request1 + "] #1:  result [" + result1 + "] ");
                }
                else
                {
                    TestLogger.logger.debug("Request [" + request1 +
                            "] #1:  sleepResponse [NULL] from callback handler");

                    // see what the Future says
                    TestLogger.logger.debug("Request [" + request1 +
                            "] #1:  ....check Future response...");
                    Object futureResult = sr1.get();
                    TestLogger.logger.debug("Request [" + request1 +
                            "] #1:  ....Future response [" + futureResult + "]...");
                }

            } catch (Exception exc) {

                TestLogger.logger.debug("Request [" + request1 + "] :  got exception [" +
                        exc.getClass().getName() + "]  [" + exc.getMessage() + "] ");
                gotResponseException = true;
            }
        }

        assertTrue("Did not receive an exception from trying to submit the request when the executor service is shutdown.",gotRequestException);

        //assertTrue("Did not receive an exception from trying to access the response when the executor service is shutdown.",gotResponseException);
    }




    /**
     * Auxiliary method used for doing isAsleep checks. Will perform isAsleep
     * up to a MAX_ISASLEEP_CHECK number of checks. Will sleep for
     * SLEEP_ISASLEEP_SEC seconds in between requests. If reaches maximum number
     * fo retries then will fail the test
     */
    private boolean isAsleepCheck(String MESSAGE, AsyncPort port) {
        boolean asleep = false;
        int check = 30;
        String msg = null;
        do {
            msg = port.isAsleep(MESSAGE);
            asleep = (msg != null);

            // fail the test if we ran out of checks
            if ((check--) == 0)
                fail("Serve did not receive sleep after several retries");

            // sleep for a bit
            try {
                Thread.sleep(30);
            } 
            catch (InterruptedException e) {
            }

        } while (!asleep);

        if (asleep) {
            assertTrue("Sleeping on an incorrect message", MESSAGE.equals(msg));
        }

        return true;
    }
    

    private AsyncService getService(Executor ex) {
        AsyncService service = new AsyncService();

        if (ex!= null)
            service.setExecutor(ex);
        
        if (service.getExecutor() == null)
        {
            TestLogger.logger.debug(myClassName + " : getService() : executor is null");
        }
        else
        {
            TestLogger.logger.debug(myClassName + " : getService() : executor is available ");
        }

        return service;
    }


    private AsyncPort getPort(AsyncService service) throws Exception {

        AsyncPort port = service.getAsyncPort();
        assertNotNull("Port is null", port);

        Map<String, Object> rc = ((BindingProvider) port).getRequestContext();
        rc.put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY,
                server.getEndpoint("AsyncService.DocLitWrappedPortImplPort"));
        
        return port;

    }

    /**
     * Auxiliary method used for obtaining a proxy pre-configured with a
     * specific Executor
     */
    private AsyncPort getPort(Executor ex) throws Exception {
        AsyncService service = getService(ex);

        AsyncPort port = service.getAsyncPort();
        assertNotNull("Port is null", port);

        Map<String, Object> rc = ((BindingProvider) port).getRequestContext();
        rc.put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY,
                server.getEndpoint("AsyncService.DocLitWrappedPortImplPort"));
        
        return port;
    }
}
