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

import com.intrasoft.ermis.common.json.parsing.JsonConverter;
import com.intrasoft.ermis.common.json.parsing.JsonConverterFactory;
import com.intrasoft.ermis.common.logging.ErmisLogger;
import com.intrasoft.ermis.contracts.core.dto.declaration.common.CustomsDataPayloadContentTypeEnum;
import com.intrasoft.ermis.contracts.core.dto.declaration.common.RegimeEnum;
import com.intrasoft.ermis.contracts.core.dto.declaration.riskassessment.v2.RiskAssessmentRequestEvent;
import com.intrasoft.ermis.contracts.core.dto.declaration.riskassessment.v2.RiskAssessmentResponseEvent;
import com.intrasoft.ermis.ddntav5152.messages.RiskAnalysisIdentificationType;
import com.intrasoft.ermis.transit.common.config.services.GetCountryCodeService;
import com.intrasoft.ermis.transit.common.config.timer.TimerDelayExecutor;
import com.intrasoft.ermis.transit.common.enums.DirectionEnum;
import com.intrasoft.ermis.transit.common.enums.MessageDomainTypeEnum;
import com.intrasoft.ermis.transit.common.exceptions.BaseException;
import com.intrasoft.ermis.transit.common.mappers.MessageTypeMapper;
import com.intrasoft.ermis.transit.common.mappers.RiskAnalysisMapper;
import com.intrasoft.ermis.transit.common.util.MessageUtil;
import com.intrasoft.ermis.transit.common.util.RandomUtil;
import com.intrasoft.ermis.transit.contracts.core.domain.TimerInfo;
import com.intrasoft.ermis.transit.contracts.core.enums.RiskAnalysisCodeEnum;
import com.intrasoft.ermis.transit.officeofexitfortransit.core.configuration.OfficeOfExitForTransitDDNTASpecificationConfigurationProperties;
import com.intrasoft.ermis.transit.officeofexitfortransit.core.outbound.RiskAssessmentRequestEventProducer;
import com.intrasoft.ermis.transit.officeofexitfortransit.domain.CustomsDataDomain;
import com.intrasoft.ermis.transit.officeofexitfortransit.domain.Message;
import com.intrasoft.ermis.transit.officeofexitfortransit.domain.Movement;
import com.intrasoft.ermis.transit.officeofexitfortransit.domain.RiskResult;
import com.intrasoft.ermis.transit.officeofexitfortransit.domain.enums.BpmnFlowMessagesEnum;
import com.intrasoft.ermis.transit.officeofexitfortransit.domain.enums.BpmnFlowVariablesEnum;
import com.intrasoft.ermis.transit.officeofexitfortransit.domain.events.RiskAssessmentRequestDomainEvent;
import java.math.BigInteger;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor(onConstructor_ = {@Autowired})
public class RiskAssessmentServiceImpl implements RiskAssessmentService {

    private static final JsonConverter JSON_CONVERTER = JsonConverterFactory.getDefaultJsonConverter();

    private static final String NOT_AVAILABLE = "NOT_AVAILABLE";

    private static final String RISK_ASSESSMENT_DESTINATION = "RISK_ASSESSMENT";

    private static final String RANDOM_RISK_ASSESSMENT = "RANDOM_RISK_ASSESSMENT";

    private static final String OOEXIT_FOR_TRANSIT_RISK_ANALYSIS_DELAY_TIMER_RS = "getOoExitForTransitRiskAnalysisDelayTimerRS";

    private static final int MIN_RISK = 1;

    private static final int MAX_RISK = 100;

    private final ErmisLogger log;

    private final BpmnFlowService bpmnFlowService;

    private final DossierDataService dossierDataService;

    private final RiskAssessmentRequestEventProducer riskAssessmentRequestEventProducer;

    private final RiskAnalysisMapper riskAnalysisMapper;

    private final TimerDelayExecutor timerDelayExecutor;

    private final OfficeOfExitForTransitDDNTASpecificationConfigurationProperties specificationConfigurationProperties;

    private final GetCountryCodeService getCountryCodeService;

