package com.nike.backstopper.handler.jaxrs;

import com.nike.backstopper.apierror.ApiError;
import com.nike.backstopper.apierror.ApiErrorBase;
import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors;
import com.nike.backstopper.apierror.projectspecificinfo.ProjectSpecificErrorCodeRange;
import com.nike.backstopper.apierror.testutil.BarebonesCoreApiErrorForTesting;
import com.nike.backstopper.apierror.testutil.ProjectApiErrorsForTesting;
import com.nike.backstopper.exception.ApiException;
import com.nike.backstopper.handler.ApiExceptionHandlerUtils;
import com.nike.backstopper.handler.ErrorResponseInfo;
import com.nike.backstopper.handler.UnexpectedMajorExceptionHandlingError;
import com.nike.backstopper.handler.jaxrs.config.JaxRsApiExceptionHandlerListenerList;
import com.nike.backstopper.model.DefaultErrorContractDTO;

import com.tngtech.java.junit.dataprovider.DataProvider;
import com.tngtech.java.junit.dataprovider.DataProviderRunner;

import org.assertj.core.api.ThrowableAssert;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.internal.util.reflection.Whitebox;

import java.util.UUID;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import static java.util.Collections.singletonList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.catchThrowable;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

/**
 * Tests the functionality of {@link JaxRsApiExceptionHandler }
 *
 * @author dsand7
 * @author Michael Irwin
 */
@RunWith(DataProviderRunner.class)
public class JaxRsApiExceptionHandlerTest {

    private JaxRsApiExceptionHandler handlerSpy;
    private JaxRsUnhandledExceptionHandler unhandledSpy;
    private JaxRsApiExceptionHandlerListenerList listenerList;
    private static final ApiError EXPECTED_ERROR = new ApiErrorBase("test", 99008, "test", 8);

    private static final ProjectApiErrors testProjectApiErrors = ProjectApiErrorsForTesting.withProjectSpecificData(
        singletonList(EXPECTED_ERROR), ProjectSpecificErrorCodeRange.ALLOW_ALL_ERROR_CODES
    );

    @Before
    public void beforeMethod() {
        HttpServletRequest mockRequest = mock(HttpServletRequest.class);
        when(mockRequest.getRequestURI()).thenReturn("/fake/path");
        when(mockRequest.getMethod()).thenReturn("GET");
        when(mockRequest.getQueryString()).thenReturn("queryString");

        listenerList = new JaxRsApiExceptionHandlerListenerList(testProjectApiErrors, ApiExceptionHandlerUtils.DEFAULT_IMPL);

        unhandledSpy = spy(new JaxRsUnhandledExceptionHandler(testProjectApiErrors, ApiExceptionHandlerUtils.DEFAULT_IMPL));

        handlerSpy = spy(new JaxRsApiExceptionHandler(
            testProjectApiErrors,
            listenerList,
            ApiExceptionHandlerUtils.DEFAULT_IMPL, unhandledSpy));
        Whitebox.setInternalState(handlerSpy, "request", mockRequest);
        Whitebox.setInternalState(handlerSpy, "response", mock(HttpServletResponse.class));
    }

    @Test
    public void constructor_throws_IllegalArgumentException_if_jaxRsUnhandledExceptionHandler_is_null() {
        // when
        Throwable ex = catchThrowable(new ThrowableAssert.ThrowingCallable() {
            @Override
            public void call() throws Throwable {
                new JaxRsApiExceptionHandler(testProjectApiErrors, listenerList, ApiExceptionHandlerUtils.DEFAULT_IMPL, null);
            }
        });

        // then
        assertThat(ex).isInstanceOf(IllegalArgumentException.class);
    }

    @Test
    public void toResponseReturnsCorrectResponseForCoreApiErrorException() {

        ApiError expectedError = BarebonesCoreApiErrorForTesting.MALFORMED_REQUEST;
        ApiException.Builder exceptionBuilder = ApiException.Builder.newBuilder();
        exceptionBuilder.withExceptionMessage("test this");
        exceptionBuilder.withApiErrors(expectedError);
        Response actualResponse = handlerSpy.toResponse(exceptionBuilder.build());

        Assert.assertEquals(expectedError.getHttpStatusCode(), actualResponse.getStatus());
    }

    @Test
    public void toResponseReturnsCorrectResponseForJaxRsWebApplicationException() {

        NotFoundException exception = new NotFoundException("uri not found!");
        Response actualResponse = handlerSpy.toResponse(exception);

        Assert.assertEquals(HttpServletResponse.SC_NOT_FOUND, actualResponse.getStatus());
    }

    @Test
    public void prepareFrameworkRepresentationContainsAppropriateStatusCode() {

        Response.ResponseBuilder response = handlerSpy.prepareFrameworkRepresentation(new DefaultErrorContractDTO(null, null),
                                                                                      HttpServletResponse.SC_OK, null, null, null);
        Assert.assertEquals(HttpServletResponse.SC_OK, response.build().getStatus());
    }

