package com.atlassian.bitbucket.test.rest.server.stub.mirror;

import com.atlassian.bitbucket.test.DefaultFuncTestData;
import com.atlassian.bitbucket.test.rest.server.stub.StubResponse;
import com.atlassian.bitbucket.test.rest.server.stub.webhook.StubWebHookSink;
import com.atlassian.fugue.Option;
import com.atlassian.httpclient.api.Request.Method;
import com.atlassian.jwt.SigningAlgorithm;
import com.atlassian.jwt.core.writer.JsonSmartJwtJsonBuilder;
import com.atlassian.jwt.core.writer.JwtClaimsBuilder;
import com.atlassian.jwt.core.writer.NimbusJwtWriterFactory;
import com.atlassian.jwt.httpclient.CanonicalHttpUriRequest;
import com.atlassian.jwt.writer.JwtJsonBuilder;
import com.atlassian.jwt.writer.JwtWriterFactory;
import org.codehaus.jackson.map.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.UnsupportedEncodingException;
import java.security.NoSuchAlgorithmException;
import java.util.Collections;
import java.util.Date;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

import static com.confluex.mock.http.matchers.HttpMatchers.get;
import static com.confluex.mock.http.matchers.HttpMatchers.post;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.util.concurrent.TimeUnit.SECONDS;

/**
 * A stub mirror that can provide an Atlassian Connect descriptor and lifecycle callback methods sufficient to
 * act as a mirror of a Bitbucket server. Each callback can be individually mocked (to capture request data and fashion
 * responses) as can the descriptor that is generated.
 * <p>
 * Example usage is as follows:
 * <pre>
 * // starts listening on a random port and serving up a descriptor.xml and return 200 for POSTs to all callbacks
 * mirror = new StubMirror(mirrorId);
 * // specialise how the installed callback is handled
 * mirror.onInstalledCallback(mockResponseAndCaptureRequestDetails());
 * // trigger the upstream server to install the mirror
 * // ..
 * // ..
 * // ensure the installed callback was called (waitFor...(timeout) will return instantly if a matching request has
 * // already occurred
 * assertTrue(mirror.waitForInstalledCallback(TimeUnit.SECONDS.toMillis(10)), "installed callback not invoked");
 * // assert properties on the captured request
 * // ..
 * // ..
 * </pre>
 */
public class StubMirror extends StubWebHookSink {
    /**
     * How long to wait when we expect a request to be delivered
     */
    public static final long DEFAULT_EXPECTED_TIMEOUT = SECONDS.toMillis(30);
    /**
     * How long to wait when we expect a request to not be delivered
     */
    public static final long DEFAULT_ENSURE_TIMEOUT = SECONDS.toMillis(5);

    private static Logger log = LoggerFactory.getLogger(StubMirror.class);

    private final String addonKey;
    private final String id;
    private final JwtWriterFactory jwtWriter;
    private final String serverId;
    private volatile Option<String> sharedSecret;

    public StubMirror(String id) {
        checkNotNull(id, "id");

        this.id = id + "-" + Long.toString(new Date().getTime(), 32);
        this.serverId = UUID.randomUUID().toString();
        this.addonKey = "addon-" + serverId;
        this.jwtWriter = new NimbusJwtWriterFactory();
        this.sharedSecret = Option.none();
    }

    public StubServerResponder<StubMirror> captureSharedSecret(final int statusCode) {
        return (mirror, request) -> {
            try {
                Map properties = new ObjectMapper().readValue(request.getBody(), Map.class);
                String sharedSecret = (String) properties.get("sharedSecret");
                if (sharedSecret == null) {
                    log.error("No shared secret found during stub mirror install callback. Request body:\n{}", request.getBody());
                }
                StubMirror.this.sharedSecret = Option.some(sharedSecret);
            } catch (Exception e) {
                log.error("Failed to capture shared secret during stub mirror install callback. Request body:\n{}", request.getBody(), e);
            }

            return new StubResponse(statusCode);
        };
    }

    public boolean canSignRequests() {
        return sharedSecret.isDefined();
    }

    @Override
    public void configureDefaultResponders() {
        onDescriptorRequested(serveDefaultDescriptor(200));
        onInstalledCallback(captureSharedSecret(200));
        onUninstalledCallback(statusCode(200));
        onEnabledCallback(statusCode(200));
        onDisabledCallback(statusCode(200));
        onWebhookEventCallback(statusCode(200));
    }

    public String getAddonKey() {
        return addonKey;
    }

    public String getDescriptorPath() {
        return getDescriptorPath(serverId);
    }