    @Override
    public void sendRiskAssessmentRequest(String processBusinessKey, String movementId, String... messageIds) {

        log.info("ENTER sendRiskAssessmentRequest()");
        if (Objects.isNull(messageIds)) {

            throw new BaseException("No messageIds provided for Risk Assessment!");
        }
        // Risk Assessment was either triggered by IE160 OR IE165. The trigger-message will have an ID of !NOT_AVAILABLE.
        List<Message> messageList = Arrays.stream(messageIds)
                                          .filter(messageId -> !(NOT_AVAILABLE.equals(messageId)))
                                          .map(messageId -> Objects.requireNonNull(dossierDataService.getMessageById(messageId)))
                                          .collect(Collectors.toList());

        if (!messageList.isEmpty()) {
            // Retrieve Movement:
            Movement movement = dossierDataService.getMovementById(movementId);
            // Use a stream index for each customsDataDomainMessage sequenceNumber:
            List<CustomsDataDomain> customsDataDomainList = IntStream.range(0, messageList.size())
                                                                     .mapToObj(index ->
                                                                             createCustomsDataDomainMessage(messageList.get(index),
                                                                                     BigInteger.valueOf(++index)))
                                                                     .collect(Collectors.toList());
            // Add movementId to CustomsData:
            customsDataDomainList.add(createMovementIdCustomsDataRecord(movementId, BigInteger.valueOf(customsDataDomainList.size() + 1L)));
            // Add acceptanceDateTime to CustomsData:
            customsDataDomainList.add(createAcceptanceDateTimeCustomsDataRecord(movement.getAcceptanceDateTime(),
                    BigInteger.valueOf(customsDataDomainList.size() + 1L)));

            String mrn = movement.getMrn();

            RiskAssessmentRequestDomainEvent requestDomainEvent = RiskAssessmentRequestDomainEvent
                    .builder()
                    .requestId(MessageUtil.generateMessageIdentification())
                    .requestDateTime(LocalDateTime.now(ZoneOffset.UTC))
                    .regime(RegimeEnum.TRANSIT.getCode())
                    .processKey(mrn)
                    .processVersion("1")
                    .correlationKey(movementId) // correlationKey will match processBusinessKey == movementId
                    .customsData(new HashSet<>(customsDataDomainList))
                    .build();

            persistRiskAssessmentRequestMessage(movementId, requestDomainEvent, messageList.get(0).getDestination());
            riskAssessmentRequestEventProducer.publish(requestDomainEvent);
            log.info("EXIT sendRiskAssessmentRequest()");
        } else {

            throw new BaseException("No Messages were found for provided IDs!");
        }
    }

    @Override
    public void onRiskAssessmentResponseReceived(String movementId, String messageId) {

        log.info("ENTER onRiskAssessmentResponseReceived()");
        Message message = dossierDataService.getMessageById(messageId);

        // Create processVariables map.
        Map<String, Object> processVariables = new HashMap<>();
        processVariables.put(BpmnFlowVariablesEnum.RISK_ASSESSMENT_RESPONSE_ID.getValue(), messageId);

        if (message != null) {

            bpmnFlowService.correlateMessageWithValues(movementId, BpmnFlowMessagesEnum.RISK_ASSESSMENT_RESPONSE_MESSAGE.getValue(), processVariables);
        } else {

            throw new BaseException("Could not find Risk Assessment Message!");
        }
        log.info("EXIT onRiskAssessmentResponseReceived()");
    }

    @Override
    public RiskResult assignRiskFactor(String movementId, String riskAssessmentResponseId) {

        log.info("ENTER assignRiskFactor()");
        Message riskResponseMessage = dossierDataService.getMessageById(riskAssessmentResponseId);

        RiskAssessmentResponseEvent riskAssessmentResponseEvent =
                JSON_CONVERTER.convertStringToObject(riskResponseMessage.getPayload(), RiskAssessmentResponseEvent.class);

        RiskResult riskResult = RiskResult.builder()
                                          .movementId(movementId)
                                          .overallRiskResult(riskAssessmentResponseEvent.getTotalScore().intValue())
                                          .riskAnalysis(JSON_CONVERTER.convertToJsonString(
                                                  riskAnalysisMapper.toRiskAnalysisIdentification(riskAssessmentResponseEvent)))
                                          .build();

        dossierDataService.saveRiskResult(riskResult);
        log.info("EXIT assignRiskFactor()");

        return riskResult;
    }

