package cz.moneta.test.harness.endpoints.jira;

import com.fasterxml.jackson.databind.JsonNode;
import cz.moneta.test.harness.annotations.Environment;
import cz.moneta.test.harness.annotations.JiraTestCase;
import cz.moneta.test.harness.connectors.VaultConnector;
import cz.moneta.test.harness.constants.HarnessConfigConstants;
import cz.moneta.test.harness.support.rest.RawRestRequest;
import itSodTesting.sharedResources.utils.getUser.LoggedUser;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.glassfish.jersey.media.multipart.FormDataMultiPart;
import org.glassfish.jersey.media.multipart.MultiPart;
import org.glassfish.jersey.media.multipart.file.FileDataBodyPart;
import org.junit.jupiter.api.extension.ExtensionContext;

import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.core.MediaType;
import java.io.File;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

import static com.fasterxml.jackson.databind.node.JsonNodeType.*;

public final class JiraTestResultPublisher {

    private static final Logger LOG = LogManager.getLogger(JiraTestResultPublisher.class);

    private ExtensionContext.Store configStore;

    private JiraTestResultPublisher() {
        // hide constructor
    }

    public static boolean doesTestCaseExist(String testCase, ExtensionContext extensionContext) {
        ExtensionContext.Store configStore = extensionContext.getStore(ExtensionContext.Namespace.create("CONFIG"));
        JiraEndpoint endpoint = getJiraZephyrScaleEndpoint(configStore);
        String authorizationToken = getAuthorizationToken(configStore);

        try {
            String path = "testcase/" + testCase;
            Integer status = RawRestRequest.jsonBuilder(endpoint)
                    .withPath(path)
                    .withHeader("Authorization", "Basic " + authorizationToken)
                    .get()
                    .getStatus();

            if (status == 200) {
                LOG.debug(String.format("Jira TestCase with id %s exists", testCase));
                return true;
            } else {
                LOG.warn(String.format("Jira TestCase with id %s does not exist", testCase));
                return false;
            }
        } catch (WebApplicationException e) {
            LOG.warn(String.format("Error while looking up TestCase id %s in Jira: ", testCase), e);
            return false;
        }
    }

    private static JiraEndpoint getJiraZephyrScaleEndpoint(ExtensionContext.Store configStore) {
        return getJiraEndpoint("reports.tmfj.url", configStore);
    }

    private static JiraEndpoint getJiraCoreEndpoint(ExtensionContext.Store configStore) {
        return getJiraEndpoint("endpoints.jira.core", configStore);
    }

    private static JiraEndpoint getJiraEndpoint(String configKey, ExtensionContext.Store configStore) {
        String url = configStore.get(configKey, String.class);
        if (StringUtils.isEmpty(url)) {
            throw new IllegalStateException("You need to configure " + configKey + " to work with JIRA");
        }
        return new JiraEndpoint(url);
    }

    public static String createTestResult(String testCase, ExtensionContext extensionContext, Optional<Throwable> executionException) {
        String testResultId;
        String status = executionException.map(e -> "Fail").orElse("Pass");
        ExtensionContext.Store configStore = extensionContext.getStore(ExtensionContext.Namespace.create("CONFIG"));
        JiraEndpoint endpoint = getJiraZephyrScaleEndpoint(configStore);
        String authorizationToken = getAuthorizationToken(configStore);

        JiraTestResult pojoTestResult = createPojo(testCase, extensionContext, status, configStore, executionException);
        String cycleKey = (String) configStore.get("reports.tmfj.test.cycle");
        //Parameter is for cases when we do not want to publish executions which are not part of a test cycle
        boolean publishToTestCycleOnly = Boolean.parseBoolean(configStore.get("reports.tmfj.publishtocycleonly", String.class));
        if (belongsToCycle(extensionContext, testCase, cycleKey)) {
            LOG.debug("Publishing test results for test case {} in test cycle {}.", testCase, cycleKey);
            testResultId = postTestResult(authorizationToken, pojoTestResult, endpoint, cycleKey);
        } else if (publishToTestCycleOnly) {
            LOG.debug("Skipping test results publishing for test case {} since reports.tmfj.publishtocycleonly is set to true.", testCase);
            testResultId = null;
        } else {
            LOG.debug("Publishing test results for test case {}.", testCase, cycleKey);
            testResultId = postTestResult(authorizationToken, pojoTestResult, endpoint, null);
        }

        Optional.ofNullable(extensionContext.getStore(ExtensionContext.Namespace.create("UPLOADS")).get("jira.uploads"))
                .filter(s -> !testResultId.isEmpty())
                .map(s -> (Set<String>) s)
                .ifPresent(s -> postSnapshots(s, authorizationToken, endpoint, testResultId));
        return testResultId;
    }

