package com.customer.work.model;

import com.customer.work.service.JsonUtils;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.flowable.audit.api.AuditService;
import com.flowable.audit.api.runtime.AuditInstance;
import com.flowable.core.spring.security.SecurityUtils;
import com.flowable.platform.service.task.CompleteFormRepresentation;
import com.flowable.platform.service.task.PlatformTaskService;
import jakarta.mail.Address;
import org.apache.commons.lang3.tuple.Pair;
import org.assertj.core.api.Assertions;
import org.flowable.bpmn.model.*;
import org.flowable.bpmn.model.Process;
import org.flowable.cmmn.api.runtime.CaseInstance;
import org.flowable.cmmn.engine.CmmnEngine;
import org.flowable.common.engine.api.identity.AuthenticationContext;
import org.flowable.common.engine.impl.identity.Authentication;
import org.flowable.engine.ManagementService;
import org.flowable.engine.ProcessEngine;
import org.flowable.engine.TaskService;
import org.flowable.engine.history.HistoricProcessInstance;
import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.job.service.impl.persistence.entity.TimerJobEntity;
import org.flowable.spring.security.SpringSecurityAuthenticationContext;
import org.flowable.task.api.Task;
import org.flowable.variable.api.history.HistoricVariableInstance;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.platform.commons.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StreamUtils;
import org.springframework.web.client.RestClient;

import java.io.File;
import java.io.FileOutputStream;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

@Component
public class FlowableModelTestUtils {

    protected final ProcessEngine processEngine;
    protected final CmmnEngine cmmnEngine;
    protected final TaskService taskService;
    protected final PlatformTaskService platformTaskService;
    protected final ManagementService managementService;
    protected final JsonUtils jsonUtils;
    protected final FlowableExcelMapper flowableExcelMapper;
    protected static final Logger logger = LoggerFactory.getLogger(FlowableModelTestUtils.class);
    protected final TestMailServer testMailServer;
    protected final AuditService auditService;


    public static String TENANT_ID = null;
    public static String ROOT_PROCESS_ID = "ROOT_PROCESS_ID";
    public static String TEST_PROCESS_ID = "TEST_PROCESS_ID";


    public FlowableModelTestUtils(ProcessEngine processEngine,
                                  CmmnEngine cmmnEngine,
                                  TaskService taskService,
                                  PlatformTaskService platformTaskService,
                                  ManagementService managementService,
                                  JsonUtils jsonUtils,
                                  FlowableExcelMapper flowableExcelMapper,
                                  TestMailServer testMailServer,
                                  AuditService auditService) {
        this.processEngine = processEngine;
        this.cmmnEngine = cmmnEngine;
        this.taskService = taskService;
        this.platformTaskService = platformTaskService;
        this.managementService = managementService;
        this.jsonUtils = jsonUtils;
        this.flowableExcelMapper = flowableExcelMapper;
        this.testMailServer = testMailServer;
        this.auditService = auditService;
    }

    public void checkAndAndAssertAuditRecord(Map<String, Object> map, int auditNumber, String hint, String message, String category, String type) {
        if (map != null) {
            Object check = map.get("audit");
            if (check instanceof Integer && (Integer) check > 0) {
                assertAuditRecord(auditNumber, hint, message, category, type);
            }
        }
    }

    protected void assertAuditRecord(int auditNumber, String hint, String message, String category, String type) {
        List<AuditInstance> auditTrail = getAuditTrail();
        int auditTrailSize = auditTrail.size();
        Assertions.assertThat(auditTrailSize).as(hint + ": invalid auditNumber " + auditTrailSize).isGreaterThanOrEqualTo(auditNumber);
        if (auditTrailSize == 0) return;

        AuditInstance auditInstance = auditTrail.get(auditNumber - 1);
        Assertions.assertThat(auditInstance.getPayload().get("message")).as(hint).isEqualTo(message);
        Assertions.assertThat(auditInstance.getPayload().get("category")).as(hint).isEqualTo(category);
        Assertions.assertThat(auditInstance.getType()).as(hint).isEqualTo(type);
    }

