package com.intrasoft.ermis.transit.management.core.service;

import static com.intrasoft.ermis.transit.common.util.MessageUtil.SKIP_MOVEMENT_EXISTENCE_LIST;
import static com.intrasoft.ermis.transit.common.util.MessageUtil.extractMRNCountryCode;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.intrasoft.ermis.common.event.ErmisEvent;
import com.intrasoft.ermis.common.logging.ErmisLogger;
import com.intrasoft.ermis.common.xml.enums.NamespacesEnum;
import com.intrasoft.ermis.common.xml.parsing.XmlConverter;
import com.intrasoft.ermis.ddntav5152.messages.AddressType07;
import com.intrasoft.ermis.ddntav5152.messages.CC015CType;
import com.intrasoft.ermis.ddntav5152.messages.CC056CType;
import com.intrasoft.ermis.ddntav5152.messages.CC057CType;
import com.intrasoft.ermis.ddntav5152.messages.CC906CType;
import com.intrasoft.ermis.ddntav5152.messages.CC917CType;
import com.intrasoft.ermis.ddntav5152.messages.CD027CType;
import com.intrasoft.ermis.ddntav5152.messages.CD038CType;
import com.intrasoft.ermis.ddntav5152.messages.CD906CType;
import com.intrasoft.ermis.ddntav5152.messages.CustomsOfficeOfDepartureType03;
import com.intrasoft.ermis.ddntav5152.messages.CustomsOfficeOfDestinationActualType03;
import com.intrasoft.ermis.ddntav5152.messages.FunctionalErrorType04;
import com.intrasoft.ermis.ddntav5152.messages.HolderOfTheTransitProcedureType08;
import com.intrasoft.ermis.ddntav5152.messages.MessageTypes;
import com.intrasoft.ermis.ddntav5152.messages.RepresentativeType01;
import com.intrasoft.ermis.ddntav5152.messages.TraderAtDestinationType03;
import com.intrasoft.ermis.ddntav5152.messages.TransitOperationType20;
import com.intrasoft.ermis.ddntav5152.messages.TransitOperationType21;
import com.intrasoft.ermis.ddntav5152.messages.TransitOperationType32;
import com.intrasoft.ermis.documentsv1.messages.CCE46AType;
import com.intrasoft.ermis.documentsv1.messages.MessageERMISTypes;
import com.intrasoft.ermis.documentsv1.validator.DocumentsXmlValidator;
import com.intrasoft.ermis.platform.contracts.dossier.enums.StatisticsSystemEnum;
import com.intrasoft.ermis.transit.common.config.services.GetCountryCodeService;
import com.intrasoft.ermis.transit.common.enums.CcnReportTypeEnum;
import com.intrasoft.ermis.transit.common.enums.DirectionEnum;
import com.intrasoft.ermis.transit.common.enums.DomainEnum;
import com.intrasoft.ermis.transit.common.enums.MessageDomainTypeEnum;
import com.intrasoft.ermis.transit.common.enums.MessageStatusEnum;
import com.intrasoft.ermis.transit.common.enums.OfficeRoleTypeStatuses;
import com.intrasoft.ermis.transit.common.enums.OfficeRoleTypesEnum;
import com.intrasoft.ermis.transit.common.enums.ProcessingStatusTypeEnum;
import com.intrasoft.ermis.transit.common.exceptions.BaseException;
import com.intrasoft.ermis.transit.common.exceptions.NotFoundException;
import com.intrasoft.ermis.transit.common.exceptions.RetryableException;
import com.intrasoft.ermis.transit.common.mappers.MessageTypeMapper;
import com.intrasoft.ermis.transit.common.transition.period.TransitionPeriodService;
import com.intrasoft.ermis.transit.common.util.IE906Generator;
import com.intrasoft.ermis.transit.common.util.MessageUtil;
import com.intrasoft.ermis.transit.common.util.Pair;
import com.intrasoft.ermis.transit.contracts.core.enums.AarRejectionReasonEnum;
import com.intrasoft.ermis.transit.contracts.core.enums.MessageXPathsEnum;
import com.intrasoft.ermis.transit.contracts.management.enums.MessageAttributeKeysEnum;
import com.intrasoft.ermis.transit.contracts.management.enums.MovementAttributeKeysEnum;
import com.intrasoft.ermis.transit.management.core.configuration.ManagementRetryConfigurationProperties;
import com.intrasoft.ermis.transit.management.core.exception.BadRequestException;
import com.intrasoft.ermis.transit.management.core.port.inbound.DDNTAMessageProcessingService;
import com.intrasoft.ermis.transit.management.core.port.outbound.DDNTAMessageProducer;
import com.intrasoft.ermis.transit.management.core.port.outbound.DocumentResponseProducer;
import com.intrasoft.ermis.transit.management.core.port.outbound.DossierDataService;
import com.intrasoft.ermis.transit.management.core.port.outbound.ProcessMessageCommandProducer;
import com.intrasoft.ermis.transit.management.core.port.outbound.ValidationService;
import com.intrasoft.ermis.transit.management.core.util.MessageValidationUtils;
import com.intrasoft.ermis.transit.management.core.util.ViolationUtils;
import com.intrasoft.ermis.transit.management.domain.CcnReport;
import com.intrasoft.ermis.transit.management.domain.Message;
import com.intrasoft.ermis.transit.management.domain.MessageAttribute;
import com.intrasoft.ermis.transit.management.domain.MessageOverview;
import com.intrasoft.ermis.transit.management.domain.Movement;
import com.intrasoft.ermis.transit.management.domain.MovementAttribute;
import com.intrasoft.ermis.transit.management.domain.MovementQuery;
import com.intrasoft.ermis.transit.management.domain.MovementsOrViolations;
import com.intrasoft.ermis.transit.management.domain.ProcessingStatus;
import com.intrasoft.ermis.transit.management.domain.RejectedMessage;
import com.intrasoft.ermis.transit.management.domain.StatisticalMessage;
import com.intrasoft.ermis.transit.management.domain.Violation;
import com.intrasoft.ermis.transit.shared.security.service.UserDetailsService;
import java.io.IOException;
import java.io.StringReader;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.xml.bind.JAXBException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

//TODO Class (and corresponding interface) name should be changed to be compatible with a broader variety of message types

@Service
@RequiredArgsConstructor
@SuppressWarnings("findsecbugs:XPATH_INJECTION")
public class DDNTAMessageProcessingServiceImpl implements DDNTAMessageProcessingService {

    private static final List<String> retryList = Arrays.asList(MessageTypes.CD_018_C.value(), MessageTypes.CD_152_C.value());

    public static final String CC = "CC";

    private static final String TB = "TB";

    private static final String NECA = "NECA.";

    private static final XPath XPATH = XPathFactory.newInstance().newXPath();

    private static final String IDENTIFICATION_NUMBER = "//HolderOfTheTransitProcedure/identificationNumber";

    private static final String TIR_IDENTIFICATION_NUMBER = "//HolderOfTheTransitProcedure/TIRHolderIdentificationNumber";

    private static final String NAME = "//HolderOfTheTransitProcedure/name";

    private static final String ADDRESS = "//HolderOfTheTransitProcedure/Address";

    private static final String STREETANDNUMBER = "//HolderOfTheTransitProcedure/Address/streetAndNumber";

    private static final String POSTCODE = "//HolderOfTheTransitProcedure/Address/postcode";

    private static final String CITY = "//HolderOfTheTransitProcedure/Address/city";

    private static final String COUNTRY = "//HolderOfTheTransitProcedure/Address/country";

    private static final String REPRESENTATIVE = "//Representative";

    private static final String STATUS = "//Representative/status";

    private static final String IDENTIFICATION_NUMBER_REPRESENTATIVE = "//Representative/identificationNumber";

    private static final String IDENTIFICATION_NUMBER_TRADER_AT_DEST = "//TraderAtDestination/identificationNumber";

    private static final String SIMPLIFIED_AUTHORISATION = "C521";

    private static final String DECLARATION_TYPE = "//TransitOperation/declarationType";

    private static final Set<String> OODEST_SKIP_OFFICE_ID_VALIDATION_MESSAGE_TYPES = Set.of(MessageTypes.CD_150_C.value(), MessageTypes.CC_007_C.value());

    private static final int E1115_MAX_LENGTH = 140;

    private static final String XSD_ERROR_REASON = "XSD";

    private static final List<String> INCOMING_REJECTION_MESSAGES_LIST =
            Arrays.asList(MessageTypes.CC_906_C.value(), MessageTypes.CC_917_C.value(), MessageTypes.CD_906_C.value(), MessageTypes.CD_917_C.value());

    private final ErmisLogger log;

    private final DossierDataService dossierDataService;

    private final ValidationService validationService;

    private final ProcessMessageCommandProducer processMessageCommandProducer;

    private final DDNTAMessageProducer ddntaMessageProducer;

    private final GetCountryCodeService getCountryCodeService;

    private final MessageTypeMapper messageTypeMapper;

    private final TransitionPeriodService transitionPeriodService;

    private final CargoMessageService cargoMessageService;

    private final ObjectMapper objectMapper;

    private final ConfigurationSwitchService configurationSwitchService;

    private final UserDetailsService userDetailsService;

    private final MessageValidationService messageValidationService;

    private final GenerateMessageService generateMessageService;

    // NOTE: Following bean is used in @Retryable SpEL expressions. do not delete
    private final ManagementRetryConfigurationProperties managementRetryConfigurationProperties;

    @Value("${trs.management.external-messages-xsd-validation}")
    private boolean isExternalMessagesXsdValidationEnabled;

    private final DDNTAMessageCustomValidationServiceImpl ddntaMessageCustomValidationService;

    private final DocumentResponseProducer documentResponseProducer;

    private final DocumentsXmlValidator documentsXmlValidator = new DocumentsXmlValidator();

    private static final Map<String, String> MESSAGE_TO_OFFICE = Map.of(
            MessageTypes.CD_142_C.value(), OfficeRoleTypesEnum.DESTINATION.getValue(),
            MessageTypes.CD_144_C.value(), OfficeRoleTypesEnum.DESTINATION.getValue(),
            MessageTypes.CD_002_C.value(), OfficeRoleTypesEnum.DEPARTURE.getValue(),
            MessageTypes.CD_114_C.value(), OfficeRoleTypesEnum.DEPARTURE.getValue(),
            MessageTypes.CD_164_C.value(), OfficeRoleTypesEnum.DEPARTURE.getValue()
    );

    // List of Message types that need not undergo R&C Validation:
    private static final List<String> SKIP_RC_VALIDATION_LIST = List.of(
            MessageTypes.CC_015_C.value(),
            com.intrasoft.ermis.cargov1.messages.MessageTypes.TB_015_C.value()
    );

    // List of Message types that need to undergo Authorization Validation:
    // (All CC0015C validations are deferred to OoDep)
    private static final List<String> AUTHORIZATION_VALIDATION_LIST = List.of(
            MessageTypes.CC_007_C.value()
    );

    // List of Message Types that need special handling to determine their target office
    private static final List<String> CUSTOM_TARGET_LIST = List.of(
            MessageTypes.CD_094_C.value(),
            MessageTypes.CD_150_C.value(),
            MessageTypes.CD_152_C.value()
    );

    @Override
    @Retryable(value = {RetryableException.class}, maxAttemptsExpression = "#{@managementRetryConfigurationProperties.getMaxAttempts()}",
            backoff = @Backoff(delayExpression = "#{@managementRetryConfigurationProperties.getDelay()}",
                    multiplierExpression = "#{@managementRetryConfigurationProperties.getMultiplier()}"))
    public void processMessage(String messageXml) throws RetryableException, XPathExpressionException, BadRequestException {
        Message message = setUpMessage(messageXml, false, DirectionEnum.RECEIVED);

        //implemented due to PSD-5641 for IE006,IE018 and IE063,IE152 messages pairs.
        if (retryList.contains(message.getMessageType())) {
            checkWhetherRelatedMessageIsUnderProcess(message);
        }
        continueProcessMessage(message);
    }

    @Recover
    public void recoverProcessMessage(String messageXml, RetryableException exception) throws BadRequestException, XPathExpressionException {
        log.debug("Recovering processMessage method for: " + exception);
        Message message = setUpMessage(messageXml, false, DirectionEnum.RECEIVED);
        continueProcessMessage(message);
    }

    private void continueProcessMessage(Message message) throws BadRequestException, XPathExpressionException {
        // persist
        Message persistedMessage = dossierDataService.persistMessage(message);

        log.info(":=> processMessage() : Received and persisted new Message -> id: [{}], type: [{}], MRN: [{}]",
                message.getMessageIdentification(), message.getMessageType(), message.getMrn());
        // validate and create required movements
        MovementsOrViolations movementsOrViolations = validateMessage(persistedMessage);

        if (!movementsOrViolations.getViolations().isEmpty()) {
            dossierDataService.updateMessage(persistedMessage.getId(), MessageStatusEnum.REJECTED.name());

            movementsOrViolations.getViolations()
                                 .forEach(violation -> log.warn(
                                         ":=> processMessage() : {} Violation(s) found on Message Type [{}]. violationInfo:{}. Will send error message and return",
                                         movementsOrViolations.getViolations().size(), message.getMessageType(), violation.toString()));
            handleViolations(movementsOrViolations, persistedMessage, false);
        } else {

            dossierDataService.updateMessage(persistedMessage.getId(), MessageStatusEnum.ACCEPTED.name());

            handleMovements(movementsOrViolations, persistedMessage);
        }
        handleIncomingRejectionMessage(persistedMessage, movementsOrViolations);
    }

    private void checkWhetherRelatedMessageIsUnderProcess(Message message) throws RetryableException {
        List<MessageOverview> messageOverviewList =
                dossierDataService.getMessagesOverviewByMrnAndMessageTypes(message.getMrn(), MessageTypes.CD_018_C.value().equals(message.getMessageType()) ?
                        List.of(MessageTypes.CD_006_C) :
                        List.of(MessageTypes.CD_152_C));
        if (!messageOverviewList.isEmpty()) {
            List<MessageAttribute> messageAttributeList = dossierDataService.findMessageAttributesByMessageId(messageOverviewList.get(0).getId());
            if (messageAttributeList.isEmpty() || messageAttributeList.stream().noneMatch(messageAttribute ->
                    messageAttribute.getAttrKey().equals(MessageTypes.CD_006_C.value().equals(messageOverviewList.get(0).getMessageType()) ?
                            MessageAttributeKeysEnum.CD006C_MESSAGE_PROCESSED.getValue() :
                            MessageAttributeKeysEnum.CD063C_MESSAGE_PROCESSED.getValue())
            )) {
                log.error("Retry processing of message with identification: {} and type: {}", message.getMessageIdentification(), message.getMessageType());
                throw new RetryableException("Message is Under Process!");
            }
        }
    }

    @SneakyThrows
    private void handleIncomingCD906C(Message message, MovementsOrViolations movements) {
        CD906CType cd906CType = XmlConverter.buildMessageFromXML(message.getPayload(), CD906CType.class);

        createAndPersistRejectedMessageInfo(cd906CType, message.getId(), DirectionEnum.RECEIVED, movements.getMovements());
    }

    private void handleIncomingRejectionMessage(Message message, MovementsOrViolations movements) {
        if (INCOMING_REJECTION_MESSAGES_LIST.contains(message.getMessageType())) {
            //if 906 or 917 message arrives, the corresponding rejected message must be marked as rejected
            List<MessageOverview> messageOverviewList =
                    dossierDataService.getMessagesOverview(null, null, getCorrelationIdentifier(message.getPayload()), null, null, null, 0, 20);

            if (!messageOverviewList.isEmpty()) {
                Message rejectedMessage = dossierDataService.getMessageById(messageOverviewList.get(0).getId(), false);
                dossierDataService.updateMessage(rejectedMessage.getId(), MessageStatusEnum.REJECTED.name());
            }

            // CD906 should also create a new entry in the rejectMessage table
            if (MessageTypes.CD_906_C.value().equals(message.getMessageType())) {
                handleIncomingCD906C(message, movements);
            }
        }
    }

    @Override
    public void processManualMessage(String messageXml) throws BadRequestException, XPathExpressionException {
        log.info("DDNTAMessageProcessingServiceImpl - processManualMessage");

        // persist
        Message message = dossierDataService.persistMessage(setUpMessage(messageXml, true, DirectionEnum.RECEIVED));
        log.info(":=> processManualMessage() : Received and persisted new DDNTA Message -> id: [{}], type: [{}], MRN: [{}]",
                message.getMessageIdentification(), message.getMessageType(), message.getMrn());
        // Create userId message attribute, to be used my Movement History:
        createSubmissionUserIdMessageAttribute(message.getId());
        // validate and create required movements
        MovementsOrViolations mov = validateMessage(message);
        if (!mov.getViolations().isEmpty()) {
            log.warn(":=> processManualMessage() : {} Violation(s) found on Message Type [{}]. Will send error message and return",
                    mov.getViolations().size(), message.getMessageType());
            dossierDataService.updateMessage(message.getId(), MessageStatusEnum.REJECTED.name());
            handleViolations(mov, message, true);
        } else {
            dossierDataService.updateMessage(message.getId(), MessageStatusEnum.ACCEPTED.name());
            handleMovements(mov, message);
        }
    }

    @Override
    @SneakyThrows
    public void processMessage903(String messageXml) {
        // persist
        Message message = dossierDataService.persistMessage(setUpMessage(messageXml, false, DirectionEnum.RECEIVED));
        log.info(":=> processMessage903() : Received and persisted new DDNTA Message -> id: [{}], type: [{}], MRN: [{}]",
                message.getMessageIdentification(), message.getMessageType(), message.getMrn());

        String system = message.getDestination().substring(0, message.getDestination().indexOf('.'));
        StatisticsSystemEnum statisticsSystemEnum = StatisticsSystemEnum.fromMessageSenderPrefix(system);
        List<Violation> violations = new ArrayList<>();
        if (statisticsSystemEnum == null) {
            violations.add(Violation.builder().reason("System " + system + " is not valid.").rejectedValue(message.getDestination()).build());
        } else if (!dossierDataService.isExistStatisticalMessageByMessageId(message.getMessageIdentification())) {
            dossierDataService.saveStatisticalMessage(
                    createStatisticalMessage903(message.getMessageIdentification(), message.getPayload(), statisticsSystemEnum.getMessageSenderPrefix()));
        } else {
            log.warn("Ignoring saving 903 message to statistical message table with message ID {} because it has been already saved.",
                    message.getMessageIdentification());
        }

        violations = validationService.validateMovementMessage(message.getId());

        if (!CollectionUtils.isEmpty(violations)) {
            MovementsOrViolations mov = MovementsOrViolations.of(Collections.emptyList(), violations);
            log.warn(":=> processMessage903() : {} Violation(s) found on Message Type [{}]. Will send error message and return",
                    violations.size(), message.getMessageType());
            handleViolations(mov, message, false);
        }
    }

    public void handle027(String messageXml) throws BadRequestException {
        try (StringReader sr = new StringReader(messageXml)) {
            InputSource xml = new InputSource(sr);
            Node root = (Node) XPATH.evaluate("/", xml, XPathConstants.NODE);

            String mrn = XPATH.evaluate(MessageXPathsEnum.MESSAGE_MRN.getValue(), root);

            if (dossierDataService.existsMovementByMrn(mrn)) {
                processMessage(messageXml);
            } else {
                generateIE038ForUnknownMovement(messageXml);
            }
        } catch (XPathExpressionException | JAXBException e) {
            log.error("ERROR in handle027()", e);
        } catch (BadRequestException e) {
            throw new BadRequestException(e.getViolationList());
        }
    }

    public void handle038(String messageXml) throws BadRequestException {
        try (StringReader sr = new StringReader(messageXml)) {
            InputSource xml = new InputSource(sr);
            Node root = (Node) XPATH.evaluate("/", xml, XPathConstants.NODE);
            String mrn = XPATH.evaluate(MessageXPathsEnum.MESSAGE_MRN.getValue(), root);

            processMessage(messageXml);

            String officeOfRequest = XPATH.evaluate(MessageXPathsEnum.DESTINATION_OFFICE_OF_REQUEST_OFFICE_ID.getValue(), root);
            handleMovementQueryGlobalResponse(mrn, messageXml, officeOfRequest);
        } catch (XPathExpressionException e) {
            log.error("ERROR in handle038()", e);
        } catch (BadRequestException e) {
            throw new BadRequestException(e.getViolationList());
        }
    }

    @SneakyThrows
    @Override
    public void processCcnReport(ErmisEvent ermisEvent) {
        CcnReportTypeEnum ccnReportTypeEnum = CcnReportTypeEnum.fromValue(ermisEvent.getEventType());
        if (ccnReportTypeEnum == null)
            throw new IllegalArgumentException(String.format("Not a valid CCN Report Message ErmisEvent type: [%s]", ermisEvent.getEventType()));
        CcnReport ccnReport = CcnReport.builder()
                                       .ccnReportTypeEnum(ccnReportTypeEnum)
                                       .mrn(ermisEvent.getProperties().get("declarationId"))
                                       .originalMessageId(ermisEvent.getProperties().get("originalMessageId"))
                                       .build();

        List<Message> originalMessages = dossierDataService.getMessagesByCriteria(ccnReport.getOriginalMessageId())
                                                           .stream()
                                                           .filter(message -> Objects.equals(message.getDirection(), DirectionEnum.SENT.getDirection()))
                                                           //.filter(message -> Objects.equals(message.getStatus(),MessageStatusEnum.ACCEPTED.name()))
                                                           .collect(Collectors.toList());
        if (!originalMessages.isEmpty()) {
            ccnReport.setOriginalMessageType(originalMessages.get(0).getMessageType());
        }
        Message reportMessage = dossierDataService.persistMessage(
                setupCcnReportMessage(ccnReport, originalMessages.isEmpty() ? MessageStatusEnum.REJECTED : MessageStatusEnum.ACCEPTED));
        log.info(":=> processCcnReport() : Received and persisted new CCN Report Message -> id: [{}], type: [{}], MRN: [{}] for Original Message Id: [{}]",
                reportMessage.getId(), reportMessage.getMessageType(), reportMessage.getMrn(), ccnReport.getOriginalMessageId());

        if (originalMessages.isEmpty()) {
            log.error("processCcnReport() ERROR: Original Message NOT FOUND for CcnReport.originalMessageId: [{}]", ccnReport.getOriginalMessageId());
        } else {
            List<Movement> existingMovements = dossierDataService.findAssociatedMovements(ccnReport.getOriginalMessageId(), DirectionEnum.SENT.getDirection());
            handleMessageMovementAssociation(existingMovements, reportMessage);
        }
    }

