content: /*
   * Copyright 2010-2025 Hyland Software, Inc. and its affiliates.
   *
   * Licensed 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.activiti.engine.test.bpmn.gateway;
  
  import static java.util.Collections.singletonMap;
  import static org.assertj.core.api.Assertions.assertThat;
  import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
  
  import java.util.ArrayList;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  import org.activiti.engine.ActivitiException;
  import org.activiti.engine.impl.test.PluggableActivitiTestCase;
  import org.activiti.engine.runtime.Job;
  import org.activiti.engine.runtime.ProcessInstance;
  import org.activiti.engine.task.Task;
  import org.activiti.engine.test.Deployment;
  
  /**
  
   */
  public class ExclusiveGatewayTest extends PluggableActivitiTestCase {
  
      @Deployment
      public void testDivergingExclusiveGateway() {
          for (int i = 1; i <= 3; i++) {
              ProcessInstance pi = runtimeService.startProcessInstanceByKey(
                  "exclusiveGwDiverging",
                  singletonMap("input", i)
              );
              assertThat(taskService.createTaskQuery().singleResult().getName()).isEqualTo("Task " + i);
              runtimeService.deleteProcessInstance(pi.getId(), "testing deletion");
          }
      }
  
      @Deployment
      public void testSkipExpression() {
          for (int i = 1; i <= 3; i++) {
              Map<String, Object> variables = new HashMap<String, Object>();
              variables.put("_ACTIVITI_SKIP_EXPRESSION_ENABLED", true);
              variables.put("input", -i);
  
              ProcessInstance pi = runtimeService.startProcessInstanceByKey(
                  "exclusiveGwDivergingSkipExpression",
                  variables
              );
              assertThat(taskService.createTaskQuery().singleResult().getName()).isEqualTo("Task " + i);
              runtimeService.deleteProcessInstance(pi.getId(), "testing deletion");
          }
      }
  
      @Deployment
      public void testMergingExclusiveGateway() {
          runtimeService.startProcessInstanceByKey("exclusiveGwMerging");
          assertThat(taskService.createTaskQuery().count()).isEqualTo(3);
      }
  
      // If there are multiple outgoing seqFlow with valid conditions, the first
      // defined one should be chosen.
      @Deployment
      public void testMultipleValidConditions() {
          runtimeService.startProcessInstanceByKey("exclusiveGwMultipleValidConditions", singletonMap("input", 5));
          assertThat(taskService.createTaskQuery().singleResult().getName()).isEqualTo("Task 2");
      }
  
      @Deployment
      public void testNoSequenceFlowSelected() {
          assertThatExceptionOfType(ActivitiException.class)
              .isThrownBy(() ->
                  runtimeService.startProcessInstanceByKey("exclusiveGwNoSeqFlowSelected", singletonMap("input", 4))
              )
              .withMessageContaining(
                  "No outgoing sequence flow of the exclusive gateway " +
                  "'exclusiveGw' could be selected for continuing the process"
              );
      }
  
      /**
       * Test for bug ACT-10: whitespaces/newlines in expressions lead to exceptions
       */
      @Deployment
      public void testWhitespaceInExpression() {
          // Starting a process instance will lead to an exception if whitespace
          // are incorrectly handled
          runtimeService.startProcessInstanceByKey("whiteSpaceInExpression", singletonMap("input", 1));
      }
  
      @Deployment(
          resources = {
              "org/activiti/engine/test/bpmn/gateway/ExclusiveGatewayTest.testDivergingExclusiveGateway.bpmn20.xml",
          }
      )
      public void testUnknownVariableInExpression() {
          // Instead of 'input' we're starting a process instance with the name 'iinput' (ie. a typo)
          assertThatExceptionOfType(ActivitiException.class)
              .isThrownBy(() ->
                  runtimeService.startProcessInstanceByKey("exclusiveGwDiverging", singletonMap("iinput", 1))
              )
              .withMessageContaining("Unknown property used in expression");
      }
  
      @Deployment
      public void testDecideBasedOnBeanProperty() {
          runtimeService.startProcessInstanceByKey(
              "decisionBasedOnBeanProperty",
              singletonMap("order", new ExclusiveGatewayTestOrder(150))
          );
  
          Task task = taskService.createTaskQuery().singleResult();
          assertThat(task).isNotNull();
          assertThat(task.getName()).isEqualTo("Standard service");
      }
  
      @Deployment
      public void testDecideBasedOnListOrArrayOfBeans() {
          List<ExclusiveGatewayTestOrder> orders = new ArrayList<ExclusiveGatewayTestOrder>();
          orders.add(new ExclusiveGatewayTestOrder(50));
          orders.add(new ExclusiveGatewayTestOrder(300));
          orders.add(new ExclusiveGatewayTestOrder(175));
  
          ProcessInstance pi = runtimeService.startProcessInstanceByKey(
              "decisionBasedOnListOrArrayOfBeans",
              singletonMap("orders", orders)
          );
  
          Task task = taskService.createTaskQuery().processInstanceId(pi.getId()).singleResult();
          assertThat(task).isNotNull();
          assertThat(task.getName()).isEqualTo("Gold Member service");
  
          // Arrays are usable in exactly the same way
          ExclusiveGatewayTestOrder[] orderArray = orders.toArray(new ExclusiveGatewayTestOrder[orders.size()]);
          orderArray[1].setPrice(10);
          pi = runtimeService.startProcessInstanceByKey(
              "decisionBasedOnListOrArrayOfBeans",
              singletonMap("orders", orderArray)
          );
  
          task = taskService.createTaskQuery().processInstanceId(pi.getId()).singleResult();
          assertThat(task).isNotNull();
          assertThat(task.getName()).isEqualTo("Basic service");
      }
  
      @Deployment
      public void testDecideBasedOnBeanMethod() {
          runtimeService.startProcessInstanceByKey(
              "decisionBasedOnBeanMethod",
              singletonMap("order", new ExclusiveGatewayTestOrder(300))
          );
  
          Task task = taskService.createTaskQuery().singleResult();
          assertThat(task).isNotNull();
          assertThat(task.getName()).isEqualTo("Gold Member service");
      }
  
      @Deployment
      public void testInvalidMethodExpression() {
          assertThatExceptionOfType(ActivitiException.class)
              .isThrownBy(() ->
                  runtimeService.startProcessInstanceByKey(
                      "invalidMethodExpression",
                      singletonMap("order", new ExclusiveGatewayTestOrder(50))
                  )
              )
              .withMessageContaining("Unknown method used in expression");
      }
  
      @Deployment
      public void testDefaultSequenceFlow() {
          // Input == 1 -> default is not selected
          String procId = runtimeService
              .startProcessInstanceByKey("exclusiveGwDefaultSequenceFlow", singletonMap("input", 1))
              .getId();
          Task task = taskService.createTaskQuery().singleResult();
          assertThat(task.getName()).isEqualTo("Input is one");
          runtimeService.deleteProcessInstance(procId, null);
  
          runtimeService.startProcessInstanceByKey("exclusiveGwDefaultSequenceFlow", singletonMap("input", 5)).getId();
          task = taskService.createTaskQuery().singleResult();
          assertThat(task.getName()).isEqualTo("Default input");
      }
  
      public void testInvalidProcessDefinition() {
          String defaultFlowWithCondition =
              "<?xml version='1.0' encoding='UTF-8'?>" +
              "<definitions id='definitions' xmlns='http://www.omg.org/spec/BPMN/20100524/MODEL' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xmlns:activiti='http://activiti.org/bpmn' targetNamespace='Examples'>" +
              "  <process id='exclusiveGwDefaultSequenceFlow'> " +
              "    <startEvent id='theStart' /> " +
              "    <sequenceFlow id='flow1' sourceRef='theStart' targetRef='exclusiveGw' /> " +
              "    <exclusiveGateway id='exclusiveGw' name='Exclusive Gateway' default='flow3' /> " +
              "    <sequenceFlow id='flow2' sourceRef='exclusiveGw' targetRef='theTask1'> " +
              "      <conditionExpression xsi:type='tFormalExpression'>${input == 1}</conditionExpression> " +
              "    </sequenceFlow> " +
              "    <sequenceFlow id='flow3' sourceRef='exclusiveGw' targetRef='theTask2'> " +
              "      <conditionExpression xsi:type='tFormalExpression'>${input == 3}</conditionExpression> " +
              "    </sequenceFlow> " +
              "    <userTask id='theTask1' name='Input is one' /> " +
              "    <userTask id='theTask2' name='Default input' /> " +
              "  </process>" +
              "</definitions>";
  
          assertThatExceptionOfType(Exception.class).isThrownBy(() ->
              repositoryService.createDeployment().addString("myprocess.bpmn20.xml", defaultFlowWithCondition).deploy()
          );
  
          String noOutgoingFlow =
              "<?xml version='1.0' encoding='UTF-8'?>" +
              "<definitions id='definitions' xmlns='http://www.omg.org/spec/BPMN/20100524/MODEL' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xmlns:activiti='http://activiti.org/bpmn' targetNamespace='Examples'>" +
              "  <process id='exclusiveGwDefaultSequenceFlow'> " +
              "    <startEvent id='theStart' /> " +
              "    <sequenceFlow id='flow1' sourceRef='theStart' targetRef='exclusiveGw' /> " +
              "    <exclusiveGateway id='exclusiveGw' name='Exclusive Gateway' /> " +
              "  </process>" +
              "</definitions>";
          assertThatExceptionOfType(ActivitiException.class)
              .as("Could deploy a process definition with a XOR Gateway without outgoing sequence flows.")
              .isThrownBy(() ->
                  repositoryService.createDeployment().addString("myprocess.bpmn20.xml", noOutgoingFlow).deploy()
              );
      }
  
      @Deployment
      public void testAsyncExclusiveGateway() {
          ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(
              "asyncExclusive",
              singletonMap("input", 1)
          );
  
          Job job = managementService.createJobQuery().processInstanceId(processInstance.getId()).singleResult();
          assertThat(job).isNotNull();
  
          managementService.executeJob(job.getId());
          Task task = taskService.createTaskQuery().processInstanceId(processInstance.getId()).singleResult();
          assertThat(task.getName()).isEqualTo("Input is one");
      }
  
      // From https://github.com/Activiti/Activiti/issues/796
      @Deployment
      public void testExclusiveDirectlyToEnd() {
          Map<String, Object> variables = new HashMap<String, Object>();
          variables.put("input", 1);
          ProcessInstance startProcessInstanceByKey = runtimeService.startProcessInstanceByKey(
              "exclusiveGateway",
              variables
          );
          long count = historyService
              .createHistoricActivityInstanceQuery()
              .processInstanceId(startProcessInstanceByKey.getId())
              .unfinished()
              .count();
          assertThat(count).isEqualTo(0);
      }
  }
  