package com.intrasoft.ermis.transit.control.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.riskassessment.v2.RiskAssessmentResponseEvent;
import com.intrasoft.ermis.ddntav5152.messages.MessageTypes;
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.config.timer.TimerDelayExecutor;
import com.intrasoft.ermis.transit.common.enums.OfficeRoleTypesEnum;
import com.intrasoft.ermis.transit.common.exceptions.BaseException;
import com.intrasoft.ermis.transit.common.exceptions.NotFoundException;
import com.intrasoft.ermis.transit.contracts.core.domain.TimerInfo;
import com.intrasoft.ermis.transit.contracts.core.enums.RiskAnalysisCodeEnum;
import com.intrasoft.ermis.transit.control.domain.DomainBase;
import com.intrasoft.ermis.transit.control.domain.ExternalSystemResponseProperty;
import com.intrasoft.ermis.transit.control.domain.Message;
import com.intrasoft.ermis.transit.control.domain.MessageOverview;
import com.intrasoft.ermis.transit.control.domain.Movement;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;


@Service
@AllArgsConstructor(onConstructor_ = {@Autowired})
public class ControlServiceImpl implements ControlService {

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

    private static final String CTL_AUTOMATIC_RELEASE_DELAY_TIMER_RS = "getControlAutomaticReleaseDelayTimerRS";

    private static final String RISK_FACTOR_CONFIG_RS = "getRiskFactorConfigRS";

    private static final String CONTROL_DECISION_NOTIFICATION_ENABLED_RS = "getControlDecisionNotificationEnabledRS";

    private static final String SEND_CONTROL_DECISION_NOTIFICATION_AUTOMATICALLY_ENABLED_RS = "getSendControlDecisionNotificationAutomaticallyEnabledRS";

    private static final String DEPEND_ON_RISK_RESPONSE_FOR_NOTIFYING_DECLARANT_ENABLED_RS = "getDependOnRiskResponseForNotifyingDeclarantEnabledRS";

    private static final String VALUE_LITERAL = "value";

    public static final String GET_MANUAL_NO_CONTROL_BUTTONS_ENABLED_CONFIG_RS = "getManualNoControlButtonsEnabledConfigRS";

    private TimerDelayExecutor timerDelayExecutor;

    private CustomControlTimerService customControlTimerService;

    private final ErmisLogger log;

    private final DossierDataService dossierDataService;

    private final ErmisConfigurationService ermisConfigurationService;

    @Override
    public TimerInfo getAutomaticReleaseTimerDelayConfig(String movementId) throws Exception {

        Movement movement = dossierDataService.getMovementById(movementId);
        TimerInfo timerInfo = null;

        if (movement != null) {

            String cc015cId = getCC015CIdFromMovement(movement);

            List<ExternalSystemResponseProperty> externalSystemResponsePropertyList =
                    new ArrayList<>(dossierDataService.getExternalSystemResponsePropertyListByMessageId(cc015cId));

            if (!externalSystemResponsePropertyList.isEmpty()) {
                // Check if there was a Custom Control Timer provided by Authorization Validation:
                timerInfo = customControlTimerService.getCustomTimerDelay(LocalDateTime.now(ZoneOffset.UTC),
                        externalSystemResponsePropertyList, false).orElseThrow(() -> new BaseException("Failed to setup timer!"));

            } else {
                // If not, retrieve the Control Timer via Configuration:
                timerInfo = timerDelayExecutor.getTimerDelayConfig(LocalDateTime.now(ZoneOffset.UTC), LocalDateTime.now(),
                        CTL_AUTOMATIC_RELEASE_DELAY_TIMER_RS, false).orElseThrow(() -> new BaseException("Failed to setup timer!"));

            }
        }
        return timerInfo;
    }

    @Override
    public boolean setNoControl(String movementId) {
        log.info("Set No Control for message with ID: {}", movementId);
        // Return "No Control".
        return false;
    }

