package org.jsmart.zerocode.core.runner;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.google.inject.name.Named;
import com.univocity.parsers.csv.CsvParser;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiConsumer;
import org.jsmart.zerocode.core.domain.ScenarioSpec;
import org.jsmart.zerocode.core.domain.Step;
import org.jsmart.zerocode.core.domain.builders.ZeroCodeExecReportBuilder;
import org.jsmart.zerocode.core.domain.builders.ZeroCodeIoWriteBuilder;
import org.jsmart.zerocode.core.engine.assertion.FieldAssertionMatcher;
import org.jsmart.zerocode.core.engine.executor.ApiServiceExecutor;
import org.jsmart.zerocode.core.engine.preprocessor.ScenarioExecutionState;
import org.jsmart.zerocode.core.engine.preprocessor.StepExecutionState;
import org.jsmart.zerocode.core.engine.preprocessor.ZeroCodeAssertionsProcessor;
import org.jsmart.zerocode.core.engine.preprocessor.ZeroCodeExternalFileProcessor;
import org.jsmart.zerocode.core.engine.preprocessor.ZeroCodeParameterizedProcessor;
import org.jsmart.zerocode.core.engine.validators.ZeroCodeValidator;
import org.jsmart.zerocode.core.logbuilder.ZerocodeCorrelationshipLogger;
import org.jsmart.zerocode.core.utils.ApiTypeUtils;
import org.junit.runner.Description;
import org.junit.runner.notification.RunNotifier;
import org.slf4j.Logger;

import static java.util.Optional.ofNullable;
import static org.jsmart.zerocode.core.constants.ZerocodeConstants.KAFKA_TOPIC;
import static org.jsmart.zerocode.core.domain.builders.ZeroCodeExecReportBuilder.newInstance;
import static org.jsmart.zerocode.core.engine.mocker.RestEndPointMocker.wireMockServer;
import static org.jsmart.zerocode.core.kafka.helper.KafkaCommonUtils.printBrokerProperties;
import static org.jsmart.zerocode.core.utils.ApiTypeUtils.apiType;
import static org.jsmart.zerocode.core.utils.RunnerUtils.getFullyQualifiedUrl;
import static org.jsmart.zerocode.core.utils.RunnerUtils.getParameterSize;
import static org.jsmart.zerocode.core.utils.SmartUtils.prettyPrintJson;
import static org.slf4j.LoggerFactory.getLogger;

@Singleton
public class ZeroCodeMultiStepsScenarioRunnerImpl implements ZeroCodeMultiStepsScenarioRunner {

    private static final Logger LOGGER = getLogger(ZeroCodeMultiStepsScenarioRunnerImpl.class);

    @Inject
    private ObjectMapper objectMapper;

    @Inject
    private ZeroCodeAssertionsProcessor zeroCodeAssertionsProcessor;

    @Inject
    private ZeroCodeExternalFileProcessor extFileProcessor;

    @Inject
    private ZeroCodeParameterizedProcessor parameterizedProcessor;

    @Inject
    private ApiServiceExecutor apiExecutor;

    @Inject
    private CsvParser csvParser;

    @Inject
    private ApiTypeUtils apiTypeUtils;

    @Inject
    ZeroCodeValidator validator;

    @Inject(optional = true)
    @Named("web.application.endpoint.host")
    private String host;

    @Inject(optional = true)
    @Named("web.application.endpoint.port")
    private String port;

    @Inject(optional = true)
    @Named("web.application.endpoint.context")
    private String applicationContext;

    @Inject(optional = true)
    @Named("kafka.bootstrap.servers")
    private String kafkaServers;

    //guice -ends

    private ZerocodeCorrelationshipLogger correlLogger;

    private static StepNotificationHandler notificationHandler = new StepNotificationHandler();

    private ZeroCodeIoWriteBuilder ioWriterBuilder;

    private ZeroCodeExecReportBuilder resultReportBuilder;