    @Override
    public void processCCE46A(String messageXml) {
        try {
            documentsXmlValidator.validateXmlPayload(messageXml, MessageERMISTypes.CCE_46_A.value());

            CCE46AType cce46AType = XmlConverter.buildMessageFromXML(messageXml, CCE46AType.class);

            Message savedMessage = dossierDataService.persistMessage(setupCCE46AMessage(cce46AType, messageXml));
            Optional<MessageOverview> optionalMessage =
                    dossierDataService.getMessagesOverview(null, MessageERMISTypes.CCE_60_A.value(), cce46AType.getCorrelationIdentifier(), null, null, null, 0,
                                              20).stream()
                                      .findFirst();

            MessageOverview message = optionalMessage.orElseThrow(() ->
                    new NotFoundException("Couldn't find any message for correlationIdentifier of cce46A: " + cce46AType.getCorrelationIdentifier()));

            List<Movement> existingMovements = dossierDataService.findAssociatedMovements(message.getMessageIdentification(), DirectionEnum.SENT.getDirection());
            if (CollectionUtils.isEmpty(existingMovements)) {
                throw new NotFoundException("Couldn't find any related movements for mrn "
                        + cce46AType.getTransitOperation().getMRN() + " and lrn "
                        + cce46AType.getTransitOperation().getLRN());
            }

            dossierDataService.associateMessageWithMovement(savedMessage.getId(), existingMovements.get(0).getId());
            documentResponseProducer.publish(cce46AType, existingMovements.get(0).getId());
        } catch (SAXParseException exception) {
            log.error("CCE46A payload is not valid:");
            log.error(exception.getMessage());
            throw new BaseException("CCE46A payload is not valid", exception);
        } catch (JAXBException exception) {
            throw new BaseException("Error building class type from string xml", exception);
        } catch (IOException | SAXException exception) {
            throw new RuntimeException("Error during XSD validation", exception);
        }
    }

    private Message setupCCE46AMessage(CCE46AType cce46AType, String messageXml) {
        return Message.builder()
                      .payload(messageXml)
                      .messageIdentification(cce46AType.getMessageIdentification())
                      .messageType(cce46AType.getMessageType().value())
                      .mrn(cce46AType.getTransitOperation().getMRN())
                      .source(cce46AType.getMessageSender())
                      .destination(cce46AType.getMessageRecipient())
                      .domain(MessageDomainTypeEnum.EXTERNAL.name())
                      .direction(DirectionEnum.RECEIVED.getDirection())
                      .manualInserted(false)
                      .status("")
                      .build();
    }

    private Message setupCcnReportMessage(CcnReport ccnReport, MessageStatusEnum messageStatusEnum) throws JsonProcessingException {
        return Message.builder()
                      .messageIdentification(MessageUtil.generateMessageIdentification())
                      .messageType(ccnReport.getCcnReportTypeEnum().name())
                      .source("CCN")
                      .destination("ERMIS")
                      .payload(objectMapper.writeValueAsString(ccnReport))
                      .domain(MessageDomainTypeEnum.COMMON.name())
                      .mrn(ccnReport.getMrn())
                      .direction(DirectionEnum.RECEIVED.getDirection())
                      .status(messageStatusEnum.name())
                      .manualInserted(false)
                      .build();
    }

    public void generateIE038ForUnknownMovement(String messageXml) throws JAXBException, XPathExpressionException {
        Message message = dossierDataService.persistMessage(setUpMessage(messageXml, false, DirectionEnum.RECEIVED));

        validateMovementMessage(message.getId());

        //Gather data
        CD027CType cd027CType = XmlConverter.buildMessageFromXML(messageXml, CD027CType.class);

        //Compose message
        CD038CType cd038CType = composeCD038CMessage(cd027CType);

        dossierDataService.persistMessage(
                setUpMessage(XmlConverter.marshal(cd038CType, NamespacesEnum.NTA.getValue(), MessageTypes.CD_038_C.value()), false, DirectionEnum.SENT));

        ddntaMessageProducer.publish(cd038CType, determineErmisEventBusinessKey(null, ObjectUtils.isNotEmpty(cd038CType.getTransitOperation()) ?
                cd038CType.getTransitOperation().getMRN() : null));
    }

    private CD038CType composeCD038CMessage(CD027CType cd027CType) {
        CD038CType messageOut = messageTypeMapper.fromCD027CtoCD38C(cd027CType);
        messageOut.setMessageType(MessageTypes.CD_038_C);
        messageOut.setPreparationDateAndTime(LocalDateTime.now());
        messageOut.setMessageIdentification(MessageUtil.generateMessageIdentification());
        messageOut.setCorrelationIdentifier(cd027CType.getMessageIdentification());

        log.debug("Handling 038 composition for missing MRN");
        messageOut.setConsignment(null);
        messageOut.setTransitOperation(new TransitOperationType32());
        messageOut.getTransitOperation().setRequestRejectionReasonCode(AarRejectionReasonEnum.MRN_UNKNOWN.getValue());
        messageOut.getTransitOperation().setMRN(cd027CType.getTransitOperation().getMRN());
        messageOut.setCustomsOfficeOfDeparture(null);
        messageOut.setHolderOfTheTransitProcedure(null);
        messageOut.setCustomsOfficeOfDestinationDeclared(null);
        messageOut.getCustomsOfficeOfExitForTransitDeclared().clear();
        messageOut.getCustomsOfficeOfTransitDeclared().clear();
        messageOut.setControlResult(null);

        messageOut.setMessageSender(constructMessageSender());
        messageOut.setMessageRecipient(cd027CType.getMessageSender());

        return messageOut;
    }

    public String constructMessageSender() {
        return MessageTypeMapper.NTA.concat(getCountryCodeService.getCountryCode());
    }

    /**
     * In case Ermis Transit is the one who asked (sent the IE027) then we also need to update our movement query entry Otherwise Ermis Transit is answering
     * (with IE038) someone else's question (IE027)
     *
     * @param mrn
     * @param messageXml
     * @param officeOfRequest
     */
    public void handleMovementQueryGlobalResponse(String mrn, String messageXml, String officeOfRequest) {
        log.info("handleMovementQueryGlobalResponse() start");

        try {
            MovementQuery movementQuery = dossierDataService.findMovementQueriesByCriteria(mrn);

            movementQuery.setResponsePayload(messageXml);
            movementQuery.setRespondent(officeOfRequest);
            dossierDataService.updateMovementQuery(movementQuery);
        } catch (NotFoundException notFoundException) {
            log.info("Movement Query does not exist in our system, skip it");
        } catch (BaseException exception) {
            log.error("Found more than one Movement Query");
        }
        log.info("handleMovementQueryGlobalResponse() end");
    }

    @SneakyThrows(BadRequestException.class)
    private void validateMovementMessage(String messageId) {
        List<Violation> violations = validationService.validateMovementMessage(messageId);

        if (!CollectionUtils.isEmpty(violations))
            throw new BadRequestException(violations);
    }

    private MovementsOrViolations validateMessage(Message message) {

        MovementsOrViolations movementOrViolations = MovementsOrViolations.of(null, null);

        // get valid office roles & statuses
        List<OfficeRoleTypeStatuses> validStates = MessageValidationUtils.getValidStates(message.getMessageType());

        // get existing movements
        List<Movement> existingMovements = getExistingMovements(message, validStates);

        // create dummy movements for office roles that have no existing movement
        List<Movement> movementsToCreate = createMovements(existingMovements, message, validStates);

        if (isExternalMessagesXsdValidationEnabled && MessageUtil.isExternalMessage(message.getMessageType()) && !message.isManualInserted()) {
            movementOrViolations = MovementsOrViolations.of(null, validationService.validateXsd(message.getId()));
        }

        // If no XSD violations exist (for external messages) or if it's a common domain message, proceed with state validation
        if (movementOrViolations.getViolations().isEmpty()) {
            if (existingMovements.isEmpty() && MessageUtil.isErrorMessageType(message.getMessageType())) {
                log.warn("validateMessage(): Could not correlate incoming rejection message of type {}. Ignoring... -> id=[{}], messageId=[{}], MRN=[{}]",
                        message.getMessageType(), message.getId(), message.getMessageIdentification(), Optional.ofNullable(message.getMrn()).orElse("-"));
                return MovementsOrViolations.of(existingMovements, List.of());
            }
        }

        /**
         * Special case of IE170 when MRN Communication is enabled.
         * SOS ! This validation needs to be executed BEFORE the {@link #getExistingMovements} call  -> Modified due to bug ERMIS-40312
         */
        List<Violation> violationsIE170Mrn = ddntaMessageCustomValidationService.validateIE170MrnIfMrnCommunicationIsEnabled(message);
        if(!violationsIE170Mrn.isEmpty()) {
            return MovementsOrViolations.of(existingMovements, violationsIE170Mrn);
        }

        if (existingMovements.isEmpty() && MessageUtil.isErrorMessageType(message.getMessageType())) {
            log.warn("validateMessage(): Could not correlate incoming rejection message of type {}. Ignoring... -> id=[{}], messageId=[{}], MRN=[{}]",
                    message.getMessageType(), message.getId(), message.getMessageIdentification(), Optional.ofNullable(message.getMrn()).orElse("-"));
            return MovementsOrViolations.of(existingMovements, List.of());
        }


        List<Movement> allMovements = Stream.concat(existingMovements.stream(), movementsToCreate.stream()).collect(Collectors.toList());

        movementOrViolations = messageValidationService.performStateValidation(message, allMovements);

        // If no state violations exist, do R&C validation (if required)
        if (movementOrViolations.getViolations().isEmpty() && isRCValidationNeeded(message)) {
            movementOrViolations =
                    MovementsOrViolations.of(movementOrViolations.getMovements(),
                            // Manually submitted messages need to also pass XSD validation (CCN does it normally)
                            message.isManualInserted()
                                    ? validationService.validateMovementMessageAllValidations(message.getId())
                                    : validationService.validateMovementMessage(message.getId()));
        }

        // If no R&C violations exist, do Authorization validation (if required)
        if (movementOrViolations.getViolations().isEmpty() && isAuthorizationValidationNeeded(message)) {
            movementOrViolations =
                    MovementsOrViolations.of(movementOrViolations.getMovements(), validationService.validateSimplifiedProcedureMessage(message.getId()));
        }

        if (!movementOrViolations.getViolations().isEmpty()) {
            movementOrViolations = MovementsOrViolations.of(
                    determineWhichMovementsToUseForAssociation(movementOrViolations.getViolations(), existingMovements, movementsToCreate, message),
                    movementOrViolations.getViolations());
        }

        return movementOrViolations;
    }

    private boolean isRCValidationNeeded(Message message) {
        return MessageUtil.isNotErrorMessageType(message.getMessageType()) && (!SKIP_RC_VALIDATION_LIST.contains(message.getMessageType())
                || message.isManualInserted());
    }

    private boolean isAuthorizationValidationNeeded(Message message) {
        return MessageUtil.isNotErrorMessageType(message.getMessageType()) && AUTHORIZATION_VALIDATION_LIST.contains(message.getMessageType());
    }

    private List<Movement> getExistingMovements(Message message, List<OfficeRoleTypeStatuses> validOfficeRoleTypes) {
        List<Movement> existingMovements = new ArrayList<>();

        Set<String> validOfficeRoleTypesSet =
                validOfficeRoleTypes.stream()
                                    .map(officeRoleTypeStatuses -> officeRoleTypeStatuses.getOfficeRoleType().getValue())
                                    .collect(Collectors.toSet());

        if (MessageUtil.isErrorMessageType(message.getMessageType())) {
            String correlationIdentifier = getCorrelationIdentifier(message.getPayload());
            existingMovements = dossierDataService.findAssociatedMovements(correlationIdentifier, DirectionEnum.SENT.getDirection());
        } else if (CUSTOM_TARGET_LIST.contains(message.getMessageType())) {
            // Get the proper movement for message types that need special handling
            existingMovements = getTargetMovements(message);
        } else if (!StringUtils.isBlank(message.getMrn())) {
            existingMovements = dossierDataService.findByCriteria(message.getMrn(), null, null, null, 50);
        } else if (!StringUtils.isBlank(message.getLrn()) && !MessageTypes.CC_015_C.value().equals(message.getMessageType()) &&
                !com.intrasoft.ermis.cargov1.messages.MessageTypes.TB_015_C.value().equals(message.getMessageType())) {

            List<String> validDuplicateLrnStates = configurationSwitchService.getValidStatesForDuplicateLRN();
            validDuplicateLrnStates.add(ProcessingStatusTypeEnum.OODEP_NONE.getDescription());

            existingMovements = dossierDataService.findByCriteria(null, message.getLrn(), null, null, 99,
                    configurationSwitchService.reuseLrnEnabled() ? getActiveMovementStates(validDuplicateLrnStates) : null
            );
        }


        return existingMovements.stream()
                                .filter(movement -> validOfficeRoleTypesSet.contains(movement.getOfficeRoleType()))
                                .collect(Collectors.toList());
    }

    @NotNull
    private List<String> getActiveMovementStates(List<String> validDuplicateLrnStates) {
        return Arrays.stream(ProcessingStatusTypeEnum.values()).filter(processingStatusTypeEnum ->
                (!validDuplicateLrnStates.contains(processingStatusTypeEnum.getDescription()))
        ).map(ProcessingStatusTypeEnum::getItemCode).collect(Collectors.toList());
    }

    private void handleMovements(MovementsOrViolations mov, Message message) {
        mov.getMovements().forEach(movement -> {
            boolean isNewMovement = movement.getId() == null;
            Movement savedMovement = isNewMovement ? dossierDataService.createMovement(movement) : movement;
            if (isNewMovement) {
                saveTransportId(message.getPayload(), savedMovement.getId());
            }

            dossierDataService.associateMessageWithMovement(message.getId(), savedMovement.getId());

            if (MessageUtil.isErrorMessageType(message.getMessageType())) {
                // Error messages are not handled by office flows so there's no need for a PMC
                return;
            }

            processMessageCommandProducer.publish(message.getId(), message.getMessageType(), savedMovement);
        });

        if (mov.getMovements().isEmpty()) {
            if (SKIP_MOVEMENT_EXISTENCE_LIST.contains(message.getMessageType())) {
                if (MessageTypes.CD_094_C.value().equals(message.getMessageType())) {
                    String targetOfficeRole = decideOfficeRoleType(message);
                    processMessageCommandProducer.publish(message.getId(), message.getMessageType(), targetOfficeRole);
                } else {
                    processMessageCommandProducer.publish(message.getId(), message.getMessageType(), MESSAGE_TO_OFFICE.get(message.getMessageType()));
                }
            } else {
                log.warn("No associated movement found for messageId={} and messageType={}, but it is not marked to skip movement existence check",
                        message.getId(),
                        message.getMessageType());
            }
        }
    }

    private String decideOfficeRoleType(Message message) {

        String targetOfficeRole = null;

        try (StringReader stringReader = new StringReader(message.getPayload())) {

            InputSource xml = new InputSource(stringReader);
            XPath xpath = XPathFactory.newInstance().newXPath();
            Node root = (Node) xpath.evaluate("/", xml, XPathConstants.NODE);

            if (MessageTypes.CD_094_C.value().equals(message.getMessageType())) {

                if (!xpath.evaluate(MessageXPathsEnum.DESTINATION_OFFICE_ID.getValue(), root).isBlank()) {

                    targetOfficeRole = OfficeRoleTypesEnum.DESTINATION.getValue();
                } else if (!xpath.evaluate(MessageXPathsEnum.TRANSIT_OFFICE_ID.getValue(), root).isBlank()) {

                    targetOfficeRole = OfficeRoleTypesEnum.TRANSIT.getValue();
                } else if (!xpath.evaluate(MessageXPathsEnum.EXIT_FOR_TRANSIT_OFFICE_ID.getValue(), root).isBlank()) {

                    targetOfficeRole = OfficeRoleTypesEnum.EXIT.getValue();
                } else {
                    // If no Office is found within the IE094, it should default to Destination role.
                    targetOfficeRole = OfficeRoleTypesEnum.DESTINATION.getValue();
                }
            }

        } catch (Exception e) {

            log.error("ERROR: Could not retrieve XML payload from Message.");
        }

        return targetOfficeRole;
    }

    private void handleViolations(MovementsOrViolations mov, Message message, boolean manuallySubmitted) throws BadRequestException, XPathExpressionException {
        if (manuallySubmitted) {
            sendBadRequest(mov.getViolations());
        } else if (XSD_ERROR_REASON.equals(mov.getViolations().get(0).getReason())) {
            List<Movement> savedMovements = handleMessageMovementAssociation(mov.getMovements(), message);

            if (MessageTypes.CC_015_C.value().equals(message.getMessageType())) {
                dossierDataService.saveProcessingStatus(ProcessingStatus.builder()
                                                                        .status(ProcessingStatusTypeEnum.OODEP_REJECTED.getItemCode())
                                                                        .movementId(savedMovements.get(0).getId())
                                                                        .build());
            }

            sendXsdErrorMessage(message.getPayload(), MovementsOrViolations.of(savedMovements, mov.getViolations()));
        } else {
            if (MessageUtil.isCargoMessage(message.getMessageType())) {
                sendCargoErrorMessage(message.getPayload(), message.getMessageType(), mov);
            } else {
                handleMessageMovementAssociation(mov.getMovements(), message);
                sendErrorMessage(message.getPayload(), message.getMessageType(), mov);
            }
        }
    }

    private List<Movement> handleMessageMovementAssociation(List<Movement> movements, Message message) {
        final List<Movement> savedMovements = new ArrayList<>();

        movements.forEach(movement -> {
            Movement savedMovement = movement.getId() != null ? movement : dossierDataService.createMovement(movement);
            savedMovements.add(savedMovement);
            dossierDataService.associateMessageWithMovement(message.getId(), savedMovement.getId());
        });

        return savedMovements;
    }

    private void sendBadRequest(List<Violation> violations) throws BadRequestException {
        throw new BadRequestException(violations);
    }

    private void sendErrorMessage(String messageXml, String messageType, MovementsOrViolations mov) throws XPathExpressionException {
        if (MessageTypes.CC_007_C.value().equals(messageType) ||
                MessageTypes.CC_044_C.value().equals(messageType)) {
            CC057CType cc057 = createCC057CMessage(messageXml, mov.getViolations());
            Message message = dossierDataService.persistMessage(
                    setUpMessage(XmlConverter.marshal(cc057, NamespacesEnum.NTA.getValue(), MessageTypes.CC_057_C.value()), false, DirectionEnum.SENT));
            handleMessageMovementAssociation(mov.getMovements(), message);
            ddntaMessageProducer.publish(cc057, determineErmisEventBusinessKey(ObjectUtils.isNotEmpty(mov.getMovements()) ? mov.getMovements().get(0) : null,
                    ObjectUtils.isNotEmpty(cc057.getTransitOperation()) ? cc057.getTransitOperation().getMRN() : null));
        } else if (MessageTypes.CC_013_C.value().equals(messageType) ||
                MessageTypes.CC_014_C.value().equals(messageType) ||
                MessageTypes.CC_170_C.value().equals(messageType) ||
                MessageTypes.CC_141_C.value().equals(messageType) ||
                MessageTypes.CC_015_C.value().equals(messageType)) { // Only in case of custom Validations. No movement will be created
            CC056CType cc056 = createCC056CMessage(messageXml, mov);
            Message message = dossierDataService.persistMessage(
                    setUpMessage(XmlConverter.marshal(cc056, NamespacesEnum.NTA.getValue(), MessageTypes.CC_056_C.value()), false, DirectionEnum.SENT));
            handleMessageMovementAssociation(mov.getMovements(), message);
            ddntaMessageProducer.publish(cc056, determineErmisEventBusinessKey(ObjectUtils.isNotEmpty(mov.getMovements()) ? mov.getMovements().get(0) : null,
                    ObjectUtils.isNotEmpty(cc056.getTransitOperation()) ? cc056.getTransitOperation().getMRN() : null));
        } else if (messageType.startsWith(CC)) {
            CC906CType cc906 = createCCM906CMessage(messageXml, mov.getViolations());
            cc906.setMessageSender(constructMessageSender());
            String namespace = decideNamespaceForCC906C(cc906);
            Message message = dossierDataService.persistMessage(
                    setUpMessage(XmlConverter.marshal(cc906, namespace, MessageTypes.CC_906_C.value()), false, DirectionEnum.SENT));
            handleMessageMovementAssociation(mov.getMovements(), message);
            ddntaMessageProducer.publish(cc906, messageType, namespace, determineErmisEventBusinessKey(ObjectUtils.isNotEmpty(mov.getMovements()) ?
                            mov.getMovements().get(0) : null, ObjectUtils.isNotEmpty(cc906.getHeader()) ? cc906.getHeader().getMRN() : null));
        } else {
            CD906CType cd906 = createCDM906CMessage(messageXml, mov.getViolations());
            cd906.setMessageSender(constructMessageSender());
            Message message = dossierDataService.persistMessage(
                    setUpMessage(XmlConverter.marshal(cd906, NamespacesEnum.NTA.getValue(), MessageTypes.CD_906_C.value()), false, DirectionEnum.SENT));
            handleMessageMovementAssociation(mov.getMovements(), message);
            ddntaMessageProducer.publish(cd906, determineErmisEventBusinessKey(ObjectUtils.isNotEmpty(mov.getMovements()) ? mov.getMovements().get(0) : null,
                    ObjectUtils.isNotEmpty(cd906.getHeader()) ? cd906.getHeader().getMRN() : null));
            createAndPersistRejectedMessageInfo(cd906, message.getId(), DirectionEnum.SENT, mov.getMovements());
        }
    }

