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


import static com.intrasoft.ermis.transit.common.util.TimerInfoUtil.convertToDelayWithTimeUnit;

import com.intrasoft.ermis.common.logging.ErmisLogger;
import com.intrasoft.ermis.common.xml.parsing.XmlConverter;
import com.intrasoft.ermis.ddntav5152.messages.CC015CType;
import com.intrasoft.ermis.ddntav5152.messages.CC170CType;
import com.intrasoft.ermis.ddntav5152.messages.MessageTypes;
import com.intrasoft.ermis.platform.shared.client.configuration.core.ConfigurationDataPort;
import com.intrasoft.ermis.platform.shared.client.configuration.core.domain.Configuration;
import com.intrasoft.ermis.platform.shared.client.reference.data.core.ReferenceDataAdapter;
import com.intrasoft.ermis.platform.shared.client.reference.data.core.domain.CodeListDomain;
import com.intrasoft.ermis.platform.shared.client.reference.data.core.domain.CodeListItem;
import com.intrasoft.ermis.transit.common.config.services.ErmisConfigurationService;
import com.intrasoft.ermis.transit.common.config.timer.TimerDelayExecutor;
import com.intrasoft.ermis.transit.common.config.util.ConfigurationUtil;
import com.intrasoft.ermis.transit.common.enums.CountryCodesEnum;
import com.intrasoft.ermis.transit.common.enums.ProcessingStatusTypeEnum;
import com.intrasoft.ermis.transit.common.exceptions.BaseException;
import com.intrasoft.ermis.transit.contracts.core.domain.TimerInfo;
import com.intrasoft.ermis.transit.contracts.core.enums.DeclarationTypeEnum;
import com.intrasoft.ermis.transit.contracts.core.enums.TimerInfoEnum;
import com.intrasoft.ermis.transit.officeofdeparture.domain.Message;
import com.intrasoft.ermis.transit.officeofdeparture.domain.MessageOverview;
import com.intrasoft.ermis.transit.officeofdeparture.domain.Movement;
import com.intrasoft.ermis.transit.officeofdeparture.domain.MovementAttribute;
import com.intrasoft.ermis.transit.officeofdeparture.domain.enums.MovementAttributesEnum;
import com.intrasoft.ermis.transit.officeofdeparture.service.property.PropertyService;
import com.intrasoft.proddev.referencedata.shared.enums.CodelistKeyEnum;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import javax.xml.bind.JAXBException;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;


@Service
@RequiredArgsConstructor(onConstructor_ = {@Autowired})
public class ReleaseTransitServiceImpl implements ReleaseTransitService {

    private static final String RECOVERY_RECOMMENDED_TIMER_RS = "getRecoveryRecommendedTimerDelayConfigRS";

    private static final String INFORM_GUARANTOR_TIMER_RS = "getOoDepInformGuarantorTimerRS";

    private static final String RECEIPT_CONTROL_RESULTS_TIMER_RS = "getReceiptControlResultsTimerDelayConfigRS";

    private static final String MINIMUM_AND_MAXIMUM_EXPECTED_ARRIVAL_TIME_RS = "getMinimumAndMaximumDaysForArrivalRS";

    private static final String TIME_TO_ENQUIRE_HOLDER_TIMER_RS = "getTimeToEnquireHolderDelayTimerRS";

    public static final String END_OF_DAY_DATE_TIME_FORMAT_SUFFIX = "T23:59:59";

    public static final String GET_CCN_GW_LOOPBACK_ENABLED_RS = "getCcnGwLoopbackEnabledRS";

    private final ErmisLogger log;

    private final DossierDataService dossierDataService;

    private final TimerDelayExecutor timerDelayExecutor;

    private final PropertyService propertyService;

    private final SimplifiedDeclarationService simplifiedDeclarationService;

    private final ReferenceDataAdapter referenceDataAdapter;

    private final DeclarationService declarationService;

    private final ConfigurationDataPort configurationDataPort;

    private final ErmisConfigurationService ermisConfigurationService;

    private final ProcessingStatusService processingStatusService;