    @Override
    public TimerInfo getRiskAnalysisTimerDelayConfig() {

        return timerDelayExecutor.getTimerDelayConfig(LocalDateTime.now(ZoneOffset.UTC), LocalDateTime.now(ZoneOffset.UTC),
                OOEXIT_FOR_TRANSIT_RISK_ANALYSIS_DELAY_TIMER_RS, false).orElseThrow(() -> new BaseException("Failed to setup risk analysis timer!"));
    }

    @Override
    public RiskResult assignRandomRiskFactor(String movementId) {

        Movement movement = dossierDataService.getMovementById(movementId);
        log.info("ENTER assignRandomRiskFactor()");
        RiskResult riskResult = RiskResult.builder()
                                          .movementId(movementId)
                                          .overallRiskResult(generateRandomRiskFactor())
                                          .riskAnalysis(JSON_CONVERTER.convertToJsonString(assignRandomRiskCode()))
                                          .build();

        dossierDataService.saveRiskResult(riskResult);
        handleRandomRiskResultActions(riskResult.getOverallRiskResult(), movement);

        log.info("EXIT assignRandomRiskFactor()");
        return riskResult;
    }

    @Override
    public void forwardMessageToRiskAnalysisSystem(String movementId, String messageId) {

        log.info("ENTER forwardMessageToRiskAnalysisSystem()");
        // Verify the Message exists:
        Message message = dossierDataService.getMessageById(messageId);

        if (message != null) {
            // Retrieve Movement:
            Movement movement = dossierDataService.getMovementById(movementId);

            Set<CustomsDataDomain> customsDataDomainSet = new HashSet<>();
            // Only IE181 will be included in the request, so sequenceNumber == 1.
            customsDataDomainSet.add(createCustomsDataDomainMessage(message, BigInteger.ONE));
            // Add movementId to CustomsData:
            customsDataDomainSet.add(createMovementIdCustomsDataRecord(movementId, BigInteger.valueOf(customsDataDomainSet.size() + 1L)));
            // Add acceptanceDateTime to CustomsData:
            customsDataDomainSet.add(createAcceptanceDateTimeCustomsDataRecord(movement.getAcceptanceDateTime(),
                    BigInteger.valueOf(customsDataDomainSet.size() + 1L)));

            String mrn = movement.getMrn();

            RiskAssessmentRequestDomainEvent requestDomainEvent = RiskAssessmentRequestDomainEvent
                    .builder()
                    .requestId(MessageUtil.generateMessageIdentification())
                    .requestDateTime(LocalDateTime.now(ZoneOffset.UTC))
                    .regime(RegimeEnum.TRANSIT.getCode())
                    .processKey(mrn)
                    .processVersion("1")
                    .customsData(customsDataDomainSet)
                    .correlationKey(movementId)
                    .build();

            persistRiskAssessmentRequestMessage(movementId, requestDomainEvent, message.getDestination());
            riskAssessmentRequestEventProducer.publish(requestDomainEvent);
            log.info("EXIT forwardMessageToRiskAnalysisSystem()");
        } else {

            throw new BaseException("No Messages were found for provided ID!");
        }
    }

    private CustomsDataDomain createCustomsDataDomainMessage(Message message, BigInteger sequenceNumber) {

        return CustomsDataDomain.builder()
                                .sequenceNumber(sequenceNumber)
                                .type(message.getMessageType())
                                .payload(message.getPayload())
                                .payloadType(CustomsDataPayloadContentTypeEnum.XML.name())
                                .payloadSpec(specificationConfigurationProperties.getDDNTASpecification())
                                .build();
    }