    @SneakyThrows
    private void sendXsdErrorMessage(String messageXml, MovementsOrViolations mov) {
        CC917CType cc917CType = generateMessageService.generateCC917C(messageXml, mov.getViolations());
        Message message = dossierDataService.persistMessage(
                setUpMessage(XmlConverter.marshal(cc917CType, NamespacesEnum.NTA.getValue(), MessageTypes.CC_917_C.value()), false, DirectionEnum.SENT));
        handleMessageMovementAssociation(mov.getMovements(), message);
        ddntaMessageProducer.publish(cc917CType, determineErmisEventBusinessKey(ObjectUtils.isNotEmpty(mov.getMovements()) ?
                mov.getMovements().get(0) : null, ObjectUtils.isNotEmpty(cc917CType.getHeader()) ? cc917CType.getHeader().getMRN() : null));
    }

    private String decideNamespaceForCC906C(CC906CType cc906CTypeMessage) {
        String recipient = cc906CTypeMessage.getMessageRecipient();
        if (recipient != null && recipient.startsWith(NECA)) {
            return NamespacesEnum.AES.getValue();
        } else {
            return NamespacesEnum.NTA.getValue();
        }
    }

    private void sendCargoErrorMessage(String messageXml, String messageType, MovementsOrViolations mov) throws XPathExpressionException {

        if (com.intrasoft.ermis.cargov1.messages.MessageTypes.TB_007_C.value().equals(messageType)) {

            com.intrasoft.ermis.cargov1.messages.CC057CType cc057cTypeMessage =
                    cargoMessageService.createCargoCC057CMessage(messageXml, mov.getViolations());

            Message message = dossierDataService.persistMessage(
                    setUpMessage(XmlConverter.marshal(cc057cTypeMessage, NamespacesEnum.NTA.getValue(),
                            com.intrasoft.ermis.cargov1.messages.MessageTypes.CC_057_C.value()), false, DirectionEnum.SENT));

            handleMessageMovementAssociation(mov.getMovements(), message);
            ddntaMessageProducer.cargoPublish(cc057cTypeMessage, determineErmisEventBusinessKey(ObjectUtils.isNotEmpty(mov.getMovements()) ?
                    mov.getMovements().get(0) : null, ObjectUtils.isNotEmpty(cc057cTypeMessage.getTransitOperation()) ?
                    cc057cTypeMessage.getTransitOperation().getMRN() : null) );
        }
    }

    private void createAndPersistRejectedMessageInfo(CD906CType cd906CType, String rejectionMessageId, DirectionEnum direction, List<Movement> movements) {
        List<Message> correlatedMessages = dossierDataService.getMessagesByCriteria(cd906CType.getCorrelationIdentifier());

        if (!correlatedMessages.isEmpty()) {

            RejectedMessage rejectedMessage = RejectedMessage.builder()
                                                             .mrn(cd906CType.getHeader().getMRN())
                                                             .amendmentSent(false)
                                                             .destination(cd906CType.getMessageRecipient())
                                                             .source(cd906CType.getMessageSender())
                                                             .direction(direction.getDirection())
                                                             .domain(DomainEnum.NCTS.getValue())
                                                             .rejectionMessageId(rejectionMessageId)
                                                             .referencedMessageId(correlatedMessages.get(0).getId())
                                                             .referencedMessageType(correlatedMessages.get(0).getMessageType())
                                                             .build();

            if (movements.isEmpty()) {
                dossierDataService.saveRejectedMessage(rejectedMessage);
            } else {
                movements.forEach(movement -> {
                    rejectedMessage.setMovementId(movement.getId());
                    rejectedMessage.setOfficeId(movement.getOfficeId());
                    dossierDataService.saveRejectedMessage(rejectedMessage);
                });
            }

        } else {
            throw new NotFoundException("Couldn't find correlated message");
        }
    }

    private List<Movement> createMovements(List<Movement> existingMovements, Message message,
            List<OfficeRoleTypeStatuses> validStates) {
        List<Movement> createdMovements = new ArrayList<>();

        if (MessageUtil.isErrorMessageType(message.getMessageType())) {
            return createdMovements;
        }

        List<String> officeRoleTypesWithoutExistingMovements = getOfficeRoleTypesWithoutExistingMovements(validStates,
                existingMovements);

        if (MessageTypes.CC_015_C.value().equals(message.getMessageType())) {
            officeRoleTypesWithoutExistingMovements = getAppropriateOfficeRoleType(message.getPayload(), officeRoleTypesWithoutExistingMovements);
        }

        if (officeRoleTypesWithoutExistingMovements.isEmpty()) {
            return createdMovements;
        }

        String operatingCountryCode = getCountryCodeService.getCountryCode();
        for (String officeRoleType : officeRoleTypesWithoutExistingMovements) {
            if (shouldCreateMovement(officeRoleType, message, operatingCountryCode, existingMovements)) {
                List<String> officeIds = getOfficeIds(message.getPayload(), officeRoleType);
                for (String officeId : officeIds) {
                    if (isOfficeIdValid(message, officeRoleType, officeId, operatingCountryCode)) {
                        createdMovements.add(setUpMovement(message, officeRoleType,
                                officeId));
                    } else {
                        log.warn(
                                "createMovements(): OfficeId: [{}] for MRN: [{}], MessageType: [{}], and OfficeRoleType: [{}] is not Valid. Will Not create NEW Movement",
                                officeId, message.getMrn(), message.getMessageType(), officeRoleType);
                    }
                }
            }
        }

        return createdMovements;
    }

    private List<String> getOfficeRoleTypesWithoutExistingMovements(List<OfficeRoleTypeStatuses> validOfficeRoleTypeStatusesList,
            List<Movement> existingMovements) {

        if (existingMovements.isEmpty()) {
            return validOfficeRoleTypeStatusesList.stream()
                                                  .filter(officeRoleTypeStatuses -> officeRoleTypeStatuses.getStatuses().isEmpty() ||
                                                          MessageValidationUtils.noneStatusAllowed(officeRoleTypeStatuses.getOfficeRoleType(),
                                                                  officeRoleTypeStatuses.getStatuses()))
                                                  .map(officeRoleTypeStatuses -> officeRoleTypeStatuses.getOfficeRoleType().getValue())
                                                  .collect(Collectors.toList());
        }

        List<String> officeRoleTypesWithoutExistingMovement = new ArrayList<>();
        for (OfficeRoleTypeStatuses officeRoleType : validOfficeRoleTypeStatusesList) {
            if (officeRoleTypeNotInMovement(existingMovements, officeRoleType)) {
                officeRoleTypesWithoutExistingMovement.add(officeRoleType.getOfficeRoleType().getValue());
            }
        }
        return officeRoleTypesWithoutExistingMovement;
    }

    private boolean officeRoleTypeNotInMovement(List<Movement> existingMovements, OfficeRoleTypeStatuses officeRoleType) {
        return existingMovements.stream()
                                .noneMatch(existingMovement -> existingMovement.getOfficeRoleType()
                                                                               .equals(officeRoleType.getOfficeRoleType()
                                                                                                     .getValue()));
    }

    private String getCorrelationIdentifier(String messageXml) {
        String correlationIdentifier = null;
        try (StringReader sr = new StringReader(messageXml)) {
            InputSource xml = new InputSource(sr);
            Node root = (Node) XPATH.evaluate("/", xml, XPathConstants.NODE);

            correlationIdentifier = XPATH.evaluate(MessageXPathsEnum.CORRELATION_IDENTIFIER.getValue(), root);

        } catch (XPathExpressionException ex) {
            log.error("Exception while setting the office Id", ex);
        }
        return correlationIdentifier;
    }

    private List<Movement> getTargetMovements(Message message) {
        List<Movement> movementList = new ArrayList<>();

        MessageTypes messageType = MessageTypes.fromValue(message.getMessageType());
        List<Pair<String, Movement>> pairs;
        switch (messageType) {
            case CD_094_C:
                pairs = getCD094CTargets(message);
                break;
            case CD_150_C:
                pairs = getCD150CTargets(message);
                break;
            case CD_152_C:
                pairs = getCD152Targets(message);
                break;
            default:
                pairs = List.of();
        }

        movementList.addAll(pairs.stream()
                                 .map(Pair::getE2)
                                 .collect(Collectors.toList()));
        return movementList;
    }

    private List<Pair<String, Movement>> getCD094CTargets(Message message) {

        try (StringReader sr = new StringReader(message.getPayload())) {
            InputSource xml = new InputSource(sr);
            XPath xpath = XPathFactory.newInstance().newXPath();
            Node root = (Node) xpath.evaluate("/", xml, XPathConstants.NODE);

            String targetOfficeId = null;
            Movement targetMovement;

            if (!xpath.evaluate(MessageXPathsEnum.TRANSIT_OFFICE_ID.getValue(), root).isBlank()) {
                targetOfficeId = xpath.evaluate(MessageXPathsEnum.TRANSIT_OFFICE_ID.getValue(), root);
                targetMovement =
                        dossierDataService.findSingleMovementByCriteria(message.getMrn(), OfficeRoleTypesEnum.TRANSIT.getValue(), targetOfficeId, null);
            } else if (!xpath.evaluate(MessageXPathsEnum.EXIT_FOR_TRANSIT_OFFICE_ID.getValue(), root).isBlank()) {
                targetOfficeId = xpath.evaluate(MessageXPathsEnum.EXIT_FOR_TRANSIT_OFFICE_ID.getValue(), root);
                targetMovement =
                        dossierDataService.findSingleMovementByCriteria(message.getMrn(), OfficeRoleTypesEnum.EXIT.getValue(), targetOfficeId, null);
            } else {
                // If Destination OR no Office is found within the IE094 , it should default to Destination role Movement.
                targetMovement =
                        dossierDataService.findSingleMovementByCriteria(message.getMrn(), OfficeRoleTypesEnum.DESTINATION.getValue(), null, null);
                if (ObjectUtils.isNotEmpty(targetMovement)) {
                    targetOfficeId = targetMovement.getOfficeId();
                }
            }

            if (targetOfficeId != null && targetMovement != null) {
                return List.of(Pair.of(targetOfficeId, targetMovement));
            }
        } catch (Exception e) {
            log.warn("Could not find target movement for incoming IE094 message [id={}, messageId={}]",
                    message.getId(), message.getMessageIdentification());
        }

        return List.of();
    }

    private List<Pair<String, Movement>> getCD150CTargets(Message message) {
        try (StringReader sr = new StringReader(message.getPayload())) {
            InputSource xml = new InputSource(sr);
            XPath xpath = XPathFactory.newInstance().newXPath();
            Node root = (Node) xpath.evaluate("/", xml, XPathConstants.NODE);

            Movement targetMovement;
            String customsOfficeOfRecoveryRequested = xpath.evaluate(MessageXPathsEnum.RECOVERY_REQUESTED_OFFICE_ID.getValue(), root);
            String mrn = xpath.evaluate(MessageXPathsEnum.MESSAGE_MRN.getValue(), root);

            // We don't use officeIDs as for some countries only a single office can handle the recovery request, while for other countries this is not the case
            List<Movement> movementList = dossierDataService.findByCriteria(mrn, null, null, null, 99);
            targetMovement = findTargetMovementForIncomingIE150(mrn, movementList, getCountryCodeService.getCountryCode());

            if (targetMovement != null) {
                return List.of(Pair.of(customsOfficeOfRecoveryRequested, targetMovement));
            }
        } catch (Exception e) {
            throw new BaseException(String.format("ERROR on finding target movement for incoming IE150 message [id=%s, messageId=%s]",
                    message.getId(), message.getMessageIdentification()), e);
        }

        return List.of();
    }

    private List<Pair<String, Movement>> getCD152Targets(Message message) {
        try {
            InputSource xml = new InputSource(new StringReader(message.getPayload()));
            XPath xpath = XPathFactory.newInstance().newXPath();
            Node root = (Node) xpath.evaluate("/", xml, XPathConstants.NODE);

            String messageSender = xpath.evaluate(MessageXPathsEnum.MESSAGE_SENDER.getValue(), root);
            String messageSource = messageSender.substring(messageSender.lastIndexOf(".") + 1);
            String mrn = xpath.evaluate(MessageXPathsEnum.MESSAGE_MRN.getValue(), root);

            List<Movement> currentMovementList = dossierDataService.findByCriteria(mrn, null, null, null, 99);
            List<Movement> targetMovements = findTargetMovementForIncomingIE152(mrn, messageSource, currentMovementList);

            if (!targetMovements.isEmpty()) {
                List<Pair<String, Movement>> listOfMovements = new ArrayList<>();
                for (Movement m : targetMovements) {
                    listOfMovements.add(Pair.of("", m));
                }
                return listOfMovements;
            }
        } catch (XPathExpressionException e) {
            throw new BaseException(String.format("ERROR while reading incoming IE152 messageXML [id=%s, messageId=%s]",
                    message.getId(), message.getMessageIdentification()), e);
        }
        return List.of();
    }

    private List<String> getAppropriateOfficeRoleType(String messageXml, List<String> officeRoleTypesWithoutExistingMovements) {
        List<String> officeRoleTypes = new ArrayList<>();
        try (StringReader sr = new StringReader(messageXml)) {

            InputSource xml = new InputSource(sr);

            Node root = (Node) XPATH.evaluate("/", xml, XPathConstants.NODE);

            String declarationType = XPATH.evaluate(DECLARATION_TYPE, root);

            if (isDeclarationTypeOfficeOfLodgment(declarationType)) {
                // office role should be that of LODGEMENT
                officeRoleTypes = officeRoleTypesWithoutExistingMovements.stream().filter(office -> office.equals(OfficeRoleTypesEnum.LODGEMENT.getValue()))
                                                                         .collect(Collectors.toList());
            } else {
                officeRoleTypes = officeRoleTypesWithoutExistingMovements.stream().filter(office -> office.equals(OfficeRoleTypesEnum.DEPARTURE.getValue()))
                                                                         .collect(Collectors.toList());
            }

        } catch (XPathExpressionException ex) {
            log.error("Exception while setting the officeRoleTypes for incoming CC015C", ex);
        }
        log.info("getAppropriateOfficeRoleType() for OfficeRoleTypes: [{}]. Will return: {}", officeRoleTypesWithoutExistingMovements, officeRoleTypes);
        return officeRoleTypes;
    }

    private boolean isDeclarationTypeOfficeOfLodgment(String declarationType) {
        return "EXIT".equals(declarationType) || "ENTRY".equals(declarationType);
    }

    private List<String> getOfficeIds(String messageXml, String officeRoleType) {
        List<String> officeIds = new ArrayList<>();
        try (StringReader sr = new StringReader(messageXml)) {

            InputSource xml = new InputSource(sr);

            Node root = (Node) XPATH.evaluate("/", xml, XPathConstants.NODE);

            if (officeRoleType.equals(OfficeRoleTypesEnum.DEPARTURE.getValue()) || officeRoleType.equals(OfficeRoleTypesEnum.LODGEMENT.getValue())) {
                String officeOfDepartureId = XPATH.evaluate(MessageXPathsEnum.DEPARTURE_OFFICE_ID.getValue(), root);
                officeIds.add(officeOfDepartureId);
            } else if (officeRoleType.equals(OfficeRoleTypesEnum.DESTINATION.getValue()) ||
                    officeRoleType.equals(OfficeRoleTypesEnum.CARGO_DESTINATION.getValue())) {

                String officeOfDestinationId = getDestinationOfficeId(XPATH, root);
                if (StringUtils.isNotEmpty(officeOfDestinationId)) {
                    officeIds.add(officeOfDestinationId);
                }
            } else if (officeRoleType.equals(OfficeRoleTypesEnum.TRANSIT.getValue())) {
                addTransitIds(XPATH, root, officeIds);
            } else if (officeRoleType.equals(OfficeRoleTypesEnum.INCIDENT.getValue())) {
                addOfficeIncidentIds(XPATH, root, officeIds);
            } else if (officeRoleType.equals(OfficeRoleTypesEnum.EXIT.getValue())) {
                addExitForTransitIds(XPATH, root, officeIds);
            }

            addCommonOfficeIds(XPATH, root, officeIds);

        } catch (XPathExpressionException ex) {
            log.error("Exception while setting the office Id", ex);
        }
        log.info("getOfficeIds() for OfficeRoleType: [{}]. Will return: {}", officeRoleType, officeIds);
        return officeIds;
    }


    private String getDestinationOfficeId(XPath xpath, Node root) {
        String destinationOfficeId = null;
        try {

            if (!xpath.evaluate(MessageXPathsEnum.DESTINATION_DECLARED_OFFICE_ID.getValue(), root).isBlank()) {
                destinationOfficeId = xpath.evaluate(MessageXPathsEnum.DESTINATION_DECLARED_OFFICE_ID.getValue(), root);
            } else if (!xpath.evaluate(MessageXPathsEnum.DESTINATION_ACTUAL_OFFICE_ID.getValue(), root).isBlank()) {
                destinationOfficeId = xpath.evaluate(MessageXPathsEnum.DESTINATION_ACTUAL_OFFICE_ID.getValue(), root);
            } else if (!xpath.evaluate(MessageXPathsEnum.DESTINATION_OFFICE_OF_REQUEST_OFFICE_ID.getValue(), root)
                             .isBlank()) { // covers destinationId office for IE142
                destinationOfficeId = xpath.evaluate(MessageXPathsEnum.DESTINATION_OFFICE_OF_REQUEST_OFFICE_ID.getValue(), root);
            }
        } catch (XPathExpressionException ex) {
            log.error("Exception while getting the destination office Id", ex);
        }
        return destinationOfficeId;
    }

    private void addTransitIds(XPath xpath, Node root, List<String> officeIds) throws XPathExpressionException {
        NodeList officeOfTransitIdList = (NodeList) xpath.evaluate(MessageXPathsEnum.TRANSIT_DECLARED_OFFICE_ID.getValue(), root, XPathConstants.NODESET);
        if (officeOfTransitIdList.getLength() > 0) {
            for (int i = 0; i < officeOfTransitIdList.getLength(); i++) {
                officeIds.add(officeOfTransitIdList.item(i).getNodeValue());
            }
        } else {
            NodeList officeOfTransitActualIdList =
                    (NodeList) xpath.evaluate(MessageXPathsEnum.TRANSIT_ACTUAL_OFFICE_ID.getValue(), root, XPathConstants.NODESET);
            for (int i = 0; i < officeOfTransitActualIdList.getLength(); i++) {
                officeIds.add(officeOfTransitActualIdList.item(i).getNodeValue());
            }
        }
    }

    private void addOfficeIncidentIds(XPath xpath, Node root, List<String> officeIds) throws XPathExpressionException {

        NodeList officeOfIncidentIdList = (NodeList) xpath.evaluate(MessageXPathsEnum.INCIDENT_DECLARED_OFFICE_ID.getValue(), root, XPathConstants.NODESET);

        if (officeOfIncidentIdList.getLength() > 0) {
            for (int i = 0; i < officeOfIncidentIdList.getLength(); i++) {
                officeIds.add(officeOfIncidentIdList.item(i).getNodeValue());
            }
        }
    }

    private void addExitForTransitIds(XPath xpath, Node root, List<String> officeIds) throws XPathExpressionException {

        NodeList officeOfExitForTransitIdList =
                (NodeList) xpath.evaluate(MessageXPathsEnum.EXIT_FOR_TRANSIT_DECLARED_OFFICE_ID.getValue(), root, XPathConstants.NODESET);

        if (officeOfExitForTransitIdList.getLength() > 0) {

            for (int i = 0; i < officeOfExitForTransitIdList.getLength(); i++) {
                officeIds.add(officeOfExitForTransitIdList.item(i).getNodeValue());
            }
        }
    }

    private void addCommonOfficeIds(XPath xpath, Node root, List<String> officeIds) {
        try {
            if (!xpath.evaluate(MessageXPathsEnum.RECOVERY_REQUESTED_OFFICE_ID.getValue(), root).isBlank()) {
                String recoveryOfficeId = xpath.evaluate(MessageXPathsEnum.RECOVERY_REQUESTED_OFFICE_ID.getValue(), root);
                if (!officeIds.contains(recoveryOfficeId)) {
                    officeIds.add(recoveryOfficeId);
                }
            }

        } catch (XPathExpressionException ex) {
            log.warn("Could not find a recovery requested office Id", ex);
        }
    }

    private Movement setUpMovement(Message message, String officeRoleType, String officeId) {
        return Movement.builder()
                       .officeRoleType(officeRoleType)
                       .officeId(officeId)
                       .mrn(message.getMrn())
                       .lrn(message.getLrn())
                       .simplified(isMovementSimplified(message.getPayload()))
                       .build();
    }