    @Override
    public boolean checkIfDeclarationIsTypeTIR(String movementId) {

        log.info("ENTER checkIfDeclarationIsTypeTIR()");
        boolean isTypeTIR = false;

        CC015CType cc015cMessage = declarationService.getDeclarationPayload(movementId);
        if (cc015cMessage.getTransitOperation() != null && cc015cMessage.getTransitOperation().getDeclarationType() != null) {
            List<CodeListItem> typeOfDeclarationTIRList =
                    referenceDataAdapter.getCodeListItems(CodelistKeyEnum.TYPE_OF_DECLARATION_TIR.getKey(), LocalDate.now(), null, null, "EN", CodeListDomain.NCTSP5);
            Set<String> typeOfDeclarationTIRListSet = typeOfDeclarationTIRList.stream().map(CodeListItem::getCode).collect(Collectors.toSet());

            isTypeTIR = typeOfDeclarationTIRListSet.stream().anyMatch(type -> type.equals(cc015cMessage.getTransitOperation().getDeclarationType()));
        }

        log.info("EXIT checkIfDeclarationIsTypeTIR()");
        return isTypeTIR;
    }

    @Override
    public boolean checkIfDeclarationIsElectronicallySubmitted(String movementId) {

        log.info("ENTER checkIfDeclarationIsElectronicallySubmitted()");
        log.info("EXIT checkIfDeclarationIsElectronicallySubmitted()");
        return declarationService.checkIfDeclarationIsElectronicallySubmitted(movementId);
    }

    @Override
    public boolean checkIfDeclaredOfficeOfExitForTransitExist(String movementId) {
        log.info("ENTER checkIfDeclaredOfficeOfExitExist()");
        CC015CType cc015cMessage = declarationService.getDeclarationPayload(movementId);
        log.info("EXIT checkIfDeclaredOfficeOfExitExist()");
        return cc015cMessage.getCustomsOfficeOfExitForTransitDeclared() == null || cc015cMessage.getCustomsOfficeOfExitForTransitDeclared().isEmpty();
    }

    @Override
    public boolean isCcnGwLoopbackEnabled() {
        String isCcnGwLoopbackEnabled = null;
        List<Configuration> configurationList = configurationDataPort.getConfigurationsByRuleTemplateName(GET_CCN_GW_LOOPBACK_ENABLED_RS);

        Optional<Configuration> configurationOptional =
                configurationList.stream().filter(configuration -> this.isApplicable(configuration, LocalDateTime.now(ZoneOffset.UTC))).findFirst();
        if (configurationOptional.isPresent()) {
            isCcnGwLoopbackEnabled = configurationOptional.get().getEvaluationParams().get("ccnGwLoopbackEnabled");
        }
        return Boolean.parseBoolean(isCcnGwLoopbackEnabled);
    }

    @Override
    public void updateState(String movementId, String processingStatus) {
        Movement movement = dossierDataService.getMovementById(movementId);

        if (ProcessingStatusTypeEnum.OODEP_MOVEMENT_RELEASED.getItemCode().equals(movement.getProcessingStatus())
                || ProcessingStatusTypeEnum.OODEP_ARRIVED.getItemCode().equals(movement.getProcessingStatus())) {
            processingStatusService.saveProcessingStatus(movementId, processingStatus);
        }
    }

    @Override
    public String getArrivalAdviceTimerDelayConfig(String movementId, String date, Boolean isPrelodged) throws Exception {
        String dueDate = date + END_OF_DAY_DATE_TIME_FORMAT_SUFFIX;

        CC015CType cc015CType = declarationService.getDeclarationPayload(movementId);

        // If message is simplified, arrivalAdviceTimer is set to limitDate of message:
        if (simplifiedDeclarationService.checkIfDeclarationIsSimplified(movementId, propertyService.getManualReleaseForTransitProperty())) {
            // If simplified Declaration is prelodged, return IE170 limitDate, if not found, return latest Declaration limitDate:
            // (IE013 limitDate if amended, else IE015 limitDate.)
            if (isPrelodged != null && isPrelodged) {
                CC170CType cc170cType = getCC170CMessage(movementId);
                if (cc170cType != null && ObjectUtils.isNotEmpty(cc170cType.getTransitOperation().getLimitDate())) {
                    dueDate = calculateExpectedArrivalTime(cc170cType.getTransitOperation().getLimitDate());
                } else {
                    dueDate = calculateExpectedArrivalTime(cc015CType.getTransitOperation().getLimitDate());
                }
            } else {
                if (cc015CType.getTransitOperation().getLimitDate() != null) {
                    dueDate = calculateExpectedArrivalTime(cc015CType.getTransitOperation().getLimitDate());
                }
            }
        }

        persistExpectedArrivalDate(movementId, dueDate);

        return dueDate;
    }