    private CustomsDataDomain createMovementIdCustomsDataRecord(String movementId, BigInteger sequenceNumber) {

        return CustomsDataDomain.builder()
                                .sequenceNumber(sequenceNumber)
                                .type("MOVEMENT_ID")
                                .payload(movementId)
                                .payloadType(CustomsDataPayloadContentTypeEnum.STRING.name())
                                .build();
    }

    private CustomsDataDomain createAcceptanceDateTimeCustomsDataRecord(LocalDateTime acceptanceDateTime, BigInteger sequenceNumber) {

        return CustomsDataDomain.builder()
                                .sequenceNumber(sequenceNumber)
                                .type("ACCEPTANCE_DATE_TIME")
                                .payload(JSON_CONVERTER.convertToJsonString(acceptanceDateTime))
                                .payloadType(CustomsDataPayloadContentTypeEnum.JSON.name())
                                .build();
    }

    private void persistRiskAssessmentRequestMessage(String movementId, RiskAssessmentRequestDomainEvent requestDomainEvent, String source) {

        Message message = Message.builder()
                                 .messageIdentification(requestDomainEvent.getRequestId())
                                 .messageType(RiskAssessmentRequestEvent.class.getSimpleName())
                                 .source(source)
                                 .destination(RISK_ASSESSMENT_DESTINATION)
                                 .domain(MessageDomainTypeEnum.ERMIS.name())
                                 .payload(JSON_CONVERTER.convertToJsonString(requestDomainEvent))
                                 .direction(DirectionEnum.SENT.getDirection())
                                 .build();

        Message savedMessage = dossierDataService.saveMessage(message);
        dossierDataService.associateMessageWithMovement(savedMessage.getId(), movementId);
    }

    private Integer generateRandomRiskFactor() {
        return RandomUtil.generateSecureRandomInt(MIN_RISK, MAX_RISK);
    }

    private RiskAnalysisIdentificationType assignRandomRiskCode() {

        RiskAnalysisIdentificationType riskAnalysisIdentificationType = new RiskAnalysisIdentificationType();
        riskAnalysisIdentificationType.setCode(RiskAnalysisCodeEnum.Z.getCode());

        return riskAnalysisIdentificationType;
    }

    /**
     * Creates, persists and associates a RiskAssessmentResponseEvent for Random Risk.
     *
     * @param riskResultScore Total Score to be used in RiskAssessmentResponseEvent.
     * @param movement Movement to associate with and retrieve MRN from.
     */
    private void handleRandomRiskResultActions(Integer riskResultScore, Movement movement) {
        // Create RiskAssessmentResponseEvent for Random Risk:
        RiskAssessmentResponseEvent riskAssessmentResponseEvent = new RiskAssessmentResponseEvent();
        riskAssessmentResponseEvent.setCode(RiskAnalysisCodeEnum.Z.getCode());
        riskAssessmentResponseEvent.setTotalScore(BigInteger.valueOf(riskResultScore));
        riskAssessmentResponseEvent.setRegime(RegimeEnum.TRANSIT.getCode());
        // Redundant - identifying data fields:
        riskAssessmentResponseEvent.setProcessKey(movement.getMrn());
        riskAssessmentResponseEvent.setProcessVersion(String.valueOf(BigInteger.ONE));
        riskAssessmentResponseEvent.setRiskAssessmentResults(new ArrayList<>());
        // Create Message:
        Message message = Message.builder()
                                 .messageIdentification(MessageUtil.generateMessageIdentification())
                                 .messageType(RiskAssessmentResponseEvent.class.getSimpleName())
                                 .source(RANDOM_RISK_ASSESSMENT)
                                 .destination(MessageTypeMapper.NTA + getCountryCodeService.getCountryCode())
                                 .domain(MessageDomainTypeEnum.ERMIS.name())
                                 .payload(JSON_CONVERTER.convertToJsonString(riskAssessmentResponseEvent))
                                 .direction(DirectionEnum.RECEIVED.getDirection())
                                 .build();
        // Persist and Associate:
        Message savedMessage = dossierDataService.saveMessage(message);
        dossierDataService.associateMessageWithMovement(savedMessage.getId(), movement.getId());
    }
}

package com.intrasoft.ermis.transit.officeofexitfortransit.service;

