/*
 * 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.digitalocean.it;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

import com.github.tomakehurst.wiremock.WireMockServer;
import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import org.apache.camel.quarkus.test.wiremock.MockServer;
import org.eclipse.microprofile.config.ConfigProvider;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

import static io.restassured.RestAssured.given;
import static org.awaitility.Awaitility.await;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

@SuppressWarnings("unchecked")
@QuarkusTest
@QuarkusTestResource(DigitaloceanTestResource.class)
public class DigitaloceanDropletTest {
    static final long timeout = 5;
    static TimeUnit timeoutUnit = TimeUnit.SECONDS;
    static boolean waitBlockStorageAction = false;

    @MockServer
    WireMockServer server;

    @BeforeAll
    public static void initTimeoutUnit() {
        // add timeout if not using MockServer
        // when using a Digitalocean Key, it takes at least 2 minutes to create a droplet or snapshot
        Optional<String> key = ConfigProvider.getConfig().getOptionalValue("DIGITALOCEAN_AUTH_TOKEN", String.class);
        if (key.isPresent()) {
            timeoutUnit = TimeUnit.MINUTES;
            waitBlockStorageAction = true;
        }
    }

    /**
     * Testing droplet producer and all tests of producers related to an existing droplet
     *
     */
    @Test
    void testDroplets() throws InterruptedException {
        // insert multiple droplets
        Integer dropletId = RestAssured.given().contentType(ContentType.JSON).put("/digitalocean/droplet/droplet1")
                .then().extract().body().as(Integer.class);
        assertNotNull(dropletId);

        // get the droplet by dropletId1 until its status is active and ready
        // it takes time only if using a oAuthToken from Digitalocean
        waitActiveDroplet(dropletId);

        // test actions on droplet
        performActions(dropletId);

        // test actions
        testResultActions(dropletId);

        // test neighbors
        testNeighbors(dropletId);

        // test floating IP
        testFloatingIP(dropletId);

        // test images
        testImages();

        // test snapshots
        testSnapshots();

        // test block storages
        testBlockStorages(dropletId);

        // delete the droplet with id droplet 1
        given()
                .when()
                .delete("/digitalocean/droplet/" + dropletId)
                .then()
                .statusCode(202);
    }

    private void performActions(Integer dropletId) {
        // action : enable backups
        given()
                .contentType(ContentType.JSON)
                .get("/digitalocean/droplet/backups/enable/" + dropletId)
                .then()
                .body("resourceId", equalTo(dropletId))
                .body("type", equalTo("ENABLE_BACKUPS"));

        // action : power off, before taking a snapshot
        given()
                .contentType(ContentType.JSON)
                .get("/digitalocean/droplet/off/" + dropletId)
                .then()
                .body("resourceId", equalTo(dropletId));

        // take a snapshot
        given()
                .contentType(ContentType.JSON)
                .body("snapshot1")
                .post("/digitalocean/droplet/snapshot/" + dropletId)
                .then()
                .body("resourceId", equalTo(dropletId));

        waitForSnapshot(dropletId);

        // action : disable backups
        given()
                .when()
                .get("/digitalocean/droplet/backups/disable/" + dropletId)
                .then()
                .body("resourceId", equalTo(dropletId));

        // action : power on
        given()
                .contentType(ContentType.JSON)
                .get("/digitalocean/droplet/on/" + dropletId)
                .then()
                .body("resourceId", equalTo(dropletId));

        // Reboot droplet
        given()
                .contentType(ContentType.JSON)
                .get("/digitalocean/droplet/reboot/" + dropletId)
                .then()
                .body("resourceId", equalTo(dropletId));

        // enable Ipv6
        given()
                .contentType(ContentType.JSON)
                .get("/digitalocean/droplet/ipv6/" + dropletId)
                .then()
                .body("resourceId", equalTo(dropletId));
    }

    /**
     * Test the result of actions performed
     *
     */
    private void testResultActions(Integer dropletId) {
        // getting the droplet actions
        List<Map<String, Object>> actions = RestAssured.given().contentType(ContentType.JSON)
                .get("/digitalocean/droplet/actions/" + dropletId)
                .then().extract().body().as(List.class);
        List<String> types = Arrays.asList("ENABLE_BACKUPS", "DISABLE_BACKUPS", "SNAPSHOT", "POWER_ON", "POWER_OFF", "REBOOT",
                "ENABLE_IPV6");
        assertActions(actions, types);

        // getting the snapshot action id
        Integer snapshotActionId = getSnapshotActionId(actions);

        // getting one action
        given()
                .get("/digitalocean/action/" + snapshotActionId)
                .then()
                .body("id", equalTo(snapshotActionId));

        // getting all actions on the account
        actions = RestAssured.given().contentType(ContentType.JSON)
                .get("/digitalocean/actions/")
                .then().extract().body().as(List.class);
        types = Arrays.asList("ENABLE_BACKUPS", "DISABLE_BACKUPS", "SNAPSHOT", "POWER_ON", "POWER_OFF", "REBOOT");
        assertActions(actions, types);
    }

    /**
     * Gets the snapshots and waits until the snapshot is created in Digitalocean.
     *
     */
    private void waitForSnapshot(Integer dropletId) {
        await().atMost(timeout, timeoutUnit).until(() -> {
            String path = "/digitalocean/droplet/snapshots/" + dropletId;
            List<Map<String, Object>> result = given().when().get(path).then().extract().as(List.class);
            // Look for the snapshot
            Optional<Map<String, Object>> optional = result.stream()
                    .filter(s -> "snapshot1".equals(s.get("name")))
                    .findAny();
            return optional.isPresent();
        });
    }

    /**
     * Gets the droplet and waits until the droplet is Active in Digitalocean.
     *
     */
    private void waitActiveDroplet(Integer dropletId) {
        await().atMost(timeout, timeoutUnit).until(() -> {
            String path = "/digitalocean/droplet/" + dropletId;
            Map<String, Object> droplet = given()
                    .contentType(ContentType.JSON).get(path).then().extract().as(Map.class);
            return droplet != null && dropletId.equals(droplet.get("id")) && "ACTIVE".equals(droplet.get("status"));
        });
    }

    /**
     * Assert all the actions
     *
     */
    private void assertActions(List<Map<String, Object>> actions, List<String> types) {
        // verify there are actions
        assertNotNull(actions);
        // verify there are at least the 7 created actions in the test
        assertTrue(actions.size() >= types.size());
        types.forEach(type -> assertAction(actions, type));
    }

    /**
     * assert a single action
     *
     */
    private void assertAction(List<Map<String, Object>> actions, String actionType) {
        Optional<Map<String, Object>> optional = actions.stream()
                .filter(a -> actionType.equals(a.get("type")))
                .findAny();
        assertTrue(optional.isPresent());
    }

    /**
     * Get action of type SNAPSHOT
     *
     */
    private Integer getSnapshotActionId(List<Map<String, Object>> actions) {
        return actions.stream()
                .filter(a -> "SNAPSHOT".equals(a.get("type")))
                .findAny()
                .map(b -> (Integer) b.get("id"))
                .orElse(null);
    }

    private void testNeighbors(int dropletId) {
        given()
                .when()
                .get("/digitalocean/droplet/neighbors/" + dropletId)
                .then()
                .statusCode(200);
    }

    private void testImages() {
        // getting all images
        List<Map<String, Object>> images = RestAssured.given().contentType(ContentType.JSON)
                .get("/digitalocean/images")
                .then().extract().body().as(List.class);
        // there's at least the image created for the backup
        assertNotNull(images);

        // get one image id
        Integer imageId = images.stream().findAny().map(image -> (Integer) image.get("id")).orElse(null);
        assertNotNull(imageId);

        // get the image with id
        given().contentType(ContentType.JSON)
                .get("/digitalocean/images/" + imageId)
                .then()
                .body("id", equalTo(imageId));

        // test user's images
        images = RestAssured.given().contentType(ContentType.JSON)
                .get("/digitalocean/images/user")
                .then().extract().body().as(List.class);
        assertNotNull(images);
    }

    private void testSnapshots() {
        // getting all snapshots
        List<Map<String, Object>> images = RestAssured.given().contentType(ContentType.JSON)
                .get("/digitalocean/snapshots")
                .then().extract().body().as(List.class);
        // there's at least the image created for the backup
        assertNotNull(images);

        // get one snapshot id
        Integer snapshotId = images.stream().findAny().map(snapshot -> (Integer) snapshot.get("id")).orElse(null);
        assertNotNull(snapshotId);

        // get the snapshot with id
        given().contentType(ContentType.JSON)
                .get("/digitalocean/snapshots/" + snapshotId)
                .then()
                .body("id", equalTo(snapshotId));

        // delete snapshot
        given().contentType(ContentType.JSON)
                .delete("/digitalocean/snapshots/" + snapshotId)
                .then()
                .statusCode(200);
    }

    private void testFloatingIP(Integer dropletId) {
        AtomicReference<String> floatingIp = new AtomicReference<>();
        // create a floating IP for the droplet
        await().atMost(timeout, timeoutUnit).until(() -> {
            floatingIp.set(given().contentType(ContentType.JSON)
                    .body(dropletId)
                    .put("/digitalocean/floatingIP")
                    .then().extract().body().asString());
            return !floatingIp.get().equals("");
        });

        // gets the floating IP
        given()
                .contentType(ContentType.JSON)
                .get("/digitalocean/floatingIP/" + floatingIp)
                .then()
                .body("ip", equalTo(floatingIp.get()));

        // gets all the floating IP
        List<Map<String, Object>> floatingIps = given()
                .contentType(ContentType.JSON)
                .get("/digitalocean/floatingIP")
                .then().extract().body().as(List.class);
        Optional<Map<String, Object>> floatingIpResult = floatingIps.stream()
                .filter(f -> floatingIp.get().equals(f.get("ip")))
                .findAny();
        assertTrue(floatingIpResult.isPresent());

        // unassign a floating IP
        final Map<String, Object>[] action = new Map[1];
        await().atMost(timeout, timeoutUnit).until(() -> {
            action[0] = given()
                    .contentType(ContentType.JSON)
                    .get("/digitalocean/floatingIP/unassign/" + floatingIp)
                    .then().extract().body().as(Map.class);
            return action[0] != null && "UNASSIGN_FLOATING_IP".equals(action[0].get("type"));
        });
        Integer actionId = (Integer) action[0].get("id");

        // get all action of a floating IP
        List<Map<String, Object>> actions = given()
                .contentType(ContentType.JSON)
                .get("/digitalocean/floatingIP/actions/" + floatingIp)
                .then().extract().body().as(List.class);
        assertNotNull(actions);
        Optional<Map<String, Object>> actionUnassign = actions.stream()
                .filter(a -> actionId.equals(a.get("id")))
                .findAny();
        assertTrue(actionUnassign.isPresent());

        // delete a floating ip
        await().atMost(timeout, timeoutUnit).until(() -> {
            Map<String, Object> delete = given()
                    .contentType(ContentType.JSON)
                    .delete("/digitalocean/floatingIP/" + floatingIp)
                    .then().extract().body().as(Map.class);
            return delete != null && (Boolean) delete.get("isRequestSuccess");
        });
    }

    private void testBlockStorages(Integer dropletId) throws InterruptedException {
        // create a volume
        String volumeId = given()
                .contentType(ContentType.JSON)
                .body("volume1")
                .when()
                .put("/digitalocean/blockStorages")
                .then().extract().body().asString();
        assertNotNull(volumeId);

        // get the volume
        given()
                .get("/digitalocean/blockStorages/" + volumeId)
                .then()
                .body("name", equalTo("volume1"));

        // get all volumes in the region FRA1
        List<Map<String, Object>> volumes = given()
                .get("/digitalocean/blockStorages")
                .then()
                .extract().body().as(List.class);
        assertNotNull(volumes);
        Map<String, Object> expectedResult = volumes.stream()
                .filter(v -> "volume1".equals(v.get("name")))
                .findAny().orElse(null);
        assertNotNull(expectedResult);

        // attach to droplet
        given()
                .contentType(ContentType.JSON)
                .body(dropletId)
                .when()
                .post("/digitalocean/blockStorages/attach/volume1")
                .then()
                .body("type", equalTo("ATTACH"));

        // wait until the volume is attached : or else it's impossible to detach
        waitAttachedVolume(dropletId, volumeId);

        // detach from droplet
        given()
                .contentType(ContentType.JSON)
                .body(dropletId)
                .when()
                .post("/digitalocean/blockStorages/detach/volume1")
                .then()
                .statusCode(200);

        // wait until detach action is performed :: only when using digitalocean
        // if the volume is droped before being detached, there's an error in delete action, and it will be impossible to get the drop result
        if (waitBlockStorageAction) {
            Thread.sleep(10000);
        }

        // drop the volume
        given()
                .contentType(ContentType.JSON)
                .body(volumeId)
                .when()
                .delete("/digitalocean/blockStorages")
                .then()
                .body("isRequestSuccess", equalTo(true));

    }

    /**
     * Gets the droplet and waits until the volume is attached to the droplet
     *
     */
    private void waitAttachedVolume(Integer dropletId, String volumeId) {
        await().atMost(timeout, timeoutUnit).until(() -> {
            String path = "/digitalocean/droplet/" + dropletId;
            Map<String, Object> droplet = given()
                    .contentType(ContentType.JSON).get(path).then().extract().as(Map.class);
            return droplet.get("volumeIds") != null && ((List<String>) droplet.get("volumeIds")).contains(volumeId);
        });
    }
}