    /**
     * <pre>
     * @param arrivalAdviceDateInIso8601 : Initially was in 8601 Date format YYYY-MM-DD, then it was modified to 8601 DateAndTime Format with 'T23:59:59' in the time part
     *                                   The current implementation detects the Date or DateTime format by checking for the existence of the time designator "T" in the value
     * @return the expiration date of the Timer in ISO8601 Date and time format. e.g. 2024-01-30T23:59:59.
     *         Will add the suffix '23:59:59' to the calculated 8601 formatted Date
     *         If the "Delay" Rule Instance parameter value is a non-positive number the method will return NULL and the Timer will not be started by the Camunda Flow
     */
    @Override
    public String getInformGuarantorTimerDelayConfig(String arrivalAdviceDateInIso8601) {
        LocalDate arrivalAdviceDate = LocalDate.parse(arrivalAdviceDateInIso8601,
                arrivalAdviceDateInIso8601.contains("T") ? DateTimeFormatter.ISO_LOCAL_DATE_TIME : DateTimeFormatter.ISO_LOCAL_DATE);
        Map<String, String> params = ermisConfigurationService.getConfigurationByTemplateName(INFORM_GUARANTOR_TIMER_RS)
                                                              .getEvaluationParams();
        // If the [delay] param value is a non-positive integer then return NULL and the timer will not start
        if (Integer.parseInt(params.get(TimerInfoEnum.DELAY.getValue())) <= 0) {
            return null;
        } else {
            String timeUnit = params.get(TimerInfoEnum.TIMEUNIT.getValue());
            String delay = params.get(TimerInfoEnum.DELAY.getValue());
            Duration delayWithTimeUnit = convertToDelayWithTimeUnit(timeUnit, delay);
            LocalDateTime timerExpirationDateTime = arrivalAdviceDate.atStartOfDay().plus(delayWithTimeUnit);
            return timerExpirationDateTime.format(DateTimeFormatter.ISO_LOCAL_DATE) + END_OF_DAY_DATE_TIME_FORMAT_SUFFIX;
        }
    }

    @Override
    public TimerInfo getRecoveryRecommendedTimerDelayConfig(String movementId) {
        Movement movement = dossierDataService.getMovementById(movementId);

        return getTimerInfo(movement.getCreatedDateTime(), RECOVERY_RECOMMENDED_TIMER_RS, true);
    }

    @Override
    public TimerInfo getTimeToEnquireHolderTimerDelayConfig(String movementId) {
        return getTimerInfo(LocalDateTime.now(ZoneOffset.UTC), TIME_TO_ENQUIRE_HOLDER_TIMER_RS, true);
    }

    @Override
    public TimerInfo getReceiptControlResultsTimerDelayConfig(String date) {
        return getTimerInfo(LocalDateTime.parse(date), RECEIPT_CONTROL_RESULTS_TIMER_RS, true);
    }

    private TimerInfo getTimerInfo(LocalDateTime date, String ruleSet, boolean expiresAtEndOfDay) {
        TimerInfo timerInfo;

        Optional<TimerInfo> optionalTimerInfo =
                timerDelayExecutor.getTimerDelayConfig(date, LocalDateTime.now(), ruleSet, expiresAtEndOfDay);

        if (optionalTimerInfo.isPresent()) {
            timerInfo = optionalTimerInfo.get();
        } else {
            throw new IllegalStateException("Failed to setup timer!");
        }

        return timerInfo;
    }

    @Override
    public boolean isOfficeOfTransitDeclared(String movementId) {
        log.info("ENTER isOfficeOfTransitDeclared()");
        CC015CType cc015cMessage = declarationService.getDeclarationPayload(movementId);
        log.info("EXIT isOfficeOfTransitDeclared()");
        return cc015cMessage.getCustomsOfficeOfTransitDeclared() == null || cc015cMessage.getCustomsOfficeOfTransitDeclared().isEmpty();
    }