import static com.intrasoft.ermis.transit.officeofexitfortransit.util.DataGenerators.createIE160Message;
import static com.intrasoft.ermis.transit.officeofexitfortransit.util.DataGenerators.createIE165Message;
import static com.intrasoft.ermis.transit.officeofexitfortransit.util.DataGenerators.createIE181Message;
import static com.intrasoft.ermis.transit.officeofexitfortransit.util.DataGenerators.createRiskAssessmentResponseEvent;
import static com.intrasoft.ermis.transit.officeofexitfortransit.util.DataGenerators.generateRandomMRN;
import static com.intrasoft.ermis.transit.officeofexitfortransit.util.Stubs.stubSaveMessage;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.internal.verification.VerificationModeFactory.times;

import com.intrasoft.ermis.common.json.parsing.JsonConverter;
import com.intrasoft.ermis.common.json.parsing.JsonConverterFactory;
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.contracts.core.dto.declaration.common.CustomsDataPayloadContentTypeEnum;
import com.intrasoft.ermis.contracts.core.dto.declaration.riskassessment.v2.RiskAssessmentResponseEvent;
import com.intrasoft.ermis.ddntav5152.messages.CD160CType;
import com.intrasoft.ermis.ddntav5152.messages.CD165CType;
import com.intrasoft.ermis.ddntav5152.messages.CD181CType;
import com.intrasoft.ermis.ddntav5152.messages.MessageTypes;
import com.intrasoft.ermis.transit.common.config.services.GetCountryCodeService;
import com.intrasoft.ermis.transit.common.config.timer.TimerDelayExecutor;
import com.intrasoft.ermis.transit.common.exceptions.BaseException;
import com.intrasoft.ermis.transit.common.mappers.RiskAnalysisMapper;
import com.intrasoft.ermis.transit.common.mappers.RiskAnalysisMapperImpl;
import com.intrasoft.ermis.transit.contracts.core.domain.TimerInfo;
import com.intrasoft.ermis.transit.officeofexitfortransit.core.configuration.OfficeOfExitForTransitDDNTASpecificationConfigurationProperties;
import com.intrasoft.ermis.transit.officeofexitfortransit.core.outbound.RiskAssessmentRequestEventProducer;
import com.intrasoft.ermis.transit.officeofexitfortransit.core.service.BpmnFlowService;
import com.intrasoft.ermis.transit.officeofexitfortransit.core.service.DossierDataService;
import com.intrasoft.ermis.transit.officeofexitfortransit.core.service.RiskAssessmentServiceImpl;
import com.intrasoft.ermis.transit.officeofexitfortransit.domain.CustomsDataDomain;
import com.intrasoft.ermis.transit.officeofexitfortransit.domain.Message;
import com.intrasoft.ermis.transit.officeofexitfortransit.domain.Movement;
import com.intrasoft.ermis.transit.officeofexitfortransit.domain.RiskResult;
import com.intrasoft.ermis.transit.officeofexitfortransit.domain.enums.BpmnFlowMessagesEnum;
import com.intrasoft.ermis.transit.officeofexitfortransit.domain.enums.BpmnFlowVariablesEnum;
import com.intrasoft.ermis.transit.officeofexitfortransit.domain.events.RiskAssessmentRequestDomainEvent;
import java.math.BigInteger;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import org.apache.commons.lang3.Range;
import org.junit.jupiter.api.DisplayName;
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.ValueSource;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
class RiskAssessmentServiceUT {

    private static final JsonConverter JSON_CONVERTER = JsonConverterFactory.getDefaultJsonConverter();

    private static final String NOT_AVAILABLE = "NOT_AVAILABLE";

    private static final String OOEXIT_FOR_TRANSIT_RISK_ANALYSIS_DELAY_TIMER_RS = "getOoExitForTransitRiskAnalysisDelayTimerRS";

    private static final int MIN_RISK = 1;

    private static final int MAX_RISK = 100;

    private final String MOVEMENT_ID = UUID.randomUUID().toString();

    private final String MRN = generateRandomMRN();

    @Mock
    private ErmisLogger ermisLogger;