    public void checkAndAssertEmail(Map<String, Object> map, int emailNumber, String hint, String subject, String receivers) {
        if (map != null) {
            Object check = map.get("email");
            if (check instanceof Integer && (Integer) check > 0) {
                assertEmail(emailNumber, hint, subject, receivers);
            }
        }
    }

    protected void assertEmail(int emailNumber, String hint, String subject, String receivers) {
        List<EmailDto> emails = getMailList();
        int emailListSize = emails.size();
        Assertions.assertThat(emailListSize).as(hint + ": invalid email number " + emailListSize).isGreaterThanOrEqualTo(emailNumber);
        if  (emailNumber == 0) return;

        EmailDto email = emails.get(emailNumber - 1);
        String emailSubject = email.getSubject();
        Assertions.assertThat(emailSubject).as(hint + ": invalid subject " + emailSubject).endsWith(subject);

        List<Pair<String, Boolean>> receiverCheckList = new ArrayList<>();
        for (String receiver : receivers.split("[,\\s]+")) {
            receiverCheckList.add(Pair.of(receiver, false));
        }
        List<String> emailReceiverList = email.getReceiverList();
        Assertions.assertThat(emailReceiverList.size()).as(hint + ": invalid number of receivers " + emailReceiverList).isEqualTo(receiverCheckList.size());

        for (String emailReceiver : emailReceiverList) {
            for (int i = 0; i < receiverCheckList.size(); i++) {
                Pair<String, Boolean> checkReceiver = receiverCheckList.get(i);
                if (checkReceiver.getLeft().equals(emailReceiver) && !checkReceiver.getRight()) {
                    receiverCheckList.set(i, Pair.of(emailReceiver, true));
                    break;
                } else if (i == receiverCheckList.size() - 1) {
                    Assertions.fail(hint + ": invalid email receiver " + emailReceiverList);
                }
            }
        }
    }

    public Map<String, Object> getHistoryCasePayload(String caseInstanceId) {
        List<HistoricVariableInstance> historicVariableInstanceList = cmmnEngine.getCmmnHistoryService()
                .createHistoricVariableInstanceQuery()
                .caseInstanceId(caseInstanceId)
                .list();
        return convertHistVariableListToMap(historicVariableInstanceList);
    }

    public Map<String, Object> getRuntimeCasePayload(String caseId) {
        return cmmnEngine.getCmmnRuntimeService().getVariables(caseId);
    }

    public CaseInstance startCaseInstance(String key, Map<String, Object> variables) {
        return cmmnEngine.getCmmnRuntimeService()
                .createCaseInstanceBuilder()
                .caseDefinitionKey(key)
                .tenantId(TENANT_ID) // Must not be "default"
                .variables(variables)
                .start();
    }

    public ProcessInstance startProcessInstance(String key, Map<String, Object> variables) {
        return processEngine.getRuntimeService()
                .createProcessInstanceBuilder()
                .processDefinitionKey(key)
                .tenantId(TENANT_ID) // Must not be "default"
                .variables(variables)
                .start();
    }

    public Task getOpenTask(String taskKey) {
        Task task = taskService.createTaskQuery().taskDefinitionKey(taskKey).singleResult();
        Assertions.assertThat(task).as("No open task with key {} found", taskKey).isNotNull();
        return task;
    }

    public Task getOpenTaskByName(String taskName) {
        Task task = taskService.createTaskQuery().taskName(taskName).singleResult();
        Assertions.assertThat(task).as("No open task with name {} found", taskName).isNotNull();
        return task;
    }

    public void claimOpenTask(String taskKey, String userId) {
        setTestAuthenticatedUser(userId, TENANT_ID);
        taskService.claim(getOpenTask(taskKey).getId(), userId);
    }

    public List<AuditInstance> getAuditTrail() {
        return auditService.createAuditInstanceQuery()
                //.tenantId(TENANT_ID) TODO: Must not be null, should work with default tenant
                .list();
    }