    public boolean isT1DeclarationMovementType(String movementId) {
        CC015CType cc015CType = declarationService.getDeclarationPayload(movementId);
        return cc015CType.getTransitOperation() != null && cc015CType.getTransitOperation().getDeclarationType() != null
                && DeclarationTypeEnum.T1.getValue().equals(cc015CType.getTransitOperation().getDeclarationType());
    }

    /* Should return TRUE, if DeclarationType is 'T' and:
     * ALL Consignment Items of one House Consignment are 'T1'
     * OR
     * A House Consignment consists of a mix of Consignment Items
     * [mix of ‘T1’ and (‘T2’ or ‘T2F’)]
     * ---
     * This can be reduced to the following logic:
     * A House Consignment exists with at least one 'T1' Consignment Item.
     * ---
     * ELSE FALSE
     */
    public boolean isMixedDeclarationTypeWithT1ConsignmentItem(String movementId) {
        CC015CType cc015cTypeMessage = declarationService.getDeclarationPayload(movementId);
        return cc015cTypeMessage.getTransitOperation() != null && DeclarationTypeEnum.T.getValue()
                                                                                       .equals(cc015cTypeMessage.getTransitOperation().getDeclarationType())
                && cc015cTypeMessage.getConsignment().getHouseConsignment().stream().anyMatch(houseConsignment ->
                houseConsignment.getConsignmentItem().stream().anyMatch(item -> DeclarationTypeEnum.T1.getValue().equals(item.getDeclarationType())));
    }

    @Override
    public boolean isRussiaCountryOfDestination(String movementId) {

        CC015CType cc015cMessage = declarationService.getDeclarationPayload(movementId);

        if (Objects.nonNull(cc015cMessage.getConsignment()) &&
                !StringUtils.isBlank(cc015cMessage.getConsignment().getCountryOfDestination()) &&
                CountryCodesEnum.RUSSIA.getCountryCode().equals(cc015cMessage.getConsignment().getCountryOfDestination())) {
            return true;
        } else if (!cc015cMessage.getConsignment().getHouseConsignment().isEmpty()) {
            return cc015cMessage.getConsignment()
                                .getHouseConsignment()
                                .stream()
                                .flatMap(houseConsignment -> houseConsignment.getConsignmentItem().stream())
                                .filter(Objects::nonNull)
                                .anyMatch(consignmentItem -> CountryCodesEnum.RUSSIA.getCountryCode().equals(consignmentItem.getCountryOfDestination()));
        }
        return false;
    }

    private CC170CType getCC170CMessage(String movementId) {

        CC170CType cc170cType = null;

        try {
            List<MessageOverview> ie170MessageOverviewList = dossierDataService.getMessagesOverview(movementId, MessageTypes.CC_170_C.value(), false);
            if (!ie170MessageOverviewList.isEmpty()) {
                Message cc170cTypeMessage = dossierDataService.getMessageById(ie170MessageOverviewList.get(0).getId());
                cc170cType = XmlConverter.buildMessageFromXML(cc170cTypeMessage.getPayload(), CC170CType.class);
            }
        } catch (JAXBException e) {
            throw new BaseException("Error building message from XML string ", e);
        } catch (Exception e) {
            throw new BaseException("Error getting message from DB ", e);
        }

        return cc170cType;
    }

    private boolean isApplicable(Configuration configuration, LocalDateTime referenceDate) {
        return ConfigurationUtil.isApplicableForEffectiveDate(configuration, referenceDate);
    }

    private String calculateExpectedArrivalTime(LocalDate payloadLimitDate) {

        Configuration configuration = ermisConfigurationService.getConfigurationByTemplateName(MINIMUM_AND_MAXIMUM_EXPECTED_ARRIVAL_TIME_RS);

        long minimumDays;
        long maximumDays;

        try {
            minimumDays = Long.parseLong(configuration.getEvaluationParams().get("minimumDays"));
            maximumDays = Long.parseLong(configuration.getEvaluationParams().get("maximumDays"));

            if (minimumDays <= 0 || maximumDays <= 0) {
                throw new NumberFormatException();
            }
        } catch (NumberFormatException e) {
            throw new BaseException("Wrong configuration parameters");
        }

        if (payloadLimitDate.isBefore(LocalDate.now(ZoneOffset.UTC).plusDays(minimumDays)) ||
                payloadLimitDate.isAfter(LocalDate.now(ZoneOffset.UTC).plusDays(maximumDays))) {
            return LocalDateTime.now(ZoneOffset.UTC).plusDays(minimumDays).withHour(23).withMinute(59).withSecond(59).format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
        }
        LocalDateTime payloadLimitDateTime = payloadLimitDate.atStartOfDay();
        return payloadLimitDateTime.withHour(23).withMinute(59).withSecond(59).format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
    }