    @Mock
    private DossierDataService dossierDataService;

    @Mock
    private TimerDelayExecutor timerDelayExecutor;

    @Mock
    private BpmnFlowService bpmnFlowService;

    @Mock
    private RiskAssessmentRequestEventProducer riskAssessmentRequestEventProducer;

    @Mock
    private GetCountryCodeService getCountryCodeService;

    @Spy
    private OfficeOfExitForTransitDDNTASpecificationConfigurationProperties properties;

    @Spy
    private RiskAnalysisMapper riskAnalysisMapper = new RiskAnalysisMapperImpl();

    @Spy
    @InjectMocks
    private RiskAssessmentServiceImpl riskAssessmentService;

    @Captor
    private ArgumentCaptor<String> businessKeyArgumentCaptor;

    @Captor
    private ArgumentCaptor<String> messageNameArgumentCaptor;

    @Captor
    private ArgumentCaptor<Map<String, Object>> valueMapArgumentCaptor;

    @Captor
    private ArgumentCaptor<RiskAssessmentRequestDomainEvent> riskAssessmentRequestDomainEventArgumentCaptor;

    @ParameterizedTest
    @ValueSource(strings = {"CD160C", "CD165C"})
    @DisplayName("Test - Send Risk Assessment Request")
    void testSendRiskAssessmentRequest(String messageType) {

        System.setProperty("com.sun.xml.bind.v2.bytecode.ClassTailor.noOptimize", "true");
        // Test data:
        Movement mockMovement = createMovement();
        // Mocks:
        Message message = createMessage(messageType, MRN);
        CustomsDataDomain customsDataDomain = createCustomsDataDomainMessage(message);
        CustomsDataDomain movementIdCustomsDataDomain = createCustomsDataMovementId(MOVEMENT_ID);
        CustomsDataDomain acceptanceDateTimeCustomsDataDomain = createCustomsDataAcceptanceDateTime(mockMovement.getAcceptanceDateTime());
        when(dossierDataService.getMessageById(messageType)).thenReturn(message);
        when(dossierDataService.saveMessage(any())).thenAnswer(stubSaveMessage());
        when(dossierDataService.getMovementById(any())).thenReturn(mockMovement);
        doNothing().when(dossierDataService).associateMessageWithMovement(anyString(), eq(MOVEMENT_ID));
        // Call Risk Assessment Service method:
        riskAssessmentService.sendRiskAssessmentRequest(MOVEMENT_ID, MOVEMENT_ID, NOT_AVAILABLE, messageType);
        // Verification & Assertions:
        verify(riskAssessmentRequestEventProducer, times(1)).publish(riskAssessmentRequestDomainEventArgumentCaptor.capture());
        assertEquals(MRN, riskAssessmentRequestDomainEventArgumentCaptor.getValue().getProcessKey());
        assertEquals(3, riskAssessmentRequestDomainEventArgumentCaptor.getValue().getCustomsData().size());
        assertTrue(riskAssessmentRequestDomainEventArgumentCaptor.getValue().getCustomsData().contains(customsDataDomain));
        assertTrue(riskAssessmentRequestDomainEventArgumentCaptor.getValue().getCustomsData().contains(movementIdCustomsDataDomain));
        assertTrue(riskAssessmentRequestDomainEventArgumentCaptor.getValue().getCustomsData().contains(acceptanceDateTimeCustomsDataDomain));
    }

    @Test
    @DisplayName("Test - On Risk Assessment Received")
    void testOnRiskAssessmentReceived() {

        System.setProperty("com.sun.xml.bind.v2.bytecode.ClassTailor.noOptimize", "true");

        String messageType = RiskAssessmentResponseEvent.class.getSimpleName();
        // Mocks:
        Message message = createMessage(messageType, null);
        when(dossierDataService.getMessageById(messageType)).thenReturn(message);
        // Call Risk Assessment Service method:
        riskAssessmentService.onRiskAssessmentResponseReceived(MOVEMENT_ID, messageType);
        // Verifications & Assertions:
        verify(bpmnFlowService, times(1)).correlateMessageWithValues(
                businessKeyArgumentCaptor.capture(), messageNameArgumentCaptor.capture(), valueMapArgumentCaptor.capture());
        assertEquals(MOVEMENT_ID, businessKeyArgumentCaptor.getValue());
        assertEquals(BpmnFlowMessagesEnum.RISK_ASSESSMENT_RESPONSE_MESSAGE.getValue(), messageNameArgumentCaptor.getValue());
        assertEquals(1, valueMapArgumentCaptor.getValue().size());
        assertEquals(messageType, valueMapArgumentCaptor.getValue().get(BpmnFlowVariablesEnum.RISK_ASSESSMENT_RESPONSE_ID.getValue()));
    }

