package io.dropwizard.auth;

import io.dropwizard.jersey.DropwizardResourceConfig;
import io.dropwizard.logging.BootstrapLogging;
import org.glassfish.jersey.servlet.ServletProperties;
import org.glassfish.jersey.test.DeploymentContext;
import org.glassfish.jersey.test.JerseyTest;
import org.glassfish.jersey.test.ServletDeploymentContext;
import org.glassfish.jersey.test.TestProperties;
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
import org.glassfish.jersey.test.spi.TestContainerException;
import org.glassfish.jersey.test.spi.TestContainerFactory;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.HttpHeaders;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown;

public abstract class AuthBaseTest<T extends DropwizardResourceConfig> extends JerseyTest {
    protected static final String ADMIN_ROLE = "ADMIN";
    protected static final String ADMIN_USER = "good-guy";
    protected static final String ORDINARY_USER = "ordinary-guy";
    protected static final String BADGUY_USER = "bad-guy";
    protected static final String CUSTOM_PREFIX = "Custom";
    protected static final String BEARER_PREFIX = "Bearer";
    protected static final String BASIC_PREFIX = "Basic";
    protected static final String ORDINARY_USER_ENCODED_TOKEN = "b3JkaW5hcnktZ3V5OnNlY3JldA==";
    protected static final String GOOD_USER_ENCODED_TOKEN = "Z29vZC1ndXk6c2VjcmV0";
    protected static final String BAD_USER_ENCODED_TOKEN = "YmFkLWd1eTpzZWNyZXQ=";

    static {
        BootstrapLogging.bootstrap();
    }

    @Override
    protected TestContainerFactory getTestContainerFactory()
        throws TestContainerException {
        return new GrizzlyWebTestContainerFactory();
    }

    protected abstract DropwizardResourceConfig getDropwizardResourceConfig();
    protected abstract Class<T> getDropwizardResourceConfigClass();
    protected abstract String getPrefix();
    protected abstract String getOrdinaryGuyValidToken();
    protected abstract String getGoodGuyValidToken();
    protected abstract String getBadGuyToken();

    @Override
    @BeforeEach
    public void setUp() throws Exception {
        super.setUp();
    }

    @Override
    @AfterEach
    public void tearDown() throws Exception {
        super.tearDown();
    }

    @Override
    protected DeploymentContext configureDeployment() {
        forceSet(TestProperties.CONTAINER_PORT, "0");
        return ServletDeploymentContext.builder(getDropwizardResourceConfig())
            .initParam(ServletProperties.JAXRS_APPLICATION_CLASS, getDropwizardResourceConfigClass().getName())
            .build();
    }

    @Test
    public void respondsToMissingCredentialsWith401() throws Exception {
        try {
            target("/test/admin").request().get(String.class);
            failBecauseExceptionWasNotThrown(WebApplicationException.class);
        } catch (WebApplicationException e) {
            assertThat(e.getResponse().getStatus()).isEqualTo(401);
            assertThat(e.getResponse().getHeaders().get(HttpHeaders.WWW_AUTHENTICATE))
                .containsOnly(getPrefix() + " realm=\"realm\"");
        }
    }

    @Test
    public void resourceWithoutAuth200() {
        assertThat(target("/test/noauth").request()
            .get(String.class))
            .isEqualTo("hello");
    }

    @Test
    public void resourceWithAuthenticationWithoutAuthorizationWithCorrectCredentials200() {
        assertThat(target("/test/profile").request()
            .header(HttpHeaders.AUTHORIZATION, getPrefix() + " " + getOrdinaryGuyValidToken())
            .get(String.class))
            .isEqualTo("'" + ORDINARY_USER + "' has user privileges");
    }

    @Test
    public void resourceWithAuthenticationWithoutAuthorizationNoCredentials401() {
        try {
            target("/test/profile").request().get(String.class);
            failBecauseExceptionWasNotThrown(WebApplicationException.class);
        } catch (WebApplicationException e) {
            assertThat(e.getResponse().getStatus()).isEqualTo(401);
            assertThat(e.getResponse().getHeaders().get(HttpHeaders.WWW_AUTHENTICATE))
                .containsOnly(getPrefix() + " realm=\"realm\"");
        }
    }