    /**
     * Persist expected arrival date as movement attribute for future references (e.g. diversion messages) Note: can be utilized be release related messages (IE
     * 029/001/050/160) which currently utilize bpmn variable
     *
     * @param movementId id of the movement
     * @param expectedArrivalDateTime the expected arrival due date at date time format
     */
    private void persistExpectedArrivalDate(String movementId, String expectedArrivalDateTime) {
        log.info("persistExpectedArrivalDate for movementId={} and expectedArrivalDateTime={}", movementId, expectedArrivalDateTime);
        MovementAttribute movementAttribute = MovementAttribute.builder()
                                                   .movementId(movementId)
                                                   .attrKey(MovementAttributesEnum.EXPECTED_ARRIVAL_DATE.getValue())
                                                   .attrValue(expectedArrivalDateTime.split("T")[0])
                                                   .build();
        dossierDataService.saveMovementAttribute(movementAttribute);
    }
}

package com.intrasoft.ermis.trs.officeofdeparture.service;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import com.intrasoft.ermis.common.logging.ErmisLogger;
import com.intrasoft.ermis.common.xml.parsing.XmlConverter;
import com.intrasoft.ermis.ddntav5152.messages.CC015CType;
import com.intrasoft.ermis.ddntav5152.messages.CC170CType;
import com.intrasoft.ermis.ddntav5152.messages.ConsignmentItemType09;
import com.intrasoft.ermis.ddntav5152.messages.ConsignmentType20;
import com.intrasoft.ermis.ddntav5152.messages.HouseConsignmentType10;
import com.intrasoft.ermis.ddntav5152.messages.MessageTypes;
import com.intrasoft.ermis.ddntav5152.messages.TransitOperationType06;
import com.intrasoft.ermis.platform.shared.client.configuration.core.domain.Configuration;
import com.intrasoft.ermis.transit.common.config.services.ErmisConfigurationService;
import com.intrasoft.ermis.transit.common.exceptions.BaseException;
import com.intrasoft.ermis.transit.contracts.core.enums.DeclarationTypeEnum;
import com.intrasoft.ermis.transit.officeofdeparture.domain.Message;
import com.intrasoft.ermis.transit.officeofdeparture.service.DeclarationService;
import com.intrasoft.ermis.transit.officeofdeparture.service.ReleaseTransitServiceImpl;
import com.intrasoft.ermis.transit.officeofdeparture.service.SimplifiedDeclarationService;
import com.intrasoft.ermis.transit.officeofdeparture.service.property.PropertyService;
import com.intrasoft.ermis.trs.officeofdeparture.util.GeneratorHelper;
import java.time.LocalDate;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Named;
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.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
public class ReleaseTransitServiceUT extends GeneratorHelper {

    @Mock
    private ErmisLogger log;

    private static final String MINIMUM_AND_MAXIMUM_EXPECTED_ARRIVAL_TIME_RS = "getMinimumAndMaximumDaysForArrivalRS";

    private static final String MINIMUM_DAYS = "1";

    private static final String MAXIMUM_DAYS = "30";

    private static final String ERROR_MESSAGE = "Wrong configuration parameters";

    private static final String EXISTING_DUE_DATE = "2020-02-02";

    @Mock
    private ErmisConfigurationService ermisConfigurationService;

    @Mock
    private PropertyService propertyService;

    @Mock
    private SimplifiedDeclarationService simplifiedDeclarationService;

    @Mock
    private DeclarationService declarationService;

    @InjectMocks
    private ReleaseTransitServiceImpl releaseTransitService;