    private static boolean belongsToCycle(ExtensionContext extensionContext, String testCase, String testCycle) {
        if (testCycle == null) {
            return false;
        }

        ExtensionContext.Store configStore = extensionContext.getStore(ExtensionContext.Namespace.create("CONFIG"));
        JiraEndpoint endpoint = getJiraZephyrScaleEndpoint(configStore);
        String authorizationToken = getAuthorizationToken(configStore);

        try {
            String path = "testrun/" + testCycle;
            RawRestRequest.Response response = RawRestRequest.jsonBuilder(endpoint)
                    .withPath(path)
                    .withHeader("Authorization", "Basic " + authorizationToken)
                    .get();

            JsonNode items = response.extract("items");
            if (items.getNodeType() == ARRAY) {
                for (JsonNode node : items) {
                    if (node.getNodeType() == OBJECT) {
                        if (testCase.equalsIgnoreCase(node.get("testCaseKey").asText())) {
                            return true;
                        }
                    }
                }
            }
            return false;
        } catch (WebApplicationException | NullPointerException | IllegalArgumentException e) {
            LOG.warn(String.format("Error while looking up TestRun %s in Jira: ", testCycle), e);
            return false;
        }
    }

    private static String getAuthorizationToken(ExtensionContext.Store configStore) {
        return (String) Optional.ofNullable(getAccessToken("jira", configStore))
                .orElseGet(() -> Optional.ofNullable(configStore.get("reports.tmfj.token", String.class))
                        .orElseThrow(() -> new IllegalStateException(String.format("Jira token not found either through " +
                        "vault nor in properties. Please define 'reports.tmfj.token' properties in order to use local configuration."))));
    }

    private static String getExecutedByUserKey(ExtensionContext extensionContext) {
        //Get username only for employees. Other accounts might not be in Jira and are defaulted to jenkins.user.
        String user = Optional.of(LoggedUser.getUserName())
                .filter(username -> username.matches("\\d{9}"))
                //jenkins.user is Jenkins technical user with access to Jira
                .orElse("jenkins.user");

        ExtensionContext.Store configStore = extensionContext.getStore(ExtensionContext.Namespace.create("CONFIG"));
        JiraEndpoint endpoint = getJiraCoreEndpoint(configStore);
        String authorizationToken = getAuthorizationToken(configStore);

        try {
            String path = "user";
            RawRestRequest.Response response = RawRestRequest.jsonBuilder(endpoint)
                    .withPath(path)
                    .withHeader("Authorization", "Basic " + authorizationToken)
                    .withParam("username", user)
                    .get();

            JsonNode key = response.extract("key");
            return key.asText();

        } catch (WebApplicationException | NullPointerException | IllegalArgumentException e) {
            LOG.error(String.format("Error while getting userkey for username %s in Jira: ", user), e);
            return user;
        }
    }

    private static JiraTestResult createPojo(String testCase, ExtensionContext extensionContext, String status, ExtensionContext.Store configStore, Optional<Throwable> executionException) {
        JiraTestResult pojoTestResult = new JiraTestResult();
        pojoTestResult.setProjectKey(testCase.split("-")[0]);
        pojoTestResult.setTestCaseKey(testCase);
        pojoTestResult.setStatus(status);
        pojoTestResult.setExecutedBy(getExecutedByUserKey(extensionContext));
        pojoTestResult.setVersion(configStore.getOrDefault("reports.tmfj.release.version", String.class, null));
        Optional.ofNullable(configStore.get(HarnessConfigConstants.ENVIRONMENT_TYPE))
                .map(String::valueOf)
                .map(Environment::fromString)
                .map(Environment::getJiraAlias)
                .ifPresent(pojoTestResult::setEnvironment);

        Object commentObject = extensionContext.getStore(ExtensionContext.Namespace.GLOBAL).get("reports.tmfj.execution.comment");
        if (commentObject != null) {
            pojoTestResult.setComment(String.format("Data from Test Execution:<br>%s", commentObject.toString().replaceAll("\n", "<br>")));
        }

        Object commentObjectUfoSessionId = extensionContext.getStore(ExtensionContext.Namespace.GLOBAL).get("reports.tmfj.execution.commentufosessionid");

        executionException.map(Throwable::getMessage)
                .ifPresent(errorComment -> {
                    StringBuilder comment = new StringBuilder();
                    if (pojoTestResult.getComment() != null) {
                        comment.append(pojoTestResult.getComment());
                        comment.append("<br>");
                    }
                    comment.append("Error Details:<br>");
                    if (commentObjectUfoSessionId != null) {
                        comment.append(String.format("UFO Session ID: %s<br>", commentObjectUfoSessionId));
                    }
                    comment.append(errorComment);
                    pojoTestResult.setComment(comment.toString());
                });

        Optional.ofNullable(configStore.get(HarnessConfigConstants.JENKINS_BUILD_URL))
                .map(String::valueOf)
                .ifPresent(value -> pojoTestResult.setFieldInCustomFields("Jenkins Execution", value));

        return pojoTestResult;
    }

