package edu.ohio.ais.rundeck;

import com.dtolabs.rundeck.core.execution.workflow.steps.PluginStepContextImpl;
import com.dtolabs.rundeck.core.execution.workflow.steps.StepException;
import com.dtolabs.rundeck.core.execution.workflow.steps.StepFailureReason;
import com.dtolabs.rundeck.core.plugins.configuration.Description;
import com.github.tomakehurst.wiremock.client.WireMock;
import com.github.tomakehurst.wiremock.junit.WireMockRule;
import edu.ohio.ais.rundeck.util.OAuthClientTest;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import static org.junit.Assert.*;

public class HttpWorkflowStepPluginTest {
    protected static final String REMOTE_URL = "/trigger";
    protected static final String BOGUS_URL = "/bogus";
    protected static final String REMOTE_BASIC_URL = "/trigger-basic";
    protected static final String REMOTE_SLOW_URL = "/slow-trigger";
    protected static final String REMOTE_OAUTH_URL = "/oauth";
    protected static final String REMOTE_OAUTH_EXPIRED_URL = "/oauth-expired";
    protected static final String ERROR_URL_500 = "/error500";
    protected static final String ERROR_URL_401 = "/error401";
    protected static final String OAUTH_CLIENT_MAP_KEY = OAuthClientTest.CLIENT_VALID + "@"
            + OAuthClientTest.BASE_URI + OAuthClientTest.ENDPOINT_TOKEN;

    protected static final int REQUEST_TIMEOUT = 2*1000;
    protected static final int SLOW_TIMEOUT = 3*1000;

    protected HttpWorkflowStepPlugin plugin;
    protected OAuthClientTest oAuthClientTest = new OAuthClientTest();

    /**
     * Setup options for simple execution for the given method.
     * @param method HTTP Method to use.
     * @return Options for the execution.
     */
    public Map<String, Object> getExecutionOptions(String method) {
        Map<String, Object> options = new HashMap<>();

        options.put("remoteUrl", OAuthClientTest.BASE_URI + REMOTE_URL);
        options.put("method", method);

        return options;
    }

    /**
     * Setup options for execution for the given method using HTTP BASIC.
     * @param method HTTP Method to use.
     * @return Options for the execution.
     */
    public Map<String, Object> getBasicOptions(String method) {
        Map<String, Object> options = getExecutionOptions(method);

        options.put("username", OAuthClientTest.CLIENT_VALID);
        options.put("password", OAuthClientTest.CLIENT_SECRET);
        options.put("authentication", HttpWorkflowStepPlugin.AUTH_BASIC);

        return options;
    }

    /**
     * Setup options for simple execution for the given method using OAuth 2.0.
     * @param method HTTP Method to use.
     * @return Options for the execution.
     */
    public Map<String, Object> getOAuthOptions(String method) {
        Map<String, Object> options = getBasicOptions(method);

        options.put("remoteUrl", OAuthClientTest.BASE_URI + REMOTE_OAUTH_URL);
        options.put("oauthTokenEndpoint", OAuthClientTest.BASE_URI + OAuthClientTest.ENDPOINT_TOKEN);
        options.put("oauthValidateEndpoint", OAuthClientTest.BASE_URI + OAuthClientTest.ENDPOINT_VALIDATE);
        options.put("authentication", HttpWorkflowStepPlugin.AUTH_OAUTH2);

        return options;
    }

    @Rule
    public WireMockRule wireMockRule = new WireMockRule(18089);