    @ParameterizedTest
    @MethodSource("namedArguments")
    @DisplayName("Test that limit date is not between the allowed bounds for normal simplified")
    void test_getExpectedArrivalTimerIfLimitDateIsWrongForNormalSimplifiedDeclaration(LocalDate limitDate) {

        try {
            CC015CType cc015CType = XmlConverter.buildMessageFromXML(loadTextFile("CC015C_valid"), CC015CType.class);

            cc015CType.getTransitOperation().setLimitDate(limitDate);

            when(declarationService.getDeclarationPayload(MOVEMENT_ID)).thenReturn(cc015CType);
            when(simplifiedDeclarationService.checkIfDeclarationIsSimplified(anyString(), anyBoolean())).thenReturn(true);
            when(ermisConfigurationService.getConfigurationByTemplateName(MINIMUM_AND_MAXIMUM_EXPECTED_ARRIVAL_TIME_RS))
                   .thenReturn(createConfiguration(MINIMUM_DAYS, MAXIMUM_DAYS));

            String dueDate = releaseTransitService.getArrivalAdviceTimerDelayConfig(MOVEMENT_ID, EXISTING_DUE_DATE, false);

            assertEquals(LocalDate.now(ZoneOffset.UTC).plusDays(Long.parseLong(MINIMUM_DAYS)), LocalDate.parse(dueDate, DateTimeFormatter.ISO_LOCAL_DATE_TIME));
            assertNotEquals(cc015CType.getTransitOperation().getLimitDate(), LocalDate.parse(dueDate, DateTimeFormatter.ISO_LOCAL_DATE_TIME));
        } catch (Exception e) {
            fail();
        }
    }

    @Test
    @DisplayName("Test that limit date is between the allowed bounds for presented prelodged simplified")
    void test_getExpectedArrivalTimerIfLimitDateIsCorrectForPrelodgedSimplifiedWithGPR() {

        try {
            CC015CType cc015CType = XmlConverter.buildMessageFromXML(loadTextFile("CC015C_valid"), CC015CType.class);
            CC170CType cc170CType = XmlConverter.buildMessageFromXML(loadTextFile("CC170C"), CC170CType.class);

            cc015CType.getTransitOperation().setLimitDate(LocalDate.now(ZoneOffset.UTC).plusDays(15));
            cc170CType.getTransitOperation().setLimitDate(LocalDate.now(ZoneOffset.UTC).plusDays(20));

            Message cc170cMessage = createCC170CMessage(cc170CType);

            when(declarationService.getDeclarationPayload(MOVEMENT_ID)).thenReturn(cc015CType);
            when(dossierDataService.getMessagesOverview(MOVEMENT_ID, MessageTypes.CC_170_C.value(), false)).thenReturn(List.of(cc170cMessage));
            when(dossierDataService.getMessageById(anyString())).thenReturn(cc170cMessage);
            when(simplifiedDeclarationService.checkIfDeclarationIsSimplified(anyString(), anyBoolean())).thenReturn(true);
            when(ermisConfigurationService.getConfigurationByTemplateName(MINIMUM_AND_MAXIMUM_EXPECTED_ARRIVAL_TIME_RS))
                   .thenReturn(createConfiguration(MINIMUM_DAYS, MAXIMUM_DAYS));

            String dueDate = releaseTransitService.getArrivalAdviceTimerDelayConfig(MOVEMENT_ID, EXISTING_DUE_DATE, true);

            assertEquals(cc170CType.getTransitOperation().getLimitDate(), LocalDate.parse(dueDate, DateTimeFormatter.ISO_LOCAL_DATE_TIME));
            assertNotEquals(cc015CType.getTransitOperation().getLimitDate(), LocalDate.parse(dueDate, DateTimeFormatter.ISO_LOCAL_DATE_TIME));
        }  catch (Exception e) {
            fail();
        }
    }