    private Boolean stepOutcomeGreen;

    @Override
    public synchronized boolean runScenario(ScenarioSpec scenario, RunNotifier notifier, Description description) {

        LOGGER.info("\n-------------------------- BDD: Scenario:{} -------------------------\n", scenario.getScenarioName());

        ioWriterBuilder = ZeroCodeIoWriteBuilder.newInstance().timeStamp(LocalDateTime.now());

        ScenarioExecutionState scenarioExecutionState = new ScenarioExecutionState();

        int scenarioLoopTimes = deriveScenarioLoopTimes(scenario);

        boolean wasExecSuccessful = false;

        for (int scnCount = 0; scnCount < scenarioLoopTimes; scnCount++) {

            LOGGER.info("{}\n     Executing Scenario Count No. or parameter No. or Row No. | {} | {}",
                    "\n-------------------------------------------------------------------------",
                    scnCount,
                    "\n-------------------------------------------------------------------------");

            ScenarioSpec parameterizedScenario = parameterizedProcessor.resolveParameterized(scenario, scnCount);

            resultReportBuilder = newInstance()
                    .loop(scnCount)
                    .scenarioName(parameterizedScenario.getScenarioName());

            wasExecSuccessful = executeSteps(notifier, description, scenarioExecutionState, parameterizedScenario);

            ioWriterBuilder.result(resultReportBuilder.build());
        }

        stopIfWireMockServerRunning();

        ioWriterBuilder.printToFile(scenario.getScenarioName() + correlLogger.getCorrelationId() + ".json");

        if (wasExecSuccessful) {
            return stepOutcomeGreen;
        }

        /*
         *  There were no steps to execute and the framework will display the test status as Green than Red.
         *  Red symbolises failure, but nothing has failed here.
         *
         *  Note-This behaviour can be changed on user demands by doing 'return false'.
         */
        return true;
    }

    private boolean executeSteps(RunNotifier notifier,
                                 Description description,
                                 ScenarioExecutionState scenarioExecutionState,
                                 ScenarioSpec parameterizedScenario) {

        ScenarioSpec scenario = parameterizedScenario;

        for (Step thisStep : parameterizedScenario.getSteps()) {
            if (thisStep.getIgnoreStep()) {
                LOGGER.info("Step \"" + thisStep.getName() + "\" is ignored because of ignoreStep property.");
                continue;
            }

            correlLogger = ZerocodeCorrelationshipLogger.newInstance(LOGGER);

            Boolean wasExecSuccess = executeRetryWithSteps(notifier, description, scenarioExecutionState, scenario, thisStep);
            if (wasExecSuccess != null) return wasExecSuccess;
        }

        return false;
    }

    private Boolean executeRetryWithSteps(RunNotifier notifier,
                                          Description description,
                                          ScenarioExecutionState scenarioExecutionState,
                                          ScenarioSpec scenario, Step thisStep) {
        thisStep = extFileProcessor.resolveExtJsonFile(thisStep);
        List<Step> thisSteps = extFileProcessor.createFromStepFile(thisStep, thisStep.getId());
        if(null == thisSteps || thisSteps.isEmpty()) thisSteps.add(thisStep);
        Boolean wasExecSuccess = null;
        for(Step step : thisSteps) {
             wasExecSuccess = executeRetry(notifier,
                    description,
                    scenarioExecutionState,
                    scenario,
                    step);
            if (wasExecSuccess != null) {
                return wasExecSuccess;
            }
        }
        return null;
    }