    @Test
    public void resourceWithValidOptionalAuthentication200() {
        assertThat(target("/test/optional").request()
            .header(HttpHeaders.AUTHORIZATION, getPrefix() + " " + getOrdinaryGuyValidToken())
            .get(String.class))
            .isEqualTo("principal was present");
    }

    @Test
    public void resourceWithInvalidOptionalAuthentication200() {
        assertThat(target("/test/optional").request()
            .header(HttpHeaders.AUTHORIZATION, getPrefix() + " " + getBadGuyToken())
            .get(String.class))
            .isEqualTo("principal was not present");
    }

    @Test
    public void resourceWithoutOptionalAuthentication200() {
        assertThat(target("/test/optional").request()
            .get(String.class))
            .isEqualTo("principal was not present");
    }

    @Test
    public void resourceWithAuthorizationPrincipalIsNotAuthorized403() {
        try {
            target("/test/admin").request()
                .header(HttpHeaders.AUTHORIZATION, getPrefix() + " " + getOrdinaryGuyValidToken())
                .get(String.class);
            failBecauseExceptionWasNotThrown(WebApplicationException.class);
        } catch (WebApplicationException e) {
            assertThat(e.getResponse().getStatus()).isEqualTo(403);
        }
    }


    @Test
    public void resourceWithDenyAllAndNoAuth401() {
        try {
            target("/test/denied").request().get(String.class);
            failBecauseExceptionWasNotThrown(WebApplicationException.class);
        } catch (WebApplicationException e) {
            assertThat(e.getResponse().getStatus()).isEqualTo(401);
        }
    }

    @Test
    public void resourceWithDenyAllAndAuth403() {
        try {
            target("/test/denied").request()
                .header(HttpHeaders.AUTHORIZATION, getPrefix() + " " + getGoodGuyValidToken())
                .get(String.class);
            failBecauseExceptionWasNotThrown(WebApplicationException.class);
        } catch (WebApplicationException e) {
            assertThat(e.getResponse().getStatus()).isEqualTo(403);
        }
    }

    @Test
    public void transformsCredentialsToPrincipals() throws Exception {
        assertThat(target("/test/admin").request()
            .header(HttpHeaders.AUTHORIZATION, getPrefix() + " " + getGoodGuyValidToken())
            .get(String.class))
            .isEqualTo("'" + ADMIN_USER + "' has admin privileges");
    }

    @Test
    public void transformsCredentialsToPrincipalsWhenAuthAnnotationExistsWithoutMethodAnnotation() throws Exception {
        assertThat(target("/test/implicit-permitall").request()
            .header(HttpHeaders.AUTHORIZATION, getPrefix() + " " + getGoodGuyValidToken())
            .get(String.class))
            .isEqualTo("'" + ADMIN_USER + "' has user privileges");
    }


    @Test
    public void respondsToNonBasicCredentialsWith401() throws Exception {
        try {
            target("/test/admin").request()
                .header(HttpHeaders.AUTHORIZATION, "Derp irrelevant")
                .get(String.class);
            failBecauseExceptionWasNotThrown(WebApplicationException.class);
        } catch (WebApplicationException e) {
            assertThat(e.getResponse().getStatus()).isEqualTo(401);
            assertThat(e.getResponse().getHeaders().get(HttpHeaders.WWW_AUTHENTICATE))
                .containsOnly(getPrefix() + " realm=\"realm\"");
        }
    }

    @Test
    public void respondsToExceptionsWith500() throws Exception {
        try {
            target("/test/admin").request()
                .header(HttpHeaders.AUTHORIZATION, getPrefix() + " " + getBadGuyToken())
                .get(String.class);
            failBecauseExceptionWasNotThrown(WebApplicationException.class);
        } catch (WebApplicationException e) {
            assertThat(e.getResponse().getStatus()).isEqualTo(500);
        }
    }
}