    public String getDescriptorUrl() {
        return getUrl(getDescriptorPath());
    }

    public String getDisabledPath() {
        return String.format("/%s/disabled", serverId);
    }

    public String getEnabledPath() {
        return String.format("/%s/enabled", serverId);
    }

    public String getId() {
        return id;
    }

    public String getInstalledPath() {
        return String.format("/%s/installed", serverId);
    }

    public Option<String> getSharedSecret() {
        return sharedSecret;
    }

    public String getUninstalledPath() {
        return String.format("/%s/uninstalled", serverId);
    }

    @Override
    public String getWebhookEventsPath() {
        return String.format("/%s/events", serverId);
    }

    public void onDescriptorRequested(StubServerResponder<StubMirror> responder) {
        respondTo(get(getDescriptorPath()), responder);
    }

    public void onDisabledCallback(StubServerResponder<StubMirror> responder) {
        respondTo(post(getDisabledPath()), responder);
    }

    public void onEnabledCallback(StubServerResponder<StubMirror> responder) {
        respondTo(post(getEnabledPath()), responder);
    }

    public void onInstalledCallback(StubServerResponder<StubMirror> responder) {
        respondTo(post(getInstalledPath()), responder);
    }

    public void onUninstalledCallback(StubServerResponder<StubMirror> responder) {
        respondTo(post(getUninstalledPath()), responder);
    }

    public String signRequest(Method method, String baseUrl, String requestUrl) throws UnsupportedEncodingException, NoSuchAlgorithmException {
        return signRequest(method, baseUrl, requestUrl, Collections.<String, String[]>emptyMap());
    }

    public String signRequest(Method method, String baseUrl, String requestUrl, Map<String, String[]> parameters)
            throws UnsupportedEncodingException, NoSuchAlgorithmException {

        if (!canSignRequests()) {
            throw new IllegalStateException("This mirror has not been installed or the install request was not captured");
        }

        checkNotNull(method, "method");
        checkNotNull(baseUrl, "baseUrl");
        checkNotNull(baseUrl, "requestUrl");
        checkArgument(requestUrl.startsWith(baseUrl), "requestUrl must start with baseUrl");

        String relativeUrl = requestUrl.substring(DefaultFuncTestData.getBaseURL().length());

        CanonicalHttpUriRequest canonical = new CanonicalHttpUriRequest(method.name(), relativeUrl, "", parameters);
        long issuedAt = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis());
        long expiresAt = issuedAt + 180L;
        JwtJsonBuilder jwtBuilder = new JsonSmartJwtJsonBuilder()
                .issuedAt(issuedAt)
                .expirationTime(expiresAt)
                .issuer(addonKey);

        JwtClaimsBuilder.appendHttpRequestClaims(jwtBuilder, canonical);

        return jwtWriter
                .macSigningWriter(SigningAlgorithm.HS256, sharedSecret.get())
                .jsonToJwt(jwtBuilder.build());
    }

    public boolean waitForDescriptorRequest(long timeout) {
        return server.waitFor(get(getDescriptorPath()), timeout);
    }

    public boolean waitForDisabledCallback(long timeout) {
        return waitForPost(getDisabledPath(), timeout);
    }

    public boolean waitForEnabledCallback(long timeout) {
        return waitForPost(getEnabledPath(), timeout);
    }

    public boolean waitForInstalledCallback(long timeout) {
        return waitForPost(getInstalledPath(), timeout);
    }

    public boolean waitForUninstalledCallback(long timeout) {
        return waitForPost(getUninstalledPath(), timeout);
    }

    public static StubServerResponder<StubMirror> serveDefaultDescriptor(final int statusCode) {
        return (mirror, request) ->
                new StubResponse(statusCode, DescriptorUtils.generateDescriptor(
                        mirror.getAddonKey(), "Atlassian Stash Stub Mirror", mirror.getBaseUrl(),
                        mirror.getInstalledPath(), mirror.getUninstalledPath(), mirror.getEnabledPath(),
                        mirror.getDisabledPath(), mirror.getWebhookEventsPath(), mirror.getBaseUrl()));
    }

    public static String getDescriptorUrl(int port, String mirrorId) {
        return joinUrlAndPath(getBaseUrl(port), getDescriptorPath(mirrorId));
    }

    public static String getDescriptorUrl(String baseUrl, String mirrorId) {
        return joinUrlAndPath(baseUrl, getDescriptorPath(mirrorId));
    }

    private static String getDescriptorPath(String mirrorId) {
        return String.format("/add-ons/%s/descriptor.xml", mirrorId);
    }
}