    private static String getAccessToken(String path, ExtensionContext.Store store) {
        return  Optional.ofNullable(store.get(HarnessConfigConstants.VAULT_URL_CONFIG)).map(String::valueOf)
                .flatMap(u -> Optional.ofNullable(store.get(HarnessConfigConstants.VAULT_USERNAME_CONFIG)).map(String::valueOf)
                        .flatMap(l -> Optional.ofNullable(store.get(HarnessConfigConstants.VAULT_PASSWORD_CONFIG)).map(String::valueOf)
                                .map(p -> new VaultConnector(u, l, p))))
                .flatMap(v -> Optional.of("vault.path.base.common")
                        .map(store::get)
                        .flatMap(p -> v.getValue(p + "/" + path, "token"))
                        .map(String::valueOf))
                .orElse(null);
    }

    private static void postSnapshots(Set<String> files, String authorizationToken, JiraEndpoint je, String testResultId) {
        Map<String, Object> headers = new HashMap<>();
        headers.put("Content-Type", "multipart/form-data");
        headers.put("Authorization", "Basic " + authorizationToken);
        headers.put("X-Atlassian-Token", "no-check");
        if (!files.isEmpty()) {
            MultiPart multiPart = new FormDataMultiPart();
            files.removeIf(f -> f == null || !f.endsWith(".png"));
            files.forEach(f -> {
                FileDataBodyPart screenshot = new FileDataBodyPart("file", new File(f));
                multiPart.bodyPart(screenshot).setMediaType(MediaType.MULTIPART_FORM_DATA_TYPE);
            });
            try {
                je.post("testresult/" + testResultId + "/attachments", Entity.entity(multiPart, multiPart.getMediaType()), String.class, headers, false);
            } catch (WebApplicationException e) {
                LOG.warn("Snapshot to JIRA could not be uploaded: ", e);
            }
        }
    }

    private static String postTestResult(String authorizationToken, JiraTestResult tr, JiraEndpoint je, String cycleKey) {
        String testResultId = null;
        try {
            String path = "testresult";
            if (cycleKey != null) {
                path = String.format("testrun/%s/testcase/%s/", cycleKey, tr.getTestCaseKey()) + path;
            }

            RawRestRequest.Response response = RawRestRequest.jsonBuilder(je)
                    .withPath(path)
                    .withHeader("Authorization", "Basic " + authorizationToken)
                    .withPojoPayload(tr)
                    .post();

            if (response.getStatus() == 201) {
                testResultId = response.extract("id").asText();
                LOG.info("Jira TestResult created id: {}", testResultId);
            } else {
                JsonNode errorMessages = response.extract("errorMessages");
                errorMessages.forEach(message -> LOG.warn("Jira TestResult could not be created: {}", message.asText()));
            }
        } catch (WebApplicationException | NullPointerException |IllegalArgumentException e) {
            LOG.warn("Jira TestResult could not be created: ", e);
        }
        return testResultId;
    }

    public static void logJiraTestResult(ExtensionContext extensionContext, Throwable throwable) {
        if (Boolean.parseBoolean(extensionContext.getStore(ExtensionContext.Namespace.create("CONFIG")).get("reports.tmfj.publish", String.class))) {
            if (throwable != null) {
                Arrays.stream(extensionContext.getRequiredTestClass().getMethods())
                        .flatMap(method -> Arrays.stream(method.getDeclaredAnnotationsByType(JiraTestCase.class)))
                        .map(JiraTestCase::id)
                        .forEach(testCaseId -> createTestResult(testCaseId, extensionContext, Optional.ofNullable(throwable)));
                return;
            }

            Arrays.stream(extensionContext.getRequiredTestMethod().getAnnotationsByType(JiraTestCase.class))
                    .map(JiraTestCase::id)
                    .forEach(testCaseId -> JiraTestResultPublisher.createTestResult(testCaseId, extensionContext, extensionContext.getExecutionException()));
        }
    }

    public static void logJiraTestResult(ExtensionContext extensionContext) {
        logJiraTestResult(extensionContext, null);
    }
}
