/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.camel.quarkus.component.jira.it;

import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

import com.atlassian.jira.rest.client.api.JiraRestClient;
import com.atlassian.jira.rest.client.api.JiraRestClientFactory;
import com.atlassian.jira.rest.client.api.domain.Attachment;
import com.atlassian.jira.rest.client.api.domain.BasicWatchers;
import com.atlassian.jira.rest.client.api.domain.Issue;
import com.atlassian.jira.rest.client.api.domain.IssueLink;
import com.atlassian.jira.rest.client.api.domain.Resolution;
import com.atlassian.jira.rest.client.api.domain.Worklog;
import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import io.restassured.path.json.JsonPath;
import org.apache.camel.component.jira.oauth.JiraOAuthAuthenticationHandler;
import org.apache.camel.component.jira.oauth.OAuthAsynchronousJiraRestClientFactory;
import org.eclipse.microprofile.config.Config;
import org.eclipse.microprofile.config.ConfigProvider;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

import static org.awaitility.Awaitility.await;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.matchesPattern;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

@QuarkusTest
@QuarkusTestResource(JiraTestResource.class)
public class JiraTest {

    private static final String ISSUE_ATTACHMENT = "Camel Quarkus Test Issue Attachment";
    private static final String ISSUE_COMMENT = "Camel Quarkus Test Issue Comment";
    private static final String ISSUE_DESCRIPTION = "Camel Quarkus Test Issue Description";
    private static final String ISSUE_SUMMARY = "Camel Quarkus Test Issue Summary";
    private static final String ISSUE_TYPE = "Task";
    private static final String UPDATED_ISSUE_SUMMARY = "Updated summary";
    private static JiraRestClient REST_CLIENT;

    @BeforeAll
    public static void beforeAll() {
        Config config = ConfigProvider.getConfig();
        String jiraUrl = config.getValue("camel.component.jira.jira-url", String.class);
        Optional<String> username = config.getOptionalValue("camel.component.jira.username", String.class);
        Optional<String> password = config.getOptionalValue("camel.component.jira.password", String.class);
        Optional<String> accessToken = config.getOptionalValue("camel.component.jira.access-token", String.class);
        Optional<String> consumerKey = config.getOptionalValue("camel.component.jira.consumer-key", String.class);
        Optional<String> privateKey = config.getOptionalValue("camel.component.jira.private-key", String.class);
        Optional<String> verificationCode = config.getOptionalValue("camel.component.jira.verification-code", String.class);

        JiraRestClientFactory factory = new OAuthAsynchronousJiraRestClientFactory();
        URI jiraServerUri = URI.create(jiraUrl);
        if (username.isPresent() && password.isPresent()) {
            REST_CLIENT = factory.createWithBasicHttpAuthentication(jiraServerUri, username.get(), password.get());
        } else if (accessToken.isPresent() && consumerKey.isPresent() && privateKey.isPresent()
                && verificationCode.isPresent()) {
            JiraOAuthAuthenticationHandler oAuthHandler = new JiraOAuthAuthenticationHandler(
                    consumerKey.get(),
                    verificationCode.get(),
                    privateKey.get(),
                    accessToken.get(),
                    jiraUrl);
            REST_CLIENT = factory.create(jiraServerUri, oAuthHandler);
        } else {
            throw new IllegalStateException("Unable to create Jira client");
        }
    }

    @Test
    public void issueCrud() {
        // Start issue consumer
        RestAssured.given()
                .pathParam("routeId", "jiraIssues")
                .pathParam("action", "start")
                .post("/jira/route/{routeId}/{action}")
                .then()
                .statusCode(204);

        // Create issue
        RestAssured.given()
                .contentType(ContentType.TEXT)
                .queryParam("summary", ISSUE_SUMMARY)
                .queryParam("type", ISSUE_TYPE)
                .body(ISSUE_DESCRIPTION)
                .when()
                .post("/jira/issue")
                .then()
                .statusCode(201)
                .body(
                        "description", equalTo(ISSUE_DESCRIPTION),
                        "key", matchesPattern("[A-Z]+-[0-9]+"),
                        "summary", equalTo(ISSUE_SUMMARY),
                        "type", equalTo(ISSUE_TYPE));

        // Read issue
        JsonPath issueJson = RestAssured.given()
                .queryParam("source", "new")
                .get("/jira/issue")
                .then()
                .statusCode(200)
                .body(
                        "description", equalTo(ISSUE_DESCRIPTION),
                        "key", matchesPattern("[A-Z]+-[0-9]+"),
                        "summary", equalTo(ISSUE_SUMMARY),
                        "type", equalTo(ISSUE_TYPE))
                .extract()
                .jsonPath();

        String issueKey = issueJson.getString("key");

        // Stop issue consumer
        RestAssured.given()
                .pathParam("routeId", "jiraIssues")
                .pathParam("action", "stop")
                .post("/jira/route/{routeId}/{action}")
                .then()
                .statusCode(204);

        // Start updates consumer
        RestAssured.given()
                .pathParam("routeId", "jiraUpdates")
                .pathParam("action", "start")
                .post("/jira/route/{routeId}/{action}")
                .then()
                .statusCode(204);

        // Update issue summary
        RestAssured.given()
                .pathParam("key", issueKey)
                .body(UPDATED_ISSUE_SUMMARY)
                .patch("/jira/issue/{key}")
                .then()
                .statusCode(200);

        // Verify update
        RestAssured.given()
                .queryParam("source", "update")
                .get("/jira/issue")
                .then()
                .statusCode(200)
                .body(
                        "description", equalTo(ISSUE_DESCRIPTION),
                        "key", matchesPattern("[A-Z]+-[0-9]+"),
                        "summary", equalTo(UPDATED_ISSUE_SUMMARY),
                        "type", equalTo(ISSUE_TYPE));

        // Stop updates consumer
        RestAssured.given()
                .pathParam("routeId", "jiraUpdates")
                .pathParam("action", "stop")
                .post("/jira/route/{routeId}/{action}")
                .then()
                .statusCode(204);

        // Delete issue
        RestAssured.given()
                .pathParam("key", issueKey)
                .delete("/jira/issue/{key}")
                .then()
                .statusCode(204);

        // Verify deletion
        await().atMost(10, TimeUnit.SECONDS).pollDelay(1, TimeUnit.SECONDS).until(() -> {
            return RestAssured.given()
                    .pathParam("key", issueKey)
                    .get("/jira/issue/{key}")
                    .then()
                    .extract()
                    .statusCode() == 404;
        });
    }