    private boolean isMovementSimplified(String payload) {
        Document message = XmlConverter.convertStringToXMLDocument(payload);
        boolean isMovementSimplified = false;
        try {
            NodeList authorisationTypeNodeList = (NodeList) XPATH.compile("//Authorisation/type").evaluate(message, XPathConstants.NODESET);
            for (int i = 0; i < authorisationTypeNodeList.getLength(); i++) {
                if (SIMPLIFIED_AUTHORISATION.equals(authorisationTypeNodeList.item(i).getTextContent())) {
                    isMovementSimplified = true;
                    break;
                }
            }
        } catch (XPathExpressionException e) {
            log.error("Error getting XPath ", e);
        }
        return isMovementSimplified;
    }

    @Override
    public Message setUpMessage(String messageXml, boolean manualInserted, DirectionEnum direction) throws XPathExpressionException {
        Message message;
        try (StringReader sr = new StringReader(messageXml)) {

            InputSource xml = new InputSource(sr);

            Node root = (Node) XPATH.evaluate("/", xml, XPathConstants.NODE);

            final String messageType = XPATH.evaluate(MessageXPathsEnum.MESSAGE_TYPE.getValue(), root);

            message = Message.builder()
                             .messageIdentification(XPATH.evaluate(MessageXPathsEnum.MESSAGE_IDENTIFICATION.getValue(), root))
                             .messageType(messageType)
                             .source(XPATH.evaluate(MessageXPathsEnum.MESSAGE_SENDER.getValue(), root))
                             .destination(XPATH.evaluate(MessageXPathsEnum.MESSAGE_RECIPIENT.getValue(), root))
                             .payload(messageXml)
                             .domain(messageType.startsWith(CC) || messageType.startsWith(TB) ?
                                     MessageDomainTypeEnum.EXTERNAL.name() : MessageDomainTypeEnum.COMMON.name())
                             .lrn(XPATH.evaluate(MessageXPathsEnum.MESSAGE_LRN.getValue(), root))
                             .mrn(getMrnFromXpath(root, messageType))
                             .manualInserted(manualInserted)
                             .direction(direction.getDirection())
                             .build();

            log.debug("Message assembled: {}", message.toString());
        } catch (XPathExpressionException ex) {
            log.error("Exception while setting up the message for dossier ", ex);
            throw new XPathExpressionException("XPathExpressionException while processing incoming message. " + ex.getCause().getMessage());
        }

        return message;
    }

    @Nullable
    private String getMrnFromXpath(Node root, String messageType) throws XPathExpressionException {
        if (MessageTypes.CD_974_C.value().equals(messageType))
            return XPATH.evaluate(MessageXPathsEnum.CD974C_MESSAGE_MRN.getValue(), root).isBlank() ?
                    null :
                    XPATH.evaluate(MessageXPathsEnum.CD974C_MESSAGE_MRN.getValue(), root);
        return XPATH.evaluate(MessageXPathsEnum.MESSAGE_MRN.getValue(), root).isBlank() ? null : XPATH.evaluate(MessageXPathsEnum.MESSAGE_MRN.getValue(), root);
    }

    protected CD906CType createCDM906CMessage(String messageXml, List<Violation> violations) {

        List<Violation> reportedViolations;

        if (transitionPeriodService.isNowNCTSTransitionPeriod()) {
            reportedViolations = trimRejectedValues(violations);
        } else {
            reportedViolations = violations;
        }
        CD906CType cd906cType = IE906Generator.generateCD906CFromInvalidDDNTAMessage(messageXml, ViolationUtils.toIE906ViolationList(reportedViolations));
        cd906cType.setMessageSender(constructMessageSender());
        return cd906cType;
    }

    protected CC906CType createCCM906CMessage(String messageXml, List<Violation> violations) {
        CC906CType cc906cType = IE906Generator.generateCC906CFromInvalidDDNTAMessage(messageXml, ViolationUtils.toIE906ViolationList(violations));
        cc906cType.setMessageSender(constructMessageSender());
        return cc906cType;
    }

    protected CC056CType createCC056CMessage(String messageXml, MovementsOrViolations mov) {

        CC056CType messageOut = new CC056CType();
        try (StringReader sr = new StringReader(messageXml)) {

            InputSource xml = new InputSource(sr);

            Node root = (Node) XPATH.evaluate("/", xml, XPathConstants.NODE);

            messageOut.setMessageSender(constructMessageSender());
            messageOut.setMessageRecipient(XPATH.evaluate(MessageXPathsEnum.MESSAGE_SENDER.getValue(), root));
            messageOut.setPreparationDateAndTime(LocalDateTime.now(ZoneOffset.UTC));
            messageOut.setMessageIdentification(MessageUtil.generateMessageIdentification());
            messageOut.setMessageType(MessageTypes.CC_056_C);
            messageOut.setCorrelationIdentifier(XPATH.evaluate(MessageXPathsEnum.MESSAGE_IDENTIFICATION.getValue(), root));
            TransitOperationType20 transitOperationType20 = new TransitOperationType20();
            transitOperationType20.setLRN(checkIfXpathIsEmptyString(MessageXPathsEnum.MESSAGE_LRN.getValue(), root));
            transitOperationType20.setMRN(checkIfXpathIsEmptyString(MessageXPathsEnum.MESSAGE_MRN.getValue(), root));
            transitOperationType20.setBusinessRejectionType(XPATH.evaluate(MessageXPathsEnum.MESSAGE_TYPE.getValue(), root).substring(2, 5));
            transitOperationType20.setRejectionDateAndTime(LocalDateTime.now(ZoneOffset.UTC));
            transitOperationType20.setRejectionCode("12");
            transitOperationType20.setRejectionReason(null);
            messageOut.setTransitOperation(transitOperationType20);
            CustomsOfficeOfDepartureType03 customsOfficeOfDepartureType03 = createCustomsOfficeOfDeparture(root, mov.getMovements());
            messageOut.setCustomsOfficeOfDeparture(customsOfficeOfDepartureType03);
            HolderOfTheTransitProcedureType08 holderOfTheTransitProcedureType08 = new HolderOfTheTransitProcedureType08();
            holderOfTheTransitProcedureType08.setIdentificationNumber(checkIfXpathIsEmptyString(IDENTIFICATION_NUMBER, root));
            holderOfTheTransitProcedureType08.setTIRHolderIdentificationNumber(checkIfXpathIsEmptyString(TIR_IDENTIFICATION_NUMBER, root));
            holderOfTheTransitProcedureType08.setName(checkIfXpathIsEmptyString(NAME, root));
            holderOfTheTransitProcedureType08.setAddress(createAddress(root));
            messageOut.setHolderOfTheTransitProcedure(holderOfTheTransitProcedureType08);
            messageOut.setRepresentative(createRepresentative(root));

            for (Violation violation : mov.getViolations()) {
                messageOut.getFunctionalError().add(getCCFunctionalErrorType04(violation));
            }

        } catch (XPathExpressionException e) {
            log.error(e.getLocalizedMessage());
        }

        return messageOut;
    }

    protected CC057CType createCC057CMessage(String messageXml, List<Violation> violations) {

        CC057CType messageOut = new CC057CType();
        try (StringReader sr = new StringReader(messageXml)) {

            InputSource xml = new InputSource(sr);

            Node root = (Node) XPATH.evaluate("/", xml, XPathConstants.NODE);

            messageOut.setMessageSender(constructMessageSender());
            messageOut.setMessageRecipient(XPATH.evaluate(MessageXPathsEnum.MESSAGE_SENDER.getValue(), root));
            messageOut.setPreparationDateAndTime(LocalDateTime.now(ZoneOffset.UTC));
            messageOut.setMessageIdentification(MessageUtil.generateMessageIdentification());
            messageOut.setMessageType(MessageTypes.CC_057_C);
            messageOut.setCorrelationIdentifier(XPATH.evaluate(MessageXPathsEnum.MESSAGE_IDENTIFICATION.getValue(), root));
            TransitOperationType21 transitOperationType21 = new TransitOperationType21();
            transitOperationType21.setMRN(XPATH.evaluate(MessageXPathsEnum.MESSAGE_MRN.getValue(), root));
            transitOperationType21.setBusinessRejectionType(XPATH.evaluate(MessageXPathsEnum.MESSAGE_TYPE.getValue(), root).substring(2, 5));
            transitOperationType21.setRejectionDateAndTime(LocalDateTime.now(ZoneOffset.UTC));
            transitOperationType21.setRejectionCode("12");
            transitOperationType21.setRejectionReason(null);
            messageOut.setTransitOperation(transitOperationType21);
            CustomsOfficeOfDestinationActualType03 customsOfficeOfDestinationActualType03 = new CustomsOfficeOfDestinationActualType03();
            customsOfficeOfDestinationActualType03.setReferenceNumber(XPATH.evaluate(MessageXPathsEnum.DESTINATION_ACTUAL_OFFICE_ID.getValue(), root));
            messageOut.setCustomsOfficeOfDestinationActual(customsOfficeOfDestinationActualType03);
            TraderAtDestinationType03 traderAtDestinationType03 = new TraderAtDestinationType03();
            traderAtDestinationType03.setIdentificationNumber(XPATH.evaluate(IDENTIFICATION_NUMBER_TRADER_AT_DEST, root));
            messageOut.setTraderAtDestination(traderAtDestinationType03);

            for (Violation violation : violations) {
                messageOut.getFunctionalError().add(getCCFunctionalErrorType04(violation));
            }

        } catch (XPathExpressionException e) {
            log.error(e.getLocalizedMessage());
        }

        return messageOut;
    }

    private StatisticalMessage createStatisticalMessage903(String messageIdentification, String payload, String regime) {
        return StatisticalMessage.builder()
                                 .messageId(messageIdentification)
                                 .messageType(MessageTypes.CD_903_D.value())
                                 .payload(payload)
                                 .regime(regime)
                                 .build();
    }

    private FunctionalErrorType04 getCCFunctionalErrorType04(Violation violation) {
        FunctionalErrorType04 functionalError = new FunctionalErrorType04();
        functionalError.setErrorPointer(violation.getFieldName());
        functionalError.setErrorCode(violation.getCode());
        functionalError.setErrorReason(violation.getReason());
        if (!Objects.isNull(violation.getRejectedValue()) && StringUtils.isNotBlank(violation.getRejectedValue().toString()))
            functionalError.setOriginalAttributeValue(violation.getRejectedValue().toString());

        return functionalError;
    }

    private boolean isOfficeIdValid(Message message, String officeRoleType, String officeId, String countryCode) {
        OfficeRoleTypesEnum officeRoleTypesEnum = OfficeRoleTypesEnum.of(officeRoleType);

        switch (officeRoleTypesEnum) {
            case DEPARTURE:
                return true;
            case DESTINATION:
                if (OODEST_SKIP_OFFICE_ID_VALIDATION_MESSAGE_TYPES.contains(message.getMessageType())) {
                    return true;
                } else {
                    return officeId.startsWith(countryCode) ||
                            getCountryCodeService.getAccommodatingCountryCodes()
                                                 .contains(MessageUtil.extractOfficeCountryCode(officeId));
                }
            case INCIDENT:
            case TRANSIT:
            case EXIT:
            case LODGEMENT:
            case CARGO_DESTINATION:
                return officeId.startsWith(countryCode) ||
                        getCountryCodeService.getAccommodatingCountryCodes()
                                             .contains(MessageUtil.extractOfficeCountryCode(officeId));
            default:
                return false;
        }
    }

    /**
     * Used to handle special cases that need an extra check in order to decide whether to create a movement for a specific office role
     */
    private boolean shouldCreateMovement(String officeRoleType, Message message, String countryCode, List<Movement> existingMovements) {

        if(MessageUtil.isCCOrCDMessage(message.getMessageType())) {
            MessageTypes messageType = MessageTypes.fromValue(message.getMessageType());

            switch (messageType) {
                case CD_150_C:
                    return shouldCreateMovementForCD150C(officeRoleType, message, countryCode, existingMovements);
                case CD_114_C:
                    return shouldCreateMovementForCD114C(officeRoleType, message, existingMovements);
                case CD_094_C:
                case CD_144_C:
                case CD_142_C:
                case CD_002_C:
                case CD_164_C:
            case CD_059_C:
                    return false;
                default:
                    return true;
            }
        } else if (MessageUtil.isCargoMessage(message.getMessageType())) {
            return true;
        } else {
            log.error("Invalid message type for message {}", message.getMessageType());
            return false;
        }

    }

    /**
     * For officeRoleType in (DESTINATION) then create Movement only if CountryCode of the IE150 MRN is NOT the Operating Country Code
     */
    private boolean shouldCreateMovementForCD150C(String officeRoleType, Message message, String operatingCountryCode, List<Movement> existingMovements) {
        OfficeRoleTypesEnum officeRoleTypesEnum = OfficeRoleTypesEnum.of(officeRoleType);
        if (!existingMovements.isEmpty()) {
            log.info("shouldCreateMovementForCD150C() : is FALSE for OfficeRoleType [{}] cause [{}] already existing movement(s) where found: {}",
                    officeRoleType,
                    existingMovements.size(), existingMovements.stream().map(Movement::toStringShort).collect(Collectors.toList()));
            return false;
        }
        switch (officeRoleTypesEnum) {
            case DESTINATION:
                if (!extractMRNCountryCode(message.getMrn()).equalsIgnoreCase(operatingCountryCode)) {
                    log.info("shouldCreateMovementForCD150C() : is TRUE for OfficeRoleType: [{}] and MRN: [{}]", officeRoleType, message.getMrn());
                    return true;
                } else {
                    log.info("shouldCreateMovementForCD150C() : is FALSE for OfficeRoleType: [{}] and MRN: [{}]. (MRN country is same as Operating Country)",
                            officeRoleType, message.getMrn());
                    return false;
                }
            case TRANSIT:
            case EXIT:
            case INCIDENT:
                log.info("shouldCreateMovementForCD150C() : is FALSE for OfficeRoleType {}", officeRoleType);
                return false;
            default:
                return true;  // That one i have no idea ! Left as is .....
        }
    }

    private boolean shouldCreateMovementForCD114C(String officeRoleType, Message message, List<Movement> existingMovements) {
        OfficeRoleTypesEnum officeRoleTypesEnum = OfficeRoleTypesEnum.of(officeRoleType);
        if (!existingMovements.isEmpty()) {
            log.info("shouldCreateMovementForCD114C() : is FALSE for OfficeRoleType [{}] cause [{}] already existing movement(s) were found: {}",
                    officeRoleType,
                    existingMovements.size(), existingMovements.stream().map(Movement::toStringShort).collect(Collectors.toList()));
            return false;
        }
        switch (officeRoleTypesEnum) {
            case DEPARTURE:
                log.info("shouldCreateMovementForCD114C() : is FALSE for OfficeRoleType: [{}] and MRN: [{}].",
                        officeRoleType, message.getMrn());
                return false;
            case TRANSIT:
                log.info("shouldCreateMovementForCD114C() : is TRUE for OfficeRoleType {}", officeRoleType);
                return true;
            default:
                return true;
        }
    }

    @SneakyThrows
    private AddressType07 createAddress(Node root) {
        if ((boolean) XPATH.evaluate(ADDRESS, root, XPathConstants.BOOLEAN)) {
            AddressType07 address = new AddressType07();
            address.setStreetAndNumber(XPATH.evaluate(STREETANDNUMBER, root));
            address.setPostcode(checkIfXpathIsEmptyString(POSTCODE, root));
            address.setCity(XPATH.evaluate(CITY, root));
            address.setCountry(XPATH.evaluate(COUNTRY, root));

            return address;
        } else {
            return null;
        }
    }

    @SneakyThrows
    private RepresentativeType01 createRepresentative(Node root) {
        if ((boolean) XPATH.evaluate(REPRESENTATIVE, root, XPathConstants.BOOLEAN)) {
            RepresentativeType01 representative = new RepresentativeType01();
            representative.setIdentificationNumber(XPATH.evaluate(IDENTIFICATION_NUMBER_REPRESENTATIVE, root));
            representative.setStatus(XPATH.evaluate(STATUS, root));

            return representative;
        } else {
            return null;
        }
    }

    @SneakyThrows
    private CustomsOfficeOfDepartureType03 createCustomsOfficeOfDeparture(Node root, List<Movement> movements) {
        CustomsOfficeOfDepartureType03 customsOfficeOfDeparture = new CustomsOfficeOfDepartureType03();

        if ((boolean) XPATH.evaluate(MessageXPathsEnum.DEPARTURE_OFFICE_ID.getValue(), root, XPathConstants.BOOLEAN)) {
            customsOfficeOfDeparture.setReferenceNumber(XPATH.evaluate(MessageXPathsEnum.DEPARTURE_OFFICE_ID.getValue(), root));
        } else if (!movements.isEmpty()) {

            String movementId = movements.get(0).getId();

            List<MessageOverview> messageOverviews = dossierDataService.getMessagesOverview(MessageTypes.CC_015_C.value(), movementId, false);
            Message cc015message;
            if (!messageOverviews.isEmpty()) {
                cc015message = dossierDataService.getMessageById(messageOverviews.get(0).getId(), false);
                CC015CType cc015cTypeMessage = XmlConverter.buildMessageFromXML(cc015message.getPayload(), CC015CType.class);
                customsOfficeOfDeparture.setReferenceNumber(cc015cTypeMessage.getCustomsOfficeOfDeparture().getReferenceNumber());
            }
        } else {
            customsOfficeOfDeparture.setReferenceNumber(XPATH.evaluate(MessageXPathsEnum.ENQUIRY_AT_DEPARTURE_OFFICE_ID.getValue(), root));
        }

        return customsOfficeOfDeparture;
    }

    @SneakyThrows
    private String checkIfXpathIsEmptyString(String expression, Node root) {
        return (boolean) XPATH.evaluate(expression, root, XPathConstants.BOOLEAN)
                ? XPATH.evaluate(expression, root)
                : null;
    }

    /**
     * <pre>
     * If MRN belongs to Operating Country
     *      -> Select Departure only
     * ELSE If MRN does NOT belong to Operating Country perform the checks in the following order :
     *      If Movement exists at Destination -> Handle at Destination
     *      Else If Movement exists at any Office of Transit -> Handle at Office of Transit
     *      Else If Movement exists at any Office of Exit for Transit -> Handle at Office of Exit for Transit
     *      Else if Movement exists at any Office of Incident -> Handle at Office of Incident
     * ELSE return NULL and a new Movement should be created at Office of Destination
     */
    protected Movement findTargetMovementForIncomingIE150(String mrn, List<Movement> movementListByMRN, String operatingCountryCode) {
        if (movementListByMRN.isEmpty()) {
            return null;
        }
        AtomicReference<Movement> targetMovementFound = new AtomicReference<>();
        // Create a Map of MovementsList by OfficeRoleType
        Map<String, List<Movement>> movementsByOfficeRoleTypeMap = getMovementsByOfficeRoleType(movementListByMRN);

        // If MRN belongs to Operating Country -> Select the movement at Departure
        if (MessageUtil.extractMRNCountryCode(mrn).equals(operatingCountryCode)) {
            List<Movement> movementsForDeparture = movementsByOfficeRoleTypeMap.getOrDefault(OfficeRoleTypesEnum.DEPARTURE.getValue(), List.of());
            if (movementsForDeparture.isEmpty()) {
                throw new NotFoundException(String.format(
                        "findTargetMovementForIncomingIE150() : MRN belongs to Operating Country: [%s] and No Movement found for MRN: [%s] and OfficeRoleType: [%s]. Cannot proceed!",
                        operatingCountryCode, mrn, OfficeRoleTypesEnum.DEPARTURE.getValue()));
            } else {
                if (movementsForDeparture.size() > 1) {
                    log.warn(
                            "findTargetMovementForIncomingIE150() : MRN belongs to Operating Country: [{}]. More than one: [{}] movements where found for MRN: [{}] and OfficeRoleType: [{}]. Will peak the one with the most recent \"updated_dttm\" WITHOUT any state validation",
                            operatingCountryCode, movementsForDeparture.size(), mrn, OfficeRoleTypesEnum.DEPARTURE.getValue());
                    movementsForDeparture.sort(Comparator.comparing(Movement::getUpdatedDateTime).reversed());
                }
                targetMovementFound.set(movementsForDeparture.get(0));
                log.info("findTargetMovementForIncomingIE150() : MRN belongs to Operating Country: [{}]. Target Movement selected is: {}", operatingCountryCode,
                        targetMovementFound.get().toStringShort());
                return targetMovementFound.get();
            }
        }

        /* The Order of the OfficeRoleTypes in the List below is according to a specified lookup PRIORITY. Should not be modified. The first Movement found is the chosen one */
        for (OfficeRoleTypesEnum officeRoleTypesEnum :
                List.of(OfficeRoleTypesEnum.DESTINATION,
                        OfficeRoleTypesEnum.TRANSIT,
                        OfficeRoleTypesEnum.EXIT,
                        OfficeRoleTypesEnum.INCIDENT)) {

            List<Movement> movementsByOfficeRoleTypeList = movementsByOfficeRoleTypeMap.getOrDefault(officeRoleTypesEnum.getValue(), List.of());
            if (!movementsByOfficeRoleTypeList.isEmpty()) {
                if (movementsByOfficeRoleTypeList.size() > 1) {
                    // If more than one, pick up the most recently updated one. SOS ! Will not check for State Validity at this point. Movement might be rejected later on because of being in invalid state
                    log.warn(
                            "findTargetMovementForIncomingIE150() : More than one : [{}] movements where found for MRN: [{}] and OfficeRoleType: [{}]. Will peak the one with the most recent \"updated_dttm\" WITHOUT any state validation",
                            movementsByOfficeRoleTypeList.size(), mrn, officeRoleTypesEnum.getValue());
                    movementsByOfficeRoleTypeList.sort(Comparator.comparing(Movement::getUpdatedDateTime).reversed());
                }
                targetMovementFound.set(movementsByOfficeRoleTypeList.get(0));
                log.info("findTargetMovementForIncomingIE150() : Target Movement selected is of Office: [{}] -> {}", officeRoleTypesEnum.getValue(),
                        targetMovementFound.get().toStringShort());
                break;
            }
        }
        if (targetMovementFound.get() == null) {
            log.info(
                    "findTargetMovementForIncomingIE150() : No matching movements found for MRN: [{}] in any OfficeRoleType. Will return NULL and will probably create new movement in DESTINATION",
                    mrn);
        }
        return targetMovementFound.get();
    }