    @Before
    public void setUp() {
        plugin = new HttpWorkflowStepPlugin();
        oAuthClientTest.setUp(); // We need to setup the OAuth endpoints too.

        synchronized(HttpWorkflowStepPlugin.oauthClients) {
            HttpWorkflowStepPlugin.oauthClients.clear();
        }

        // Test all endpoints by simply iterating.
        for(String method : HttpWorkflowStepPlugin.HTTP_METHODS) {
            // Simple endpoint
            WireMock.stubFor(WireMock.request(method, WireMock.urlEqualTo(REMOTE_URL)).atPriority(100)
                    .willReturn(WireMock.aResponse()
                            .withStatus(200)));

            // HTTP Basic
            WireMock.stubFor(WireMock.request(method, WireMock.urlEqualTo(REMOTE_BASIC_URL))
                    .withBasicAuth(OAuthClientTest.CLIENT_VALID, OAuthClientTest.CLIENT_SECRET)
                    .willReturn(WireMock.aResponse()
                            .withStatus(200)));

            // OAuth with a fresh token
            WireMock.stubFor(WireMock.request(method, WireMock.urlEqualTo(REMOTE_OAUTH_URL))
                    .withHeader("Authorization", WireMock.equalTo("Bearer " + OAuthClientTest.ACCESS_TOKEN_VALID))
                    .willReturn(WireMock.aResponse()
                            .withStatus(200)));

            // BASIC that returns a 401
            WireMock.stubFor(WireMock.request(method, WireMock.urlEqualTo(ERROR_URL_401))
                    .willReturn(WireMock.aResponse()
                            .withStatus(401)));

            // OAuth with an expired token
            WireMock.stubFor(WireMock.request(method, WireMock.urlEqualTo(REMOTE_OAUTH_EXPIRED_URL))
                    .withHeader("Authorization", WireMock.equalTo("Bearer " + OAuthClientTest.ACCESS_TOKEN_EXPIRED))
                    .willReturn(WireMock.aResponse()
                            .withStatus(401)));
            WireMock.stubFor(WireMock.request(method, WireMock.urlEqualTo(REMOTE_OAUTH_EXPIRED_URL))
                    .withHeader("Authorization", WireMock.equalTo("Bearer " + OAuthClientTest.ACCESS_TOKEN_VALID))
                    .willReturn(WireMock.aResponse()
                            .withStatus(200)));

            // 500 Error
            WireMock.stubFor(WireMock.request(method, WireMock.urlEqualTo(ERROR_URL_500))
                    .willReturn(WireMock.aResponse()
                            .withStatus(500)));
        }

        // Simple bogus URL that yields a 404
        WireMock.stubFor(WireMock.request("GET", WireMock.urlEqualTo(BOGUS_URL))
                        .willReturn(WireMock.aResponse().withStatus(404)));

        // Timeout test
        WireMock.stubFor(WireMock.request("GET", WireMock.urlEqualTo(REMOTE_SLOW_URL))
                .willReturn(WireMock.aResponse().withFixedDelay(SLOW_TIMEOUT).withStatus(200)));
    }

    @Test()
    public void canGetPluginDescription() {
        Description description = this.plugin.getDescription();

        assertEquals(description.getName(), HttpWorkflowStepPlugin.SERVICE_PROVIDER_NAME);
    }

    @Test()
    public void canValidateConfiguration() {
        Map<String, Object> options = new HashMap<>();

        try {
            this.plugin.executeStep(new PluginStepContextImpl(), options);
            fail("Expected configuration exception.");
        } catch (StepException se) {
            assertEquals(se.getFailureReason(), StepFailureReason.ConfigurationFailure);
        }

        options.put("remoteUrl", REMOTE_URL);
        options.put("method", "GET");
        options.put("authentication", HttpWorkflowStepPlugin.AUTH_BASIC);

        try {
            this.plugin.executeStep(new PluginStepContextImpl(), options);
            fail("Expected configuration exception.");
        } catch (StepException se) {
            assertEquals(se.getFailureReason(), StepFailureReason.ConfigurationFailure);
        }

        options.put("authentication", HttpWorkflowStepPlugin.AUTH_OAUTH2);

        try {
            this.plugin.executeStep(new PluginStepContextImpl(), options);
            fail("Expected configuration exception.");
        } catch (StepException se) {
            assertEquals(se.getFailureReason(), StepFailureReason.ConfigurationFailure);
        }
    }

    @Test()
    public void canCallSimpleEndpoint() throws StepException {
        for(String method : HttpWorkflowStepPlugin.HTTP_METHODS) {
            this.plugin.executeStep(new PluginStepContextImpl(), this.getExecutionOptions(method));
        }
    }

    @Test()
    public void canSetCustomTimeout() throws StepException {
        Map<String, Object> options = new HashMap<>();

        options.put("remoteUrl", OAuthClientTest.BASE_URI + REMOTE_URL);
        options.put("method", "GET");
        options.put("timeout", REQUEST_TIMEOUT);

        this.plugin.executeStep(new PluginStepContextImpl(), options);

        try {
            options.put("remoteUrl", OAuthClientTest.BASE_URI + REMOTE_SLOW_URL);
            this.plugin.executeStep(new PluginStepContextImpl(), options);
            fail("Expected exception " + StepException.class.getCanonicalName() + " not thrown.");
        } catch(StepException se) {}

        options.put("timeout", SLOW_TIMEOUT + 1000);
        this.plugin.executeStep(new PluginStepContextImpl(), options);
    }

    @Test()
    public void canCallBasicEndpoint() throws StepException {
        for(String method : HttpWorkflowStepPlugin.HTTP_METHODS) {
            Map<String, Object> options = this.getBasicOptions(method);
            options.put("remoteUrl", OAuthClientTest.BASE_URI + REMOTE_BASIC_URL);

            this.plugin.executeStep(new PluginStepContextImpl(), options);
        }
    }