    @Test
    public void comments() {
        // Create issue
        String issueKey = createIssue();
        try {
            // Start comments consumer
            RestAssured.given()
                    .pathParam("routeId", "jiraComments")
                    .pathParam("action", "start")
                    .post("/jira/route/{routeId}/{action}")
                    .then()
                    .statusCode(204);

            // Add comment
            RestAssured.given()
                    .pathParam("key", issueKey)
                    .body(ISSUE_COMMENT)
                    .post("/jira/issue/{key}/comment")
                    .then()
                    .statusCode(201);

            // Verify comment
            RestAssured.get("/jira/issue/comment")
                    .then()
                    .statusCode(200)
                    .body(equalTo(ISSUE_COMMENT));

            // Stop comments consumer
            RestAssured.given()
                    .pathParam("routeId", "jiraComments")
                    .pathParam("action", "stop")
                    .post("/jira/route/{routeId}/{action}")
                    .then()
                    .statusCode(204);
        } finally {
            deleteIssue(issueKey);
        }
    }

    @Test
    public void attachments() {
        // Create issue
        String issueKey = createIssue();
        try {
            // Create attachment
            RestAssured.given()
                    .contentType(ContentType.TEXT)
                    .pathParam("key", issueKey)
                    .body(ISSUE_ATTACHMENT)
                    .when()
                    .post("/jira/issue/{key}/attach")
                    .then()
                    .statusCode(201);

            // Verify attachment
            await().atMost(10, TimeUnit.SECONDS).pollDelay(1, TimeUnit.SECONDS).until(() -> {
                Issue issue = getClient().getIssueClient().getIssue(issueKey).claim();
                assertNotNull(issue);

                Iterable<Attachment> iterable = issue.getAttachments();
                return iterable != null && iterable.iterator().hasNext();
            });

            Issue issue = getClient().getIssueClient().getIssue(issueKey).claim();
            Attachment attachment = issue.getAttachments().iterator().next();
            assertTrue(attachment.getFilename().startsWith("cq-jira"));
        } finally {
            deleteIssue(issueKey);
        }
    }

    @Test
    public void watchers() {
        // Create issue
        String issueKey = createIssue();
        try {
            // Unwatch issue
            JiraRestClient client = getClient();
            String username = client.getSessionClient().getCurrentSession().claim().getUsername();
            RestAssured.given()
                    .contentType(ContentType.TEXT)
                    .pathParam("key", issueKey)
                    .queryParam("action", "unwatch")
                    .body(username)
                    .when()
                    .post("/jira/issue/{key}/watch")
                    .then()
                    .statusCode(200);

            // Verify unwatch
            await().atMost(10, TimeUnit.SECONDS).pollDelay(1, TimeUnit.SECONDS).until(() -> {
                Issue issue = getClient().getIssueClient().getIssue(issueKey).claim();
                assertNotNull(issue);

                BasicWatchers watchers = issue.getWatchers();
                return watchers != null && !watchers.isWatching();
            });

            // Watch issue
            RestAssured.given()
                    .contentType(ContentType.TEXT)
                    .pathParam("key", issueKey)
                    .queryParam("action", "watch")
                    .body(username)
                    .when()
                    .post("/jira/issue/{key}/watch")
                    .then()
                    .statusCode(200);

            // Verify unwatch
            await().atMost(10, TimeUnit.SECONDS).pollDelay(1, TimeUnit.SECONDS).until(() -> {
                Issue issue = getClient().getIssueClient().getIssue(issueKey).claim();
                assertNotNull(issue);

                BasicWatchers watchers = issue.getWatchers();
                return watchers != null && watchers.isWatching();
            });
        } finally {
            deleteIssue(issueKey);
        }
    }