    protected List<Movement> findTargetMovementForIncomingIE152(String mrn, String messageSender, List<Movement> currentMovementListByMRN) {
        String operatingCountry = getCountryCodeService.getCountryCode();
        String mrnCountryCode = MessageUtil.extractMRNCountryCode(mrn);

        Map<String, List<Movement>> movementsByOfficeRoleTypeMap = getMovementsByOfficeRoleType(currentMovementListByMRN);

        List<Movement> targetMovementsFound = new ArrayList<>();
        if (isOnlyForDeparture(mrnCountryCode, operatingCountry, messageSender)) {
            // only for Departure
            List<Movement> movementsForDeparture = movementsByOfficeRoleTypeMap.getOrDefault(OfficeRoleTypesEnum.DEPARTURE.getValue(), List.of());
            if (!movementsForDeparture.isEmpty()) {
                log.info("findTargetMovementForIncomingIE152() : Found [{}] matching movements found for MRN: [{}] in DEPARTURE.", movementsForDeparture.size(),
                        mrn);
                movementsForDeparture.sort(Comparator.comparing(Movement::getUpdatedDateTime).reversed());
                targetMovementsFound.add(movementsForDeparture.get(0));
            } else {
                log.info("findTargetMovementForIncomingIE152() : No matching movements found for MRN: [{}] in DEPARTURE.", mrn);
            }
        } else {
            // to all others offices except of Departure
            for (OfficeRoleTypesEnum officeRoleTypesEnum :
                    List.of(OfficeRoleTypesEnum.DESTINATION,
                            OfficeRoleTypesEnum.TRANSIT,
                            OfficeRoleTypesEnum.EXIT,
                            OfficeRoleTypesEnum.INCIDENT)) {

                List<Movement> movementsByOfficeRoleTypeList = movementsByOfficeRoleTypeMap.getOrDefault(officeRoleTypesEnum.getValue(), List.of());
                if (!movementsByOfficeRoleTypeList.isEmpty()) {
                    log.info("findTargetMovementForIncomingIE152() : Found [{}] matching movements found for MRN: [{}] in [{}].",
                            movementsByOfficeRoleTypeList.size(),
                            mrn, officeRoleTypesEnum.getValue());
                    movementsByOfficeRoleTypeList.sort(Comparator.comparing(Movement::getUpdatedDateTime).reversed());
                    targetMovementsFound.add(movementsByOfficeRoleTypeList.get(0));
                } else {
                    log.info("findTargetMovementForIncomingIE152() : No matching movements found for MRN: [{}] in [{}].", mrn, officeRoleTypesEnum.getValue());
                }
            }
        }
        return targetMovementsFound;
    }

    private boolean isOnlyForDeparture(String mrnCountryCode, String operatingCountry, String messageSender) {
        return mrnCountryCode.equals(operatingCountry) && !messageSender.equals(operatingCountry);
    }

    private Map<String, List<Movement>> getMovementsByOfficeRoleType(List<Movement> movements) {
        return movements.stream().collect(
                Collectors.groupingBy(Movement::getOfficeRoleType,
                        LinkedHashMap::new,
                        Collectors.toCollection(ArrayList::new)));
    }

    private List<Violation> trimRejectedValues(List<Violation> violations) {

        return violations.stream()
                         .map(violation -> {
                             if (violation.getRejectedValue() != null) {
                                 String rejectedValue = violation.getRejectedValue().toString();
                                 int maxLength = Math.min(rejectedValue.length(), E1115_MAX_LENGTH);
                                 String trimmedRejectedValue = rejectedValue.substring(0, maxLength);
                                 return violation.toBuilder().rejectedValue(trimmedRejectedValue).build();
                             } else {
                                 return violation;
                             }
                         }).collect(Collectors.toList());
    }

    private void createSubmissionUserIdMessageAttribute(String messageId) {

        MessageAttribute messageAttribute = MessageAttribute.builder()
                                                            .messageId(messageId)
                                                            .attrKey(MessageAttributeKeysEnum.SUBMISSION_USER_ID.getValue())
                                                            .attrValue(userDetailsService.getUsername())
                                                            .build();

        dossierDataService.saveMessageAttributeList(List.of(messageAttribute));
    }

    private void saveTransportId(String xmlPayload, String movementId) {
        try (StringReader sr = new StringReader(xmlPayload)) {
            InputSource xml = new InputSource(sr);
            Node root = (Node) XPATH.evaluate("/", xml, XPathConstants.NODE);
            NodeList nodeList = (NodeList) XPATH.evaluate(MessageXPathsEnum.BORDER_TRANSPORT_ID.getValue(), root, XPathConstants.NODESET);

            String[] results = new String[nodeList.getLength()];
            for (int index = 0; index < nodeList.getLength(); index++) {
                Node node = nodeList.item(index);
                results[index] = node.getTextContent();
            }

            if (results.length > 0) {
                MovementAttribute movementAttribute = MovementAttribute.builder()
                                                                       .movementId(movementId)
                                                                       .attrKey(MovementAttributeKeysEnum.ARRIVAL_OR_BORDER_TRANSPORT_ID.getValue())
                                                                       .attrValue(Arrays.toString(results))
                                                                       .build();

                dossierDataService.saveMovementAttribute(movementAttribute);
            }
        } catch (Exception e) {
            log.error("ERROR: Could not retrieve Transport Id value from Message payload.", e);
        }
    }

    private List<Movement> determineWhichMovementsToUseForAssociation(List<Violation> violations, List<Movement> existingMovements,
            List<Movement> createdMovements, Message message) {

        return XSD_ERROR_REASON.equals(violations.get(0).getReason()) && MessageTypes.CC_015_C.value().equals(message.getMessageType())
                && !message.isManualInserted() ?
                createdMovements :
                existingMovements;
    }

    private String determineErmisEventBusinessKey(Movement movement, String mrn) {
        // If both Movement and MRN are NULL, then NULL is returned:
        return (movement != null) ? movement.getId() : mrn;
    }
}

package com.intrasoft.ermis.transit.management.core.service;

import static com.intrasoft.ermis.transit.management.core.util.ViolationUtils.createViolation;
import static com.intrasoft.ermis.transit.shared.testing.util.FileHelpers.xmlToString;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.ArgumentMatchers.eq;

import com.intrasoft.ermis.common.logging.ErmisLogger;
import com.intrasoft.ermis.common.xml.parsing.XmlConverter;
import com.intrasoft.ermis.ddntav5152.messages.CC056CType;
import com.intrasoft.ermis.ddntav5152.messages.CC057CType;
import com.intrasoft.ermis.ddntav5152.messages.CC906CType;
import com.intrasoft.ermis.ddntav5152.messages.CD150CType;
import com.intrasoft.ermis.ddntav5152.messages.CD906CType;
import com.intrasoft.ermis.ddntav5152.messages.FunctionalErrorType03;
import com.intrasoft.ermis.ddntav5152.messages.MessageTypes;
import com.intrasoft.ermis.transit.common.config.services.GetCountryCodeService;
import com.intrasoft.ermis.transit.common.enums.DirectionEnum;
import com.intrasoft.ermis.transit.common.enums.MessageOfficeTypeExpectedStatusesEnum;
import com.intrasoft.ermis.transit.common.enums.OfficeRoleTypesEnum;
import com.intrasoft.ermis.transit.common.enums.ProcessingStatusTypeEnum;
import com.intrasoft.ermis.transit.common.exceptions.NotFoundException;
import com.intrasoft.ermis.transit.common.transition.period.TransitionPeriodService;
import com.intrasoft.ermis.transit.common.util.MessageUtil;
import com.intrasoft.ermis.transit.management.core.exception.BadRequestException;
import com.intrasoft.ermis.transit.management.core.port.outbound.DDNTAMessageProducer;
import com.intrasoft.ermis.transit.management.core.port.outbound.DossierDataService;
import com.intrasoft.ermis.transit.management.core.port.outbound.ProcessMessageCommandProducer;
import com.intrasoft.ermis.transit.management.core.port.outbound.ValidationService;
import com.intrasoft.ermis.transit.management.domain.Message;
import com.intrasoft.ermis.transit.management.domain.Movement;
import com.intrasoft.ermis.transit.management.domain.MovementQuery;
import com.intrasoft.ermis.transit.management.domain.MovementsOrViolations;
import com.intrasoft.ermis.transit.management.domain.Violation;
import java.io.StringReader;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import javax.xml.bind.JAXBException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import lombok.SneakyThrows;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.EnumSource;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatchers;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.w3c.dom.Node;
import org.xml.sax.InputSource;


@ExtendWith(MockitoExtension.class)
class DDNTAMessageProcessingServiceImplUT {

    @Mock
    private ErmisLogger ermisLogger;

    @Mock
    private DossierDataService dossierDataService;

    @Mock
    ProcessMessageCommandProducer processMessageCommandProducer;

    @Mock
    DDNTAMessageProducer ddntaMessageProducer;

    @Mock
    GetCountryCodeService getCountryCodeService;

    @Mock
    ValidationService validationService;

    @Mock
    DDNTAMessageCustomValidationServiceImpl ddntaMessageCustomValidationService;

    @Mock
    TransitionPeriodService transitionPeriodService;

    @Mock
    MessageValidationService messageValidationService;

    @Captor
    ArgumentCaptor<Message> messageArgumentCaptor;

    @Captor
    ArgumentCaptor<String> idArgumentCaptor;

    @Captor
    ArgumentCaptor<String> statusArgumentCaptor;

    @Captor
    ArgumentCaptor<Movement> createdMovementArgumentCaptor;

    @Captor
    ArgumentCaptor<Movement> publishedMovementArgumentCaptor;

    @Captor
    private ArgumentCaptor<String> messageIdCaptor;

    @Captor
    private ArgumentCaptor<String> movementIdCaptor;

    @Captor
    private ArgumentCaptor<String> messageTypeCaptor;

    @Captor
    private ArgumentCaptor<CD906CType> msgCD906CCaptor;

    @Captor
    private ArgumentCaptor<CC906CType> msgCC906CCaptor;

    @Captor
    private ArgumentCaptor<CC056CType> msgCC056CCaptor;

    @Captor
    private ArgumentCaptor<CC057CType> msgCC057CCaptor;

    @InjectMocks
    private DDNTAMessageProcessingServiceImpl messageProcessingService;

    private static final String OFFICE_OF_REQUEST = "//CustomsOfficeOfRequest/referenceNumber";

    @Test
    @SneakyThrows
    void testProcessMessage_015_createMovementPerOfficeRoleType_success() {
        // given
        String messageXml = xmlToString("CC015C.xml");
        Message savedMessage = messageProcessingService.setUpMessage(messageXml, false, DirectionEnum.RECEIVED);
        savedMessage.setId(UUID.randomUUID().toString());

        // when
        Message message = messageProcessingService.setUpMessage(messageXml, false, DirectionEnum.RECEIVED);
        Movement movement = getMovement("ceb0cc2b-dd74-495b-87cb-6ab27c7687fd", OfficeRoleTypesEnum.DEPARTURE.getValue(), "AA000000", null,
                "1112236201149", ProcessingStatusTypeEnum.OODEP_NONE.getItemCode());
        when(dossierDataService.createMovement(any())).thenReturn(movement);
        when(dossierDataService.persistMessage(any())).thenReturn(savedMessage);
        when(getCountryCodeService.getCountryCode()).thenReturn("DK");
        when(messageValidationService.performStateValidation(any(), anyList())).thenReturn(
                MovementsOrViolations.of(List.of(nullifyMovementId(movement)), List.of()));
        messageProcessingService.processMessage(messageXml);

        // then
        verify(dossierDataService, times(1)).persistMessage(messageArgumentCaptor.capture());
        verify(dossierDataService, times(1)).createMovement(createdMovementArgumentCaptor.capture());
        verify(dossierDataService, times(1)).associateMessageWithMovement(messageIdCaptor.capture(), movementIdCaptor.capture());
        verify(processMessageCommandProducer, times(1)).publish(messageIdCaptor.capture(), messageTypeCaptor.capture(),
                publishedMovementArgumentCaptor.capture());

        assertThat(messageIdCaptor.getValue()).isEqualTo(savedMessage.getId());
        assertThat(movementIdCaptor.getValue()).isEqualTo(movement.getId());
        assertThat(messageTypeCaptor.getValue()).isEqualTo(message.getMessageType());
        List<Movement> publishedMovements = publishedMovementArgumentCaptor.getAllValues();
        assertThat(publishedMovements).hasSize(1);
        assertTrue(publishedMovements.stream()
                                     .filter(m -> m.getOfficeRoleType().equals(OfficeRoleTypesEnum.DEPARTURE.getValue()))
                                     .allMatch(m -> m.getOfficeId().equals("AA000000")));
    }

    @Test
    @SneakyThrows
    void testProcessMessage_010_associateMessageWithMovementPerOfficeRoleType_success() {
        // given
        String messageXml = xmlToString("CD010C.xml");
        Message savedMessage = messageProcessingService.setUpMessage(messageXml, false, DirectionEnum.RECEIVED);
        savedMessage.setId(UUID.randomUUID().toString());

        // when
        Movement destinationMovement =
                getMovement("ceb0cc2b-dd74-495b-87cb-6ab27c7687fd", OfficeRoleTypesEnum.DESTINATION.getValue(), "AK020053", "31DE061F60A83E16K1",
                        "1112236201149",
                        ProcessingStatusTypeEnum.OODEST_AAR_CREATED.getItemCode());
        Movement exitMovement =
                getMovement("aab0cc2b-dd74-495b-87cb-6ab27c7687fd", OfficeRoleTypesEnum.EXIT.getValue(), "DK005600", "31DE061F60A83E16K1", "1112236201149",
                        ProcessingStatusTypeEnum.OOTRXS_AXR_CREATED.getItemCode());
        List<Movement> existingMovements = new ArrayList<>();
        existingMovements.add(destinationMovement);
        existingMovements.add(exitMovement);
        when(dossierDataService.findByCriteria("31DE061F60A83E16K1", null, null, null, 50)).thenReturn(existingMovements);
        when(dossierDataService.persistMessage(any())).thenReturn(savedMessage);
        when(getCountryCodeService.getCountryCode()).thenReturn("MM");
        when(messageValidationService.performStateValidation(any(), anyList())).thenReturn(MovementsOrViolations.of(existingMovements, List.of()));

        messageProcessingService.processMessage(messageXml);

        // then
        verify(dossierDataService, times(1)).persistMessage(messageArgumentCaptor.capture());
        verify(dossierDataService, times(0)).createMovement(createdMovementArgumentCaptor.capture());
        verify(dossierDataService, times(2)).associateMessageWithMovement(messageIdCaptor.capture(), movementIdCaptor.capture());
        verify(processMessageCommandProducer, times(2)).publish(messageIdCaptor.capture(), messageTypeCaptor.capture(),
                publishedMovementArgumentCaptor.capture());

        assertThat(createdMovementArgumentCaptor.getAllValues()).isEmpty();
        List<Movement> publishedMovements = publishedMovementArgumentCaptor.getAllValues();

        assertEquals(1, (int) publishedMovements.stream()
                                                .filter(m -> m.getOfficeRoleType().equals(OfficeRoleTypesEnum.DESTINATION.getValue()))
                                                .filter(m -> m.getOfficeId().equals("AK020053")).count());
        assertEquals(1, (int) publishedMovements.stream()
                                                .filter(m -> m.getOfficeRoleType().equals(OfficeRoleTypesEnum.EXIT.getValue()))
                                                .filter(m -> m.getOfficeId().equals("DK005600")).count());
    }

    @Test
    @SneakyThrows
    void testProcessMessage_001_createMovementPerOfficeRoleType_success() {
        // given
        String messageXml = xmlToString("CD001C.xml");
        String messageType = "CD001C";
        Message savedMessage = messageProcessingService.setUpMessage(messageXml, false, DirectionEnum.RECEIVED);
        savedMessage.setId(UUID.randomUUID().toString());

        // when
        Message message = messageProcessingService.setUpMessage(messageXml, false, DirectionEnum.RECEIVED);
        Movement movement = getMovement("ceb0cc2b-dd74-495b-87cb-6ab27c7687fd", OfficeRoleTypesEnum.DESTINATION.getValue(), "MK001051", "21DE061F60A83E16K0",
                "1112236201149", ProcessingStatusTypeEnum.OODEST_NONE.getItemCode());
        when(dossierDataService.createMovement(any())).thenReturn(movement);
        when(dossierDataService.persistMessage(any())).thenReturn(savedMessage);
        when(getCountryCodeService.getCountryCode()).thenReturn("DK");
        when(messageValidationService.performStateValidation(any(), anyList())).thenReturn(
                MovementsOrViolations.of(List.of(nullifyMovementId(movement)), List.of()));

        messageProcessingService.processMessage(messageXml);

        // then
        verify(dossierDataService, times(1)).persistMessage(messageArgumentCaptor.capture());
        verify(dossierDataService, times(1)).createMovement(createdMovementArgumentCaptor.capture());
        verify(dossierDataService, times(1)).associateMessageWithMovement(messageIdCaptor.capture(), movementIdCaptor.capture());
        verify(processMessageCommandProducer, times(1)).publish(messageIdCaptor.capture(), messageTypeCaptor.capture(),
                publishedMovementArgumentCaptor.capture());

        assertEquals(savedMessage.getId(), messageIdCaptor.getValue());
        assertEquals(movement.getId(), movementIdCaptor.getValue());
        assertEquals(message.getMessageType(), messageTypeCaptor.getValue());
        List<Movement> publishedMovements = publishedMovementArgumentCaptor.getAllValues();
        assertEquals(1, publishedMovements.size());
        for (Movement move : publishedMovements) {
            assertTrue(validOfficeRoleTypeForMessageType(move, messageType));
        }

        assertTrue(publishedMovements.stream()
                                     .filter(m -> m.getOfficeRoleType().equals(OfficeRoleTypesEnum.DESTINATION.getValue()))
                                     .allMatch(m -> m.getOfficeId().equals("MK001051")));
    }

    @Test
    @SneakyThrows
    void testProcessMessage_001_createMovementPerOfficeRoleType_same_NTA_success() {
        // given
        String messageXml = xmlToString("CD001C.xml");
        String messageType = "CD001C";
        Message savedMessage = messageProcessingService.setUpMessage(messageXml, false, DirectionEnum.RECEIVED);
        savedMessage.setId(UUID.randomUUID().toString());

        //There's already a movement in DEPARTURE. Only DESTINATION should be triggered to handle this
        when(dossierDataService.findByCriteria(savedMessage.getMrn(), null, null, null, 50)).thenReturn(List.of(
                getMovement("ceb0cc2b-dd74-495b-87cb-6ab27c7687fe", OfficeRoleTypesEnum.DEPARTURE.getValue(), "MK001051", savedMessage.getMrn(),
                        "1112236201150", ProcessingStatusTypeEnum.OODEP_MOVEMENT_RELEASED.getItemCode())
        ));

        // when
        Message message = messageProcessingService.setUpMessage(messageXml, false, DirectionEnum.RECEIVED);
        Movement movement = getMovement("ceb0cc2b-dd74-495b-87cb-6ab27c7687fd", OfficeRoleTypesEnum.DESTINATION.getValue(), "MK001051", "21DE061F60A83E16K0",
                "1112236201149", ProcessingStatusTypeEnum.OODEST_NONE.getItemCode());
        when(dossierDataService.createMovement(any())).thenReturn(movement);
        when(dossierDataService.persistMessage(any())).thenReturn(savedMessage);
        when(getCountryCodeService.getCountryCode()).thenReturn("DK");
        when(messageValidationService.performStateValidation(any(), anyList())).thenReturn(
                MovementsOrViolations.of(List.of(nullifyMovementId(movement)), List.of()));

        messageProcessingService.processMessage(messageXml);

        // then
        verify(dossierDataService, times(1)).persistMessage(messageArgumentCaptor.capture());
        verify(dossierDataService, times(1)).createMovement(createdMovementArgumentCaptor.capture());
        verify(dossierDataService, times(1)).associateMessageWithMovement(messageIdCaptor.capture(), movementIdCaptor.capture());
        verify(processMessageCommandProducer, times(1)).publish(messageIdCaptor.capture(), messageTypeCaptor.capture(),
                publishedMovementArgumentCaptor.capture());

        assertEquals(savedMessage.getId(), messageIdCaptor.getValue());
        assertEquals(movement.getId(), movementIdCaptor.getValue());
        assertEquals(message.getMessageType(), messageTypeCaptor.getValue());
        List<Movement> publishedMovements = publishedMovementArgumentCaptor.getAllValues();
        assertEquals(1, publishedMovements.size());
        for (Movement move : publishedMovements) {
            assertTrue(validOfficeRoleTypeForMessageType(move, messageType));
        }

        assertTrue(publishedMovements.stream()
                                     .filter(m -> m.getOfficeRoleType().equals(OfficeRoleTypesEnum.DESTINATION.getValue()))
                                     .allMatch(m -> m.getOfficeId().equals("MK001051")));
    }