    private Boolean executeRetry(RunNotifier notifier,
                                 Description description,
                                 ScenarioExecutionState scenarioExecutionState,
                                 ScenarioSpec scenario,
                                 Step thisStep) {

        final String logPrefixRelationshipId = correlLogger.createRelationshipId();
        String executionResult = "-response unavailable-";

        // --------------------------------------
        // Save step execution state in a context
        // --------------------------------------
        final String requestJsonAsString = thisStep.getRequest().toString();
        StepExecutionState stepExecutionState = new StepExecutionState();
        stepExecutionState.addStep(thisStep.getName());
        String resolvedRequestJson = zeroCodeAssertionsProcessor.resolveStringJson(
                requestJsonAsString,
                scenarioExecutionState.getResolvedScenarioState());
        stepExecutionState.addRequest(resolvedRequestJson);

        // -----------------------
        // Handle retry mechanism
        // -----------------------
        boolean retryTillSuccess = false;
        int retryDelay = 0;
        int retryMaxTimes = 1;
        if (thisStep.getRetry() != null) {
            retryMaxTimes = thisStep.getRetry().getMax();
            retryDelay = thisStep.getRetry().getDelay();
            retryTillSuccess = true;
        }

        String thisStepName = thisStep.getName();

        for (int retryCounter = 0; retryCounter < retryMaxTimes; retryCounter++) {
            try {

                executionResult = executeApi(logPrefixRelationshipId, thisStep, resolvedRequestJson, scenarioExecutionState);

                // logging response
                final LocalDateTime responseTimeStamp = LocalDateTime.now();
                correlLogger.aResponseBuilder()
                        .relationshipId(logPrefixRelationshipId)
                        .responseTimeStamp(responseTimeStamp)
                        .response(executionResult);
                correlLogger.aResponseBuilder().customLog(thisStep.getCustomLog());
                stepExecutionState.addResponse(executionResult);
                scenarioExecutionState.addStepState(stepExecutionState.getResolvedStep());

                // ---------------------------------
                // Handle assertion section -START
                // ---------------------------------
                String resolvedAssertionJson = zeroCodeAssertionsProcessor.resolveStringJson(
                        thisStep.getAssertions().toString(),
                        scenarioExecutionState.getResolvedScenarioState()
                );

                // -----------------
                // logging assertion
                // -----------------
                List<FieldAssertionMatcher> failureResults = compareStepResults(thisStep, executionResult, resolvedAssertionJson);

                if (!failureResults.isEmpty()) {
                    StringBuilder builder = new StringBuilder();

                    // Print expected Payload along with assertion errors
                    builder.append("Assumed Payload: \n" + prettyPrintJson(resolvedAssertionJson) + "\n");
                    builder.append("Assertion Errors: \n");

                    failureResults.forEach(f -> {
                        builder.append(f.toString() + "\n");
                    });
                    correlLogger.assertion(resolvedAssertionJson != null ? builder.toString() : expectedValidatorsAsJson(thisStep));
                } else {
                    correlLogger.assertion(resolvedAssertionJson != null && !"null".equalsIgnoreCase(resolvedAssertionJson) ?
                            prettyPrintJson(resolvedAssertionJson) : expectedValidatorsAsJson(thisStep));
                }

                if (retryTillSuccess && (retryCounter + 1 < retryMaxTimes) && !failureResults.isEmpty()) {
                    LOGGER.info("\n---------------------------------------\n" +
                            "        Retry: Attempt number: {}", retryCounter + 2 +
                            "\n---------------------------------------\n");
                    waitForDelay(retryDelay);

                    // Set stepOutcomeGreen to true - Not to write report at finally with printToFile().
                    stepOutcomeGreen = true;
                    continue;
                }

                boolean ignoreStepFailures = scenario.getIgnoreStepFailures() == null ? false : scenario.getIgnoreStepFailures();
                if (!failureResults.isEmpty()) {
                    stepOutcomeGreen = notificationHandler.handleAssertion(
                            notifier,
                            description,
                            scenario.getScenarioName(),
                            thisStepName,
                            failureResults,
                            notificationHandler::handleAssertionFailed);

                    correlLogger.stepOutcome(stepOutcomeGreen);

                    if (ignoreStepFailures == true) {
                        // ---------------------------------------------------------------------
                        // Make it Green so that the report doesn't get generated again,
                        // in the finally block i.e. printToFile. Once the scenario completes
                        // execution, all reports(passed n failed) wriitten to file at once.
                        // ---------------------------------------------------------------------
                        stepOutcomeGreen = true;

                        // Do not stop execution. Force-continue to the next step
                        continue;
                    }

                    return true;
                }

                // -----------------
                // Handle PASS cases
                // -----------------
                stepOutcomeGreen = notificationHandler.handleAssertion(
                        notifier,
                        description,
                        scenario.getScenarioName(),
                        thisStepName,
                        failureResults,
                        notificationHandler::handleAssertionPassed);
                // ---------------------------------
                // Handle assertion section -END
                // ---------------------------------

                correlLogger.stepOutcome(stepOutcomeGreen);

                if (retryTillSuccess) {
                    LOGGER.info("Retry: Leaving early with successful assertion");
                    break;
                }

            } catch (Exception ex) {

                ex.printStackTrace();
                LOGGER.info("###Exception while executing a step in the zerocode dsl.");

                // logging exception message
                final LocalDateTime responseTimeStampEx = LocalDateTime.now();
                correlLogger.aResponseBuilder()
                        .relationshipId(logPrefixRelationshipId)
                        .responseTimeStamp(responseTimeStampEx)
                        .response(executionResult)
                        .exceptionMessage(ex.getMessage());

                // Step threw an exception. Handle Exception cases
                stepOutcomeGreen = notificationHandler.handleAssertion(
                        notifier,
                        description,
                        scenario.getScenarioName(),
                        thisStepName,
                        (new RuntimeException("ZeroCode Step execution failed. Details:" + ex)),
                        notificationHandler::handleStepException);

                correlLogger.stepOutcome(stepOutcomeGreen);

                return true;

            } finally {
                correlLogger.print();

                // Build step report for each step. Add the report step to the step list.
                resultReportBuilder.step(correlLogger.buildReportSingleStep());

                /*
                 * FAILED and Exception reports are generated here
                 * TODO- Remove this block in the future release - After testing exception cases
                 */
                if (!stepOutcomeGreen) {
                    //ioWriterBuilder.result(resultReportBuilder.build());
                    //ioWriterBuilder.printToFile(scenario.getScenarioName() + correlLogger.getCorrelationId() + ".json");
                }
            }
        }

        return null;
    }