    @Test
    @DisplayName("Test that limit date is between the allowed bounds for non presented prelodged simplified")
    void test_getExpectedArrivalTimerIfLimitDateIsCorrectForPrelodgedSimplifiedWithoutGPR() {

        try {
            CC015CType cc015CType = XmlConverter.buildMessageFromXML(loadTextFile("CC015C_valid"), CC015CType.class);

            cc015CType.getTransitOperation().setLimitDate(LocalDate.now(ZoneOffset.UTC).plusDays(15));

            when(declarationService.getDeclarationPayload(MOVEMENT_ID)).thenReturn(cc015CType);
            when(dossierDataService.getMessagesOverview(MOVEMENT_ID, MessageTypes.CC_170_C.value(), false)).thenReturn(List.of());
            when(simplifiedDeclarationService.checkIfDeclarationIsSimplified(anyString(), anyBoolean())).thenReturn(true);
            when(ermisConfigurationService.getConfigurationByTemplateName(MINIMUM_AND_MAXIMUM_EXPECTED_ARRIVAL_TIME_RS))
                   .thenReturn(createConfiguration(MINIMUM_DAYS, MAXIMUM_DAYS));

            String dueDate = releaseTransitService.getArrivalAdviceTimerDelayConfig(MOVEMENT_ID, EXISTING_DUE_DATE, true);

            assertEquals(cc015CType.getTransitOperation().getLimitDate(), LocalDate.parse(dueDate, DateTimeFormatter.ISO_LOCAL_DATE_TIME));
        } catch (Exception e) {
            fail();
        }
    }

    @Test
    @DisplayName("Test that limit date is between the allowed bounds for normal simplified")
    void test_getExpectedArrivalTimerIfLimitDateIsCorrectForNormalSimplifiedDeclaration() {
        try {
            CC015CType cc015CType = XmlConverter.buildMessageFromXML(loadTextFile("CC015C_valid"), CC015CType.class);

            cc015CType.getTransitOperation().setLimitDate(LocalDate.now(ZoneOffset.UTC).plusDays(15));

            when(declarationService.getDeclarationPayload(MOVEMENT_ID)).thenReturn(cc015CType);
            when(simplifiedDeclarationService.checkIfDeclarationIsSimplified(anyString(), anyBoolean())).thenReturn(true);
            when(ermisConfigurationService.getConfigurationByTemplateName(MINIMUM_AND_MAXIMUM_EXPECTED_ARRIVAL_TIME_RS))
                   .thenReturn(createConfiguration(MINIMUM_DAYS, MAXIMUM_DAYS));

            String dueDate = releaseTransitService.getArrivalAdviceTimerDelayConfig(MOVEMENT_ID, EXISTING_DUE_DATE, false);

            assertEquals(cc015CType.getTransitOperation().getLimitDate(), LocalDate.parse(dueDate, DateTimeFormatter.ISO_LOCAL_DATE_TIME));
        } catch (Exception e) {
            fail();
        }
    }

    @ParameterizedTest
    @MethodSource("namedArguments2")
    @DisplayName("Test that configuration values are incorrect")
    void test_getExpectedArrivalTimerWithWrongConfigurationValues(int configurationParameter, String value) {

        String configParam1 = MINIMUM_DAYS;
        String configParam2 = MAXIMUM_DAYS;

        if(configurationParameter == 1) {
            configParam1 = value;
        } else {
            configParam2 = value;
        }

        try {
            CC015CType cc015CType = XmlConverter.buildMessageFromXML(loadTextFile("CC015C_valid"), CC015CType.class);

            cc015CType.getTransitOperation().setLimitDate(LocalDate.now(ZoneOffset.UTC).plusDays(15));

            when(declarationService.getDeclarationPayload(MOVEMENT_ID)).thenReturn(cc015CType);
            when(simplifiedDeclarationService.checkIfDeclarationIsSimplified(anyString(), anyBoolean())).thenReturn(true);
            when(ermisConfigurationService.getConfigurationByTemplateName(MINIMUM_AND_MAXIMUM_EXPECTED_ARRIVAL_TIME_RS))
                   .thenReturn(createConfiguration(configParam1, configParam2));

            BaseException thrownException =
                    assertThrows(BaseException.class, () -> releaseTransitService.getArrivalAdviceTimerDelayConfig(MOVEMENT_ID, EXISTING_DUE_DATE, false));

            assertEquals(ERROR_MESSAGE, thrownException.getMessage());
        } catch (Exception e) {
            fail();
        }
    }

    @Test
    @DisplayName("Test that the existing due date is used for non simplified")
    void test_getExpectedArrivalTimerForNoSimplified() {
        try {
            CC015CType cc015CType = XmlConverter.buildMessageFromXML(loadTextFile("CC015C_valid"), CC015CType.class);

            when(declarationService.getDeclarationPayload(MOVEMENT_ID)).thenReturn(cc015CType);
            when(simplifiedDeclarationService.checkIfDeclarationIsSimplified(anyString(), anyBoolean())).thenReturn(false);

            String dueDate = releaseTransitService.getArrivalAdviceTimerDelayConfig(MOVEMENT_ID, EXISTING_DUE_DATE, false);

            assertEquals(EXISTING_DUE_DATE+"T23:59:59", dueDate);

            verify(ermisConfigurationService, times(0)).getConfigurationByTemplateName(MINIMUM_AND_MAXIMUM_EXPECTED_ARRIVAL_TIME_RS);
        } catch (Exception e) {
            fail();
        }
    }