    @Test
    @SneakyThrows
    void testProcessMessage_002_success() {
        // given
        String messageXml = xmlToString("CD002C.xml");
        Message savedMessage = messageProcessingService.setUpMessage(messageXml, false, DirectionEnum.RECEIVED);
        savedMessage.setId(UUID.randomUUID().toString());

        // when
        when(dossierDataService.findByCriteria(anyString(), isNull(), isNull(), isNull(), anyInt())).thenReturn(new ArrayList<>());
        when(dossierDataService.persistMessage(any())).thenReturn(savedMessage);
        when(getCountryCodeService.getCountryCode()).thenReturn("DK");
        when(messageValidationService.performStateValidation(any(), anyList())).thenReturn(MovementsOrViolations.of(List.of(), List.of()));

        messageProcessingService.processMessage(messageXml);

        // then
        verify(dossierDataService, times(1)).persistMessage(messageArgumentCaptor.capture());
        // IE002 need not create Movement:
        verify(dossierDataService, times(0)).createMovement(any());
        verify(dossierDataService, times(0)).associateMessageWithMovement(any(), any());
        // IE002 is directly published to Office of Departure:
        verify(processMessageCommandProducer, times(1)).publish(messageIdCaptor.capture(), messageTypeCaptor.capture(),
                ArgumentMatchers.eq(OfficeRoleTypesEnum.DEPARTURE.getValue()));

        assertEquals(savedMessage.getId(), messageIdCaptor.getValue());
        assertEquals(savedMessage.getMessageType(), messageTypeCaptor.getValue());
    }

    @Test
    @SneakyThrows
    void testProcessMessage_010_createMovementPerOfficeRoleType_fail() {
        // given
        String messageXml = xmlToString("CD010C.xml");
        String messageType = "CD010C";
        Message savedMessage = messageProcessingService.setUpMessage(messageXml, false, DirectionEnum.RECEIVED);
        savedMessage.setId(UUID.randomUUID().toString());

        // when
        Message message = messageProcessingService.setUpMessage(messageXml, false, DirectionEnum.RECEIVED);
        Movement movement = getMovement("ceb0cc2b-dd74-495b-87cb-6ab27c7687fd", OfficeRoleTypesEnum.DESTINATION.getValue(), "DE002202",
                "31DE061F60A83E16K1", "1112236201149", ProcessingStatusTypeEnum.OODEST_INVALIDATED.getItemCode());
        List<Movement> storedMovements = new ArrayList<>();
        storedMovements.add(movement);
        when(dossierDataService.findByCriteria("31DE061F60A83E16K1", null, null, null, 50)).thenReturn(storedMovements);
        when(dossierDataService.persistMessage(any())).thenReturn(savedMessage);
        when(getCountryCodeService.getCountryCode()).thenReturn("DK");
        when(dossierDataService.getMessagesByCriteria(any())).thenReturn(List.of(savedMessage));
        when(dossierDataService.saveRejectedMessage(any())).thenAnswer(invocationOnMock -> invocationOnMock.getArgument(0));
        when(transitionPeriodService.isNowNCTSTransitionPeriod()).thenReturn(true);
        when(messageValidationService.performStateValidation(any(), anyList())).thenReturn(MovementsOrViolations.of(storedMovements, List.of(
                createViolation("/" + messageType, MessageUtil.ERROR_REASON_FOR_906,
                        null, MessageUtil.ERROR_CODE_FOR_906))));

        messageProcessingService.processMessage(messageXml);

        // then
        verify(dossierDataService, times(2)).persistMessage(messageArgumentCaptor.capture());
        verify(dossierDataService, times(0)).createMovement(createdMovementArgumentCaptor.capture());
        verify(dossierDataService, times(2)).associateMessageWithMovement(messageIdCaptor.capture(), movementIdCaptor.capture());
        verify(processMessageCommandProducer, times(0)).publish(messageIdCaptor.capture(), messageTypeCaptor.capture(),
                publishedMovementArgumentCaptor.capture());
        verify(ddntaMessageProducer, times(1)).publish(msgCD906CCaptor.capture(), eq("ceb0cc2b-dd74-495b-87cb-6ab27c7687fd"));

        assertEquals(message.getSource(), msgCD906CCaptor.getValue().getMessageRecipient());
        assertEquals(messageProcessingService.constructMessageSender(), msgCD906CCaptor.getValue().getMessageSender());
        assertEquals(message.getMrn(), msgCD906CCaptor.getValue().getHeader().getMRN());
        assertEquals("/" + messageType, msgCD906CCaptor.getValue().getFunctionalError().get(0).getErrorPointer());
        assertEquals(MessageUtil.ERROR_REASON_FOR_906, msgCD906CCaptor.getValue().getFunctionalError().get(0).getErrorReason());
        assertEquals(MessageUtil.ERROR_CODE_FOR_906, msgCD906CCaptor.getValue().getFunctionalError().get(0).getErrorCode());
    }

    @Test
    @SneakyThrows
    void testProcessMessage_014_createMovementPerOfficeRoleType_fail() {
        // given
        String messageXml = xmlToString("CC014C.xml");
        String messageType = "CC014C";
        Message savedMessage = messageProcessingService.setUpMessage(messageXml, false, DirectionEnum.RECEIVED);
        savedMessage.setId(UUID.randomUUID().toString());

        // when
        Message message = messageProcessingService.setUpMessage(messageXml, false, DirectionEnum.RECEIVED);
        Movement movement = getMovement("c4818a60-6cd1-4914-9dbc-37a428bdd93a", OfficeRoleTypesEnum.DEPARTURE.getValue(), "GB000121", "22DKWUGSKUXRQZ2EJ4",
                "SKIP_GMS_015_014",
                ProcessingStatusTypeEnum.OODEP_MOVEMENT_RELEASED.getItemCode());
        List<Movement> storedMovements = new ArrayList<>();
        storedMovements.add(movement);
        when(dossierDataService.findByCriteria("22DKWUGSKUXRQZ2EJ4", null, null, null, 50)).thenReturn(storedMovements);
        when(dossierDataService.persistMessage(any())).thenReturn(savedMessage);
        Violation violation = Violation.builder().fieldName("CC014C").code(MessageUtil.ERROR_CODE_FOR_906).reason(MessageUtil.ERROR_REASON_FOR_906).build();
        when(validationService.validateMovementMessage(savedMessage.getId())).thenReturn(List.of(violation));
        when(getCountryCodeService.getCountryCode()).thenReturn("DK");
        when(messageValidationService.performStateValidation(any(), anyList())).thenReturn(MovementsOrViolations.of(storedMovements, List.of()));

        messageProcessingService.processMessage(messageXml);

        // then
        verify(dossierDataService, times(2)).persistMessage(messageArgumentCaptor.capture());
        verify(dossierDataService, times(0)).createMovement(createdMovementArgumentCaptor.capture());
        verify(dossierDataService, times(2)).associateMessageWithMovement(messageIdCaptor.capture(), movementIdCaptor.capture());
        verify(processMessageCommandProducer, times(0)).publish(messageIdCaptor.capture(), messageTypeCaptor.capture(),
                publishedMovementArgumentCaptor.capture());
        verify(ddntaMessageProducer, times(1)).publish(msgCC056CCaptor.capture(), any());

        assertEquals(message.getSource(), msgCC056CCaptor.getValue().getMessageRecipient());
        assertEquals(messageProcessingService.constructMessageSender(), msgCC056CCaptor.getValue().getMessageSender());
        assertEquals(message.getMrn(), msgCC056CCaptor.getValue().getTransitOperation().getMRN());
        assertEquals(messageType, msgCC056CCaptor.getValue().getFunctionalError().get(0).getErrorPointer());
        assertEquals(MessageUtil.ERROR_REASON_FOR_906, msgCC056CCaptor.getValue().getFunctionalError().get(0).getErrorReason());
        assertEquals(MessageUtil.ERROR_CODE_FOR_906, msgCC056CCaptor.getValue().getFunctionalError().get(0).getErrorCode());
    }

    @Test
    @SneakyThrows
    void testProcessMessage_001_createMovementPerOfficeRoleType_fail() {
        // given
        String messageXml = xmlToString("CD001C.xml");
        String messageType = "CD001C";
        Message savedMessage = messageProcessingService.setUpMessage(messageXml, false, DirectionEnum.RECEIVED);
        savedMessage.setId(UUID.randomUUID().toString());

        // when
        Message message = messageProcessingService.setUpMessage(messageXml, false, DirectionEnum.RECEIVED);
        Movement movement = getMovement("ceb0cc2b-dd74-495b-87cb-6ab27c7687fd", OfficeRoleTypesEnum.DESTINATION.getValue(), "DE002202", "31DE061F60A83E16K1",
                "1112236201149", ProcessingStatusTypeEnum.OODEST_INVALIDATED.getItemCode());
        List<Movement> storedMovements = new ArrayList<>();
        storedMovements.add(movement);
        when(dossierDataService.findByCriteria("21DE061F60A83E16K0", null, null, null, 50)).thenReturn(storedMovements);
        when(dossierDataService.persistMessage(any())).thenReturn(savedMessage);
        when(dossierDataService.getMessagesByCriteria(any())).thenReturn(List.of(savedMessage));
        when(dossierDataService.saveRejectedMessage(any())).thenAnswer(invocationOnMock -> invocationOnMock.getArgument(0));
        when(transitionPeriodService.isNowNCTSTransitionPeriod()).thenReturn(false);
        when(getCountryCodeService.getCountryCode()).thenReturn("DK");
        when(messageValidationService.performStateValidation(any(), anyList())).thenReturn(MovementsOrViolations.of(storedMovements,
                List.of(createViolation("/" + messageType, MessageUtil.ERROR_REASON_FOR_906, null, MessageUtil.ERROR_CODE_FOR_906))));

        messageProcessingService.processMessage(messageXml);

        // then
        verify(dossierDataService, times(2)).persistMessage(messageArgumentCaptor.capture());
        verify(dossierDataService, times(0)).createMovement(createdMovementArgumentCaptor.capture());
        verify(dossierDataService, times(2)).associateMessageWithMovement(messageIdCaptor.capture(), movementIdCaptor.capture());
        verify(processMessageCommandProducer, times(0)).publish(messageIdCaptor.capture(), messageTypeCaptor.capture(),
                publishedMovementArgumentCaptor.capture());
        verify(ddntaMessageProducer, times(1)).publish(msgCD906CCaptor.capture(), eq("ceb0cc2b-dd74-495b-87cb-6ab27c7687fd"));

        assertEquals(message.getSource(), msgCD906CCaptor.getValue().getMessageRecipient());
        assertEquals(messageProcessingService.constructMessageSender(), msgCD906CCaptor.getValue().getMessageSender());
        assertEquals(message.getMrn(), msgCD906CCaptor.getValue().getHeader().getMRN());
        assertEquals("/" + messageType, msgCD906CCaptor.getValue().getFunctionalError().get(0).getErrorPointer());
        assertEquals(MessageUtil.ERROR_REASON_FOR_906, msgCD906CCaptor.getValue().getFunctionalError().get(0).getErrorReason());
        assertEquals(MessageUtil.ERROR_CODE_FOR_906, msgCD906CCaptor.getValue().getFunctionalError().get(0).getErrorCode());

    }


    @Test
    @SneakyThrows
    void testProcessMessage_044_createMovementPerOfficeRoleType_fail() {
        // given
        String messageXml = xmlToString("CC044C.xml");
        String messageType = "CC044C";
        Message savedMessage = messageProcessingService.setUpMessage(messageXml, false, DirectionEnum.RECEIVED);
        savedMessage.setId(UUID.randomUUID().toString());

        // when
        Message message = messageProcessingService.setUpMessage(messageXml, false, DirectionEnum.RECEIVED);
        Movement movement = getMovement("ceb0cc2b-dd74-495b-87cb-6ab27c7687fd", OfficeRoleTypesEnum.DESTINATION.getValue(), "DE002202", "MRN-70P6",
                "1112236201149", ProcessingStatusTypeEnum.OODEST_NONE.getItemCode());
        List<Movement> storedMovements = new ArrayList<>();
        storedMovements.add(movement);
        when(dossierDataService.findByCriteria("MRN-70P6", null, null, null, 50)).thenReturn(storedMovements);
        when(dossierDataService.persistMessage(any())).thenReturn(savedMessage);
        when(getCountryCodeService.getCountryCode()).thenReturn("DK");
        when(messageValidationService.performStateValidation(any(), anyList())).thenReturn(MovementsOrViolations.of(storedMovements, List.of(
                createViolation("/" + messageType, MessageUtil.ERROR_REASON_FOR_906,
                        null, MessageUtil.ERROR_CODE_FOR_906)
        )));
        messageProcessingService.processMessage(messageXml);

        // then
        verify(dossierDataService, times(2)).persistMessage(messageArgumentCaptor.capture());
        verify(dossierDataService, times(0)).createMovement(createdMovementArgumentCaptor.capture());
        verify(dossierDataService, times(2)).associateMessageWithMovement(messageIdCaptor.capture(), movementIdCaptor.capture());
        verify(processMessageCommandProducer, times(0)).publish(messageIdCaptor.capture(), messageTypeCaptor.capture(),
                publishedMovementArgumentCaptor.capture());
        verify(ddntaMessageProducer, times(1)).publish(msgCC057CCaptor.capture(), any());

        assertEquals(message.getSource(), msgCC057CCaptor.getValue().getMessageRecipient());
        assertEquals(messageProcessingService.constructMessageSender(), msgCC057CCaptor.getValue().getMessageSender());
        assertEquals(message.getMrn(), msgCC057CCaptor.getValue().getTransitOperation().getMRN());
        assertEquals("/" + messageType, msgCC057CCaptor.getValue().getFunctionalError().get(0).getErrorPointer());
        assertEquals(MessageUtil.ERROR_REASON_FOR_906, msgCC057CCaptor.getValue().getFunctionalError().get(0).getErrorReason());
        assertEquals(MessageUtil.ERROR_CODE_FOR_906, msgCC057CCaptor.getValue().getFunctionalError().get(0).getErrorCode());

    }

    @Test
    @SneakyThrows
    void testProcessMessage_044_success() {
        // given
        String messageXml = xmlToString("CC044C.xml");
        Message savedMessage = messageProcessingService.setUpMessage(messageXml, false, DirectionEnum.RECEIVED);
        savedMessage.setId(UUID.randomUUID().toString());

        // when
        Message message = messageProcessingService.setUpMessage(messageXml, false, DirectionEnum.RECEIVED);
        Movement movement = getMovement("ceb0cc2b-dd74-495b-87cb-6ab27c7687fd", OfficeRoleTypesEnum.DESTINATION.getValue(), "DE002202", "MRN-70P6",
                "1112236201149", "E01");
        List<Movement> storedMovements = new ArrayList<>();
        storedMovements.add(movement);
        when(dossierDataService.findByCriteria("MRN-70P6", null, null, null, 50)).thenReturn(storedMovements);
        when(dossierDataService.persistMessage(any())).thenReturn(savedMessage);
        when(messageValidationService.performStateValidation(any(), anyList())).thenReturn(MovementsOrViolations.of(List.of(movement), List.of()));
        messageProcessingService.processMessage(messageXml);

        // then
        verify(dossierDataService, times(1)).persistMessage(messageArgumentCaptor.capture());
        verify(dossierDataService, times(0)).createMovement(createdMovementArgumentCaptor.capture());
        verify(dossierDataService, times(1)).associateMessageWithMovement(messageIdCaptor.capture(), movementIdCaptor.capture());
        verify(processMessageCommandProducer, times(1)).publish(messageIdCaptor.capture(), messageTypeCaptor.capture(),
                publishedMovementArgumentCaptor.capture());
        verify(ddntaMessageProducer, times(0)).publish(msgCC906CCaptor.capture(), anyString(), anyString(), any());

        assertThat(messageIdCaptor.getValue()).isEqualTo(savedMessage.getId());
        assertThat(movementIdCaptor.getValue()).isEqualTo(movement.getId());
        assertThat(messageTypeCaptor.getValue()).isEqualTo(message.getMessageType());

    }

    @Test
    @SneakyThrows
    void testProcessMessage_saveAndPublishTwoMovements_success() {
        // given
        String messageXml = xmlToString("CD050C.xml");
        String messageType = "CD050C";

        // when
        Message message = messageProcessingService.setUpMessage(messageXml, false, DirectionEnum.RECEIVED);
        Message savedMessage = messageProcessingService.setUpMessage(messageXml, false, DirectionEnum.RECEIVED);
        savedMessage.setId(UUID.randomUUID().toString());
        Movement movementForDK204000 =
                getMovement("ceb0cc2b-dd74-495b-87cb-6ab27c7687fd", OfficeRoleTypesEnum.TRANSIT.getValue(), "DK204000", "21RS65CB8C1082C8M9", message.getLrn(),
                        ProcessingStatusTypeEnum.OOTRS_NONE.getItemCode());
        Movement movementForDK002031 =
                getMovement("ceb0cc2b-dd74-495b-87cb-6ab27c7687fd", OfficeRoleTypesEnum.TRANSIT.getValue(), "DK002031", "21RS65CB8C1082C8M9", message.getLrn(),
                        ProcessingStatusTypeEnum.OOTRS_NONE.getItemCode());

        when(dossierDataService.createMovement(any())).thenReturn(movementForDK204000);
        when(getCountryCodeService.getCountryCode()).thenReturn("DK");
        when(dossierDataService.findByCriteria("21RS65CB8C1082C8M9", null, null, null, 50)).thenReturn(new ArrayList<>());
        when(dossierDataService.persistMessage(any())).thenReturn(savedMessage);
        when(messageValidationService.performStateValidation(any(), anyList())).thenReturn(
                MovementsOrViolations.of(List.of(nullifyMovementId(movementForDK204000), nullifyMovementId(movementForDK002031)), List.of()));

        messageProcessingService.processMessage(messageXml);

        // then
        verify(dossierDataService, times(1)).persistMessage(messageArgumentCaptor.capture());
        verify(dossierDataService, times(2)).createMovement(createdMovementArgumentCaptor.capture());
        verify(dossierDataService, times(2)).associateMessageWithMovement(messageIdCaptor.capture(), movementIdCaptor.capture());
        verify(processMessageCommandProducer, times(2)).publish(messageIdCaptor.capture(), messageTypeCaptor.capture(),
                publishedMovementArgumentCaptor.capture());

        assertEquals(savedMessage.getId(), messageIdCaptor.getValue());
        assertEquals(message.getMessageType(), messageTypeCaptor.getValue());
        List<Movement> publishedMovements = publishedMovementArgumentCaptor.getAllValues();
        assertEquals(2, publishedMovements.size());
        for (Movement move : publishedMovements) {
            assertTrue(validOfficeRoleTypeForMessageType(move, messageType));
        }

        assertTrue(publishedMovements.stream()
                                     .filter(m -> m.getOfficeRoleType().equals(OfficeRoleTypesEnum.DESTINATION.getValue()))
                                     .allMatch(m -> m.getOfficeId().equals("DK204000") || m.getOfficeId().equals("DK002031")));

    }

    private boolean validOfficeRoleTypeForMessageType(Movement movement, String messageType) {
        return MessageOfficeTypeExpectedStatusesEnum.valueOf(messageType)
                                                    .getOfficeRoleTypeStatuses()
                                                    .stream()
                                                    .anyMatch(officeRoleTypeStatuses -> officeRoleTypeStatuses.getOfficeRoleType()
                                                                                                              .getValue()
                                                                                                              .equals(movement.getOfficeRoleType()));
    }

    @Test
    @SneakyThrows
    void testProcessMessage_027_success() {
        //mock
        String messageXml = xmlToString("CD027C.xml");
        Message savedMessage = messageProcessingService.setUpMessage(messageXml, false, DirectionEnum.RECEIVED);
        savedMessage.setId(UUID.randomUUID().toString());

        Movement movement = getMovement("TEST", OfficeRoleTypesEnum.DEPARTURE.getValue(), "TEST", "TEST", "",
                ProcessingStatusTypeEnum.OODEP_MOVEMENT_RELEASED.getItemCode());
        when(dossierDataService.findByCriteria(anyString(), isNull(), isNull(), isNull(), anyInt())).thenReturn(List.of(movement));
        when(dossierDataService.persistMessage(any())).thenReturn(savedMessage);
        when(messageValidationService.performStateValidation(any(), anyList())).thenReturn(MovementsOrViolations.of(List.of(movement), List.of()));

        //execute
        messageProcessingService.processMessage(messageXml);

        //confirm
        verify(dossierDataService, times(1)).persistMessage(messageArgumentCaptor.capture());
        verify(dossierDataService, times(1)).associateMessageWithMovement(messageIdCaptor.capture(), movementIdCaptor.capture());
        verify(processMessageCommandProducer, times(1)).publish(messageIdCaptor.capture(), messageTypeCaptor.capture(),
                publishedMovementArgumentCaptor.capture());
        verify(ddntaMessageProducer, times(0)).publish(msgCC906CCaptor.capture(), anyString(), anyString(), any());

        assertThat(messageIdCaptor.getValue()).isEqualTo(savedMessage.getId());
        assertThat(movementIdCaptor.getValue()).isEqualTo(movement.getId());
        assertThat(messageTypeCaptor.getValue()).isEqualTo(savedMessage.getMessageType());
    }