    @Test
    public void issueLinks() {
        List<String> issueKeys = new ArrayList<>();

        // Create issues
        for (int i = 0; i < 2; i++) {
            issueKeys.add(createIssue());
        }

        try {
            // Link issues
            String parentKey = issueKeys.get(0);
            String childKey = issueKeys.get(1);
            RestAssured.given()
                    .contentType(ContentType.TEXT)
                    .queryParam("parentKey", parentKey)
                    .queryParam("childKey", childKey)
                    .when()
                    .post("/jira/issue/link")
                    .then()
                    .statusCode(200);

            // Verify link
            await().atMost(10, TimeUnit.SECONDS).pollDelay(1, TimeUnit.SECONDS).until(() -> {
                Issue parent = getClient().getIssueClient().getIssue(parentKey).claim();
                assertNotNull(parent);

                Iterable<IssueLink> issueLinks = parent.getIssueLinks();
                assertNotNull(issueLinks);

                IssueLink link = issueLinks.iterator().next();
                assertNotNull(link);

                return childKey.equals(link.getTargetIssueKey());
            });
        } finally {
            issueKeys.forEach(this::deleteIssue);
        }
    }

    @Test
    public void workLog() {
        // Create issue
        String issueKey = createIssue();
        try {
            // Log work
            RestAssured.given()
                    .contentType(ContentType.TEXT)
                    .pathParam("key", issueKey)
                    .body("30")
                    .when()
                    .post("/jira/issue/{key}/log/work")
                    .then()
                    .statusCode(200);

            // Verify logged work
            await().atMost(10, TimeUnit.SECONDS).pollDelay(1, TimeUnit.SECONDS).until(() -> {
                Issue issue = getClient().getIssueClient().getIssue(issueKey).claim();
                assertNotNull(issue);

                Iterable<Worklog> workLogs = issue.getWorklogs();
                assertNotNull(workLogs);

                Worklog workLog = workLogs.iterator().next();
                assertNotNull(workLog);

                return workLog.getMinutesSpent() == 30 && workLog.getComment().equals("Work logged: 30");
            });
        } finally {
            deleteIssue(issueKey);
        }
    }

    @Test
    public void workflowTransition() {
        // Create issue
        String issueKey = createIssue();
        try {
            Issue originalIssue = getClient().getIssueClient().getIssue(issueKey).claim();
            assertNotNull(originalIssue);

            Resolution originalResolution = originalIssue.getResolution();
            assertNull(originalResolution);

            String transitionId = System.getenv("JIRA_TRANSITION_ID");
            if (transitionId == null) {
                // The default for the 'done' resolution on the Jira docker server
                transitionId = "41";
            }

            // Transition issue
            RestAssured.given()
                    .contentType(ContentType.TEXT)
                    .pathParam("key", issueKey)
                    .body(transitionId)
                    .when()
                    .post("/jira/issue/{key}/transition")
                    .then()
                    .statusCode(200);

            // Verify issue workflow transition
            await().atMost(10, TimeUnit.SECONDS).pollDelay(1, TimeUnit.SECONDS).until(() -> {
                Issue issue = getClient().getIssueClient().getIssue(issueKey).claim();
                assertNotNull(issue);

                Resolution resolution = issue.getResolution();
                return resolution != null;
            });
        } finally {
            deleteIssue(issueKey);
        }
    }

    private String createIssue() {
        return RestAssured.given()
                .contentType(ContentType.TEXT)
                .queryParam("summary", ISSUE_SUMMARY)
                .queryParam("type", ISSUE_TYPE)
                .body(ISSUE_DESCRIPTION)
                .when()
                .post("/jira/issue")
                .then()
                .statusCode(201)
                .body(
                        "description", equalTo(ISSUE_DESCRIPTION),
                        "key", matchesPattern("[A-Z]+-[0-9]+"),
                        "summary", equalTo(ISSUE_SUMMARY),
                        "type", equalTo(ISSUE_TYPE))
                .extract()
                .jsonPath()
                .getString("key");
    }

    private void deleteIssue(String issueKey) {
        try {
            getClient().getIssueClient()
                    .deleteIssue(issueKey, true)
                    .claim();
        } catch (Exception e) {
            // Ignore - issue may not exist
        }
    }

    private static JiraRestClient getClient() {
        if (REST_CLIENT == null) {
            Config config = ConfigProvider.getConfig();
            String jiraUrl = config.getValue("camel.component.jira.jira-url", String.class);
            Optional<String> username = config.getOptionalValue("camel.component.jira.username", String.class);
            Optional<String> password = config.getOptionalValue("camel.component.jira.password", String.class);

            JiraRestClientFactory factory = new OAuthAsynchronousJiraRestClientFactory();
            URI jiraServerUri = URI.create(jiraUrl);
            if (username.isPresent() && password.isPresent()) {
                return factory.createWithBasicHttpAuthentication(jiraServerUri, username.get(), password.get());
            }
            throw new IllegalStateException("Unable to create Jira client");
        }
        return REST_CLIENT;
    }
}