    @ParameterizedTest
    @DisplayName("Test - Is Mixed Declaration Type With T1 Consignment Item")
    @ValueSource(booleans = {true, false})
    void testIsMixedDeclarationTypeWithT1ConsignmentItem(boolean isExternalTransit) {
        // Mocks/Stubs:
        when(declarationService.getDeclarationPayload(MOVEMENT_ID)).thenReturn(buildIE015(DeclarationTypeEnum.T.getValue(), isExternalTransit));
        // Call service method:
        boolean returnedValue = releaseTransitService.isMixedDeclarationTypeWithT1ConsignmentItem(MOVEMENT_ID);
        // Verification & Assertion:
        verify(declarationService, times(1)).getDeclarationPayload(MOVEMENT_ID);
        assertEquals(isExternalTransit, returnedValue);
    }

    static Stream<Arguments> namedArguments() {
        return Stream.of(
                Arguments.of(Named.of(" after maximum days", LocalDate.now(ZoneOffset.UTC).plusDays(50))),
                Arguments.of(Named.of(" before minimum days", LocalDate.now(ZoneOffset.UTC).minusDays(10)))
        );
    }

    static Stream<Arguments> namedArguments2() {
        return Stream.of(
                Arguments.of(Named.of(" minimumDays", 1), Named.of(" alphanumeric", "abc123")),
                Arguments.of(Named.of(" minimumDays", 1), Named.of(" zero", "0")),
                Arguments.of(Named.of(" minimumDays", 1), Named.of(" negative", "-19")),
                Arguments.of(Named.of(" maximumDays", 2), Named.of(" alphanumeric", "abc123")),
                Arguments.of(Named.of(" maximumDays", 2), Named.of(" zero", "0")),
                Arguments.of(Named.of(" maximumDays", 2), Named.of(" negative", "-19"))
                );
    }

    private Configuration createConfiguration(String minDays, String maxDays) {
        Map<String, String> evalParams = Map.of("minimumDays", minDays, "maximumDays", maxDays);

        return new Configuration(
                null,
                null,
                null,
                evalParams,
                null,
                null,
                null,
                null);
    }

    private Message createCC170CMessage(CC170CType cc170CType) {
        return Message.builder()
                      .id(MESSAGE_ID)
                      .messageType(MessageTypes.CC_170_C.value())
                      .payload(XmlConverter.marshal(cc170CType))
                      .build();
    }

    private CC015CType buildIE015(String declarationType, boolean isExternalTransit) {

        CC015CType ie015 = new CC015CType();

        ie015.setTransitOperation(new TransitOperationType06());
        ie015.getTransitOperation().setDeclarationType(declarationType);

        ie015.setConsignment(new ConsignmentType20());
        ie015.getConsignment().getHouseConsignment().add(buildIE015HouseConsignment(false));
        ie015.getConsignment().getHouseConsignment().add(buildIE015HouseConsignment(isExternalTransit));

        return ie015;
    }

    private HouseConsignmentType10 buildIE015HouseConsignment(boolean isExternalTransit) {

        HouseConsignmentType10 houseConsignment = new HouseConsignmentType10();
        houseConsignment.getConsignmentItem().add(buildIE015ConsignmentItem(false));
        houseConsignment.getConsignmentItem().add(buildIE015ConsignmentItem(isExternalTransit));

        return houseConsignment;
    }

    private ConsignmentItemType09 buildIE015ConsignmentItem(boolean isExternalTransit) {

        ConsignmentItemType09 consignmentItem = new ConsignmentItemType09();
        if (isExternalTransit) {
            consignmentItem.setDeclarationType(DeclarationTypeEnum.T1.getValue());
        } else {
            consignmentItem.setDeclarationType(DeclarationTypeEnum.T2.getValue());
        }
        return consignmentItem;
    }
}
