/*
 * Copyright 2017 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;

import static org.junit.Assume.assumeFalse;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.assertj.core.api.Assertions;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.kie.api.KieServices;
import org.kie.server.api.model.ReleaseId;
import org.kie.server.api.model.definition.ProcessInstanceField;
import org.kie.server.api.model.definition.ProcessInstanceQueryFilterSpec;
import org.kie.server.api.model.definition.QueryDefinition;
import org.kie.server.api.model.instance.JobRequestInstance;
import org.kie.server.api.model.instance.ProcessInstance;
import org.kie.server.api.util.ProcessInstanceQueryFilterSpecBuilder;
import org.kie.server.client.JobServicesClient;
import org.kie.server.integrationtests.config.TestConfig;
import org.kie.server.integrationtests.shared.KieServerDeployer;
import org.kie.server.integrationtests.shared.KieServerSynchronization;

public class ProcessSearchServiceIntegrationTest extends JbpmKieServerBaseIntegrationTest {

    private static final String GROUP_ID = "org.kie.server.testing";
    private static final String VERSION = "1.0.0.Final";
    private static final String CONTAINER_ID = "definition-project";
    private static final String PROCESS_ID_EVALUATION = "definition-project.evaluation";
    private static final String PROCESS_NAME_EVALUATION = "evaluation";
    
    private static final String QUERY_NAME = "processInstancesQuery";
    private static final String PROCESS_INSTANCE_QUERY = "select pi.* from ProcessInstanceLog pi";
    
    private static ReleaseId releaseId = new ReleaseId( GROUP_ID,
                                                        CONTAINER_ID,
                                                        VERSION );

    @BeforeClass
    public static void buildAndDeployArtifacts() throws Exception {
        JobServicesClient jsc = createDefaultStaticClient().getServicesClient(JobServicesClient.class);
        long id = jsc.scheduleRequest(JobRequestInstance.builder().command("org.jbpm.executor.commands.LogCleanupCommand").build());
        KieServerSynchronization.waitForJobToFinish(jsc, id, 120000L);
        
        KieServerDeployer.buildAndDeployCommonMavenParent();
        KieServerDeployer.buildAndDeployMavenProjectFromResource("/kjars-sources/definition-project");

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

        createContainer( CONTAINER_ID,
                         releaseId );
    }
    
    @Before
    public void registerQuery() {
        QueryDefinition query = new QueryDefinition();
        query.setName(QUERY_NAME);
        query.setSource(System.getProperty("org.kie.server.persistence.ds", "jdbc/jbpm-ds"));
        query.setExpression(PROCESS_INSTANCE_QUERY);
        query.setTarget("CUSTOM");
        queryClient.registerQuery(query);
    }

    @After
    public void unregisterQuery() {
        queryClient.unregisterQuery(QUERY_NAME);
    }

    @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 testFindProcessWithIncompatibleTypeFilter() throws Exception {
        // Skip for MySQL and MariaDB until JBPM-6390 is fixed
        assumeFalse(TestConfig.isMySqlDataSource());
        assumeFalse(TestConfig.isMariaDbDataSource());

        assertClientException(
                               () -> queryClient.findProcessInstancesWithFilters( QUERY_NAME, createQueryFilterGreaterThanOrEqualsTo( ProcessInstanceField.START_DATE,
                                                                                                                                   "incompatible data type" ),
                                                                                           0,
                                                                                           100 ),
                               400,
                               "The request could not be understood by the server due to malformed syntax: ",
                               "Can't lookup on specified data set: processInstancesQuery");
    }

    @Test
    public void testFindProcessInstanceWithProcessNameAndIdEqualsToFilter() throws Exception {
        Map<String, Object> parameters = new HashMap<>();
        Long processInstanceId = processClient.startProcess( CONTAINER_ID,
                                                             PROCESS_ID_EVALUATION,
                                                             parameters );
        Assertions.assertThat( processInstanceId ).isNotNull();

        HashMap<ProcessInstanceField, Comparable<?>> compareList = new HashMap<>();
        compareList.put( ProcessInstanceField.PROCESSNAME,
                         PROCESS_NAME_EVALUATION );
        compareList.put( ProcessInstanceField.PROCESSINSTANCEID,
                         processInstanceId );

        testFindProcessInstanceWithQueryFilter( createQueryFilterAndEqualsTo( compareList ),
                                                processInstanceId );
    }

    @Test
    public void testFindProcessInstanceWithProcessInstanceIdEqualsToFilter() throws Exception {
        Map<String, Object> parameters = new HashMap<>();
        Long processInstanceId = processClient.startProcess( CONTAINER_ID,
                                                             PROCESS_ID_EVALUATION,
                                                             parameters );
        Assertions.assertThat( processInstanceId ).isNotNull();
        testFindProcessInstanceWithQueryFilter( createQueryFilterEqualsTo( ProcessInstanceField.PROCESSINSTANCEID,
                                                                           processInstanceId ),
                                                processInstanceId );
    }

    @Test
    public void testFindProcessInstanceWithStartDateGreaterThanOrEqualsToFilter() throws Exception {
        Map<String, Object> parameters = new HashMap<>();
        Long processInstanceId = processClient.startProcess( CONTAINER_ID,
                                                             PROCESS_ID_EVALUATION,
                                                             parameters );
        Assertions.assertThat( processInstanceId ).isNotNull();
        ProcessInstance pi = processClient.getProcessInstance( CONTAINER_ID,
                                                               processInstanceId );
        testFindProcessInstanceWithQueryFilter( createQueryFilterGreaterThanOrEqualsTo( ProcessInstanceField.START_DATE,
                                                                                        subtractOneMinuteFromDate( pi.getDate() ) ),
                                                processInstanceId );
    }

    @Test
    public void testFindProcessInstanceWithCorrelationKeyEqualsToFilter() throws Exception {
        Map<String, Object> parameters = new HashMap<>();
        Long processInstanceId = processClient.startProcess( CONTAINER_ID,
                                                             PROCESS_ID_EVALUATION,
                                                             parameters );
        Assertions.assertThat( processInstanceId ).isNotNull();
        testFindProcessInstanceWithQueryFilter( createQueryFilterEqualsTo( ProcessInstanceField.CORRELATIONKEY,
                                                                           String.valueOf( processInstanceId ) ),
                                                processInstanceId );
    }

    @Test
    public void testFindProcessInstanceWithExternalIdAndPidEqualsToFilter() throws Exception {
        Map<String, Object> parameters = new HashMap<>();
        Long processInstanceId = processClient.startProcess( CONTAINER_ID,
                                                             PROCESS_ID_EVALUATION,
                                                             parameters );
        Assertions.assertThat( processInstanceId ).isNotNull();

        HashMap<ProcessInstanceField, Comparable<?>> compareList = new HashMap<>();
        compareList.put( ProcessInstanceField.EXTERNALID,
                         CONTAINER_ID );
        compareList.put( ProcessInstanceField.PROCESSINSTANCEID,
                         processInstanceId );

        testFindProcessInstanceWithQueryFilter( createQueryFilterAndEqualsTo( compareList ),
                                                processInstanceId );
    }

    @Test
    public void testFindProcessInstanceWithUserIdAndPidEqualsToFilter() throws Exception {
        Map<String, Object> parameters = new HashMap<>();
        Long processInstanceId = processClient.startProcess( CONTAINER_ID,
                                                             PROCESS_ID_EVALUATION,
                                                             parameters );
        Assertions.assertThat( processInstanceId ).isNotNull();

        HashMap<ProcessInstanceField, Comparable<?>> compareList = new HashMap<>();
        compareList.put( ProcessInstanceField.PROCESSINSTANCEID,
                         processInstanceId );
        compareList.put( ProcessInstanceField.USER_IDENTITY,
                         USER_YODA );

        testFindProcessInstanceWithQueryFilter( createQueryFilterAndEqualsTo( compareList ),
                                                processInstanceId );
    }

    @Test
    public void testFindProcessInstanceWithParentIdAndProcessNameEqualsToFilter() throws Exception {
        Map<String, Object> parameters = new HashMap<>();
        Long processInstanceId = processClient.startProcess( CONTAINER_ID,
                                                             PROCESS_ID_EVALUATION,
                                                             parameters );
        Assertions.assertThat( processInstanceId ).isNotNull();

        HashMap<ProcessInstanceField, Comparable<?>> compareList = new HashMap<>();
        compareList.put( ProcessInstanceField.PARENTPROCESSINSTANCEID,
                         -1 );
        compareList.put( ProcessInstanceField.PROCESSINSTANCEID,
                         processInstanceId );

        testFindProcessInstanceWithQueryFilter( createQueryFilterAndEqualsTo( compareList ),
                                                processInstanceId );
    }

    public void testFindProcessInstanceWithStatusEqualsToFilter() throws Exception {
        Map<String, Object> parameters = new HashMap<>();
        Long processInstanceId = processClient.startProcess( CONTAINER_ID,
                                                             PROCESS_ID_EVALUATION,
                                                             parameters );
        Assertions.assertThat( processInstanceId ).isNotNull();
        testFindProcessInstanceWithQueryFilter( createQueryFilterEqualsTo( ProcessInstanceField.STATUS,
                                                                           1 ),
                                                processInstanceId );
    }

    @Test
    public void testFindTaskWithAndEqualsToFilter() throws Exception {
        Long processInstanceId = processClient.startProcess( CONTAINER_ID,
                                                             PROCESS_ID_EVALUATION );
        Assertions.assertThat( processInstanceId ).isNotNull();
        ProcessInstance process = processClient.getProcessInstance( CONTAINER_ID,
                                                                    processInstanceId );
        Assertions.assertThat( process ).isNotNull();

        HashMap<ProcessInstanceField, Comparable<?>> compareList = new HashMap<>();
        compareList.put( ProcessInstanceField.PROCESSID,
                         process.getProcessId() );
        compareList.put( ProcessInstanceField.EXTERNALID,
                         CONTAINER_ID );
        compareList.put( ProcessInstanceField.PROCESSINSTANCEID,
                         processInstanceId );
        compareList.put( ProcessInstanceField.PROCESSINSTANCEDESCRIPTION,
                         process.getProcessInstanceDescription() );
        compareList.put( ProcessInstanceField.CORRELATIONKEY,
                         process.getCorrelationKey() );
        compareList.put( ProcessInstanceField.USER_IDENTITY,
                         USER_YODA );
        compareList.put( ProcessInstanceField.PARENTPROCESSINSTANCEID,
                         process.getParentId() );
        compareList.put( ProcessInstanceField.STATUS,
                         process.getState() );
        compareList.put( ProcessInstanceField.PROCESSVERSION,
                         process.getProcessVersion() );
        compareList.put( ProcessInstanceField.PROCESSNAME,
                         process.getProcessName() );

        List<Long> resultsIds = new ArrayList<>();
        List<ProcessInstance> results = queryClient.findProcessInstancesWithFilters( QUERY_NAME, createQueryFilterAndEqualsTo( compareList ),
                                                                                              0,
                                                                                              100 );

        for ( ProcessInstance res : results ) {
            resultsIds.add( res.getId() );
        }

        Assertions.assertThat( results ).isNotNull();
        Assertions.assertThat( results ).isNotEmpty();
        Assertions.assertThat( resultsIds ).contains( process.getId() );

        ProcessInstance instance = results.stream()
                                          .filter( processInstance -> processInstance.getId().equals( process
                                                                                                             .getId() ) )
                                          .findFirst()
                                          .orElse( null );
        Assertions.assertThat( instance ).isNotNull();

        Assertions.assertThat( instance.getContainerId() ).isEqualTo( CONTAINER_ID );
        Assertions.assertThat( instance.getId() ).isEqualTo( processInstanceId );
        Assertions.assertThat( instance.getProcessName() ).isEqualTo( process.getProcessName() );
        Assertions.assertThat( instance.getCorrelationKey() ).isEqualTo( process.getCorrelationKey() );
        Assertions.assertThat( instance.getInitiator() ).isEqualTo( USER_YODA );
        Assertions.assertThat( instance.getProcessInstanceDescription() ).isEqualTo( process
                                                                                            .getProcessInstanceDescription() );
        Assertions.assertThat( instance.getParentId() ).isEqualTo( process.getParentId() );
        Assertions.assertThat( instance.getState() ).isEqualTo( process.getState() );
        Assertions.assertThat( instance.getProcessVersion() ).isEqualTo( process.getProcessVersion() );
        Assertions.assertThat( instance.getProcessId() ).isEqualTo( process.getProcessId() );
    }

    private void testFindProcessInstanceWithQueryFilter( ProcessInstanceQueryFilterSpec filter,
                                                         Long processInstanceId ) {
        List<Long> resultsIds = new ArrayList<>();
        List<ProcessInstance> results;

        results = queryClient.findProcessInstancesWithFilters( QUERY_NAME, 
                                                                        filter,
                                                                        0,
                                                                        100 );
        for ( ProcessInstance res : results ) {
            resultsIds.add( res.getId() );
        }

        Assertions.assertThat( results ).isNotNull();
        Assertions.assertThat( results ).isNotEmpty();
        Assertions.assertThat( resultsIds ).contains( processInstanceId );
    }

    private ProcessInstanceQueryFilterSpec createQueryFilterEqualsTo( ProcessInstanceField processInstanceField,
                                                                      Comparable<?> equalsTo ) {
        return new ProcessInstanceQueryFilterSpecBuilder().equalsTo( processInstanceField,
                                                                     equalsTo ).get();
    }

    private ProcessInstanceQueryFilterSpec createQueryFilterGreaterThanOrEqualsTo( ProcessInstanceField processInstanceField,
                                                                                   Comparable<?> equalsTo ) {
        return new ProcessInstanceQueryFilterSpecBuilder().greaterOrEqualTo( processInstanceField,
                                                                             equalsTo ).get();
    }

    private ProcessInstanceQueryFilterSpec createQueryFilterAndEqualsTo( Map<ProcessInstanceField, Comparable<?>> filterProperties ) {
        ProcessInstanceQueryFilterSpecBuilder result = new ProcessInstanceQueryFilterSpecBuilder();
        filterProperties.forEach( result::equalsTo );
        return result.get();
    }
}