    @Override
    public int getRiskFactor() {
        // Default riskFactor value if configuration is not found:
        int riskFactor = 60;

        try {
            Configuration configuration = ermisConfigurationService.getConfigurationByTemplateName(RISK_FACTOR_CONFIG_RS);
            riskFactor = Integer.parseInt(configuration.getEvaluationParams().get("configuredRiskFactor"));
        } catch (NotFoundException e) {
            log.warn(e.getMessage().concat(". Defaulting to 60."));
        }

        return riskFactor;
    }

    @Override
    public boolean isControlDecisionNotificationEnabled() {
        return isControlDecisionNotificationSwitchEnabled();
    }

    @Override
    public boolean shouldSendControlDecisionNotificationAutomatically() {
        return isSendControlDecisionNotificationAutomaticallySwitchEnabled();
    }

    @Override
    public boolean shouldSendNotificationToDeclarant(String movementId) {

        if (isDependOnRiskForNotifyingDeclarantSwitchEnabled()) {

            return shouldNotifyDeclarant(movementId);
        } else {
            // If switch is off, then IE060 should always be sent.
            return true;
        }
    }

    @Override
    public boolean mustCreateManualNoControlDecisionTask(String riskAnalysisCode) {
        Configuration configuration = ermisConfigurationService.getConfigurationByTemplateNameAndApplicableParameter(GET_MANUAL_NO_CONTROL_BUTTONS_ENABLED_CONFIG_RS,
                OfficeRoleTypesEnum.DEPARTURE.getValue(),
                "office");
        return Boolean.parseBoolean(configuration.getEvaluationParams().get(VALUE_LITERAL)) && RiskAnalysisCodeEnum.Z.getCode().equals(riskAnalysisCode);
    }

    private String getCC015CIdFromMovement(Movement movement) {

        String id = "";

        if (movement != null) {

            List<Message> messages = dossierDataService.getMessagesByCriteria(movement.getId());

            if (!messages.isEmpty()) {
                Optional<Message> retrievedMessage = messages.stream()
                                                             .filter(message -> MessageTypes.CC_015_C.value().equals(message.getMessageType()))
                                                             .findFirst();
                if (retrievedMessage.isPresent()) {
                    return retrievedMessage.get().getId();
                } else {
                    throw new BaseException("No message CC015C was found!");
                }
            } else {
                throw new BaseException("No messages found associated with movement:" + movement.getId());

            }
        }
        return id;
    }

    /**
     * Method holding switch logic to enable/disable Control Decision Notification (IE060).
     *
     * @return TRUE/FALSE based on found configuration template.
     */
    private boolean isControlDecisionNotificationSwitchEnabled() {

        Configuration configuration = ermisConfigurationService.getConfigurationByTemplateName(CONTROL_DECISION_NOTIFICATION_ENABLED_RS);
        return Boolean.parseBoolean(configuration.getEvaluationParams().get(VALUE_LITERAL));
    }

    /**
     * Method holding switch logic to enable/disable sending Control Decision Notification automatically (IE060).
     *
     * @return TRUE/FALSE based on found configuration template.
     */
    private boolean isSendControlDecisionNotificationAutomaticallySwitchEnabled() {

        Configuration configuration = ermisConfigurationService.getConfigurationByTemplateName(SEND_CONTROL_DECISION_NOTIFICATION_AUTOMATICALLY_ENABLED_RS);
        return Boolean.parseBoolean(configuration.getEvaluationParams().get(VALUE_LITERAL));
    }

    /**
     * Method holding switch logic to enable/disable notification suppression.
     *
     * @return TRUE/FALSE based on found configuration template.
     */
    private boolean isDependOnRiskForNotifyingDeclarantSwitchEnabled() {

        Configuration configuration = ermisConfigurationService.getConfigurationByTemplateName(DEPEND_ON_RISK_RESPONSE_FOR_NOTIFYING_DECLARANT_ENABLED_RS);
        return Boolean.parseBoolean(configuration.getEvaluationParams().get(VALUE_LITERAL));
    }