    @Test
    public void prepareFrameworkRepresentationContainsEntity() {

        Response.ResponseBuilder response = handlerSpy.prepareFrameworkRepresentation(new DefaultErrorContractDTO(null, null),
                                                                                      HttpServletResponse.SC_OK, null, null, null);
        Assert.assertNotNull(response.build().getEntity());
    }

    @DataProvider(value = {
        "true",
        "false"
    })
    @Test
    public void toResponseContainsContentTypeHeaderForHandledException(boolean overrideDefaultContentType) {
        // given
        ApiException exception = ApiException.Builder.newBuilder()
                                                     .withExceptionMessage("test this")
                                                     .withApiErrors(BarebonesCoreApiErrorForTesting.MALFORMED_REQUEST)
                                                     .build();

        String expectedContentType = MediaType.APPLICATION_JSON;
        if (overrideDefaultContentType) {
            final String finalExpectedContentType = UUID.randomUUID().toString();
            expectedContentType = finalExpectedContentType;
            doAnswer(invocation -> {
                ErrorResponseInfo<Response.ResponseBuilder> errorResponseInfo =
                    (ErrorResponseInfo<Response.ResponseBuilder>) invocation.getArguments()[0];
                return errorResponseInfo.frameworkRepresentationObj.header("Content-Type", finalExpectedContentType);
            }).when(handlerSpy).setContentType(
                any(ErrorResponseInfo.class), any(HttpServletRequest.class), any(HttpServletResponse.class),
                any(Throwable.class)
            );
        }

        // when
        Response response = handlerSpy.toResponse(exception);

        // then
        verify(handlerSpy).setContentType(
            any(ErrorResponseInfo.class), any(HttpServletRequest.class), any(HttpServletResponse.class),
            eq(exception)
        );
        assertThat(response.getMetadata()).isNotNull();
        assertThat(response.getMetadata().get("Content-Type")).isEqualTo(singletonList(expectedContentType));
    }

    @DataProvider(value = {
        "true",
        "false"
    })
    @Test
    public void toResponseContainsContentTypeHeaderForUnhandledException(boolean overrideDefaultContentType) {
        // given
        String expectedContentType = MediaType.APPLICATION_JSON;
        if (overrideDefaultContentType) {
            final String finalExpectedContentType = UUID.randomUUID().toString();
            expectedContentType = finalExpectedContentType;
            doAnswer(invocation -> {
                ErrorResponseInfo<Response.ResponseBuilder> errorResponseInfo =
                    (ErrorResponseInfo<Response.ResponseBuilder>) invocation.getArguments()[0];
                return errorResponseInfo.frameworkRepresentationObj.header("Content-Type", finalExpectedContentType);
            }).when(handlerSpy).setContentType(
                any(ErrorResponseInfo.class), any(HttpServletRequest.class), any(HttpServletResponse.class),
                any(Throwable.class)
            );
        }

        // when
        Response response = handlerSpy.toResponse(new Exception("not handled"));

        // then
        verify(handlerSpy).setContentType(
            any(ErrorResponseInfo.class), any(HttpServletRequest.class), any(HttpServletResponse.class),
            any(Throwable.class)
        );
        assertThat(response.getMetadata()).isNotNull();
        assertThat(response.getMetadata().get("Content-Type")).isEqualTo(singletonList(expectedContentType));
    }

    @Test
    public void toResponse_delegates_to_unhandled_exception_handler_if_maybeHandleException_throws_UnexpectedMajorExceptionHandlingError()
        throws UnexpectedMajorExceptionHandlingError {
        // given
        doThrow(new UnexpectedMajorExceptionHandlingError("foo", null))
            .when(handlerSpy).maybeHandleException(any(Throwable.class), any(HttpServletRequest.class), any(HttpServletResponse.class));
        Exception ex = new Exception("kaboom");
        String uniqueHeader = UUID.randomUUID().toString();
        ErrorResponseInfo<Response.ResponseBuilder> unhandledHandlerResponse =
            new ErrorResponseInfo<>(500, Response.serverError().header("unique-header", uniqueHeader), null);
        doReturn(unhandledHandlerResponse)
            .when(unhandledSpy).handleException(any(Throwable.class), any(HttpServletRequest.class), any(HttpServletResponse.class));

        // when
        Response response = handlerSpy.toResponse(ex);

        // then
        verify(handlerSpy).maybeHandleException(any(Throwable.class), any(HttpServletRequest.class), any(HttpServletResponse.class));
        verify(unhandledSpy).handleException(any(Throwable.class), any(HttpServletRequest.class), any(HttpServletResponse.class));
        assertThat(response.getMetadata().get("unique-header")).isEqualTo(singletonList(uniqueHeader));
    }

}