    private String expectedValidatorsAsJson(Step thisStep) throws JsonProcessingException {
        if(thisStep.getValidators() == null){
            return "No validators were found for this step";
        }
        return prettyPrintJson(objectMapper.writeValueAsString((thisStep.getValidators())));
    }

    private String executeApi(String logPrefixRelationshipId,
                              Step thisStep,
                              String resolvedRequestJson,
                              ScenarioExecutionState scenarioExecutionState) {

        String url = thisStep.getUrl();
        String operationName = thisStep.getOperation();
        String stepId = thisStep.getId();
        String thisStepName = thisStep.getName();

        // --------------------------------
        // Resolve the URL patterns if any
        // --------------------------------
        url = zeroCodeAssertionsProcessor.resolveStringJson(url, scenarioExecutionState.getResolvedScenarioState());

        final LocalDateTime requestTimeStamp = LocalDateTime.now();

        String executionResult;

        switch (apiType(url, operationName)) {
            case REST_CALL:
                url = getFullyQualifiedUrl(url, host, port, applicationContext);
                correlLogger.aRequestBuilder()
                        .relationshipId(logPrefixRelationshipId)
                        .requestTimeStamp(requestTimeStamp)
                        .step(thisStepName)
                        .url(url)
                        .method(operationName)
                        .id(stepId)
                        .request(prettyPrintJson(resolvedRequestJson));

                executionResult = apiExecutor.executeHttpApi(url, operationName, resolvedRequestJson);
                break;

            case JAVA_CALL:
                correlLogger.aRequestBuilder()
                        .relationshipId(logPrefixRelationshipId)
                        .requestTimeStamp(requestTimeStamp)
                        .step(thisStepName)
                        .id(stepId)
                        .url(url)
                        .method(operationName)
                        .request(prettyPrintJson(resolvedRequestJson));

                url = apiTypeUtils.getQualifiedJavaApi(url);
                executionResult = apiExecutor.executeJavaOperation(url, operationName, resolvedRequestJson);
                break;

            case KAFKA_CALL:
                if (kafkaServers == null) {
                    throw new RuntimeException(">>> 'kafka.bootstrap.servers' property can not be null for kafka operations");
                }
                printBrokerProperties(kafkaServers);
                correlLogger.aRequestBuilder()
                        .relationshipId(logPrefixRelationshipId)
                        .requestTimeStamp(requestTimeStamp)
                        .step(thisStepName)
                        .url(url)
                        .method(operationName.toUpperCase())
                        .id(stepId)
                        .request(prettyPrintJson(resolvedRequestJson));

                String topicName = url.substring(KAFKA_TOPIC.length());
                executionResult = apiExecutor.executeKafkaService(kafkaServers, topicName, operationName, resolvedRequestJson);
                break;

            case NONE:
                correlLogger.aRequestBuilder()
                        .relationshipId(logPrefixRelationshipId)
                        .requestTimeStamp(requestTimeStamp)
                        .step(thisStepName)
                        .id(stepId)
                        .url(url)
                        .method(operationName)
                        .request(prettyPrintJson(resolvedRequestJson));

                executionResult = prettyPrintJson(resolvedRequestJson);
                break;

            default:
                throw new RuntimeException("Oops! API Type Undecided. If it is intentional, " +
                        "then keep the value as empty to receive the request in the response");
        }

        return executionResult;
    }