    /**
     * Method holding decision logic to notify Declarant (IE060)
     * <br/> based on latest response data from Risk Assessment.
     *
     * @param movementId ID of movement to retrieve risk assessment response of.
     * @return TRUE/FALSE based on data found within risk assessment response.
     */
    private boolean shouldNotifyDeclarant(String movementId) {

        List<MessageOverview> riskAssessmentResponseMessageOverviewList =
                dossierDataService.getMessagesOverview(movementId, RiskAssessmentResponseEvent.class.getSimpleName());

        Comparator<MessageOverview> messageOverviewComparator = Comparator.comparing(DomainBase::getCreatedDateTime);

        if (!riskAssessmentResponseMessageOverviewList.isEmpty()) {
            // Sort and reverse riskAssessmentResponseMessageOverviewList so the latest is on index[0]:
            riskAssessmentResponseMessageOverviewList.sort(messageOverviewComparator);
            Collections.reverse(riskAssessmentResponseMessageOverviewList);

            try {

                Message riskAssessmentResponseMessage = dossierDataService.getMessageById(riskAssessmentResponseMessageOverviewList.get(0).getId());

                RiskAssessmentResponseEvent riskAssessmentResponseEvent =
                        JSON_CONVERTER.convertStringToObject(riskAssessmentResponseMessage.getPayload(), RiskAssessmentResponseEvent.class);
                // If at least one (1) control instruction has involvesNotifyingDeclarant == FALSE, then suppress notification:
                return riskAssessmentResponseEvent.getRiskAssessmentResults()
                                                  .stream()
                                                  .takeWhile(riskAssessmentResult ->
                                                          riskAssessmentResult.getControlInstructions()
                                                                              .stream()
                                                                              .anyMatch(controlInstruction ->
                                                                                      Boolean.FALSE.equals(controlInstruction.isInvolvesNotifyingDeclarant())))
                                                  .findAny()
                                                  .isEmpty();
            } catch (Exception e) {
                // Default TRUE if an Exception was thrown for provided movement ID (non-interrupting exception handling):
                log.warn("Failed to retrieve Risk Assessment Response data for movementId: " + movementId + ". Defaulting to TRUE.");
                return true;
            }
        } else {
            // Default TRUE if no Risk Assessment Response Event Messages were found for provided movement ID:
            log.warn("shouldNotifyDeclarant(): No Risk Assessment Response Event Messages found. Defaulting to TRUE.");
            return true;
        }
    }
}

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

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

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.riskassessment.ControlInstruction;
import com.intrasoft.ermis.contracts.core.dto.declaration.riskassessment.v2.RiskAssessmentResponseEvent;
import com.intrasoft.ermis.contracts.core.dto.declaration.riskassessment.v2.RiskAssessmentResult;
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.config.timer.TimerDelayExecutor;
import com.intrasoft.ermis.transit.control.domain.Message;
import com.intrasoft.ermis.transit.control.domain.MessageOverview;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.junit.jupiter.api.DisplayName;
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.ValueSource;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;


@ExtendWith(MockitoExtension.class)
class ControlServiceUT {

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

    private static final String CONTROL_DECISION_NOTIFICATION_ENABLED_RS = "getControlDecisionNotificationEnabledRS";

    private static final String SEND_CONTROL_DECISION_NOTIFICATION_AUTOMATICALLY_ENABLED_RS = "getSendControlDecisionNotificationAutomaticallyEnabledRS";

    private static final String DEPEND_ON_RISK_RESPONSE_FOR_NOTIFYING_DECLARANT_ENABLED_RS = "getDependOnRiskResponseForNotifyingDeclarantEnabledRS";

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

    private static final String LATEST_MESSAGE_ID = "MESSAGE_ID_0";

    private static final int NUMBER_OF_MESSAGE_OVERVIEWS = 3;

