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

import com.intrasoft.ermis.common.logging.ErmisLogger;
import com.intrasoft.ermis.transit.common.enums.ValidationRequestPropertyKeyEnum;
import com.intrasoft.ermis.transit.common.exceptions.RetryableException;
import com.intrasoft.ermis.transit.validation.configuration.ValidationRetryConfigurationProperties;
import com.intrasoft.ermis.transit.validation.enums.BpmnFlowVariablesEnum;
import com.intrasoft.ermis.transit.validation.enums.ValidationProcessInstanceEnum;
import com.intrasoft.ermis.transit.validation.events.ValidationRequestedDomainEvent;
import com.intrasoft.ermis.transit.validation.process.MainValidationFlowFacade;
import lombok.RequiredArgsConstructor;
import org.camunda.bpm.engine.RuntimeService;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class InitiateValidationServiceImpl implements InitiateValidationService {

    private final MainValidationFlowFacade mainValidationFlowFacade;

    private final RuntimeService runtimeService;

    private final ErmisLogger ermisLogger;

    // NOTE: Following bean is used in @Retryable SpEL expressions.
    private final ValidationRetryConfigurationProperties validationRetryConfigurationProperties;

    // TODO: Should probably add a @Recover method for RetryableException.class so that on complete failure, IE Message gets rejected.
    @Override
    @Retryable(value = {RetryableException.class}, maxAttemptsExpression = "#{@validationRetryConfigurationProperties.getMaxAttempts()}",
            backoff = @Backoff(delayExpression = "#{@validationRetryConfigurationProperties.getDelay()}",
                    multiplierExpression = "#{@validationRetryConfigurationProperties.getMultiplier()}"))
    public void initiateValidationProcessRetryable(ValidationRequestedDomainEvent validationRequest) {
        // Check if validation is already running and throw exception to trigger retries:
        checkAndThrowIfValidationAlreadyRunning(validationRequest);
        // If no RetryableException was thrown, it is safe to initiate validation:
        mainValidationFlowFacade.initProcessInstance(validationRequest);
    }

    @Override
    public void initiateValidationProcess(ValidationRequestedDomainEvent validationRequest) {
        mainValidationFlowFacade.initProcessInstance(validationRequest);
    }

    private void checkAndThrowIfValidationAlreadyRunning(ValidationRequestedDomainEvent validationRequest) throws RetryableException {
        // Retrieve LRN from request event properties:
        String lrn = validationRequest.getProperties().get(ValidationRequestPropertyKeyEnum.LRN.getValue());
        // Query Camunda to check if there is a running process for this LRN:
        boolean isValidationRunningForCurrentLRN = !runtimeService.createProcessInstanceQuery()
                                                                  .processDefinitionKey(ValidationProcessInstanceEnum.TRS_MAIN_VALIDATION_FLOW.getId())
                                                                  .variableValueEquals(BpmnFlowVariablesEnum.LRN.getValue(), lrn)
                                                                  .active()
                                                                  .list()
                                                                  .isEmpty();
        ermisLogger.info("Validation process for LRN: " + lrn
                + (isValidationRunningForCurrentLRN ? " is already running. Will retry..." : " is not running. Continuing..."));
        if (isValidationRunningForCurrentLRN) {
            throw new RetryableException("Validation already underway for LRN: " + lrn);
        }
    }
}

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

import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
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.transit.common.enums.ValidationRequestPropertyKeyEnum;
import com.intrasoft.ermis.transit.common.exceptions.RetryableException;
import com.intrasoft.ermis.transit.validation.configuration.ValidationRetryConfigurationProperties;
import com.intrasoft.ermis.transit.validation.events.ValidationRequestedDomainEvent;
import com.intrasoft.ermis.transit.validation.process.MainValidationFlowFacade;
import com.intrasoft.ermis.transit.validation.service.InitiateValidationServiceUT.TestConfig;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.camunda.bpm.engine.RuntimeService;
import org.camunda.bpm.engine.impl.ProcessInstanceQueryImpl;
import org.camunda.bpm.engine.impl.persistence.entity.ExecutionEntity;
import org.camunda.bpm.extension.mockito.CamundaMockito;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.retry.annotation.EnableRetry;
import org.springframework.test.context.ContextConfiguration;

@SpringBootTest
@ContextConfiguration(classes = TestConfig.class)
class InitiateValidationServiceUT {

    public static final int TEST_RETRIES = 5;

    private static final String LRN = "TEST_LRN";

    @MockBean
    private MainValidationFlowFacade mainValidationFlowFacade;

    @MockBean
    private RuntimeService runtimeService;

    @MockBean
    private ErmisLogger ermisLogger;

    @Autowired
    private InitiateValidationService initiateValidationService;

    @ParameterizedTest
    @DisplayName("Test - Initiate Validation Process - Retryable")
    @ValueSource(booleans = {true, false})
    void testInitiateValidationProcessRetryable(boolean processRunning) {
        // Mock the behavior to simulate that no validation is running for LRN:
        when(runtimeService.createProcessInstanceQuery()).thenReturn(new ProcessInstanceQueryImpl());
        if (processRunning) {
            // Using ExecutionEntity as ProcessInstance extends it:
            ExecutionEntity processInstance = new ExecutionEntity();
            CamundaMockito.mockProcessInstanceQuery(runtimeService).list(List.of(processInstance));
            // Call the service method:
            assertThrows(RetryableException.class, () -> initiateValidationService.initiateValidationProcessRetryable(buildRequestEvent()));
            // Verify that initProcessInstance is not called when retries are triggered:
            verify(mainValidationFlowFacade, never()).initProcessInstance(any());
            // RuntimeService should be queried #maxAttempts amount of times:
            verify(runtimeService, times(TEST_RETRIES)).createProcessInstanceQuery();
        } else {
            CamundaMockito.mockProcessInstanceQuery(runtimeService).list(Collections.emptyList());
            // Call the service method:
            initiateValidationService.initiateValidationProcessRetryable(buildRequestEvent());
            // Verify that initProcessInstance is called once
            verify(mainValidationFlowFacade, times(1)).initProcessInstance(any());
        }
    }

    @Test
    @DisplayName("Test - Initiate Validation Process")
    void testInitiateValidationProcessRetryable() {
        // Call the service method:
        initiateValidationService.initiateValidationProcess(buildRequestEvent());
        // Verify that initProcessInstance is called once
        verify(mainValidationFlowFacade, times(1)).initProcessInstance(any());
    }

    private ValidationRequestedDomainEvent buildRequestEvent() {

        return ValidationRequestedDomainEvent.builder()
                                             .properties(Map.of(ValidationRequestPropertyKeyEnum.LRN.getValue(), LRN))
                                             .build();
    }

    @EnableRetry
    @Configuration
    @Import(InitiateValidationServiceImpl.class)
    public static class TestConfig {
        @Bean(name = "validationRetryConfigurationProperties")
        public ValidationRetryConfigurationProperties validationRetryConfigurationProperties() {
            return new ValidationRetryConfigurationProperties() {
                @Override
                public int getMaxAttempts() {
                    return TEST_RETRIES;
                }

                @Override
                public long getDelay() {
                    return 100;
                }

                @Override
                public double getMultiplier() {
                    return 1.5;
                }
            };
        }
    }
}
