package it.com.atlassian.stash;

import com.atlassian.bitbucket.test.DefaultFuncTestData;
import com.atlassian.fugue.Pair;
import com.jayway.restassured.RestAssured;
import com.jayway.restassured.response.Response;
import org.apache.commons.lang3.StringUtils;

import java.time.ZoneId;
import java.util.Optional;
import java.util.function.Function;

import static com.atlassian.bitbucket.test.StatusCodesTestHelper.matching;
import static javax.ws.rs.core.Response.Status.*;
import static org.junit.Assert.fail;

/**
 * @since 4.6
 */
public class TimeZoneTestHelper {

    private TimeZoneTestHelper() {
    }

    /**
     * Get the server timezone.
     */
    public static Optional<ZoneId> getServerTimeZone() {
        return getTimeZone(TimeZoneScope.SERVER);
    }

    /**
     * Set the server timezone.
     *
     * @param zone the timezone to set. May pass {@link Optional#empty} to clear the setting.
     */
    public static void setServerTimeZone(Optional<? extends ZoneId> zone) {
        setTimeZone(zone, TimeZoneScope.SERVER);
    }

    /**
     * Get the user's timezone.
     */
    public static Optional<ZoneId> getUserTimeZone(String username, String password) {
        return getTimeZone(TimeZoneScope.USER, username, password);
    }

    /**
     * Set the timezone of the specified user.
     *
     * @param zone the timezone to set. May pass {@link Optional#empty} to clear the setting.
     */
    public static void setUserTimeZone(Optional<? extends ZoneId> zone, String username, String password) {
        setTimeZone(zone, TimeZoneScope.USER, username, password);
    }

    /**
     * Run the passed callback with the server using the passed server timezone.
     *
     * @param zone     the timezone to set as the server timezone.
     * @param callback the callback.
     * @param <T>      the type of the callback.
     * @return the result of the callback.
     */
    public static <T> T withServerTimeZone(Optional<? extends ZoneId> zone,
                                           Function<Optional<? extends ZoneId>, ? extends T> callback) {
        return withTimeZone(TimeZoneScope.SERVER, zone, callback);
    }

    /**
     * Run the passed callback with the current user using the passed timezone.
     *
     * @param zone     the timezone to set as the current user's timezone.
     * @param callback the callback.
     * @param <T>      the type of the callback.
     * @return the result of the callback.
     */
    public static <T> T withUserTimeZone(Optional<? extends ZoneId> zone,
                                         Function<Optional<? extends ZoneId>, ? extends T> callback) {
        return withTimeZone(TimeZoneScope.USER, zone, callback);
    }

    /**
     * Run the passed callback with the server and user timezone set as passed.
     *
     * @param serverTimeZone the timezone to set as the server timezone.
     * @param userTimeZone the timezone to set as the current user's timezone.
     * @param callback the callback.
     * @param <T> the type of the callback.
     * @return the result of the callback.
     */
    public static <T> T withServerAndUserTimeZone(Optional<? extends ZoneId> serverTimeZone,
                                                  Optional<? extends ZoneId> userTimeZone,
                                                  Function<Optional<? extends ZoneId>, ? extends T> callback) {
        return withServerTimeZone(serverTimeZone, (ignored) -> withUserTimeZone(userTimeZone, callback));
    }

    private static String getResourceRoot() {
        return DefaultFuncTestData.getBaseURL() + "/rest/browser-tests/latest/timezones";
    }

    private static String getServerResource() {
        return getResourceRoot() + "/server";
    }

    private static String getUserResource() {
        return getResourceRoot() + "/user";
    }

    private static Pair<String, String> getCredentials(String[] credentials) {
        String username;
        String password;
        if (credentials.length == 2) {
            username = credentials[0];
            password = credentials[1];
        } else {
            username = DefaultFuncTestData.getAdminUser();
            password = DefaultFuncTestData.getAdminPassword();
        }
        return new Pair<>(username, password);
    }

    private static Optional<ZoneId> getTimeZone(TimeZoneScope scope, String... credentials) {
        Pair<String, String> creds = getCredentials(credentials);
        Response response = RestAssured.given()
                .auth().preemptive().basic(creds.left(), creds.right())
                .log().ifValidationFails()
                .when().get(scope.resource);

        if (response.getStatusCode() == NOT_FOUND.getStatusCode()) {
            fail("Unable to set TimeZone. Do you have the `bitbucket-it-browser` plugin installed?. See https://extranet.atlassian.com/x/y4eMdQ");
        }
        response.then().statusCode(matching(OK));
        String zoneId = response.as(RestTimeZone.class).getTimeZone();
        return Optional.ofNullable(StringUtils.stripToNull(zoneId)).map(ZoneId::of);
    }

    private static void setTimeZone(Optional<? extends ZoneId> zone, TimeZoneScope scope, String... credentials) {
        Pair<String, String> creds = getCredentials(credentials);
        RestAssured.given()
                .auth().preemptive().basic(creds.left(), creds.right())
                .log().ifValidationFails()
                .body(new RestTimeZone(zone.map(ZoneId::getId).orElse(null))).contentType("application/json")
                .expect()
                .statusCode(matching(NO_CONTENT))
                .log().ifValidationFails()
                .when().put(scope.resource);
    }

    private static <T> T withTimeZone(TimeZoneScope scope, Optional<? extends ZoneId> zone,
                                      Function<Optional<? extends ZoneId>, ? extends T> call) {
        Optional<ZoneId> previousZone = getTimeZone(scope);
        //No change necessary.
        if (previousZone.equals(zone)) {
            try {
                return call.apply(zone);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        } else {
            setTimeZone(zone, scope);
            try {
                return call.apply(zone);
            } catch (Exception e) {
                throw new RuntimeException(e);
            } finally {
                setTimeZone(previousZone, scope);
            }
        }
    }

    public static class RestTimeZone {

        public String timeZone;

        public RestTimeZone() {
        }

        public RestTimeZone(String timeZone) {
            this.timeZone = timeZone;
        }

        public String getTimeZone() {
            return timeZone;
        }
    }

    private enum TimeZoneScope {
        USER(getUserResource()), SERVER(getServerResource());

        private final String resource;

        TimeZoneScope(String serverResource) {
            this.resource = serverResource;
        }
    }
}