    @Test
    @DisplayName("Test - Assign Risk Factor")
    void testAssignRiskFactor() {

        System.setProperty("com.sun.xml.bind.v2.bytecode.ClassTailor.noOptimize", "true");

        String messageType = RiskAssessmentResponseEvent.class.getSimpleName();
        // Mocks:
        Message message = createMessage(messageType, null);
        when(dossierDataService.getMessageById(messageType)).thenReturn(message);
        doNothing().when(dossierDataService).saveRiskResult(any());
        // Call Risk Assessment Service method:
        RiskResult riskResult = riskAssessmentService.assignRiskFactor(MOVEMENT_ID, messageType);
        // Assertions:
        assertEquals(BigInteger.valueOf(65), BigInteger.valueOf(riskResult.getOverallRiskResult()));
    }

    @Test
    @DisplayName("Test - Get Risk Analysis Timer Delay Config")
    void testGetRiskAnalysisTimerDelayConfig() {
        // Mock:
        when(timerDelayExecutor.getTimerDelayConfig(any(), any(), eq(OOEXIT_FOR_TRANSIT_RISK_ANALYSIS_DELAY_TIMER_RS), anyBoolean()))
                .thenReturn(Optional.of(new TimerInfo()));
        // Call Risk Assessment Service method:
        riskAssessmentService.getRiskAnalysisTimerDelayConfig();
        // Assertions:
        verify(timerDelayExecutor, times(1)).getTimerDelayConfig(any(), any(), eq(OOEXIT_FOR_TRANSIT_RISK_ANALYSIS_DELAY_TIMER_RS), anyBoolean());
    }

    @Test
    @DisplayName("Test - Assign Random Risk Factor")
    void testAssignRandomRiskFactor() {

        Range<Integer> riskFactorRange = Range.between(MIN_RISK, MAX_RISK);
        Movement mockMovement = createMovement();

        // Mocks:
        when(dossierDataService.getMovementById(any())).thenReturn(mockMovement);
        doNothing().when(dossierDataService).saveRiskResult(any());
        when(getCountryCodeService.getCountryCode()).thenReturn("DK");
        when(dossierDataService.saveMessage(any())).thenAnswer(stubSaveMessage());
        doNothing().when(dossierDataService).associateMessageWithMovement(anyString(), eq(MOVEMENT_ID));
        // Call Risk Assessment Service method:
        RiskResult riskResult = riskAssessmentService.assignRandomRiskFactor(MOVEMENT_ID);
        // Assertions:
        assertTrue(riskFactorRange.contains(riskResult.getOverallRiskResult()));
    }

