/**
 * Copyright 2017-2019 The Feign Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */
package feign.error;

import feign.Request;
import feign.codec.Decoder;
import feign.optionals.OptionalDecoder;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import org.junit.runners.Parameterized.Parameter;
import java.util.*;
import static org.assertj.core.api.Assertions.assertThat;
import static feign.error.AnnotationErrorDecoderExceptionConstructorsTest.TestClientInterfaceWithDifferentExceptionConstructors;
import static feign.error.AnnotationErrorDecoderExceptionConstructorsTest.TestClientInterfaceWithDifferentExceptionConstructors.*;

@RunWith(Parameterized.class)
public class AnnotationErrorDecoderExceptionConstructorsTest extends
    AbstractAnnotationErrorDecoderTest<TestClientInterfaceWithDifferentExceptionConstructors> {


  private static final String NO_BODY = "NO BODY";
  private static final Object NULL_BODY = null;
  private static final String NON_NULL_BODY = "A GIVEN BODY";
  private static final feign.Request REQUEST = feign.Request.create(feign.Request.HttpMethod.GET,
      "http://test", Collections.emptyMap(), Request.Body.empty(), null);
  private static final feign.Request NO_REQUEST = null;
  private static final Map<String, Collection<String>> NON_NULL_HEADERS =
      new HashMap<String, Collection<String>>();
  private static final Map<String, Collection<String>> NO_HEADERS = null;


  @Override
  public Class<TestClientInterfaceWithDifferentExceptionConstructors> interfaceAtTest() {
    return TestClientInterfaceWithDifferentExceptionConstructors.class;
  }

  @Parameters(
      name = "{0}: When error code ({1}) on method ({2}) should return exception type ({3})")
  public static Iterable<Object[]> data() {
    return Arrays.asList(new Object[][] {
        {"Test Default Constructor", 500, DefaultConstructorException.class, NO_REQUEST, NO_BODY,
            NO_HEADERS},
        {"test Default Constructor", 501, DeclaredDefaultConstructorException.class, NO_REQUEST,
            NO_BODY,
            NO_HEADERS},
        {"test Default Constructor", 502,
            DeclaredDefaultConstructorWithOtherConstructorsException.class, NO_REQUEST, NO_BODY,
            NO_HEADERS},
        {"test Declared Constructor", 503, DefinedConstructorWithNoAnnotationForBody.class,
            NO_REQUEST,
            NON_NULL_BODY, NO_HEADERS},
        {"test Declared Constructor", 504, DefinedConstructorWithAnnotationForBody.class,
            NO_REQUEST, NON_NULL_BODY, NO_HEADERS},
        {"test Declared Constructor", 505, DefinedConstructorWithAnnotationForBodyAndHeaders.class,
            NO_REQUEST, NON_NULL_BODY, NON_NULL_HEADERS},
        {"test Declared Constructor", 506,
            DefinedConstructorWithAnnotationForBodyAndHeadersSecondOrder.class, NO_REQUEST,
            NON_NULL_BODY,
            NON_NULL_HEADERS},
        {"test Declared Constructor", 507, DefinedConstructorWithAnnotationForHeaders.class,
            NO_REQUEST, NO_BODY, NON_NULL_HEADERS},
        {"test Declared Constructor", 508,
            DefinedConstructorWithAnnotationForHeadersButNotForBody.class, NO_REQUEST,
            NON_NULL_BODY,
            NON_NULL_HEADERS},
        {"test Declared Constructor", 509,
            DefinedConstructorWithAnnotationForNonSupportedBody.class, NO_REQUEST, NULL_BODY,
            NO_HEADERS},
        {"test Declared Constructor", 510,
            DefinedConstructorWithAnnotationForOptionalBody.class, NO_REQUEST,
            Optional.of(NON_NULL_BODY),
            NO_HEADERS},
        {"test Declared Constructor", 511,
            DefinedConstructorWithRequest.class, REQUEST, NO_BODY,
            NO_HEADERS},
        {"test Declared Constructor", 512,
            DefinedConstructorWithRequestAndResponseBody.class, REQUEST, NON_NULL_BODY,
            NO_HEADERS},
        {"test Declared Constructor", 513,
            DefinedConstructorWithRequestAndAnnotationForResponseBody.class, REQUEST, NON_NULL_BODY,
            NO_HEADERS},
        {"test Declared Constructor", 514,
            DefinedConstructorWithRequestAndResponseHeadersAndResponseBody.class, REQUEST,
            NON_NULL_BODY,
            NON_NULL_HEADERS},
        {"test Declared Constructor", 515,
            DefinedConstructorWithRequestAndResponseHeadersAndOptionalResponseBody.class, REQUEST,
            Optional.of(NON_NULL_BODY),
            NON_NULL_HEADERS}
    });
  }

  @Parameter // first data value (0) is default
  public String testName;

  @Parameter(1)
  public int errorCode;

  @Parameter(2)
  public Class<? extends Exception> expectedExceptionClass;

  @Parameter(3)
  public Object expectedRequest;

  @Parameter(4)
  public Object expectedBody;

  @Parameter(5)
  public Map<String, Collection<String>> expectedHeaders;

  @Test
  public void test() throws Exception {
    AnnotationErrorDecoder decoder = AnnotationErrorDecoder
        .builderFor(TestClientInterfaceWithDifferentExceptionConstructors.class)
        .withResponseBodyDecoder(new OptionalDecoder(new Decoder.Default()))
        .build();

    Exception genericException = decoder.decode(feignConfigKey("method1Test"),
        testResponse(errorCode, NON_NULL_BODY, NON_NULL_HEADERS));

    assertThat(genericException).isInstanceOf(expectedExceptionClass);

    ParametersException exception = (ParametersException) genericException;
    assertThat(exception.body()).isEqualTo(expectedBody);
    assertThat(exception.headers()).isEqualTo(expectedHeaders);
  }

  @Test
  public void testIfExceptionIsNotInTheList() throws Exception {
    AnnotationErrorDecoder decoder = AnnotationErrorDecoder
        .builderFor(TestClientInterfaceWithDifferentExceptionConstructors.class)
        .withResponseBodyDecoder(new OptionalDecoder(new Decoder.Default()))
        .build();

    Exception genericException = decoder.decode(feignConfigKey("method1Test"),
        testResponse(-1, NON_NULL_BODY, NON_NULL_HEADERS));

    assertThat(genericException)
        .isInstanceOf(ErrorHandling.NO_DEFAULT.class)
        .hasMessage("Endpoint responded with -1, reason: null");
  }

  interface TestClientInterfaceWithDifferentExceptionConstructors {

    @ErrorHandling(codeSpecific = {
        @ErrorCodes(codes = {500}, generate = DefaultConstructorException.class),
        @ErrorCodes(codes = {501}, generate = DeclaredDefaultConstructorException.class),
        @ErrorCodes(codes = {502},
            generate = DeclaredDefaultConstructorWithOtherConstructorsException.class),
        @ErrorCodes(codes = {503}, generate = DefinedConstructorWithNoAnnotationForBody.class),
        @ErrorCodes(codes = {504}, generate = DefinedConstructorWithAnnotationForBody.class),
        @ErrorCodes(codes = {505},
            generate = DefinedConstructorWithAnnotationForBodyAndHeaders.class),
        @ErrorCodes(codes = {506},
            generate = DefinedConstructorWithAnnotationForBodyAndHeadersSecondOrder.class),
        @ErrorCodes(codes = {507}, generate = DefinedConstructorWithAnnotationForHeaders.class),
        @ErrorCodes(codes = {508},
            generate = DefinedConstructorWithAnnotationForHeadersButNotForBody.class),
        @ErrorCodes(codes = {509},
            generate = DefinedConstructorWithAnnotationForNonSupportedBody.class),
        @ErrorCodes(codes = {510},
            generate = DefinedConstructorWithAnnotationForOptionalBody.class),
        @ErrorCodes(codes = {511},
            generate = DefinedConstructorWithRequest.class),
        @ErrorCodes(codes = {512},
            generate = DefinedConstructorWithRequestAndResponseBody.class),
        @ErrorCodes(codes = {513},
            generate = DefinedConstructorWithRequestAndAnnotationForResponseBody.class),
        @ErrorCodes(codes = {514},
            generate = DefinedConstructorWithRequestAndResponseHeadersAndResponseBody.class),
        @ErrorCodes(codes = {515},
            generate = DefinedConstructorWithRequestAndResponseHeadersAndOptionalResponseBody.class)
    })
    void method1Test();

    class ParametersException extends Exception {
      public Object body() {
        return NO_BODY;
      }

      public feign.Request request() {
        return null;
      }

      public Map<String, Collection<String>> headers() {
        return null;
      }
    }
    class DefaultConstructorException extends ParametersException {
    }

    class DeclaredDefaultConstructorException extends ParametersException {
      public DeclaredDefaultConstructorException() {}
    }

    class DeclaredDefaultConstructorWithOtherConstructorsException extends ParametersException {
      public DeclaredDefaultConstructorWithOtherConstructorsException() {}

      public DeclaredDefaultConstructorWithOtherConstructorsException(TestPojo testPojo) {
        throw new UnsupportedOperationException("Should not be called");
      }

      public DeclaredDefaultConstructorWithOtherConstructorsException(Throwable cause) {
        throw new UnsupportedOperationException("Should not be called");
      }

      public DeclaredDefaultConstructorWithOtherConstructorsException(String message,
          Throwable cause) {
        throw new UnsupportedOperationException("Should not be called");
      }
    }

    class DefinedConstructorWithNoAnnotationForBody extends ParametersException {
      String body;

      public DefinedConstructorWithNoAnnotationForBody() {
        throw new UnsupportedOperationException("Should not be called");
      }

      @FeignExceptionConstructor
      public DefinedConstructorWithNoAnnotationForBody(String body) {
        this.body = body;
      }

      public DefinedConstructorWithNoAnnotationForBody(TestPojo testPojo) {
        throw new UnsupportedOperationException("Should not be called");
      }

      @Override
      public Object body() {
        return body;
      }

    }

    class DefinedConstructorWithRequest extends ParametersException {
      feign.Request request;

      public DefinedConstructorWithRequest() {
        throw new UnsupportedOperationException("Should not be called");
      }

      @FeignExceptionConstructor
      public DefinedConstructorWithRequest(feign.Request request) {
        this.request = request;
      }

      public DefinedConstructorWithRequest(TestPojo testPojo) {
        throw new UnsupportedOperationException("Should not be called");
      }

      @Override
      public feign.Request request() {
        return request;
      }

    }

    class DefinedConstructorWithRequestAndResponseBody extends ParametersException {
      feign.Request request;
      String body;

      public DefinedConstructorWithRequestAndResponseBody() {
        throw new UnsupportedOperationException("Should not be called");
      }

      @FeignExceptionConstructor
      public DefinedConstructorWithRequestAndResponseBody(feign.Request request, String body) {
        this.request = request;
        this.body = body;
      }

      public DefinedConstructorWithRequestAndResponseBody(TestPojo testPojo) {
        throw new UnsupportedOperationException("Should not be called");
      }

      @Override
      public feign.Request request() {
        return request;
      }

      @Override
      public String body() {
        return body;
      }
    }

    class DefinedConstructorWithRequestAndAnnotationForResponseBody extends ParametersException {
      feign.Request request;
      String body;

      public DefinedConstructorWithRequestAndAnnotationForResponseBody() {
        throw new UnsupportedOperationException("Should not be called");
      }

      @FeignExceptionConstructor
      public DefinedConstructorWithRequestAndAnnotationForResponseBody(feign.Request request,
          @ResponseBody String body) {
        this.request = request;
        this.body = body;
      }

      public DefinedConstructorWithRequestAndAnnotationForResponseBody(TestPojo testPojo) {
        throw new UnsupportedOperationException("Should not be called");
      }

      @Override
      public feign.Request request() {
        return request;
      }

      @Override
      public String body() {
        return body;
      }
    }

    class DefinedConstructorWithRequestAndResponseHeadersAndResponseBody
        extends ParametersException {
      feign.Request request;
      String body;
      Map headers;

      public DefinedConstructorWithRequestAndResponseHeadersAndResponseBody() {
        throw new UnsupportedOperationException("Should not be called");
      }

      @FeignExceptionConstructor
      public DefinedConstructorWithRequestAndResponseHeadersAndResponseBody(feign.Request request,
          @ResponseHeaders Map headers,
          @ResponseBody String body) {
        this.request = request;
        this.body = body;
        this.headers = headers;
      }

      public DefinedConstructorWithRequestAndResponseHeadersAndResponseBody(TestPojo testPojo) {
        throw new UnsupportedOperationException("Should not be called");
      }

      @Override
      public feign.Request request() {
        return request;
      }

      @Override
      public Map headers() {
        return headers;
      }

      @Override
      public String body() {
        return body;
      }
    }

    class DefinedConstructorWithRequestAndResponseHeadersAndOptionalResponseBody
        extends ParametersException {
      feign.Request request;
      Optional<String> body;
      Map headers;

      public DefinedConstructorWithRequestAndResponseHeadersAndOptionalResponseBody() {
        throw new UnsupportedOperationException("Should not be called");
      }

      @FeignExceptionConstructor
      public DefinedConstructorWithRequestAndResponseHeadersAndOptionalResponseBody(
          feign.Request request,
          @ResponseHeaders Map headers,
          @ResponseBody Optional<String> body) {
        this.request = request;
        this.body = body;
        this.headers = headers;
      }

      public DefinedConstructorWithRequestAndResponseHeadersAndOptionalResponseBody(
          TestPojo testPojo) {
        throw new UnsupportedOperationException("Should not be called");
      }

      @Override
      public feign.Request request() {
        return request;
      }

      @Override
      public Map headers() {
        return headers;
      }

      @Override
      public Object body() {
        return body;
      }
    }

    class DefinedConstructorWithAnnotationForBody extends ParametersException {
      String body;

      public DefinedConstructorWithAnnotationForBody() {
        throw new UnsupportedOperationException("Should not be called");
      }

      @FeignExceptionConstructor
      public DefinedConstructorWithAnnotationForBody(@ResponseBody String body) {
        this.body = body;
      }

      @Override
      public Object body() {
        return body;
      }
    }

    class DefinedConstructorWithAnnotationForOptionalBody extends ParametersException {
      Optional<String> body;

      public DefinedConstructorWithAnnotationForOptionalBody() {
        throw new UnsupportedOperationException("Should not be called");
      }

      @FeignExceptionConstructor
      public DefinedConstructorWithAnnotationForOptionalBody(@ResponseBody Optional<String> body) {
        this.body = body;
      }

      @Override
      public Object body() {
        return body;
      }
    }

    class DefinedConstructorWithAnnotationForNonSupportedBody extends ParametersException {
      TestPojo body;

      @FeignExceptionConstructor
      public DefinedConstructorWithAnnotationForNonSupportedBody(@ResponseBody TestPojo body) {
        this.body = body;
      }

      @Override
      public Object body() {
        return body;
      }
    }

    class DefinedConstructorWithAnnotationForBodyAndHeaders extends ParametersException {
      String body;
      Map<String, Collection<String>> headers;

      @FeignExceptionConstructor
      public DefinedConstructorWithAnnotationForBodyAndHeaders(@ResponseBody String body,
          @ResponseHeaders Map<String, Collection<String>> headers) {
        this.body = body;
        this.headers = headers;
      }

      @Override
      public Object body() {
        return body;
      }

      @Override
      public Map<String, Collection<String>> headers() {
        return headers;
      }
    }

    class DefinedConstructorWithAnnotationForBodyAndHeadersSecondOrder extends ParametersException {
      String body;
      Map<String, Collection<String>> headers;

      @FeignExceptionConstructor
      public DefinedConstructorWithAnnotationForBodyAndHeadersSecondOrder(
          @ResponseHeaders Map<String, Collection<String>> headers, @ResponseBody String body) {
        this.body = body;
        this.headers = headers;
      }

      @Override
      public Object body() {
        return body;
      }

      @Override
      public Map<String, Collection<String>> headers() {
        return headers;
      }
    }

    class DefinedConstructorWithAnnotationForHeaders extends ParametersException {
      Map<String, Collection<String>> headers;

      @FeignExceptionConstructor
      public DefinedConstructorWithAnnotationForHeaders(
          @ResponseHeaders Map<String, Collection<String>> headers) {
        this.headers = headers;
      }

      @Override
      public Map<String, Collection<String>> headers() {
        return headers;
      }
    }

    class DefinedConstructorWithAnnotationForHeadersButNotForBody extends ParametersException {
      String body;
      Map<String, Collection<String>> headers;

      @FeignExceptionConstructor
      public DefinedConstructorWithAnnotationForHeadersButNotForBody(
          @ResponseHeaders Map<String, Collection<String>> headers, String body) {
        this.body = body;
        this.headers = headers;
      }

      @Override
      public Object body() {
        return body;
      }

      @Override
      public Map<String, Collection<String>> headers() {
        return headers;
      }
    }
  }
}