    public List<EmailDto> getMailList() {
        return Arrays.stream(testMailServer.getMessages()).map(mimeMessage -> {
            try {
                return new EmailDto(mimeMessage.getSubject(), Arrays.stream(mimeMessage.getAllRecipients())
                        .map(Address::toString).collect(Collectors.toList()), mimeMessage.getContent().toString(), mimeMessage.getContent());
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }).collect(Collectors.toList());
    }

    public boolean isSubset(JsonNode root, JsonNode test) {
        // If test is null, it is always a subset of root
        if (test == null || test.isNull()) {
            return true;
        }
        // If test is a value node, compare values
        if (test.isValueNode()) {
            if (test.isTextual() && "__N_A".equals(test.asText())) {
                return false;
            }
            if (root == null) {
                return false;
            }
            return root.isValueNode() && root.asText().equals(test.asText());
        }
        // If test is an array node, check if all elements of test exist in root
        if(test.isArray()){
            for (int i = 0; i < test.size(); i++) {
                JsonNode testElement = test.get(i);
                JsonNode rootElement = null;
                if (root != null) {
                    rootElement = root.get(i);
                }
                if (testElement.isTextual() && "__N_A".equals(testElement.asText())) {
                    if (rootElement != null && !rootElement.isNull()) {
                        return false;
                    }
                    continue;
                }
                if (!isSubset(rootElement, testElement)) {
                    return false;
                }
            }
            return true;
        }
        // If test is an object node, check if all fields in test exist in root
        if (test.isObject()){
            for (Iterator<String> iterator = test.fieldNames(); iterator.hasNext(); ) {
                String fieldName = iterator.next();
                JsonNode testValue = test.get(fieldName);
                JsonNode rootValue = null;
                if (root != null) {
                    rootValue = root.get(fieldName);
                }
                if (testValue.isTextual() && "__N_A".equals(testValue.asText())) {
                    if (root != null && root.has(fieldName)) {
                        return false;
                    }
                    continue;
                }
                if (!isSubset(rootValue, testValue)){
                    return false;
                }
            }
            return true;
        }
        // If none of the above matches, return false
        return false;
    }

    public Stream<Arguments> getJsonArgumentsFromExcel(String path) {
        ArrayList<Arguments> argumentList = new ArrayList<>();
        JsonNode book = flowableExcelMapper.excelBookResourceToJsonNode(path);
        for (JsonNode sheet : book) {
            for (JsonNode row : sheet) {
                argumentList.add(Arguments.of(path, row));
            }
        }
        return argumentList.stream();
    }

    public Stream<Arguments> getObjArgumentsFromExcel(String path) {
        ArrayList<Arguments> argumentList = new ArrayList<>();
        ArrayList<ArrayList<Object>> book = flowableExcelMapper.excelBookResourceToObj(path);
        for (ArrayList<Object> sheet : book) {
            for (Object row : sheet) {
                argumentList.add(Arguments.of(path, row));
            }
        }
        return argumentList.stream();
    }

    public ObjectNode testJsonExcelRow(String path, JsonNode row) {
        ObjectNode vars = jsonUtils.getEmptyObjectNode();
        JsonNode rootParam = row.get("root");
        if (rootParam != null) {
            Iterator<Map.Entry<String, JsonNode>> rootVars = rootParam.fields();
            while (rootVars.hasNext()) {
                Map.Entry<String, JsonNode> rootVar = rootVars.next();
                String key = rootVar.getKey();
                JsonNode value = rootVar.getValue();
                vars.set(key, value);
            }
        }
        JsonNode inParam = row.get("in");
        if (inParam != null) {
            vars.set("__IN", inParam);
        }
        JsonNode outParam = row.get("out");
        if (outParam != null) {
            vars.set("__OUT", outParam);
        }
        JsonNode idParam = row.get("id");
        JsonNode test = row.get("test");
        int timerCount = 0;
        JsonNode timerNode = row.get("timer");
        if (timerNode != null) timerCount = timerNode.asInt();
        Integer auditRecordCount = null;
        JsonNode auditNode = row.get("audit");
        if (auditNode != null) auditRecordCount = auditNode.asInt();
        Integer emailCount = null;
        JsonNode emailNode = row.get("email");
        if (emailNode != null) emailCount = emailNode.asInt();

        logger.info("TestExcelRow file={} id={} rootParam={} inParam={} outParam={} timer={} test={} audit={} email={} ",
                path, idParam, rootParam, inParam, outParam, timerCount, test, auditRecordCount, emailCount);
        ObjectNode processIds = createRootTestProcessInstance(idParam.asText(), jsonUtils.convertJsonNodeToMap(vars));
        for (int i = 0; i < timerCount; i++) {
            executeTimer(processIds.get(TEST_PROCESS_ID).asText());
        }

        ObjectNode result = jsonUtils.getEmptyObjectNode();
        Map<String, Object> rootProcessPayload = getHistProcessPayload(processIds.get(FlowableModelTestUtils.ROOT_PROCESS_ID).asText());
        JsonNode root = jsonUtils.convertMapToJsonNode(rootProcessPayload);
        if (root != null) {
            Assertions.assertThat(isSubset(root, test))
                    .withFailMessage(System.lineSeparator() + "test :" + test + System.lineSeparator() + "root :" + root).isTrue();
            result.set("root", root);
        }
        if (auditRecordCount != null) {
//            List<AuditInstance>  auditTrail = getAuditTrail(processIds.get(ROOT_PROCESS_ID).asText());
            List<AuditInstance>  auditTrail = getAuditTrail();
            int auditTrailSize = auditTrail.size();
            Assertions.assertThat(auditTrailSize).as("Invalid audit trail size: {}", auditTrailSize).isEqualTo(auditRecordCount);
            result.set("auditTrail", jsonUtils.convertListToJsonNode(Collections.singletonList(auditTrail)));
        }
        if (emailCount != null) {
            List<EmailDto> emailList = getMailList();
            int emailListSize = emailList.size();
            Assertions.assertThat(emailListSize).as("Invalid number of emails: {}", emailListSize).isEqualTo(emailCount);
            result.set("emails", jsonUtils.convertListToJsonNode(Collections.singletonList(emailList)));
        }
        return result;
    }

    public Map<String, Object> testObjExcelRow(String path, Map<String, Object> row) {
        Map<String, Object> vars = new LinkedHashMap<>();
        Map<String, Object>  rootParam = (Map<String, Object>) row.get("root");
        if (rootParam != null) {
            for (Map.Entry<String, Object> entry : rootParam.entrySet()) {
                vars.put(entry.getKey(), entry.getValue());
            }
        }
        Map<String, Object> inParam = (Map<String, Object>) row.get("in");
        if (inParam != null) {
            vars.put("__IN", inParam);
        }
        Map<String, Object> outParam = (Map<String, Object>) row.get("out");
        if (outParam != null) {
            vars.put("__OUT", outParam);
        }
        String idParam = (String) row.get("id");
        Map<String, Object> test =(Map<String, Object>) row.get("test");
        int timerCount = 0;
        Object timerNode = row.get("timer");
        if (timerNode != null) timerCount = (Integer) timerNode;
        Integer auditRecordCount = null;
        Object auditNode = row.get("audit");
        if (auditNode != null) auditRecordCount = (Integer) auditNode;
        Integer emailCount = null;
        Object emailNode = row.get("email");
        if (emailNode != null) emailCount = (Integer) emailNode;

        logger.info("TestExcelRow file={} id={} rootParam={} inParam={} outParam={} timer={} test={} audit={} email={} ",
                path, idParam, rootParam, inParam, outParam, timerCount, test, auditRecordCount, emailCount);
        ObjectNode processIds = createRootTestProcessInstance(idParam, vars);
        for (int i = 0; i < timerCount; i++) {
            executeTimer(processIds.get(TEST_PROCESS_ID).asText());
        }

        Map<String, Object> result = new LinkedHashMap<>();
        Map<String, Object> rootProcessPayload = getHistProcessPayload(processIds.get(FlowableModelTestUtils.ROOT_PROCESS_ID).asText());
        JsonNode root = jsonUtils.convertMapToJsonNode(rootProcessPayload);
        if (root != null) {
            Assertions.assertThat(isSubset(root, jsonUtils.convertMapToJsonNode(test)))
                    .withFailMessage(System.lineSeparator() + "test :" + test + System.lineSeparator() + "root :" + root).isTrue();
            result.put("root", rootProcessPayload);
        }
        if (auditRecordCount != null) {
//            List<AuditInstance> auditTrail = getAuditTrail(processIds.get(ROOT_PROCESS_ID).asText());
            List<AuditInstance> auditTrail = getAuditTrail();
            int auditTrailSize = auditTrail.size();
            Assertions.assertThat(auditTrailSize).as("Invalid audit trail size: {}", auditTrailSize).isEqualTo(auditRecordCount);
            result.put("audit", auditRecordCount);
        }
        if (emailCount != null) {
            List<EmailDto> emailList = getMailList();
            int emailListSize = emailList.size();
            Assertions.assertThat(emailListSize).as("Invalid number of emails: {}", emailListSize).isEqualTo(emailCount);
            result.put("email", emailCount);
        }
        return result;
    }

    public void exportApp(String rootUrl, String appModelKey, String username, String password) {
        RestClient restClient = RestClient.builder()
                .baseUrl(rootUrl)
                .defaultHeaders(h -> h.setBasicAuth(username, password))
                .build();

        ResponseEntity<JsonNode> responseEntity = restClient.post()
                .uri("/app/authentication?j_username=" + username + "&j_password=" + password + "&spring_security_remember_me=true&submit=Login")
                .retrieve()
                .toEntity(JsonNode.class);

        String designCookieRaw = responseEntity.getHeaders().get("Set-Cookie").stream().collect(Collectors.joining(";"));
        String flowableDesignRememberMeTokenValue = Arrays.stream(designCookieRaw.split(";")).filter(part -> part.contains("FLOWABLE_DESIGN_REMEMBER_ME")).findAny().get();
        String csrfToken = Arrays.stream(designCookieRaw.split(";")).filter(part -> part.contains("FLOWABLE_DESIGN_CSRF_TOKEN")).findAny().get();

        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.set("Cookie", flowableDesignRememberMeTokenValue + ";" + csrfToken);

        ArrayNode apps = (ArrayNode) restClient.get()
                .uri("/app/models?filter=apps&modelType=3&sort=modifiedDesc")
                .headers(h -> h.addAll(httpHeaders))
                .retrieve()
                .body(JsonNode.class)
                .path("data");
        String appModelId = StreamSupport.stream(apps.spliterator(), false).filter(app -> app.path("key").asText().equals(appModelKey)).map(app -> app.path("id").asText()).findAny().get();

        File file = new File(Paths.get("src/test/resources/test-auto-deploy-apps", appModelKey + ".zip").toString());
        restClient.get()
                .uri("/app/app-definitions/" + appModelId + "/export-bar?includeChildReferences=true")
                .header("Cookie", flowableDesignRememberMeTokenValue + ";" + csrfToken)
                .exchange((req, resp) -> {
                    StreamUtils.copy(resp.getBody(), new FileOutputStream(file, false));
                    return file;
                });
    }

    public ObjectNode emptyNode() {
        return jsonUtils.getEmptyObjectNode();
    }

    public Map<String, Object>  emptyMap() {
        return new LinkedHashMap<>();
    }

    public ObjectNode loadObjectNodeFromResources(String path) {
        if (path.isEmpty()) return jsonUtils.getEmptyObjectNode();
        else return jsonUtils.loadObjectNodeFromFile(Paths.get("src","test", "resources", path).toString());
    }

    public Map<String, Object> loadMapFromResources(String path) {
        return jsonUtils.convertJsonNodeToMap(loadObjectNodeFromResources(path));
    }

    public void executeTimer(String processId) {
        TimerJobEntity timerJobEntity = (TimerJobEntity) managementService.createTimerJobQuery().processInstanceId(processId).singleResult();
        Assertions.assertThat(timerJobEntity).withFailMessage("No active timer").isNotNull();
        managementService.moveTimerToExecutableJob(timerJobEntity.getId());
        managementService.executeJob(timerJobEntity.getId());
    }
    public void setTestAuthenticatedUser(String userId, String tenantId) {
        if (userId == null) {
            this.resetTestAuthenticatedUser();
        } else {
            List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
            if (StringUtils.isNotBlank(tenantId)) {
                grantedAuthorities.add(SecurityUtils.createTenantAuthority(tenantId));
            }
            SecurityContextHolder.getContext().setAuthentication(new TestingAuthenticationToken(userId, "", grantedAuthorities));
            AuthenticationContext authenticationContext = Authentication.getAuthenticationContext();
            if (!(authenticationContext instanceof SpringSecurityAuthenticationContext)) {
                Authentication.setAuthenticatedUserId(userId);
            }
        }
    }

    public void setTestAuthenticatedUser(String userId, String tenantId, String... groupKeys) {
        if (userId == null) {
            this.resetTestAuthenticatedUser();
        } else {
            List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
            if (StringUtils.isNotBlank(tenantId)) {
                grantedAuthorities.add(SecurityUtils.createTenantAuthority(tenantId));
            }

            for (String groupKey : groupKeys) {
                grantedAuthorities.add(SecurityUtils.createGroupAuthority(groupKey));
            }

            SecurityContextHolder.getContext().setAuthentication(new TestingAuthenticationToken(userId, "", grantedAuthorities));
            AuthenticationContext authenticationContext = Authentication.getAuthenticationContext();
            if (!(authenticationContext instanceof SpringSecurityAuthenticationContext)) {
                Authentication.setAuthenticatedUserId(userId);
            }
        }
    }

    public void resetTestAuthenticatedUser() {
        SecurityContextHolder.getContext().setAuthentication(null);
        AuthenticationContext authenticationContext = Authentication.getAuthenticationContext();
        if (!(authenticationContext instanceof SpringSecurityAuthenticationContext)) {
            Authentication.setAuthenticatedUserId(null);
        }
    }

    public void completeTaskWithFlatVars(String taskId, Map<String, Object> variables, String outcome) {
        Map<String, Object> taskVariables = platformTaskService.getTaskVariables(taskId);
        Map<String, Object> taskVariablesFlat = jsonUtils.flatten(taskVariables);
        taskVariablesFlat.putAll(variables);
        Map<String, Object> completionVars = jsonUtils.unflatten(taskVariablesFlat);

        completeTask(taskId, completionVars, outcome);
    }

    public void completeTask(String taskId, Map<String, Object> completionVariables, String outcome) {
        CompleteFormRepresentation form = new CompleteFormRepresentation();
        for (String key : completionVariables.keySet()) {
            form.setValues(key, completionVariables.get(key));
        }
        form.setOutcome(outcome);
        platformTaskService.completeTaskForm(taskId, form);
    }

    public void completeOpenTask(String taskKey, ObjectNode vars, String outcome) {
        Task task = getOpenTask(taskKey);
        Map<String, Object> flatVars = jsonUtils.convertJsonNodeToMap(vars);
        completeTaskWithFlatVars(task.getId(), flatVars, outcome);
    }

    public Map<String, Object> getHistProcessPayload(String processInstanceId) {
        List<HistoricVariableInstance> historicVariableInstanceList = processEngine.getHistoryService()
                .createHistoricVariableInstanceQuery()
                .processInstanceId(processInstanceId)
                .list();
        return convertHistVariableListToMap(historicVariableInstanceList);
    }

    private HashMap<String, Object> convertHistVariableListToMap(List<HistoricVariableInstance> historicVariableInstanceList) {
        HashMap<String, Object> variableMap = new HashMap<>();
        for (HistoricVariableInstance historicVariableInstance : historicVariableInstanceList) {
            variableMap.put(historicVariableInstance.getVariableName(), historicVariableInstance.getValue());
        }
        return variableMap;
    }

    public ObjectNode createRootTestProcessInstance(String testKey, Map<String, Object> variables) {
        // Create root process with test process as call activity
        String wrapperProcessKey = testKey + "_T";
        BpmnModel bpmnModel = createWrapperTestProcessModel(testKey, (ObjectNode) jsonUtils.convertMapToJsonNode(variables));
        processEngine.getProcessEngineConfiguration()
                .getRepositoryService()
                .createDeployment()
                .addBpmnModel(wrapperProcessKey + ".bpmn20.xml", bpmnModel)
                .tenantId(TENANT_ID) // Must not be "default"
                .deploy();

        // Start root process
        ProcessInstance rootProcessInstance = startProcessInstance(wrapperProcessKey, variables);

        // Store the process IDs for using them in assertions
        ProcessInstance runtimeTestProcessInstance = processEngine.getRuntimeService()
                .createProcessInstanceQuery()
                .superProcessInstanceId(rootProcessInstance.getId())
                .singleResult();
        String testProcessId;
        if (runtimeTestProcessInstance != null) {
            testProcessId = runtimeTestProcessInstance.getId();
        } else {
            HistoricProcessInstance historicTestProcessInstance = processEngine.getHistoryService()
                    .createHistoricProcessInstanceQuery()
                    .superProcessInstanceId(rootProcessInstance.getId())
                    .singleResult();
            // if history level is none, testProcessId is not available
            testProcessId = historicTestProcessInstance != null ? historicTestProcessInstance.getId() : null;
        }
        ObjectNode rootNode = jsonUtils.getEmptyObjectNode();
        rootNode.put(ROOT_PROCESS_ID, rootProcessInstance.getProcessInstanceId());
        rootNode.put(TEST_PROCESS_ID, testProcessId);
        return rootNode;
    }

    protected BpmnModel createWrapperTestProcessModel(String testKey, ObjectNode variables) {
        BpmnModel bpmnModel = new BpmnModel();
        Process process = new Process();
        process.setId(testKey + "_T");
        process.setName(testKey + "_T");

        StartEvent startEvent = new StartEvent();
        startEvent.setId(testKey + "_Start");
        startEvent.setName(testKey + "_Start");
        process.addFlowElement(startEvent);

        CallActivity callActivity = new CallActivity();
        callActivity.setId(testKey);
        callActivity.setCalledElement(testKey);
        JsonNode inNode = variables.get("__IN");
        if (inNode instanceof ObjectNode) {
            Map<String, Object> inMap = jsonUtils.convertObjectNodeToMap((ObjectNode) inNode);
            ArrayList<IOParameter> inParameters = new ArrayList<>();
            for (Map.Entry<String, Object> entry : inMap.entrySet()) {
                IOParameter ioParameter = new IOParameter();
                ioParameter.setSourceExpression("${__IN." + entry.getKey() + "}");
                ioParameter.setTarget(entry.getKey());
                inParameters.add(ioParameter);
            }
            callActivity.setInParameters(inParameters);
        }
        JsonNode outNode = variables.get("__OUT");
        if (outNode instanceof ObjectNode) {
            Map<String, String> outMap = (Map) jsonUtils.convertJsonNodeToMap(variables).get("__OUT");
            ArrayList<IOParameter> outParameters = new ArrayList<>();
            for (Map.Entry<String, String> entry : outMap.entrySet()) {
                IOParameter ioParameter = new IOParameter();
                ioParameter.setSource(entry.getKey());
                ioParameter.setTarget(entry.getValue());
                outParameters.add(ioParameter);
            }
            callActivity.setOutParameters(outParameters);
        }
        process.addFlowElement(callActivity);

        EndEvent endEvent = new EndEvent();
        endEvent.setId(testKey + "_End");
        endEvent.setName(testKey + "_End");
        process.addFlowElement(endEvent);

        SequenceFlow start = new SequenceFlow(testKey + "_Start", testKey);
        start.setId("Start_Sequence_Flow");
        process.addFlowElement(start);

        SequenceFlow end = new SequenceFlow(testKey, testKey + "_End");
        end.setId("End_Sequence_Flow");
        process.addFlowElement(end);

        bpmnModel.addProcess(process);
        return bpmnModel;
    }

}