    private static final int NUMBER_OF_RISK_ASSESSMENT_RESULTS = 2;

    private static final int NUMBER_OF_CONTROL_INSTRUCTIONS = 4;

    @Mock
    private ErmisLogger ermisLogger;

    @Mock
    private TimerDelayExecutor timerDelayExecutor;

    @Mock
    private CustomControlTimerService customControlTimerService;

    @Mock
    private DossierDataService dossierDataService;

    @Mock
    private ErmisConfigurationService ermisConfigurationService;

    @InjectMocks
    private ControlServiceImpl controlService;

    @ParameterizedTest
    @DisplayName("Test - Is Control Decision Notification Enabled")
    @ValueSource(strings = {"true", "false", "invalidParameter"})
    void testIsControlDecisionNotificationEnabled(String valueParameter) {
        // Stubs:
        when(ermisConfigurationService.getConfigurationByTemplateName(CONTROL_DECISION_NOTIFICATION_ENABLED_RS))
                .thenReturn(buildConfiguration(CONTROL_DECISION_NOTIFICATION_ENABLED_RS, valueParameter));
        // Call tested service method:
        boolean returnedValue = controlService.isControlDecisionNotificationEnabled();
        // Verifications & Assertions:
        verify(ermisConfigurationService, times(1)).getConfigurationByTemplateName(CONTROL_DECISION_NOTIFICATION_ENABLED_RS);
        if ("invalidParameter".equals(valueParameter)) {
            assertFalse(returnedValue);
        } else {
            assertEquals(Boolean.parseBoolean(valueParameter), returnedValue);
        }
    }

    @ParameterizedTest
    @DisplayName("Test - Should Send Control Decision Notification Automatically")
    @ValueSource(strings = {"true", "false", "invalidParameter"})
    void testShouldSendControlDecisionNotificationAutomatically(String valueParameter) {
        // Stubs:
        when(ermisConfigurationService.getConfigurationByTemplateName(SEND_CONTROL_DECISION_NOTIFICATION_AUTOMATICALLY_ENABLED_RS))
                .thenReturn(buildConfiguration(SEND_CONTROL_DECISION_NOTIFICATION_AUTOMATICALLY_ENABLED_RS, valueParameter));
        // Call tested service method:
        boolean returnedValue = controlService.shouldSendControlDecisionNotificationAutomatically();
        // Verifications & Assertions:
        verify(ermisConfigurationService, times(1))
                .getConfigurationByTemplateName(SEND_CONTROL_DECISION_NOTIFICATION_AUTOMATICALLY_ENABLED_RS);
        if ("invalidParameter".equals(valueParameter)) {
            assertFalse(returnedValue);
        } else {
            assertEquals(Boolean.parseBoolean(valueParameter), returnedValue);
        }
    }

    @ParameterizedTest
    @DisplayName("Test - Should Send Notification To Declarant")
    @CsvSource({"true,true", "true,false", "false,true", "false,false",})
    void testShouldSendNotificationToDeclarant(boolean switchEnabled, boolean suppressNotification) {
        // Stubs:
        when(ermisConfigurationService.getConfigurationByTemplateName(DEPEND_ON_RISK_RESPONSE_FOR_NOTIFYING_DECLARANT_ENABLED_RS))
                .thenReturn(buildConfiguration(DEPEND_ON_RISK_RESPONSE_FOR_NOTIFYING_DECLARANT_ENABLED_RS, String.valueOf(switchEnabled)));
        if (switchEnabled) {
            when(dossierDataService.getMessagesOverview(MOVEMENT_ID, RiskAssessmentResponseEvent.class.getSimpleName()))
                    .thenReturn(buildMessageOverviewList());
            when(dossierDataService.getMessageById(LATEST_MESSAGE_ID))
                    .thenReturn(buildMessage(suppressNotification));
        }
        // Call tested service method:
        boolean returnedValue = controlService.shouldSendNotificationToDeclarant(MOVEMENT_ID);
        // Verifications & Assertions:
        verify(ermisConfigurationService, times(1)).getConfigurationByTemplateName(DEPEND_ON_RISK_RESPONSE_FOR_NOTIFYING_DECLARANT_ENABLED_RS);
        if (switchEnabled) {
            // If switch is enabled, further REST calls should be made:
            verify(dossierDataService, times(1)).getMessagesOverview(MOVEMENT_ID, RiskAssessmentResponseEvent.class.getSimpleName());
            verify(dossierDataService, times(1)).getMessageById(LATEST_MESSAGE_ID);
            // If at least one involvesNotifyingDeclarant within control instructions is found FALSE, suppress notification:
            if (suppressNotification) {
                assertFalse(returnedValue);
            } else {
                assertTrue(returnedValue);
            }
        } else {
            // If switch is disabled, no further REST calls should be made and should notify declarant:
            verify(dossierDataService, times(0)).getMessagesOverview(MOVEMENT_ID, RiskAssessmentResponseEvent.class.getSimpleName());
            verify(dossierDataService, times(0)).getMessageById(LATEST_MESSAGE_ID);
            assertTrue(returnedValue);
        }
    }

