content: /*
   * Copyright 2017-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.cloud.qa.story;
  
  import static org.assertj.core.api.Assertions.assertThat;
  import static org.assertj.core.api.Assertions.catchThrowableOfType;
  import static org.awaitility.Awaitility.await;
  
  import feign.FeignException;
  import java.time.Duration;
  import java.util.ArrayList;
  import java.util.Collection;
  import java.util.Date;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  import net.serenitybdd.core.Serenity;
  import net.thucydides.core.annotations.Steps;
  import net.thucydides.core.steps.StepEventBus;
  import org.activiti.api.model.shared.event.VariableEvent;
  import org.activiti.api.task.model.Task;
  import org.activiti.api.task.model.builders.TaskPayloadBuilder;
  import org.activiti.api.task.model.events.TaskRuntimeEvent;
  import org.activiti.cloud.acc.core.steps.audit.AuditSteps;
  import org.activiti.cloud.acc.core.steps.audit.admin.AuditAdminSteps;
  import org.activiti.cloud.acc.core.steps.query.ProcessQuerySteps;
  import org.activiti.cloud.acc.core.steps.query.TaskQuerySteps;
  import org.activiti.cloud.acc.core.steps.query.admin.ProcessQueryAdminSteps;
  import org.activiti.cloud.acc.core.steps.query.admin.TaskQueryAdminSteps;
  import org.activiti.cloud.acc.core.steps.runtime.ProcessRuntimeBundleSteps;
  import org.activiti.cloud.acc.core.steps.runtime.TaskRuntimeBundleSteps;
  import org.activiti.cloud.acc.core.steps.runtime.TaskVariableRuntimeBundleSteps;
  import org.activiti.cloud.acc.core.steps.runtime.admin.ProcessRuntimeAdminSteps;
  import org.activiti.cloud.acc.core.steps.runtime.admin.TaskRuntimeAdminSteps;
  import org.activiti.cloud.api.process.model.CloudProcessInstance;
  import org.activiti.cloud.api.task.model.CloudTask;
  import org.activiti.cloud.qa.helpers.VariableGenerator;
  import org.jbehave.core.annotations.Given;
  import org.jbehave.core.annotations.Then;
  import org.jbehave.core.annotations.When;
  import org.springframework.hateoas.PagedModel;
  
  public class Tasks {
  
      public static final String STAND_ALONE_TASK_ID = "standAloneTaskId";
  
      @Steps
      private ProcessRuntimeBundleSteps processRuntimeBundleSteps;
  
      @Steps
      private TaskRuntimeBundleSteps taskRuntimeBundleSteps;
  
      @Steps
      private TaskVariableRuntimeBundleSteps taskVariableRuntimeBundleSteps;
  
      @Steps
      private TaskRuntimeAdminSteps taskRuntimeAdminSteps;
  
      @Steps
      private ProcessRuntimeAdminSteps processRuntimeAdminSteps;
  
      @Steps
      private ProcessQuerySteps processQuerySteps;
  
      @Steps
      private TaskQuerySteps taskQuerySteps;
  
      @Steps
      private ProcessQueryAdminSteps processQueryAdminSteps;
  
      @Steps
      private TaskQueryAdminSteps taskQueryAdminSteps;
  
      @Steps
      private AuditSteps auditSteps;
  
      @Steps
      private AuditAdminSteps auditAdminSteps;
  
      /**
       * standalone task
       */
      private CloudTask newTask;
  
      /**
       * subtask of {@link #newTask}
       */
      private Task subtask;
  
      @When("the user creates a standalone task")
      @Given("the user creates a standalone task")
      public void createTask() throws Exception {
          newTask = taskRuntimeBundleSteps.createNewTask();
          assertThat(newTask).isNotNull();
          Serenity.setSessionVariable(STAND_ALONE_TASK_ID).to(newTask.getId());
      }
  
      @When("the user creates an unassigned standalone task")
      public void createUnassignedTask() throws Exception {
          newTask = taskRuntimeBundleSteps.createNewUnassignedTask();
          assertThat(newTask).isNotNull();
          Serenity.setSessionVariable(STAND_ALONE_TASK_ID).to(newTask.getId());
      }
  
      @Then("the created task has a status assigned")
      public void taskIsCreatedAndAssigned() throws Exception {
          final CloudTask assignedTask = taskRuntimeBundleSteps.getTaskById(newTask.getId());
          assertThat(assignedTask).isNotNull();
          assertThat(assignedTask.getStatus()).isEqualTo(Task.TaskStatus.ASSIGNED);
          auditSteps.checkTaskCreatedAndAssignedEventsWhenAlreadyAssigned(assignedTask.getId());
          taskQuerySteps.checkTaskStatus(assignedTask.getId(), Task.TaskStatus.ASSIGNED);
      }
  
      @When("the user deletes the standalone task")
      public void deleteCurrentTask() {
          taskRuntimeBundleSteps.deleteTask(newTask.getId());
      }
  
      @When("the admin deletes the standalone task")
      public void adminDeleteCurrentTask() {
          taskRuntimeAdminSteps.deleteTask(newTask.getId());
      }
  
      @Then("the standalone task is deleted")
      public void checkTaskIsDeleted() throws Exception {
          taskRuntimeBundleSteps.checkTaskNotFound(newTask.getId());
          auditSteps.checkTaskDeletedEvent(newTask.getId());
          taskQuerySteps.checkTaskStatus(
              newTask.getId(),
              //TODO change to DELETED when RB is ready
              Task.TaskStatus.CANCELLED
          );
      }
  
      @When("user creates a subtask for the previously created task")
      public void createASubtask() {
          subtask = taskRuntimeBundleSteps.createSubtask(newTask.getId());
          assertThat(subtask).isNotNull();
          assertThat(subtask.getParentTaskId()).isNotNull();
          assertThat(subtask.getParentTaskId()).isNotEmpty();
      }
  
      @Then("the subtask is created and references another task")
      public void taskWithSubtaskIsCreated() {
          final CloudTask createdSubtask = taskRuntimeBundleSteps.getTaskById(subtask.getId());
          assertThat(createdSubtask).isNotNull();
          assertThat(createdSubtask.getParentTaskId()).isNotEmpty().isEqualToIgnoringCase(newTask.getId());
          auditSteps.checkSubtaskCreated(createdSubtask.getId(), newTask.getId());
          taskQuerySteps.checkSubtaskHasParentTaskId(subtask.getId(), subtask.getParentTaskId());
      }
  
      @Then("a list of one subtask is be available for the task")
      public void getSubtasksForTask() {
          final Collection<CloudTask> subtasks = taskRuntimeBundleSteps.getSubtasks(newTask.getId());
          assertThat(subtasks).isNotNull().extracting(CloudTask::getId).containsExactly(subtask.getId());
      }
  
      @Then("the task has the formKey field and correct processInstance fields")
      public void checkIfFormKeyAndProcessInstanceFieldsArePresent() {
          newTask = obtainFirstTaskFromProcess();
          assertThat(newTask).isNotNull().extracting(CloudTask::getFormKey).isEqualTo("taskForm");
  
          CloudProcessInstance processFromQuery = processQuerySteps.getProcessInstance(
              Serenity.sessionVariableCalled("processInstanceId").toString()
          );
          assertThat(processFromQuery).isNotNull();
  
          CloudTask taskFromRB = taskRuntimeBundleSteps.getTaskById(newTask.getId());
          assertThat(taskFromRB).isNotNull();
          assertThat(taskFromRB.getFormKey()).isEqualTo("taskForm");
          assertThat(taskFromRB.getProcessDefinitionId()).isEqualTo(processFromQuery.getProcessDefinitionId());
  
          CloudTask taskFromQuery = taskQuerySteps.getTaskById(newTask.getId());
          assertThat(taskFromQuery).isNotNull();
          assertThat(taskFromQuery.getFormKey()).isEqualTo("taskForm");
          assertThat(taskFromQuery.getProcessDefinitionId()).isEqualTo(processFromQuery.getProcessDefinitionId());
          assertThat(taskFromQuery.getProcessDefinitionVersion())
              .isEqualTo(processFromQuery.getProcessDefinitionVersion());
      }
  
      private CloudTask obtainFirstTaskFromProcess() {
          String processInstanceId = Serenity.sessionVariableCalled("processInstanceId").toString();
          List<CloudTask> tasksFromRB = new ArrayList<>(
              processRuntimeBundleSteps.getTaskByProcessInstanceId(processInstanceId)
          );
          assertThat(tasksFromRB).isNotEmpty();
          newTask = tasksFromRB.get(0);
          assertThat(newTask).isNotNull();
          return newTask;
      }
  
      @Then("a task variable was created with name $variableName")
      @When("a task variable was created with name $variableName")
      public void verifyVariableCreated(String variableName) throws Exception {
          newTask = obtainFirstTaskFromProcess();
  
          taskQuerySteps.checkTaskHasVariable(newTask.getId(), variableName, variableName);
  
          auditSteps.checkTaskVariableEvent(
              Serenity.sessionVariableCalled("processInstanceId").toString(),
              newTask.getId(),
              variableName,
              VariableEvent.VariableEvents.VARIABLE_CREATED
          );
      }
  
      @Then("task variable $variableName has value $variableValue")
      @When("task variable $variableName has value $variableValue")
      public void verifyVariableValue(String variableName, String variableValue) throws Exception {
          newTask = obtainFirstTaskFromProcess();
  
          taskQuerySteps.checkTaskHasVariable(newTask.getId(), variableName, variableValue);
      }
  
      @Then("we update task variable $variableName to $variableValue")
      @When("we update task variable $variableName to $variableValue")
      public void updateTaskVariableValue(String variableName, String variableValue) {
          newTask = obtainFirstTaskFromProcess();
  
          taskVariableRuntimeBundleSteps.updateVariable(newTask.getId(), variableName, variableValue);
      }
  
      @When("the user updates the name of the task to $newTaskName")
      public void setTaskName(String newTaskName) {
          String processInstanceId = Serenity.sessionVariableCalled("processInstanceId");
          Collection<CloudTask> tasksCollection = taskQuerySteps
              .getTasksByProcessInstance(processInstanceId)
              .getContent();
          List<CloudTask> tasksList = new ArrayList<>(tasksCollection);
          newTask = tasksList.get(0);
          taskRuntimeBundleSteps.setTaskName(newTask.getId(), newTaskName);
      }
  
      @When("the user updates the updatable fields of the task")
      public void updateTaskFields() {
          getTaskToUpdateForCurrentProcessInstance();
  
          Date tomorrow = new Date(System.currentTimeMillis() + 86400000);
          Serenity.setSessionVariable("tomorrow").to(tomorrow);
  
          taskRuntimeBundleSteps.setTaskName(newTask.getId(), "new-task-name");
          taskRuntimeBundleSteps.setTaskPriority(newTask.getId(), 3);
          taskRuntimeBundleSteps.setTaskDueDate(newTask.getId(), tomorrow);
          taskRuntimeBundleSteps.setTaskFormKey(newTask.getId(), "new-task-form-key");
      }
  
      private void getTaskToUpdateForCurrentProcessInstance() {
          String processInstanceId = Serenity.sessionVariableCalled("processInstanceId");
          waitForTasks(processInstanceId);
          Collection<CloudTask> tasksCollection = taskQuerySteps
              .getTasksByProcessInstance(processInstanceId)
              .getContent();
          List<CloudTask> tasksList = new ArrayList<>(tasksCollection);
          newTask = tasksList.get(0);
      }
  
      private void adminGetTaskToUpdateForCurrentProcessInstance() {
          String processInstanceId = Serenity.sessionVariableCalled("processInstanceId");
          adminWaitForTasks(processInstanceId);
          Collection<CloudTask> tasksCollection = taskQueryAdminSteps
              .getTasksByProcessInstance(processInstanceId)
              .getContent();
          List<CloudTask> tasksList = new ArrayList<>(tasksCollection);
          newTask = tasksList.get(0);
      }
  
      private void waitForTasks(String processInstanceId) {
          await()
              .untilAsserted(() -> {
                  Collection<CloudTask> tasksCollection = taskQuerySteps
                      .getTasksByProcessInstance(processInstanceId)
                      .getContent();
                  assertThat(tasksCollection).isNotEmpty();
              });
      }
  
      private void adminWaitForTasks(String processInstanceId) {
          await()
              .untilAsserted(() -> {
                  Collection<CloudTask> tasksCollection = taskQueryAdminSteps
                      .getTasksByProcessInstance(processInstanceId)
                      .getContent();
                  assertThat(tasksCollection).isNotEmpty();
              });
      }
  
      @When("the admin updates the updatable fields of the task")
      public void adminUpdateTaskFields() {
          adminGetTaskToUpdateForCurrentProcessInstance();
  
          Date tomorrow = new Date(System.currentTimeMillis() + 86400000);
          Serenity.setSessionVariable("tomorrow").to(tomorrow);
  
          taskRuntimeAdminSteps.updateTask(
              newTask.getId(),
              TaskPayloadBuilder
                  .update()
                  .withName("new-task-name")
                  .withPriority(3)
                  .withDueDate(tomorrow)
                  .withFormKey("new-task-form-key")
                  .build()
          );
      }
  
      @Then("the task has the updated fields")
      public void checkUpdatedTaskFields() {
          Date tomorrow = Serenity.sessionVariableCalled("tomorrow");
  
          //name
          assertThat(taskRuntimeBundleSteps.getTaskById(newTask.getId()).getName()).isEqualTo("new-task-name");
          assertThat(taskQuerySteps.getTaskById(newTask.getId()).getName()).isEqualTo("new-task-name");
          //priority
          assertThat(taskRuntimeBundleSteps.getTaskById(newTask.getId()).getPriority()).isEqualTo(3);
          assertThat(taskQuerySteps.getTaskById(newTask.getId()).getPriority()).isEqualTo(3);
          //dueDate
          assertThat(taskRuntimeBundleSteps.getTaskById(newTask.getId()).getDueDate()).isEqualTo(tomorrow);
          assertThat(taskQuerySteps.getTaskById(newTask.getId()).getDueDate()).isEqualTo(tomorrow);
          //formKey
          assertThat(taskRuntimeBundleSteps.getTaskById(newTask.getId()).getFormKey()).isEqualTo("new-task-form-key");
          //assertThat(taskQuerySteps.getTaskById(newTask.getId()).getFormKey()).isEqualTo("new-task-form-key");
      }
  
      @Then("the task is updated")
      public void checkIfTaskUpdated() {
          auditSteps.checkTaskUpdatedEvent(newTask.getId());
      }
  
      @Then("the user will get only root tasks when quering for root tasks")
      public void checkRootTasks() {
          String processInstanceId = Serenity.sessionVariableCalled("processInstanceId");
          await()
              .untilAsserted(() -> {
                  Collection<CloudTask> rootTasksCollection = taskQuerySteps
                      .getRootTasksByProcessInstance(processInstanceId)
                      .getContent();
  
                  assertThat(rootTasksCollection).isNotEmpty();
  
                  rootTasksCollection.forEach(task -> assertThat(task.getParentTaskId()).isNull());
              });
      }
  
      @Then("the user will get only standalone tasks when quering for standalone tasks")
      public void checkStandaloneTasks() {
          Collection<CloudTask> standaloneTasksCollection = taskQuerySteps.getStandaloneTasks().getContent();
  
          assertThat(standaloneTasksCollection).isNotNull();
          assertThat(standaloneTasksCollection).isNotEmpty();
          standaloneTasksCollection.forEach(task -> assertThat(task.isStandalone()).isTrue());
      }
  
      @Then("the standalone task can be queried using LIKE operator")
      public void queryNameAndDescriptionWithLikeOperator() {
          PagedModel<CloudTask> retrievedTasks = taskQuerySteps.getTasksByNameAndDescription(
              newTask.getName().substring(0, 2),
              newTask.getDescription().substring(0, 2)
          );
  
          for (CloudTask task : retrievedTasks) {
              assertThat(task.getName()).contains(newTask.getName().substring(0, 2));
              assertThat(task.getDescription()).contains(newTask.getDescription().substring(0, 2));
          }
      }
  
      @When("the user creates task variables")
      public void setTaskVariables() {
          for (Map.Entry<String, Object> entry : VariableGenerator.variables.entrySet()) {
              taskVariableRuntimeBundleSteps.createVariable(newTask.getId(), entry.getKey(), entry.getValue());
              taskVariableRuntimeBundleSteps.getVariables(newTask.getId());
          }
      }
  
      @Then("task variables are visible in rb and query")
      public void checkTaskVariablesAreTheSameInRBAndQuery() {
          await()
              .pollInterval(Duration.ofMillis(250L))
              .pollInSameThread()
              .untilAsserted(() -> {
                  Map<String, Object> generatedMapRuntime = new HashMap<>();
                  Map<String, Object> generatedMapQuery = new HashMap<>();
  
                  taskVariableRuntimeBundleSteps
                      .getVariables(newTask.getId())
                      .forEach(element -> generatedMapRuntime.put(element.getName(), element.getValue()));
  
                  taskQuerySteps
                      .getVariables(newTask.getId())
                      .forEach(element -> generatedMapQuery.put(element.getName(), element.getValue()));
  
                  assertThat(generatedMapRuntime).isEqualTo(VariableGenerator.variables);
                  assertThat(generatedMapQuery).isEqualTo(VariableGenerator.variables);
              });
      }
  
      @When("the user claims the standalone task")
      public void claimTask() throws Exception {
          taskRuntimeBundleSteps.claimTask(newTask.getId());
      }
  
      @When("the user releases the standalone task")
      public void releaseTask() throws Exception {
          taskRuntimeBundleSteps.releaseTask(newTask.getId());
      }
  
      @Then("the status of the task is $taskStatus in RB and Query")
      public void checkTaskStatusInRBAndQuery(Task.TaskStatus taskStatus) {
          taskRuntimeBundleSteps.checkTaskStatus(newTask.getId(), taskStatus);
          taskQuerySteps.checkTaskStatus(newTask.getId(), taskStatus);
      }
  
      @Then("the user is able to delete all tasks in query service")
      public void deleteAllTasksQuery() {
          //check standalone tasks
          assertThat(taskQueryAdminSteps.getAllTasks()).isNotEmpty();
          taskQueryAdminSteps.deleteTasks();
          assertThat(taskQueryAdminSteps.getAllTasks()).isEmpty();
      }
  
      @Then("the user retrieves the tasks and the standalone tasks separately")
      public void getTasksAndStandaloneTasks() {
          Collection<CloudTask> tasks = taskRuntimeBundleSteps.getAllTasks();
          assertThat(tasks).isNotEmpty();
          assertThat(tasks).isNotNull();
  
          Collection<CloudTask> standaloneTasks = taskRuntimeBundleSteps.getTaskWithStandalone(true);
          Collection<CloudTask> normalTasks = taskRuntimeBundleSteps.getTaskWithStandalone(false);
  
          assertThat(tasks.size()).isEqualTo(standaloneTasks.size() + normalTasks.size());
  
          normalTasks.forEach(cloudTask -> assertThat(tasks.contains(cloudTask)).isTrue());
          standaloneTasks.forEach(cloudTask -> assertThat(tasks.contains(cloudTask)).isTrue());
      }
  
      @Then("the status of the task is $taskStatus in Audit and Query")
      public void checkTaskStatusInAuditAndQuery(Task.TaskStatus taskStatus) throws Exception {
          String processInstanceId = Serenity.sessionVariableCalled("processInstanceId").toString();
          String currentTaskId = Serenity.sessionVariableCalled("currentTaskId").toString();
  
          auditSteps.checkProcessInstanceTaskEvent(
              processInstanceId,
              currentTaskId,
              TaskRuntimeEvent.TaskEvents.TASK_COMPLETED
          );
  
          taskQuerySteps.checkTaskStatus(currentTaskId, taskStatus);
      }
  
      @When("the task contains candidate groups $candidateGroups in Query")
      @Then("the task contains candidate groups $candidateGroups in Query")
      public void checkTaskCandidateGroups(String candidateGroups) {
          String currentTaskId = Serenity.sessionVariableCalled("currentTaskId").toString();
          waitForTask(currentTaskId);
          List<String> taskCandidateGroups = taskQuerySteps.getCandidateGroups(currentTaskId);
  
          assertThat(taskCandidateGroups).contains(candidateGroups.split(","));
      }
  
      @When("the task contains candidate users $user in Query")
      public void checkTaskCandidateUsers(String user) {
          String currentTaskId = Serenity.sessionVariableCalled("currentTaskId").toString();
          waitForTask(currentTaskId);
          List<String> taskCandidateUsers = taskQuerySteps.getCandidateUsers(currentTaskId);
  
          assertThat(taskCandidateUsers).contains(user);
      }
  
      private void waitForTask(String currentTaskId) {
          await()
              .untilAsserted(() -> {
                  final Throwable throwable = catchThrowableOfType(
                      () -> taskQuerySteps.getTaskById(currentTaskId),
                      FeignException.class
                  );
                  if (throwable != null) {
                      //It's important to clear step failures after an Exception, otherwise,
                      //the step will be marked to be skipped and any subsequent call to
                      //taskQuerySteps will return mocks instead of calling the real endpoint.
                      //Without clearing step failures the await block become useless.
                      StepEventBus.getEventBus().clearStepFailures();
                  }
                  assertThat(throwable).isNull();
              });
      }
  
      @When("the task does not contain candidate user $user in Query")
      public void checkUserIsNotCandidate(String user) {
          String currentTaskId = Serenity.sessionVariableCalled("currentTaskId").toString();
          waitForTask(currentTaskId);
          List<String> taskCandidateUsers = taskQuerySteps.getCandidateUsers(currentTaskId);
  
          assertThat(taskCandidateUsers).doesNotContain(user);
      }
  }
  