    private void waitForDelay(int delay) {
        if (delay > 0) {
            try {
                Thread.sleep(delay);
            } catch (InterruptedException ignored) {
            }
        }
    }

    @Override
    public boolean runChildStep(ScenarioSpec scenarioSpec, BiConsumer testPassHandler) {

        scenarioSpec.getSteps()
                .forEach(step -> testPassHandler.accept(scenarioSpec.getScenarioName(), step.getName()));

        return true;
    }

    public void overridePort(int port) {
        this.port = port + "";
    }

    public void overrideHost(String host) {
        this.host = host;
    }

    public void overrideApplicationContext(String applicationContext) {
        this.applicationContext = applicationContext;
    }

    private void stopIfWireMockServerRunning() {
        if (null != wireMockServer) {
            wireMockServer.stop();
            wireMockServer = null;
            LOGGER.info("Scenario: All mockings done via WireMock server. Dependant end points executed. Stopped WireMock.");
        }
    }

    private int deriveScenarioLoopTimes(ScenarioSpec scenario) {
        int scenarioLoopTimes = scenario.getLoop() == null ? 1 : scenario.getLoop();
        int parameterSize = getParameterSize(scenario.getParameterized());
        scenarioLoopTimes = parameterSize != 0 ? parameterSize : scenarioLoopTimes;
        return scenarioLoopTimes;
    }

    private List<FieldAssertionMatcher> compareStepResults(Step thisStep, String actualResult, String expectedResult) {
        List<FieldAssertionMatcher> failureResults = new ArrayList<>();

        // --------------------
        //  Validators (pyrest)
        // --------------------
        if (ofNullable(thisStep.getValidators()).orElse(null) != null) {
            failureResults = validator.validateFlat(thisStep, actualResult);
        }

        // ------------------------
        // STRICT mode (skyscreamer)
        // ------------------------
        else if (ofNullable(thisStep.getVerifyMode()).orElse("LENIENT").equals("STRICT")) {
            failureResults = validator.validateStrict(expectedResult, actualResult);
        }

        // --------------------------
        // LENIENT mode (skyscreamer)
        // --------------------------
        else {
            failureResults = validator.validateLenient(expectedResult, actualResult);
        }

        return failureResults;
    }

}