package org.carlspring.strongbox.controllers;

import org.carlspring.strongbox.config.IntegrationTest;
import org.carlspring.strongbox.rest.common.RestAssuredBaseTest;
import org.carlspring.strongbox.users.security.SecurityTokenProvider;

import javax.inject.Inject;
import java.util.Collections;
import java.util.Locale;

import org.jose4j.jwt.NumericDate;
import org.json.JSONObject;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.test.context.TestSecurityContextHolder;
import static io.restassured.module.mockmvc.RestAssuredMockMvc.given;
import static java.nio.charset.StandardCharsets.ISO_8859_1;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.notNullValue;

/**
 * @author adavid9
 * @author Pablo Tirado
 */
@IntegrationTest
public class JwtAuthenticationTest
        extends RestAssuredBaseTest
{

    private static final String UNAUTHORIZED_MESSAGE_CODE = "ExceptionTranslationFilter.insufficientAuthentication";

    private final MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();

    @Inject
    private SecurityTokenProvider securityTokenProvider;

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

        setContextBaseUrl(getContextBaseUrl() + "/api");
        TestSecurityContextHolder.clearContext();
        SecurityContextHolder.clearContext();
    }

    @Test
    public void testJWTAuthShouldPassWithToken()
            throws Exception
    {
        String url = getContextBaseUrl() + "/users";

        String basicAuth = "Basic YWRtaW46cGFzc3dvcmQ=";

        String body = given().header(HttpHeaders.AUTHORIZATION, basicAuth)
                             .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
                             .when()
                             .get(getContextBaseUrl() + "/login")
                             .then()
                             .statusCode(HttpStatus.OK.value())
                             .extract()
                             .asString();
        TestSecurityContextHolder.clearContext();
        SecurityContextHolder.clearContext();

        given().header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
               .when()
               .get(url)
               .then()
               .statusCode(HttpStatus.UNAUTHORIZED.value())
               .body(notNullValue());
        TestSecurityContextHolder.clearContext();
        SecurityContextHolder.clearContext();

        // this token will expire after 1 hour
        String tokenValue = getTokenValue(body);
        given().header(HttpHeaders.AUTHORIZATION, getAuthorizationHeader(tokenValue))
               .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
               .when()
               .get(url)
               .then()
               .statusCode(HttpStatus.OK.value())
               .body(notNullValue());
    }

    @Test
    public void testJWTAuthShouldFailWithoutToken()
    {
        String defaultExpectedErrorMessage = messages.getMessage(UNAUTHORIZED_MESSAGE_CODE,
                                                                 Locale.ENGLISH);
        String expectedErrorMessage = messages.getMessage(UNAUTHORIZED_MESSAGE_CODE,
                                                          defaultExpectedErrorMessage);
        String utf8ExpectedErrorMessage = new String(expectedErrorMessage.getBytes(ISO_8859_1), UTF_8);

        String url = getContextBaseUrl() + "/users";

        given().header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
               .when()
               .get(url)
               .then()
               .statusCode(HttpStatus.UNAUTHORIZED.value())
               .body("error", equalTo(utf8ExpectedErrorMessage));
    }

    @Test
    public void testJWTInvalidToken()
    {
        String url = getContextBaseUrl() + "/users";

        String invalid_token = "ABCD";

        given().header(HttpHeaders.AUTHORIZATION, getAuthorizationHeader(invalid_token))
               .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
               .when()
               .get(url)
               .then()
               .statusCode(HttpStatus.UNAUTHORIZED.value())
               .body("error", equalTo("invalid.token"));
    }

    @Test
    public void testJWTExpirationToken()
            throws Exception
    {
        String url = getContextBaseUrl() + "/users";

        // create token that will expire after 1 second
        String expiredToken = securityTokenProvider.getToken("admin", Collections.emptyMap(), 3, null);

        given().header(HttpHeaders.AUTHORIZATION, getAuthorizationHeader(expiredToken))
               .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
               .when()
               .get(url)
               .then()
               .statusCode(HttpStatus.OK.value())
               .body(notNullValue());

        Thread.sleep(3000);

        given().header(HttpHeaders.AUTHORIZATION, getAuthorizationHeader(expiredToken))
               .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
               .when()
               .get(url)
               .then()
               .statusCode(HttpStatus.UNAUTHORIZED.value())
               .body("error", equalTo("expired"));
    }

    @Test
    public void testJWTIssuedAtFuture()
            throws Exception
    {
        String url = getContextBaseUrl() + "/users";

        NumericDate futureNumericDate = NumericDate.now();
        // add five minutes to the current time to create a JWT issued in the future
        futureNumericDate.addSeconds(300);

        String token = securityTokenProvider.getToken("admin", Collections.emptyMap(), 10, futureNumericDate);

        given().header(HttpHeaders.AUTHORIZATION, getAuthorizationHeader(token))
               .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
               .when()
               .get(url)
               .then()
               .statusCode(HttpStatus.UNAUTHORIZED.value())
               .body("error", equalTo("invalid.token"));
    }

    private String getTokenValue(String body)
            throws Exception
    {
        JSONObject extractToken = new JSONObject(body);
        return extractToken.getString("token");
    }

    private String getAuthorizationHeader(String tokenValue)
    {
        return String.format("Bearer %s", tokenValue);
    }

}

@@ -256,24 +256,57 @@
 ect.Inject;%0A
+import java.nio.charset.Charset;%0A
 import java.
@@ -1038,63 +1038,8 @@
 _1;%0A
-import static java.nio.charset.StandardCharsets.UTF_8;%0A
 impo
@@ -3436,32 +3436,24 @@
 ing defaultE
-xpectedE
 rrorMessage 
@@ -3558,24 +3558,16 @@
         
-        
 Locale.E
@@ -3575,16 +3575,17 @@
 GLISH);%0A
+%0A
         
@@ -3592,24 +3592,16 @@
 String e
-xpectedE
 rrorMess
@@ -3707,31 +3707,15 @@
     
-        defaultExpected
+default
 Erro
@@ -3721,24 +3721,25 @@
 orMessage);%0A
+%0A
         Stri
@@ -3741,26 +3741,21 @@
  String 
-utf8Expect
+decod
 edErrorM
@@ -3775,24 +3775,16 @@
 String(e
-xpectedE
 rrorMess
@@ -3812,14 +3812,81 @@
 _1),
- UTF_8
+%0A                                                Charset.defaultCharset()
 );%0A%0A
@@ -4189,18 +4189,13 @@
 lTo(
-utf8Expect
+decod
 edEr

Set default charset when decoding the error message.