/*
 * Copyright 2016 Red Hat, Inc. and/or its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 *
 *      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.kie.server.integrationtests.jbpm.jms;

import static org.assertj.core.api.Assertions.assertThat;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;

import org.junit.After;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import org.junit.runners.Parameterized;
import org.kie.api.KieServices;
import org.kie.api.task.model.Status;
import org.kie.server.api.marshalling.Marshaller;
import org.kie.server.api.marshalling.MarshallerFactory;
import org.kie.server.api.marshalling.MarshallingFormat;
import org.kie.server.api.model.ReleaseId;
import org.kie.server.api.model.ServiceResponse;
import org.kie.server.api.model.ServiceResponsesList;
import org.kie.server.api.model.definition.QueryDefinition;
import org.kie.server.api.model.instance.ProcessInstance;
import org.kie.server.api.model.instance.ProcessInstanceList;
import org.kie.server.api.model.instance.TaskInstance;
import org.kie.server.api.model.instance.TaskSummary;
import org.kie.server.api.model.instance.TaskSummaryList;
import org.kie.server.client.KieServicesClient;
import org.kie.server.client.KieServicesConfiguration;
import org.kie.server.client.KieServicesFactory;
import org.kie.server.client.ProcessServicesClient;
import org.kie.server.client.QueryServicesClient;
import org.kie.server.client.jms.AsyncResponseHandler;
import org.kie.server.client.jms.BlockingResponseCallback;
import org.kie.server.client.jms.FireAndForgetResponseHandler;
import org.kie.server.client.jms.RequestReplyResponseHandler;
import org.kie.server.client.jms.ResponseCallback;
import org.kie.server.client.jms.ResponseHandler;
import org.kie.server.integrationtests.category.JMSOnly;
import org.kie.server.integrationtests.jbpm.JbpmKieServerBaseIntegrationTest;
import org.kie.server.integrationtests.shared.KieServerAssert;
import org.kie.server.integrationtests.shared.KieServerDeployer;
import org.kie.server.integrationtests.shared.KieServerSynchronization;

@Category({JMSOnly.class})
public class JmsResponseHandlerIntegrationTest extends JbpmKieServerBaseIntegrationTest {

    private static final ReleaseId RELEASE_ID = new ReleaseId("org.kie.server.testing", "definition-project", "1.0.0.Final");

    @Parameterized.Parameters(name = "{index}: {0}")
    public static Collection<Object[]> data() {
        KieServicesConfiguration jmsConfiguration = createKieServicesJmsConfiguration();
        Collection<Object[]> parameterData = new ArrayList<>(Arrays.asList(new Object[][]{
                {MarshallingFormat.JAXB, jmsConfiguration},
                {MarshallingFormat.JSON, jmsConfiguration},
                {MarshallingFormat.XSTREAM, jmsConfiguration}
        }));

        return parameterData;
    }

    @BeforeClass
    public static void buildAndDeployArtifacts() {
        KieServerDeployer.buildAndDeployCommonMavenParent();
        KieServerDeployer.buildAndDeployMavenProjectFromResource("/kjars-sources/definition-project");
        KieServerDeployer.buildAndDeployMavenProjectFromResource("/kjars-sources/query-definition-project");

        kieContainer = KieServices.Factory.get().newKieContainer(RELEASE_ID);

        createContainer(CONTAINER_ID, RELEASE_ID);
    }

    @After
    public void resetResponseHandler() {
        processClient.setResponseHandler(new RequestReplyResponseHandler());
        queryClient.setResponseHandler(new RequestReplyResponseHandler());
        taskClient.setResponseHandler(new RequestReplyResponseHandler());
    }

    @Override
    protected void addExtraCustomClasses(Map<String, Class<?>> extraClasses) throws Exception {
        extraClasses.put(PERSON_CLASS_NAME, Class.forName(PERSON_CLASS_NAME, true, kieContainer.getClassLoader()));
    }

    @Test
    public void testStartProcessUseOfFireAndForgetResponseHandler() throws Exception {
        testStartProcessResponseHandler(new FireAndForgetResponseHandler());
    }

    @Test
    public void testStartProcessUseOfAsyncResponseHandler() throws Exception {
        ResponseCallback callback = new BlockingResponseCallback(null);

        testStartProcessResponseHandler(new AsyncResponseHandler(callback));
        // now let's check if response has arrived
        ServiceResponsesList response = callback.get();
        assertThat(response).isNotNull();
        assertThat(response.getResponses()).isNotNull().hasSize(1);
        KieServerAssert.assertSuccess(response.getResponses().get(0));

        ServiceResponse<? extends Object> serviceResponse = response.getResponses().get(0);
        Object result = serviceResponse.getResult();
        assertThat(result).isNotNull();
    }

    @Test
    public void testStartProcessUseOfAsyncResponseHandlerWithMarshaller() throws Exception {
        ResponseCallback callback = new BlockingResponseCallback(createMarshaller());

        testStartProcessResponseHandler(new AsyncResponseHandler(callback));
        // now let's check if response has arrived
        Long processInstanceId = callback.get(Long.class);
        assertThat(processInstanceId).isNotNull().isPositive();
    }

    @Test
    public void testGetProcessInstancesUseOfAsyncResponseHandlerWithMarshaller() throws Exception {
        ResponseCallback callback = new BlockingResponseCallback(createMarshaller());

        testGetProcessInstancesResponseHandler(new AsyncResponseHandler(callback));
        // now let's check if response has arrived
        ProcessInstanceList processInstanceList = callback.get(ProcessInstanceList.class);
        assertThat(processInstanceList).isNotNull();
        assertThat(processInstanceList.getItems()).isNotNull().hasSize(1);
    }

    @Test
    public void testGetTasksUseOfAsyncResponseHandlerWithMarshaller() throws Exception {
        ResponseCallback callback = new BlockingResponseCallback(createMarshaller());

        testGetTaskResponseHandler(new AsyncResponseHandler(callback));
        // now let's check if response has arrived
        TaskSummaryList taskSummaryList = callback.get(TaskSummaryList.class);
        assertThat(taskSummaryList).isNotNull();
        assertThat(taskSummaryList.getItems()).isNotNull().hasSize(1);
    }

    @Test
    public void testStartAndCompleteTaskUseOfFireAndForgetResponseHandler() throws Exception {
        testStartAndCompleteTask(new FireAndForgetResponseHandler());
    }

    @Test
    public void testStartAndCompleteTaskUseOfAsyncResponseHandler() throws Exception {
        ResponseCallback callback = new BlockingResponseCallback(null);
        testStartAndCompleteTask(new AsyncResponseHandler(callback));
    }

    @Test
    public void testQueryRegistrationUseOfFireAndForgetResponseHandler() throws Exception {
        testQueryRegistration(new FireAndForgetResponseHandler());
    }

    @Test
    public void testQueryRegistrationUseOfAsyncResponseHandler() throws Exception {
        ResponseCallback callback = new BlockingResponseCallback(null);
        testQueryRegistration(new AsyncResponseHandler(callback));
    }

    @Test
    public void testGlobalConfigurationOfFireAndForgetResponseHandler() throws Exception {
        testStartProcessWithGlobalConfiguration(new FireAndForgetResponseHandler());
    }

    @Test
    public void testGlobalConfigurationOfAsyncResponseHandler() throws Exception {
        ResponseCallback callback = new BlockingResponseCallback(createMarshaller());
        testStartProcessWithGlobalConfiguration(new AsyncResponseHandler(callback));

        Long processInstanceId = callback.get(Long.class);
        assertThat(processInstanceId).isNotNull().isPositive();
    }

    /*
     * helper methods that comes with tests that can be invoked with various response handlers
     */

    private void testStartProcessResponseHandler(ResponseHandler responseHandler) throws Exception {
        List<ProcessInstance> processInstances = queryClient.findProcessInstances(0, 100);
        assertThat(processInstances).isEmpty();

        // change response handler for processClient others are not affected
        processClient.setResponseHandler(responseHandler);
        Long processInstanceId = processClient.startProcess(CONTAINER_ID, PROCESS_ID_USERTASK);
        // since we use fire and forget there will always be null response
        assertThat(processInstanceId).isNull();

        KieServerSynchronization.waitForProcessInstanceStart(queryClient, CONTAINER_ID);

        // Process should be started completely async - fire and forget.
        processInstances = queryClient.findProcessInstances(0, 100);
        assertThat(processInstances).hasSize(1);

        ProcessInstance pi = processInstances.get(0);
        assertThat(pi.getState()).isEqualTo(org.kie.api.runtime.process.ProcessInstance.STATE_ACTIVE);
    }

    private void testGetProcessInstancesResponseHandler(ResponseHandler responseHandler) throws Exception {
        List<ProcessInstance> processInstances = queryClient.findProcessInstances(0, 100);
        assertThat(processInstances).isEmpty();

        Long processInstanceId = processClient.startProcess(CONTAINER_ID, PROCESS_ID_USERTASK);
        assertThat(processInstanceId).isNotNull();

        KieServerSynchronization.waitForProcessInstanceStart(queryClient, CONTAINER_ID);

        // change response handler for queryClient others are not affected
        queryClient.setResponseHandler(responseHandler);
        // Process should be started completely async - fire and forget.
        processInstances = queryClient.findProcessInstances(0, 100);
        assertThat(processInstances).isNull();

        // set it back for the sake of verification
        queryClient.setResponseHandler(new RequestReplyResponseHandler());
        // Process should be started completely async - fire and forget.
        processInstances = queryClient.findProcessInstances(0, 100);
        assertThat(processInstances).isNotNull().hasSize(1);

        ProcessInstance pi = processInstances.get(0);
        assertThat(pi.getState()).isEqualTo(org.kie.api.runtime.process.ProcessInstance.STATE_ACTIVE);
    }

    private void testGetTaskResponseHandler(ResponseHandler responseHandler) throws Exception {
        List<ProcessInstance> processInstances = queryClient.findProcessInstances(0, 100);
        assertThat(processInstances).isEmpty();

        Long processInstanceId = processClient.startProcess(CONTAINER_ID, PROCESS_ID_USERTASK);
        assertThat(processInstanceId).isNotNull();

        KieServerSynchronization.waitForProcessInstanceStart(queryClient, CONTAINER_ID);

        // Process should be started completely async - fire and forget.
        processInstances = queryClient.findProcessInstances(0, 100);
        assertThat(processInstances).isNotNull().hasSize(1);

        ProcessInstance pi = processInstances.get(0);
        assertThat(pi.getState()).isEqualTo(org.kie.api.runtime.process.ProcessInstance.STATE_ACTIVE);

        // change response handler for taskClient others are not affected
        taskClient.setResponseHandler(responseHandler);
        List<TaskSummary> tasks = taskClient.findTasksAssignedAsPotentialOwner(USER_YODA, 0, 10);
        assertThat(tasks).isNull();
    }

    private void testStartAndCompleteTask(ResponseHandler responseHandler) throws Exception {
        Long processInstanceId = processClient.startProcess(CONTAINER_ID, PROCESS_ID_USERTASK);
        assertThat(processInstanceId).isNotNull();

        List<ProcessInstance> processInstances = queryClient.findProcessInstances(0, 100);
        assertThat(processInstances).isNotNull().hasSize(1);
        assertThat(processInstances.get(0)).isNotNull();
        assertThat(processInstances.get(0).getState()).isEqualTo(org.kie.api.runtime.process.ProcessInstance.STATE_ACTIVE);

        List<TaskSummary> tasks = taskClient.findTasksAssignedAsPotentialOwner(USER_YODA, 0, 10);
        assertThat(tasks).hasSize(1);

        Long taskId = tasks.get(0).getId();
        taskClient.setResponseHandler(responseHandler);
        taskClient.startTask(CONTAINER_ID, taskId, USER_YODA);

        taskClient.setResponseHandler(new RequestReplyResponseHandler());
        KieServerSynchronization.waitForTaskStatus(taskClient, taskId, Status.InProgress.name());

        taskClient.setResponseHandler(responseHandler);
        taskClient.completeTask(CONTAINER_ID, taskId, USER_YODA, new HashMap<String, Object>());

        taskClient.setResponseHandler(new RequestReplyResponseHandler());
        KieServerSynchronization.waitForTaskStatus(taskClient, taskId, Status.Completed.name());

        tasks = taskClient.findTasksAssignedAsPotentialOwner(USER_YODA, 0, 10);
        assertThat(tasks).hasSize(1);

        taskId = tasks.get(0).getId();
        taskClient.setResponseHandler(responseHandler);
        taskClient.startTask(CONTAINER_ID, taskId, USER_YODA);

        taskClient.setResponseHandler(new RequestReplyResponseHandler());
        KieServerSynchronization.waitForTaskStatus(taskClient, taskId, Status.InProgress.name());

        taskClient.setResponseHandler(responseHandler);
        taskClient.completeTask(CONTAINER_ID, taskId, USER_YODA, new HashMap<String, Object>());

        KieServerSynchronization.waitForProcessInstanceToFinish(processClient, CONTAINER_ID, processInstanceId);
    }

    private void testQueryRegistration(ResponseHandler responseHandler) throws Exception {
        Map<String, Object> parameters = new HashMap<String, Object>();
        parameters.put("stringData", "waiting for signal");
        parameters.put("personData", createPersonInstance(CONTAINER_ID));

        processClient.startProcess(CONTAINER_ID, PROCESS_ID_USERTASK, parameters);

        QueryDefinition query = new QueryDefinition();
        query.setName("getTasksByState");
        query.setSource(System.getProperty("org.kie.server.persistence.ds", "jdbc/jbpm-ds"));
        query.setExpression("select * from AuditTaskImpl where status = 'Reserved'");
        query.setTarget("CUSTOM");

        queryClient.setResponseHandler(responseHandler);
        queryClient.registerQuery(query);

        try {
            queryClient.setResponseHandler(new RequestReplyResponseHandler());
            KieServerSynchronization.waitForQuery(queryClient, query);

            List<TaskInstance> tasks = queryClient.query(query.getName(), QueryServicesClient.QUERY_MAP_TASK, 0, 10, TaskInstance.class);
            assertThat(tasks).isNotNull().hasSize(1);
            Long taskId = tasks.get(0).getId();

            query.setExpression("select * from AuditTaskImpl where status = 'InProgress'");

            queryClient.setResponseHandler(responseHandler);
            queryClient.replaceQuery(query);

            queryClient.setResponseHandler(new RequestReplyResponseHandler());
            KieServerSynchronization.waitForQuery(queryClient, query);

            tasks = queryClient.query(query.getName(), QueryServicesClient.QUERY_MAP_TASK, 0, 10, TaskInstance.class);
            assertThat(tasks).isNotNull().isEmpty();

            taskClient.startTask(CONTAINER_ID, taskId, USER_YODA);

            tasks = queryClient.query(query.getName(), QueryServicesClient.QUERY_MAP_TASK, 0, 10, TaskInstance.class);
            assertThat(tasks).isNotNull().hasSize(1);
            assertThat(tasks.get(0).getId()).isEqualTo(taskId);

        } catch (Exception e) {
            queryClient.setResponseHandler(new RequestReplyResponseHandler());
            queryClient.unregisterQuery(query.getName());
            throw e;
        }

        queryClient.setResponseHandler(responseHandler);
        queryClient.unregisterQuery(query.getName());

        queryClient.setResponseHandler(new RequestReplyResponseHandler());
        KieServerSynchronization.waitForQueryRemoval(queryClient, query);
    }

    private void testStartProcessWithGlobalConfiguration(ResponseHandler responseHandler) throws Exception {
        KieServicesConfiguration jmsConfiguration = createKieServicesJmsConfiguration();
        jmsConfiguration.setMarshallingFormat(marshallingFormat);
        jmsConfiguration.setResponseHandler(responseHandler);

        KieServicesClient kieServicesClient = KieServicesFactory.newKieServicesClient(jmsConfiguration);
        ProcessServicesClient fireAndForgetProcessClient = kieServicesClient.getServicesClient(ProcessServicesClient.class);

        Long processInstanceId = fireAndForgetProcessClient.startProcess(CONTAINER_ID, PROCESS_ID_USERTASK);
        assertThat(processInstanceId).isNull();

        KieServerSynchronization.waitForProcessInstanceStart(queryClient, CONTAINER_ID);

        abortAllProcesses();
    }

    private Marshaller createMarshaller() {
        return MarshallerFactory.getMarshaller(new HashSet<>(extraClasses.values()),
                configuration.getMarshallingFormat(), client.getClassLoader());
    }

}