    @Test(expected = StepException.class)
    public void canHandle500Error() throws StepException {
        Map<String, Object> options = new HashMap<>();

        options.put("remoteUrl", OAuthClientTest.BASE_URI + ERROR_URL_500);
        options.put("method", "GET");

        this.plugin.executeStep(new PluginStepContextImpl(), options);
    }

    @Test(expected = StepException.class)
    public void canHandleBadUrl() throws StepException {
        Map<String, Object> options = new HashMap<>();

        options.put("remoteUrl", OAuthClientTest.BASE_URI + BOGUS_URL);
        options.put("method", "GET");

        this.plugin.executeStep(new PluginStepContextImpl(), options);
    }

    @Test(expected = StepException.class)
    public void canHandleBadHost() throws StepException {
        Map<String, Object> options = new HashMap<>();

        options.put("remoteUrl", "http://neverGoingToBe.aProperUrl/bogus");
        options.put("method", "GET");

        this.plugin.executeStep(new PluginStepContextImpl(), options);
    }

    @Test(expected = StepException.class)
    public void canHandleBASICWrongAuthType() throws StepException {
        Map<String, Object> options = new HashMap<>();

        options.put("remoteUrl", OAuthClientTest.BASE_URI + ERROR_URL_401);
        options.put("method", "GET");
        options.put("username", OAuthClientTest.CLIENT_VALID);
        options.put("password", OAuthClientTest.CLIENT_SECRET);
        options.put("authentication", HttpWorkflowStepPlugin.AUTH_BASIC);

        this.plugin.executeStep(new PluginStepContextImpl(), options);
    }

    @Test(expected = StepException.class)
    public void canHandleAuthenticationRequired() throws StepException {
        Map<String, Object> options = new HashMap<>();

        options.put("remoteUrl", OAuthClientTest.BASE_URI + ERROR_URL_401);
        options.put("method", "GET");

        this.plugin.executeStep(new PluginStepContextImpl(), options);
    }

    @Test()
    public void canCallOAuthEndpoint() throws StepException {
        for(String method : HttpWorkflowStepPlugin.HTTP_METHODS) {
            this.plugin.executeStep(new PluginStepContextImpl(), this.getOAuthOptions(method));
        }
    }

    @Test()
    public void canCallOAuthEndpointWithExpiredToken() throws StepException {
        HttpWorkflowStepPlugin.oauthClients.put(OAUTH_CLIENT_MAP_KEY, this.oAuthClientTest.setupClient(OAuthClientTest.ACCESS_TOKEN_EXPIRED));

        for(String method : HttpWorkflowStepPlugin.HTTP_METHODS) {
            Map<String, Object> options = this.getOAuthOptions(method);
            options.put("remoteUrl", OAuthClientTest.BASE_URI + REMOTE_OAUTH_EXPIRED_URL);

            this.plugin.executeStep(new PluginStepContextImpl(), options);
        }
    }

    @Test(expected = StepException.class)
    public void cannotCallOAuthEndpointWithCredentials() throws StepException {
        Map<String, Object> options = this.getOAuthOptions("GET");
        options.put("username", OAuthClientTest.CLIENT_INVALID);
        options.put("password", OAuthClientTest.CLIENT_SECRET);

        this.plugin.executeStep(new PluginStepContextImpl(), options);
    }

    @Test(expected = StepException.class)
    public void canHandle500ErrorWithOAuth() throws StepException {
        Map<String, Object> options = getOAuthOptions("GET");

        options.put("remoteUrl", OAuthClientTest.BASE_URI + ERROR_URL_500);

        this.plugin.executeStep(new PluginStepContextImpl(), options);
    }

    @Test()
    public void canHandleMultipleThreads() throws ExecutionException, InterruptedException {
        ExecutorService executor = Executors.newFixedThreadPool(HttpWorkflowStepPlugin.HTTP_METHODS.length);
        ArrayList<Future<Boolean>> results = new ArrayList<>();

        for(String method : HttpWorkflowStepPlugin.HTTP_METHODS) {
            results.add(executor.submit(() -> {
                HttpWorkflowStepPlugin threadedPlugin = new HttpWorkflowStepPlugin();

                try {
                    threadedPlugin.executeStep(new PluginStepContextImpl(), this.getOAuthOptions(method));
                    return true;
                } catch(StepException se) {
                    se.printStackTrace();
                    return false;
                }
            }));
        }

        assertEquals(HttpWorkflowStepPlugin.HTTP_METHODS.length, results.size());

        for(Future<Boolean> result : results) {
            assertTrue(result.get());
        }
    }
}