    @Test
    @DisplayName("Test - Forward Message To Risk Analysis System")
    void testForwardMessageToRiskAnalysisSystem() {

        System.setProperty("com.sun.xml.bind.v2.bytecode.ClassTailor.noOptimize", "true");
        // Test data:
        String messageType = MessageTypes.CD_181_C.value();
        Movement mockMovement = createMovement();
        // Mocks:
        Message message = createMessage(messageType, MRN);
        CustomsDataDomain customsDataDomain = createCustomsDataDomainMessage(message);
        CustomsDataDomain movementIdCustomsDataDomain = createCustomsDataMovementId(MOVEMENT_ID);
        CustomsDataDomain acceptanceDateTimeCustomsDataDomain = createCustomsDataAcceptanceDateTime(mockMovement.getAcceptanceDateTime());

        when(dossierDataService.getMessageById(messageType)).thenReturn(message);
        when(dossierDataService.saveMessage(any())).thenAnswer(stubSaveMessage());
        when(dossierDataService.getMovementById(any())).thenReturn(mockMovement);
        doNothing().when(dossierDataService).associateMessageWithMovement(anyString(), eq(MOVEMENT_ID));
        // Call Risk Assessment Service method:
        riskAssessmentService.forwardMessageToRiskAnalysisSystem(MOVEMENT_ID, messageType);
        // Verification & Assertions:
        verify(riskAssessmentRequestEventProducer, times(1)).publish(riskAssessmentRequestDomainEventArgumentCaptor.capture());
        assertEquals(MRN, riskAssessmentRequestDomainEventArgumentCaptor.getValue().getProcessKey());
        assertEquals(3, riskAssessmentRequestDomainEventArgumentCaptor.getValue().getCustomsData().size());
        assertTrue(riskAssessmentRequestDomainEventArgumentCaptor.getValue().getCustomsData().contains(customsDataDomain));
        assertTrue(riskAssessmentRequestDomainEventArgumentCaptor.getValue().getCustomsData().contains(movementIdCustomsDataDomain));
        assertTrue(riskAssessmentRequestDomainEventArgumentCaptor.getValue().getCustomsData().contains(acceptanceDateTimeCustomsDataDomain));
    }

    private Message createMessage(String messageType, String mrn) {

        Message message = new Message();

        if (MessageTypes.CD_160_C.value().equals(messageType)) {

            CD160CType cd160TypeMessage = createIE160Message(mrn);

            message.setPayload(XmlConverter.marshal(cd160TypeMessage, NamespacesEnum.NTA.getValue(), MessageTypes.CD_160_C.value()));
        } else if (MessageTypes.CD_165_C.value().equals(messageType)) {

            CD165CType cd165TypeMessage = createIE165Message(mrn);

            message.setPayload(XmlConverter.marshal(cd165TypeMessage, NamespacesEnum.NTA.getValue(), MessageTypes.CD_165_C.value()));
        } else if (RiskAssessmentResponseEvent.class.getSimpleName().equals(messageType)) {

            RiskAssessmentResponseEvent responseEvent = createRiskAssessmentResponseEvent();

            message.setPayload(JSON_CONVERTER.convertToJsonString(responseEvent));
        } else if (MessageTypes.CD_181_C.value().equals(messageType)) {

            CD181CType cd181TypeMessage = createIE181Message();

            message.setPayload(XmlConverter.marshal(cd181TypeMessage, NamespacesEnum.NTA.getValue(), MessageTypes.CD_181_C.value()));
        } else {

            throw new BaseException("Incorrect messageType provided in test!");
        }

        return message;
    }

    private CustomsDataDomain createCustomsDataDomainMessage(Message message) {

        return CustomsDataDomain.builder()
                                .sequenceNumber(BigInteger.ONE)
                                .type(message.getMessageType())
                                .payload(message.getPayload())
                                .payloadType("XML")
                                .build();
    }

    private CustomsDataDomain createCustomsDataMovementId(String movementId) {

        return CustomsDataDomain.builder()
                                .sequenceNumber(BigInteger.valueOf(2))
                                .type("MOVEMENT_ID")
                                .payload(movementId)
                                .payloadType(CustomsDataPayloadContentTypeEnum.STRING.name())
                                .build();
    }

    private CustomsDataDomain createCustomsDataAcceptanceDateTime(LocalDateTime acceptanceDateTime) {

        return CustomsDataDomain.builder()
                                .sequenceNumber(BigInteger.valueOf(3))
                                .type("ACCEPTANCE_DATE_TIME")
                                .payload(JSON_CONVERTER.convertToJsonString(acceptanceDateTime))
                                .payloadType(CustomsDataPayloadContentTypeEnum.JSON.name())
                                .build();
    }

    private Movement createMovement() {
        return Movement.builder().id(MOVEMENT_ID).acceptanceDateTime(LocalDateTime.now(ZoneOffset.UTC)).mrn(MRN).build();
    }
}