    @Test
    @SneakyThrows
    void testProcessMessage_180_success() {
        //mock
        String messageXml = xmlToString("CD180C.xml");
        Message savedMessage = messageProcessingService.setUpMessage(messageXml, false, DirectionEnum.RECEIVED);
        savedMessage.setId(UUID.randomUUID().toString());

        Movement movement = getMovement("TEST", OfficeRoleTypesEnum.DEPARTURE.getValue(), "TEST", "TEST", "",
                ProcessingStatusTypeEnum.OODEP_MOVEMENT_RELEASED.getItemCode());

        when(dossierDataService.findByCriteria(anyString(), isNull(), isNull(), isNull(), anyInt())).thenReturn(List.of(movement));
        when(dossierDataService.persistMessage(any())).thenReturn(savedMessage);
        when(messageValidationService.performStateValidation(any(), anyList())).thenReturn(MovementsOrViolations.of(List.of(movement), List.of()));
        //execute
        messageProcessingService.processMessage(messageXml);

        //confirm
        verify(dossierDataService, times(1)).persistMessage(messageArgumentCaptor.capture());
        verify(dossierDataService, times(1)).associateMessageWithMovement(messageIdCaptor.capture(), movementIdCaptor.capture());
        verify(processMessageCommandProducer, times(1)).publish(messageIdCaptor.capture(), messageTypeCaptor.capture(),
                publishedMovementArgumentCaptor.capture());
        verify(ddntaMessageProducer, times(0)).publish(msgCC906CCaptor.capture(), anyString(), anyString(), any());

        assertThat(messageIdCaptor.getValue()).isEqualTo(savedMessage.getId());
        assertThat(movementIdCaptor.getValue()).isEqualTo(movement.getId());
        assertThat(messageTypeCaptor.getValue()).isEqualTo(savedMessage.getMessageType());
    }

    @Test
    @SneakyThrows
    void testProcessMessage_180_fail() {
        //mock
        String messageXml = xmlToString("CD180C.xml");
        Message savedMessage = messageProcessingService.setUpMessage(messageXml, false, DirectionEnum.RECEIVED);
        savedMessage.setId(UUID.randomUUID().toString());

        when(dossierDataService.findByCriteria(anyString(), isNull(), isNull(), isNull(), anyInt())).thenReturn(List.of());
        when(dossierDataService.persistMessage(any())).thenReturn(savedMessage);
        when(dossierDataService.updateMessage(any(), any())).thenReturn(savedMessage);
        when(getCountryCodeService.getCountryCode()).thenReturn("DK");
        when(messageValidationService.performStateValidation(any(), anyList())).thenReturn(MovementsOrViolations.of(List.of(), List.of(
                createViolation("/CD180C/TransitOperation/MRN", "N/A",
                        null, "90"))));
        when(dossierDataService.getMessagesByCriteria(any())).thenReturn(
                List.of(Message.builder().id(UUID.randomUUID().toString()).messageType(MessageTypes.CD_180_C.value()).build()));

        //execute
        messageProcessingService.processMessage(messageXml);

        //confirm
        verify(dossierDataService, times(2)).persistMessage(messageArgumentCaptor.capture());
        verify(dossierDataService, times(1)).updateMessage(idArgumentCaptor.capture(), statusArgumentCaptor.capture());
        verify(dossierDataService, times(0)).associateMessageWithMovement(messageIdCaptor.capture(), movementIdCaptor.capture());
        verify(processMessageCommandProducer, times(0)).publish(messageIdCaptor.capture(), messageTypeCaptor.capture(),
                publishedMovementArgumentCaptor.capture());
        verify(ddntaMessageProducer, times(1)).publish(msgCD906CCaptor.capture(), any());

        assertThat(msgCD906CCaptor.getValue().getFunctionalError().stream()
                                  .map(FunctionalErrorType03::getErrorCode)
                                  .collect(Collectors.toList()))
                .contains("90");
        assertThat(msgCD906CCaptor.getValue().getFunctionalError()
                                  .stream()
                                  .map(FunctionalErrorType03::getErrorPointer).collect(Collectors.toList()))
                .contains("/CD180C/TransitOperation/MRN");
    }

    @Test
    @SneakyThrows
    void testProcessMessage_144_success() {
        //mock
        String messageXml = xmlToString("CD144C.xml");
        Message savedMessage = messageProcessingService.setUpMessage(messageXml, false, DirectionEnum.RECEIVED);
        savedMessage.setId(UUID.randomUUID().toString());
        when(dossierDataService.persistMessage(any())).thenReturn(savedMessage);
        Movement movement = getMovement("TEST", OfficeRoleTypesEnum.DESTINATION.getValue(), "TEST", "20DE71EAF5731899M3", "",
                ProcessingStatusTypeEnum.OODEST_AAR_CREATED.getItemCode());

        List<Movement> storedMovements = new ArrayList<>();
        storedMovements.add(movement);
        when(dossierDataService.findByCriteria("20DE71EAF5731899M3", null, null, null, 50)).thenReturn(storedMovements);
        when(messageValidationService.performStateValidation(any(), anyList())).thenReturn(MovementsOrViolations.of(storedMovements, List.of()));

        //execute
        messageProcessingService.processMessage(messageXml);

        //confirm
        verify(dossierDataService, times(1)).persistMessage(messageArgumentCaptor.capture());
        verify(dossierDataService, times(1)).associateMessageWithMovement(messageIdCaptor.capture(), movementIdCaptor.capture());
        verify(processMessageCommandProducer, times(1)).publish(messageIdCaptor.capture(), messageTypeCaptor.capture(),
                publishedMovementArgumentCaptor.capture());

        assertEquals(savedMessage.getSource(), messageArgumentCaptor.getValue().getSource());
        assertEquals(savedMessage.getDestination(), messageArgumentCaptor.getValue().getDestination());
        assertEquals(savedMessage.getMrn(), messageArgumentCaptor.getValue().getMrn());
    }

    @Test
    @SneakyThrows
    void testProcessMessage_180_multiple_offices_success() {
        //mock
        String messageXml = xmlToString("CD180C.xml");
        Message savedMessage = messageProcessingService.setUpMessage(messageXml, false, DirectionEnum.RECEIVED);
        savedMessage.setId(UUID.randomUUID().toString());

        Movement ooDepMovement = getMovement("TEST", OfficeRoleTypesEnum.DEPARTURE.getValue(), "TEST", "TEST", "",
                ProcessingStatusTypeEnum.OODEP_MOVEMENT_RELEASED.getItemCode());

        when(dossierDataService.findByCriteria(anyString(), isNull(), isNull(), isNull(), anyInt())).thenReturn(List.of(ooDepMovement));
        when(dossierDataService.persistMessage(any())).thenReturn(savedMessage);
        when(messageValidationService.performStateValidation(any(), anyList())).thenReturn(MovementsOrViolations.of(List.of(ooDepMovement), List.of()));

        //execute
        messageProcessingService.processMessage(messageXml);

        //confirm
        verify(dossierDataService, times(1)).persistMessage(messageArgumentCaptor.capture());
        verify(dossierDataService, times(1)).associateMessageWithMovement(messageIdCaptor.capture(), movementIdCaptor.capture());
        verify(processMessageCommandProducer, times(1)).publish(messageIdCaptor.capture(), messageTypeCaptor.capture(),
                publishedMovementArgumentCaptor.capture());
        verify(ddntaMessageProducer, times(0)).publish(msgCC906CCaptor.capture(), anyString(), anyString(), any());

        assertThat(messageIdCaptor.getValue()).isEqualTo(savedMessage.getId());
        assertThat(messageTypeCaptor.getValue()).isEqualTo(savedMessage.getMessageType());
    }

    @Test
    @SneakyThrows
    void testProcessMessage_181_success() {
        //mock
        String messageXml = xmlToString("CD181C.xml");
        Message savedMessage = messageProcessingService.setUpMessage(messageXml, false, DirectionEnum.RECEIVED);
        savedMessage.setId(UUID.randomUUID().toString());

        Movement destMovement =
                getMovement("TEST", OfficeRoleTypesEnum.DESTINATION.getValue(), "TEST", "TEST", "",
                        ProcessingStatusTypeEnum.OODEST_AAR_REQUESTED.getItemCode());
        Movement exitMovement =
                getMovement("TEST", OfficeRoleTypesEnum.EXIT.getValue(), "TEST", "TEST", "", ProcessingStatusTypeEnum.OOTRXS_AXR_CREATED.getItemCode());
        when(dossierDataService.findByCriteria(anyString(), isNull(), isNull(), isNull(), anyInt())).thenReturn(new ArrayList<>());
        when(dossierDataService.createMovement(any())).thenReturn(destMovement);
        when(dossierDataService.persistMessage(any())).thenReturn(savedMessage);
        when(getCountryCodeService.getCountryCode()).thenReturn("DK");
        when(messageValidationService.performStateValidation(any(), anyList())).thenReturn(
                MovementsOrViolations.of(List.of(nullifyMovementId(destMovement), nullifyMovementId(exitMovement)), List.of()));

        //execute
        messageProcessingService.processMessage(messageXml);

        //confirm
        verify(dossierDataService, times(1)).persistMessage(messageArgumentCaptor.capture());
        verify(dossierDataService, times(2)).associateMessageWithMovement(messageIdCaptor.capture(), movementIdCaptor.capture());
        verify(processMessageCommandProducer, times(2)).publish(messageIdCaptor.capture(), messageTypeCaptor.capture(),
                publishedMovementArgumentCaptor.capture());
        verify(ddntaMessageProducer, times(0)).publish(msgCC906CCaptor.capture(), anyString(), anyString(), any());

        assertThat(messageIdCaptor.getValue()).isEqualTo(savedMessage.getId());
        assertThat(movementIdCaptor.getValue()).isEqualTo(destMovement.getId());
        assertThat(messageTypeCaptor.getValue()).isEqualTo(savedMessage.getMessageType());
    }

    @Test
    @SneakyThrows
    void testHandle038_success() throws XPathExpressionException {

        // given
        String messageXml = xmlToString("CD038C.xml");
        InputSource xml = new InputSource(new StringReader(messageXml));
        XPath xpath = XPathFactory.newInstance().newXPath();
        Node root = (Node) xpath.evaluate("/", xml, XPathConstants.NODE);

        String officeOfDeparture = xpath.evaluate(OFFICE_OF_REQUEST, root);

        //mock
        Message savedMessage = messageProcessingService.setUpMessage(messageXml, false, DirectionEnum.RECEIVED);
        savedMessage.setId(UUID.randomUUID().toString());

        MovementQuery movementQuery = getMovementQuery(savedMessage.getMrn());

        when(validationService.validateMovementMessage(any())).thenReturn(Collections.emptyList());
        when(dossierDataService.persistMessage(any())).thenReturn(savedMessage);
        when(dossierDataService.findMovementQueriesByCriteria(any())).thenReturn(movementQuery);
        when(messageValidationService.performStateValidation(any(), anyList())).thenReturn(MovementsOrViolations.of(List.of(), List.of()));

        //execute
        messageProcessingService.handle038(messageXml);
        assertThat(movementQuery.getResponsePayload()).isEqualTo(messageXml);
        assertThat(movementQuery.getRespondent()).isEqualTo(officeOfDeparture);
    }

    @Test
    @SneakyThrows
    void testProcessMessage_CD201C_no_validation_errors() {
        // given
        String messageXml = xmlToString("CD201C.xml");
        Message savedMessage = messageProcessingService.setUpMessage(messageXml, false, DirectionEnum.RECEIVED);
        savedMessage.setId(UUID.randomUUID().toString());

        // when
        Message message = messageProcessingService.setUpMessage(messageXml, false, DirectionEnum.RECEIVED);
        Movement movement = getMovement("ceb0cc2b-dd74-495b-87cb-6ab27c7687fd", OfficeRoleTypesEnum.DEPARTURE.getValue(), "DK005600", savedMessage.getMrn(),
                null, null);
        List<Movement> storedMovements = new ArrayList<>();
        storedMovements.add(movement);
        when(dossierDataService.findByCriteria(message.getMrn(), null, null, null, 50)).thenReturn(storedMovements);
        when(dossierDataService.persistMessage(any())).thenReturn(savedMessage);
        when(messageValidationService.performStateValidation(any(), anyList())).thenReturn(MovementsOrViolations.of(storedMovements, List.of()));

        messageProcessingService.processMessage(messageXml);

        // then
        verify(dossierDataService, times(1)).persistMessage(messageArgumentCaptor.capture());
        verify(dossierDataService, times(0)).createMovement(createdMovementArgumentCaptor.capture());
        verify(dossierDataService, times(1)).associateMessageWithMovement(messageIdCaptor.capture(), movementIdCaptor.capture());
        verify(processMessageCommandProducer, times(1)).publish(messageIdCaptor.capture(), messageTypeCaptor.capture(),
                publishedMovementArgumentCaptor.capture());
        verify(ddntaMessageProducer, times(0)).publish(msgCD906CCaptor.capture(), eq("ceb0cc2b-dd74-495b-87cb-6ab27c7687fd"));

        assertThat(messageIdCaptor.getValue()).isEqualTo(savedMessage.getId());
        assertThat(movementIdCaptor.getValue()).isEqualTo(movement.getId());
        assertThat(messageTypeCaptor.getValue()).isEqualTo(message.getMessageType());

        List<Movement> publishedMovements = publishedMovementArgumentCaptor.getAllValues();
        assertThat(publishedMovements).hasSize(1);
        assertTrue(publishedMovements.stream()
                                     .filter(m -> m.getOfficeRoleType().equals(OfficeRoleTypesEnum.DEPARTURE.getValue()))
                                     .allMatch(m -> m.getOfficeId().equals("DK005600")));

    }

    // TODO: This looks like an erroneous test. Validation service will never throw OutOfOrder Functional Errors, only R&C
    @Test
    @SneakyThrows
    void testProcessMessage_CD201C_with_validation_errors() {
        // given
        String messageXml = xmlToString("CD201C.xml");
        Message savedMessage = messageProcessingService.setUpMessage(messageXml, false, DirectionEnum.RECEIVED);
        savedMessage.setId(UUID.randomUUID().toString());

        // when
        Message message = messageProcessingService.setUpMessage(messageXml, false, DirectionEnum.RECEIVED);
        Movement movement = getMovement("ceb0cc2b-dd74-495b-87cb-6ab27c7687fd", OfficeRoleTypesEnum.DEPARTURE.getValue(), "DK005600", savedMessage.getMrn(),
                null, null);
        List<Movement> storedMovements = new ArrayList<>();
        storedMovements.add(movement);
        when(dossierDataService.findByCriteria(message.getMrn(), null, null, null, 50)).thenReturn(storedMovements);
        when(dossierDataService.persistMessage(any())).thenReturn(savedMessage);
        when(dossierDataService.getMessagesByCriteria(any())).thenReturn(List.of(savedMessage));
        when(dossierDataService.saveRejectedMessage(any())).thenAnswer(invocationOnMock -> invocationOnMock.getArgument(0));

        Violation violation = Violation.builder().fieldName("/CD201C").code(MessageUtil.ERROR_CODE_FOR_906).reason(MessageUtil.ERROR_REASON_FOR_906).build();
        when(validationService.validateMovementMessage(savedMessage.getId())).thenReturn(List.of(violation));

        when(transitionPeriodService.isNowNCTSTransitionPeriod()).thenReturn(false);

        when(getCountryCodeService.getCountryCode()).thenReturn("DK");
        when(messageValidationService.performStateValidation(any(), anyList())).thenReturn(MovementsOrViolations.of(storedMovements, List.of()));

        messageProcessingService.processMessage(messageXml);

        // then
        verify(dossierDataService, times(2)).persistMessage(messageArgumentCaptor.capture());
        verify(dossierDataService, times(0)).createMovement(createdMovementArgumentCaptor.capture());
        verify(dossierDataService, times(2)).associateMessageWithMovement(messageIdCaptor.capture(), movementIdCaptor.capture());
        verify(processMessageCommandProducer, times(0)).publish(messageIdCaptor.capture(), messageTypeCaptor.capture(),
                publishedMovementArgumentCaptor.capture());
        verify(ddntaMessageProducer, times(1)).publish(msgCD906CCaptor.capture(),eq("ceb0cc2b-dd74-495b-87cb-6ab27c7687fd"));

        List<Movement> publishedMovements = publishedMovementArgumentCaptor.getAllValues();
        assertThat(publishedMovements).isEmpty();
        assertEquals(message.getSource(), msgCD906CCaptor.getValue().getMessageRecipient());
        assertEquals(messageProcessingService.constructMessageSender(), msgCD906CCaptor.getValue().getMessageSender());
        assertEquals(message.getMrn(), msgCD906CCaptor.getValue().getHeader().getMRN());
        assertEquals("/" + message.getMessageType(), msgCD906CCaptor.getValue().getFunctionalError().get(0).getErrorPointer());
        assertEquals(MessageUtil.ERROR_REASON_FOR_906, msgCD906CCaptor.getValue().getFunctionalError().get(0).getErrorReason());
        assertEquals(MessageUtil.ERROR_CODE_FOR_906, msgCD906CCaptor.getValue().getFunctionalError().get(0).getErrorCode());
    }

    @Test
    @SneakyThrows
    void testProcessMessage_CD150C_for_OoDep_ExistingMovement() {
        // given
        String messageXml = xmlToString("CD150C_for_departure.xml");
        Message savedMessage = messageProcessingService.setUpMessage(messageXml, false, DirectionEnum.RECEIVED);
        savedMessage.setId(UUID.randomUUID().toString());
        when(dossierDataService.persistMessage(any())).thenReturn(savedMessage);
        when(validationService.validateMovementMessage(savedMessage.getId())).thenReturn(List.of());

        Movement depMovement = getMovement("depMovement", OfficeRoleTypesEnum.DEPARTURE.getValue(), "DK005600", savedMessage.getMrn(),
                null, ProcessingStatusTypeEnum.OODEP_RECOVERY_RECOMMENDED.getItemCode());

        when(dossierDataService.findByCriteria(savedMessage.getMrn(), null, null, null, 99)).thenReturn(List.of(depMovement));
        doNothing().when(dossierDataService).associateMessageWithMovement(savedMessage.getId(), depMovement.getId());

        when(getCountryCodeService.getCountryCode()).thenReturn("DK");
        when(messageValidationService.performStateValidation(any(), anyList())).thenReturn(MovementsOrViolations.of(List.of(depMovement), List.of()));

        // when
        messageProcessingService.processMessage(messageXml);

        // then
        assertEquals(MessageUtil.extractMRNCountryCode(savedMessage.getMrn()), getCountryCodeService.getCountryCode());
        verify(dossierDataService, times(1)).persistMessage(messageArgumentCaptor.capture());
        verify(dossierDataService, times(0)).createMovement(createdMovementArgumentCaptor.capture());
        verify(dossierDataService, times(1)).associateMessageWithMovement(messageIdCaptor.capture(), movementIdCaptor.capture());
        verify(processMessageCommandProducer, times(1)).publish(messageIdCaptor.capture(), messageTypeCaptor.capture(),
                publishedMovementArgumentCaptor.capture());

        List<Movement> publishedMovements = publishedMovementArgumentCaptor.getAllValues();
        assertThat(publishedMovements).containsOnly(depMovement);
    }

    /**
     * For incoming IE150 will test all OfficeRoleTypes (as declared in the CsvSource Test Parameter) with a sample Valid State for each Office Role Type
     */
    @ParameterizedTest
    @CsvSource({"DESTINATION,C07", "TRANSIT,D01", "EXIT,M02", "INCIDENT,I04"})
    @SneakyThrows
    void testProcessMessage_CD150C_for_AllOffices_ExistingMovement(String officeRoleType, String processingStatus) {
        // given
        String messageXml = xmlToString("CD150C_for_destination.xml");
        Message savedMessage = messageProcessingService.setUpMessage(messageXml, false, DirectionEnum.RECEIVED);
        savedMessage.setId(UUID.randomUUID().toString());
        when(dossierDataService.persistMessage(any())).thenReturn(savedMessage);
        when(validationService.validateMovementMessage(savedMessage.getId())).thenReturn(List.of());

        Movement existingMovement =
                getMovement("ceb0cc2b-dd74-495b-87cb-6ab27c7687fe", OfficeRoleTypesEnum.of(officeRoleType).getValue(), "DK000460", savedMessage.getMrn(),
                        null, processingStatus);
        when(dossierDataService.findByCriteria(savedMessage.getMrn(), null, null, null, 99)).thenReturn(List.of(existingMovement));
        doNothing().when(dossierDataService).associateMessageWithMovement(savedMessage.getId(), existingMovement.getId());
        when(messageValidationService.performStateValidation(any(), anyList())).thenReturn(MovementsOrViolations.of(List.of(existingMovement), List.of()));

        when(getCountryCodeService.getCountryCode()).thenReturn("DK");

        // when
        messageProcessingService.processMessage(messageXml);

        // then
        assertNotEquals(MessageUtil.extractMRNCountryCode(savedMessage.getMrn()), getCountryCodeService.getCountryCode());
        verify(dossierDataService, times(1)).persistMessage(messageArgumentCaptor.capture());
        verify(dossierDataService, times(0)).createMovement(createdMovementArgumentCaptor.capture());
        verify(dossierDataService, times(1)).associateMessageWithMovement(messageIdCaptor.capture(), movementIdCaptor.capture());
        verify(processMessageCommandProducer, times(1)).publish(messageIdCaptor.capture(), messageTypeCaptor.capture(),
                publishedMovementArgumentCaptor.capture());

        List<Movement> publishedMovements = publishedMovementArgumentCaptor.getAllValues();
        assertThat(publishedMovements).containsOnly(existingMovement);
    }