    private Configuration buildConfiguration(String ruleTemplate, String value) {

        return new Configuration(
                ruleTemplate,
                "ruleCode",
                new HashMap<>(),
                Map.of("value", value),
                "errorCode",
                LocalDateTime.now(ZoneOffset.UTC),
                LocalDateTime.now(ZoneOffset.UTC).minusDays(60),
                LocalDateTime.now(ZoneOffset.UTC).plusDays(60));
    }

    private List<MessageOverview> buildMessageOverviewList() {

        List<MessageOverview> messageOverviewList = new ArrayList<>();

        for (int i = 0; i < NUMBER_OF_MESSAGE_OVERVIEWS; i++) {

            MessageOverview messageOverview = MessageOverview.builder()
                                                             .id("MESSAGE_ID_" + i)
                                                             .messageType(RiskAssessmentResponseEvent.class.getSimpleName())
                                                             .createdDateTime(LocalDateTime.now(ZoneOffset.UTC).minusDays(i))
                                                             .build();
            messageOverviewList.add(messageOverview);
        }

        return messageOverviewList;
    }

    private Message buildMessage(boolean suppressNotification) {

        Message message = new Message();
        RiskAssessmentResponseEvent riskAssessmentResponseEvent = new RiskAssessmentResponseEvent();
        riskAssessmentResponseEvent.setRiskAssessmentResults(new ArrayList<>());
        riskAssessmentResponseEvent.getRiskAssessmentResults().addAll(buildRiskAssessmentResultList(suppressNotification));

        message.setMessageIdentification(LATEST_MESSAGE_ID);
        message.setMessageType(RiskAssessmentResponseEvent.class.getSimpleName());
        message.setPayload(JSON_CONVERTER.convertToJsonString(riskAssessmentResponseEvent));

        return message;
    }

    private List<RiskAssessmentResult> buildRiskAssessmentResultList(boolean suppressNotification) {

        List<RiskAssessmentResult> riskAssessmentResultList = new ArrayList<>();

        for (int i = 0; i < NUMBER_OF_RISK_ASSESSMENT_RESULTS; i++) {

            RiskAssessmentResult riskAssessmentResult = new RiskAssessmentResult();

            for (int j = 0; j < NUMBER_OF_CONTROL_INSTRUCTIONS; j++) {

                ControlInstruction controlInstruction = new ControlInstruction();
                controlInstruction.setInvolvesNotifyingDeclarant(true);

                riskAssessmentResult.getControlInstructions().add(controlInstruction);
            }

            riskAssessmentResultList.add(riskAssessmentResult);
        }

        if (suppressNotification) {
            riskAssessmentResultList.get(0).getControlInstructions().get(0).setInvolvesNotifyingDeclarant(false);
        }

        return riskAssessmentResultList;
    }
}