    /**
     * For incoming IE150 will test all OfficeRoleTypes (as declared in the Enum Test Parameter) with all possible declared Valid States per OfficeRoleType
     */
    @ParameterizedTest
    @EnumSource(value = OfficeRoleTypesEnum.class, names = {"DESTINATION", "TRANSIT", "EXIT", "INCIDENT"})
    void testProcessMessage_CD150C_for_AllOffices_AllStates_ExistingMovement(OfficeRoleTypesEnum officeRoleTypesEnum) throws XPathExpressionException {
        // given
        String messageXml = xmlToString("CD150C_for_destination.xml");
        Message savedMessage = messageProcessingService.setUpMessage(messageXml, false, DirectionEnum.RECEIVED);
        savedMessage.setId(UUID.randomUUID().toString());
        when(dossierDataService.persistMessage(any())).thenReturn(savedMessage);
        when(validationService.validateMovementMessage(savedMessage.getId())).thenReturn(List.of());

        Set<String> allValidStatesPerOfficeRoleType =
                MessageOfficeTypeExpectedStatusesEnum.getProcessingStatusItemCodesByMessageTypeAndRoleType(MessageTypes.CD_150_C, officeRoleTypesEnum);

        allValidStatesPerOfficeRoleType.forEach(
                state -> {
                    Movement existingMovement =
                            getMovement("movementId", officeRoleTypesEnum.getValue(), "DK000460", savedMessage.getMrn(),
                                    null, state);
                    when(dossierDataService.findByCriteria(savedMessage.getMrn(), null, null, null, 99)).thenReturn(List.of(existingMovement));
                    doNothing().when(dossierDataService).associateMessageWithMovement(savedMessage.getId(), existingMovement.getId());
                    when(getCountryCodeService.getCountryCode()).thenReturn("DK");
                    when(messageValidationService.performStateValidation(any(), anyList())).thenReturn(
                            MovementsOrViolations.of(List.of(existingMovement), List.of()));

                    // when
                    try {
                        messageProcessingService.processMessage(messageXml);
                    } catch (BadRequestException e) {
                        throw new RuntimeException(e);
                    } catch (XPathExpressionException e) {
                        throw new RuntimeException(e);
                    }

                    // then
                    assertNotEquals(MessageUtil.extractMRNCountryCode(savedMessage.getMrn()), getCountryCodeService.getCountryCode());
                }
        );
        verify(dossierDataService, times(allValidStatesPerOfficeRoleType.size())).persistMessage(messageArgumentCaptor.capture());
        verify(dossierDataService, times(0)).createMovement(createdMovementArgumentCaptor.capture());
        verify(dossierDataService, times(allValidStatesPerOfficeRoleType.size())).associateMessageWithMovement(messageIdCaptor.capture(),
                movementIdCaptor.capture());
        verify(processMessageCommandProducer, times(allValidStatesPerOfficeRoleType.size())).publish(messageIdCaptor.capture(), messageTypeCaptor.capture(),
                publishedMovementArgumentCaptor.capture());
    }

    @Test
    @SneakyThrows
    void testProcessMessage_CD150C_for_OoDest_CreateNewMovement() throws JAXBException {        // given
        String messageXml = xmlToString("CD150C_for_destination.xml");
        Message savedMessage = messageProcessingService.setUpMessage(messageXml, false, DirectionEnum.RECEIVED);
        savedMessage.setId(UUID.randomUUID().toString());

        CD150CType cd150CType = XmlConverter.buildMessageFromXML(savedMessage.getPayload(), CD150CType.class);
        Movement destMovement =
                getMovement("ceb0cc2b-dd74-495b-87cb-6ab27c7687fe", OfficeRoleTypesEnum.DESTINATION.getValue(), "DK000460", savedMessage.getMrn(),
                        null, null);

        when(dossierDataService.persistMessage(any())).thenReturn(savedMessage);
        when(validationService.validateMovementMessage(savedMessage.getId())).thenReturn(List.of());
        when(dossierDataService.findByCriteria(savedMessage.getMrn(), null, null, null, 99)).thenReturn(List.of());
        when(dossierDataService.createMovement(any())).thenReturn(destMovement);
        doNothing().when(dossierDataService).associateMessageWithMovement(savedMessage.getId(), destMovement.getId());
        when(getCountryCodeService.getCountryCode()).thenReturn("DK");
        when(messageValidationService.performStateValidation(any(), anyList())).thenReturn(
                MovementsOrViolations.of(List.of(nullifyMovementId(destMovement)), List.of()));

        // when
        messageProcessingService.processMessage(messageXml);

        // then
        assertNotEquals(MessageUtil.extractMRNCountryCode(savedMessage.getMrn()), getCountryCodeService.getCountryCode());
        verify(dossierDataService, times(1)).persistMessage(messageArgumentCaptor.capture());
        verify(dossierDataService, times(1)).createMovement(createdMovementArgumentCaptor.capture());
        verify(dossierDataService, times(1)).associateMessageWithMovement(messageIdCaptor.capture(), movementIdCaptor.capture());
        verify(processMessageCommandProducer, times(1)).publish(messageIdCaptor.capture(), messageTypeCaptor.capture(),
                publishedMovementArgumentCaptor.capture());

        Movement movementCreated = createdMovementArgumentCaptor.getValue();
        assertEquals(movementCreated.getOfficeRoleType(), OfficeRoleTypesEnum.DESTINATION.getValue());
        assertEquals(movementCreated.getOfficeId(), cd150CType.getCustomsOfficeOfRecoveryRequested().getReferenceNumber());

        List<Movement> publishedMovements = publishedMovementArgumentCaptor.getAllValues();
        assertThat(publishedMovements).containsOnly(destMovement);
    }

    @Test
    @SneakyThrows
    void testProcessMessage_906_success() {
        // given
        String cd181Xml = xmlToString("CD181C.xml");
        Message saved181 = messageProcessingService.setUpMessage(cd181Xml, false, DirectionEnum.SENT);
        saved181.setId(UUID.randomUUID().toString());

        String cd906Xml = xmlToString("CD906C.xml");
        Message saved906 = messageProcessingService.setUpMessage(cd906Xml, false, DirectionEnum.RECEIVED);
        saved906.setId(UUID.randomUUID().toString());

        Movement movement = getMovement(UUID.randomUUID().toString(), OfficeRoleTypesEnum.DESTINATION.getValue(), "DE002202", "MRN-70P6",
                "1112236201149", "E01");
        List<Movement> storedMovements = List.of(movement);

        // when
        when(dossierDataService.findAssociatedMovements("test", DirectionEnum.SENT.getDirection())).thenReturn(storedMovements);
        when(dossierDataService.persistMessage(any())).thenReturn(saved906);
        when(dossierDataService.saveRejectedMessage(any())).thenAnswer(invocationOnMock -> invocationOnMock.getArgument(0));
        when(dossierDataService.getMessagesByCriteria(any())).thenReturn(List.of(saved181));
        when(messageValidationService.performStateValidation(any(), anyList())).thenReturn(MovementsOrViolations.of(List.of(movement), List.of()));

        messageProcessingService.processMessage(cd906Xml);

        // then
        verify(validationService, times(0)).validateMovementMessage(any());
        verify(dossierDataService, times(1)).persistMessage(messageArgumentCaptor.capture());
        verify(dossierDataService, times(0)).createMovement(any());
        verify(dossierDataService, times(1)).associateMessageWithMovement(messageIdCaptor.capture(), movementIdCaptor.capture());
        verify(processMessageCommandProducer, times(0)).publish(any(), any(), any(Movement.class));
        verify(ddntaMessageProducer, times(0)).publish(msgCD906CCaptor.capture(), eq("ceb0cc2b-dd74-495b-87cb-6ab27c7687fd"));

        assertThat(messageIdCaptor.getValue()).isEqualTo(saved906.getId());
        assertThat(movementIdCaptor.getValue()).isEqualTo(movement.getId());
    }

    @Test
    @SneakyThrows
    void testProcessMessage_906_invalid() {
        // given
        String cd181Xml = xmlToString("CD181C.xml");
        Message saved181 = messageProcessingService.setUpMessage(cd181Xml, false, DirectionEnum.SENT);
        saved181.setId(UUID.randomUUID().toString());

        String cd906Xml = xmlToString("CD906C_invalid.xml");
        Message saved906 = messageProcessingService.setUpMessage(cd906Xml, false, DirectionEnum.RECEIVED);
        saved906.setId(UUID.randomUUID().toString());

        Movement movement = getMovement(UUID.randomUUID().toString(), OfficeRoleTypesEnum.DESTINATION.getValue(), "DE002202", "MRN-70P6",
                "1112236201149", "E01");
        List<Movement> storedMovements = List.of(movement);

        // when
        when(dossierDataService.findAssociatedMovements("test", DirectionEnum.SENT.getDirection())).thenReturn(storedMovements);
        when(dossierDataService.persistMessage(any())).thenReturn(saved906);
        when(dossierDataService.saveRejectedMessage(any())).thenAnswer(invocationOnMock -> invocationOnMock.getArgument(0));
        when(dossierDataService.getMessagesByCriteria(any())).thenReturn(List.of(saved181));
        when(messageValidationService.performStateValidation(any(), anyList())).thenReturn(MovementsOrViolations.of(storedMovements, List.of()));

        messageProcessingService.processMessage(cd906Xml);

        // then
        verify(validationService, times(0)).validateMovementMessage(any());
        verify(dossierDataService, times(1)).persistMessage(messageArgumentCaptor.capture());
        verify(dossierDataService, times(0)).createMovement(any());
        verify(dossierDataService, times(1)).associateMessageWithMovement(messageIdCaptor.capture(), movementIdCaptor.capture());
        verify(processMessageCommandProducer, times(0)).publish(messageIdCaptor.capture(), messageTypeCaptor.capture(),
                publishedMovementArgumentCaptor.capture());
        verify(ddntaMessageProducer, times(0)).publish(msgCD906CCaptor.capture(), eq("ceb0cc2b-dd74-495b-87cb-6ab27c7687fd"));

        assertThat(messageIdCaptor.getValue()).isEqualTo(saved906.getId());
        assertThat(movementIdCaptor.getValue()).isEqualTo(movement.getId());
    }

    @Test
    @SneakyThrows
    void testProcessMessage_906_no_movement() {
        // given
        String cd906Xml = xmlToString("CD906C.xml");
        Message saved906 = messageProcessingService.setUpMessage(cd906Xml, false, DirectionEnum.RECEIVED);
        saved906.setId(UUID.randomUUID().toString());

        // when
        when(dossierDataService.findAssociatedMovements("test", DirectionEnum.SENT.getDirection())).thenReturn(new ArrayList<>());
        when(dossierDataService.persistMessage(any())).thenReturn(saved906);
        when(dossierDataService.getMessagesByCriteria(any())).thenReturn(
                List.of(Message.builder().id(UUID.randomUUID().toString()).messageType(MessageTypes.CD_150_C.value()).build()));

        messageProcessingService.processMessage(cd906Xml);

        // then
        verify(dossierDataService, times(1)).persistMessage(messageArgumentCaptor.capture());
        verify(dossierDataService, times(0)).createMovement(any());
        verify(dossierDataService, times(0)).associateMessageWithMovement(any(), any());
        verify(processMessageCommandProducer, times(0)).publish(any(), any(), any(Movement.class));
        verify(ddntaMessageProducer, times(0)).publish(msgCD906CCaptor.capture(), eq("ceb0cc2b-dd74-495b-87cb-6ab27c7687fd"));
    }

    @Test
    @SneakyThrows
    void testProcessMessage_917_success() {
        // given
        String cd181Xml = xmlToString("CD181C.xml");
        Message saved181 = messageProcessingService.setUpMessage(cd181Xml, false, DirectionEnum.SENT);
        saved181.setId(UUID.randomUUID().toString());

        String cd917Xml = xmlToString("CD917C.xml");
        Message saved917 = messageProcessingService.setUpMessage(cd917Xml, false, DirectionEnum.RECEIVED);
        saved917.setId(UUID.randomUUID().toString());

        Movement movement = getMovement(UUID.randomUUID().toString(), OfficeRoleTypesEnum.DESTINATION.getValue(), "DE002202", "MRN-70P6",
                "1112236201149", "E01");
        List<Movement> storedMovements = List.of(movement);

        // when
        when(dossierDataService.findAssociatedMovements("test", DirectionEnum.SENT.getDirection())).thenReturn(storedMovements);
        when(dossierDataService.persistMessage(any())).thenReturn(saved917);
        when(messageValidationService.performStateValidation(any(), anyList())).thenReturn(MovementsOrViolations.of(List.of(movement), List.of()));

        messageProcessingService.processMessage(cd917Xml);

        // then
        verify(validationService, times(0)).validateMovementMessage(any());
        verify(dossierDataService, times(1)).persistMessage(messageArgumentCaptor.capture());
        verify(dossierDataService, times(0)).createMovement(any());
        verify(dossierDataService, times(1)).associateMessageWithMovement(messageIdCaptor.capture(), movementIdCaptor.capture());
        verify(processMessageCommandProducer, times(0)).publish(any(), any(), any(Movement.class));
        verify(ddntaMessageProducer, times(0)).publish(msgCD906CCaptor.capture(), eq("ceb0cc2b-dd74-495b-87cb-6ab27c7687fd"));

        assertThat(messageIdCaptor.getValue()).isEqualTo(saved917.getId());
        assertThat(movementIdCaptor.getValue()).isEqualTo(movement.getId());
    }

    protected Movement getMovement(String id, String officeRoleType, String officeId, String mrn, String lrn, String processingStatus) {
        return Movement.builder().id(id).officeRoleType(officeRoleType).officeId(officeId).mrn(mrn).lrn(lrn).processingStatus(processingStatus).build();
    }

    protected MovementQuery getMovementQuery(String mrn) {
        return MovementQuery.builder().mrn(mrn).build();
    }

    @Test
    @SneakyThrows
    void test_findTargetMovementForIncomingIE150() {
        String MRN = "22DKFUECYSBTH9ARK2";
        List<Movement> movementListByMrn = generateMovementListByMrn(MRN);

        // MRN belongs to Operating Country -> Peak DEPARTURE with most recent updatedDateTime
        assertEquals("ooDep02", messageProcessingService.findTargetMovementForIncomingIE150(MRN, movementListByMrn, "DK").getOfficeId());

        // MRN belongs to Operating Country and no DEPARTURE record found -> NotFoundException
        List<Movement> movementsWithoutDeparture = removeMovementsOfOfficeRoleType(movementListByMrn, EnumSet.of(OfficeRoleTypesEnum.DEPARTURE));
        assertThrows(NotFoundException.class, () -> {
            messageProcessingService.findTargetMovementForIncomingIE150(MRN, movementsWithoutDeparture, "DK");
        });

        // Country of MRN is not Operating Country -> Peak DESTINATION with most recent updatedDateTime
        assertEquals("ooDest02", messageProcessingService.findTargetMovementForIncomingIE150(MRN, movementListByMrn, "DE").getOfficeId());

        // Country of MRN is not Operating Country and DESTINATION removed -> Peak TRANSIT with most recent updatedDateTime
        assertEquals("ooTransit02", messageProcessingService.findTargetMovementForIncomingIE150(MRN,
                                                                    removeMovementsOfOfficeRoleType(movementListByMrn, EnumSet.of(OfficeRoleTypesEnum.DESTINATION)), "DE")
                                                            .getOfficeId());

        // Country of MRN is not Operating Country and DESTINATION and TRANSIT removed -> Peak EXIT with most recent updatedDateTime
        assertEquals("ooExit02", messageProcessingService.findTargetMovementForIncomingIE150(MRN,
                                                                 removeMovementsOfOfficeRoleType(movementListByMrn, EnumSet.of(OfficeRoleTypesEnum.DESTINATION, OfficeRoleTypesEnum.TRANSIT)), "DE")
                                                         .getOfficeId());

        // Country of MRN is not Operating Country and DESTINATION,TRANSIT and EXIT removed -> Peak INCIDENT with most recent updatedDateTime
        assertEquals("ooIncident02", messageProcessingService.findTargetMovementForIncomingIE150(MRN,
                                                                     removeMovementsOfOfficeRoleType(movementListByMrn,
                                                                             EnumSet.of(OfficeRoleTypesEnum.DESTINATION, OfficeRoleTypesEnum.TRANSIT, OfficeRoleTypesEnum.EXIT)), "DE")
                                                             .getOfficeId());

        // Country of MRN is not Operating Country and no record found for DESTINATION,TRANSIT,EXIT or INCIDENT. Return NULL
        assertNull(messageProcessingService.findTargetMovementForIncomingIE150(MRN,
                removeMovementsOfOfficeRoleType(movementListByMrn,
                        EnumSet.of(OfficeRoleTypesEnum.DESTINATION, OfficeRoleTypesEnum.TRANSIT, OfficeRoleTypesEnum.EXIT, OfficeRoleTypesEnum.INCIDENT)), "DE")
        );
    }

    @Test
    @SneakyThrows
    void test_findTargetMovementForIncomingIE152() {
        String MRN = "22DKFUECYSBTH9ARK2";
        List<Movement> movementListByMrn = generateMovementListByMrn(MRN);
        when(getCountryCodeService.getCountryCode()).thenReturn("DK");

        // return mrnCountryCode.equals(operatingCountry) && !messageSender.equals(operatingCountry); -> Departure
        assertEquals(1, messageProcessingService.findTargetMovementForIncomingIE152(MRN, "DE", movementListByMrn).size());
        assertEquals("ooDep02", messageProcessingService.findTargetMovementForIncomingIE152(MRN, "DE", movementListByMrn).get(0).getOfficeId());

        // otherwise any other existing movement except of Departure
        List<Movement> movementList = messageProcessingService.findTargetMovementForIncomingIE152(MRN, "DK", movementListByMrn);
        assertEquals(4, movementList.size());
        for (OfficeRoleTypesEnum officeRoleTypesEnum :
                List.of(OfficeRoleTypesEnum.DESTINATION,
                        OfficeRoleTypesEnum.TRANSIT,
                        OfficeRoleTypesEnum.EXIT,
                        OfficeRoleTypesEnum.INCIDENT)) {
            movementList.stream().map(movement -> assertThat(movement.getOfficeRoleType().contains(officeRoleTypesEnum.getValue())));
        }
    }



    protected static List<Movement> generateMovementListByMrn(String MRN) {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
        List<Movement> movementList = Arrays.asList(
                Movement.builder()
                        .mrn(MRN)
                        .officeId("ooIncident01")
                        .officeRoleType(OfficeRoleTypesEnum.INCIDENT.getValue())
                        .updatedDateTime(LocalDateTime.parse("2022-01-01 00:01", formatter))
                        .build(),
                Movement.builder()
                        .mrn(MRN)
                        .officeId("ooIncident02")
                        .officeRoleType(OfficeRoleTypesEnum.INCIDENT.getValue())
                        .updatedDateTime(LocalDateTime.parse("2022-01-01 00:02", formatter))
                        .build(),
                Movement.builder()
                        .mrn(MRN)
                        .officeId("ooDep01")
                        .officeRoleType(OfficeRoleTypesEnum.DEPARTURE.getValue())
                        .updatedDateTime(LocalDateTime.parse("2022-01-01 00:01", formatter))
                        .build(),
                Movement.builder()
                        .mrn(MRN)
                        .officeId("ooExit01")
                        .officeRoleType(OfficeRoleTypesEnum.EXIT.getValue())
                        .updatedDateTime(LocalDateTime.parse("2022-01-01 00:01", formatter))
                        .build(),
                Movement.builder()
                        .mrn(MRN)
                        .officeId("ooExit02")
                        .officeRoleType(OfficeRoleTypesEnum.EXIT.getValue())
                        .updatedDateTime(LocalDateTime.parse("2022-01-01 00:02", formatter))
                        .build(),
                Movement.builder()
                        .mrn(MRN)
                        .officeId("ooTransit01")
                        .officeRoleType(OfficeRoleTypesEnum.TRANSIT.getValue())
                        .updatedDateTime(LocalDateTime.parse("2022-01-01 00:01", formatter))
                        .build(),
                Movement.builder()
                        .mrn(MRN)
                        .officeId("ooTransit02")
                        .officeRoleType(OfficeRoleTypesEnum.TRANSIT.getValue())
                        .updatedDateTime(LocalDateTime.parse("2022-01-01 00:02", formatter))
                        .build(),
                Movement.builder()
                        .mrn(MRN)
                        .officeId("ooDest01")
                        .officeRoleType(OfficeRoleTypesEnum.DESTINATION.getValue())
                        .updatedDateTime(LocalDateTime.parse("2022-01-01 00:01", formatter))
                        .build(),
                Movement.builder()
                        .mrn(MRN)
                        .officeId("ooDest02")
                        .officeRoleType(OfficeRoleTypesEnum.DESTINATION.getValue())
                        .updatedDateTime(LocalDateTime.parse("2022-01-01 00:02", formatter))
                        .build(),
                Movement.builder()
                        .mrn(MRN)
                        .officeId("ooTansit02")
                        .officeRoleType("Μπανάνες")
                        .updatedDateTime(LocalDateTime.parse("2022-01-01 00:02", formatter))
                        .build(),
                Movement.builder()
                        .mrn(MRN)
                        .officeId("ooDep02")
                        .officeRoleType(OfficeRoleTypesEnum.DEPARTURE.getValue())
                        .updatedDateTime(LocalDateTime.parse("2022-01-01 00:02", formatter))
                        .build()
        );
        return movementList;
    }

    protected static List<Movement> removeMovementsOfOfficeRoleType(List<Movement> movementList, final EnumSet<OfficeRoleTypesEnum> officeRoleTypesEnumSet) {
        return
                movementList.stream()
                            .filter(movement -> !officeRoleTypesEnumSet.contains(OfficeRoleTypesEnum.of(movement.getOfficeRoleType())))
                            .collect(Collectors.toList());
    }

    private Movement nullifyMovementId(Movement movement) {
        return getMovement(null, movement.getOfficeRoleType(), movement.getOfficeId(), null, movement.getLrn(),
                ProcessingStatusTypeEnum.OODEP_NONE.getItemCode());
    }

}
