/**
 * Copyright 2018 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 reactivefeign;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder;
import com.github.tomakehurst.wiremock.junit.WireMockClassRule;
import feign.RetryableException;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import reactivefeign.publisher.RetryPublisherHttpClient;
import reactivefeign.testcase.IcecreamServiceApi;
import reactivefeign.testcase.domain.IceCreamOrder;
import reactivefeign.testcase.domain.Mixin;
import reactivefeign.testcase.domain.OrderGenerator;
import reactor.test.StepVerifier;

import java.util.Arrays;

import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
import static com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED;
import static org.apache.http.HttpHeaders.RETRY_AFTER;
import static org.apache.http.HttpStatus.SC_OK;
import static org.apache.http.HttpStatus.SC_SERVICE_UNAVAILABLE;
import static org.hamcrest.Matchers.hasProperty;
import static org.hamcrest.Matchers.isA;
import static reactivefeign.TestUtils.equalsComparingFieldByFieldRecursively;

/**
 * @author Sergii Karpenko
 */
public abstract class RetryingTest {

  @ClassRule
  public static WireMockClassRule wireMockRule = new WireMockClassRule(
      wireMockConfig().dynamicPort());

  abstract protected ReactiveFeign.Builder<IcecreamServiceApi> builder();

  @Before
  public void resetServers() {
    wireMockRule.resetAll();
  }

  @Test
  public void shouldSuccessOnRetriesMono() throws JsonProcessingException {

    IceCreamOrder orderGenerated = new OrderGenerator().generate(1);
    String orderStr = TestUtils.MAPPER.writeValueAsString(orderGenerated);

    mockResponseAfterSeveralAttempts(wireMockRule, 2, "testRetrying_success",
        "/icecream/orders/1",
        aResponse().withStatus(503).withHeader(RETRY_AFTER, "1"),
        aResponse().withStatus(200).withHeader("Content-Type", "application/json")
            .withBody(orderStr));

    IcecreamServiceApi client = builder()
        .retryWhen(ReactiveRetryers.retryWithBackoff(3, 0))
        .target(IcecreamServiceApi.class,
            "http://localhost:" + wireMockRule.port());

    StepVerifier.create(client.findOrder(1))
        .expectNextMatches(equalsComparingFieldByFieldRecursively(orderGenerated))
        .verifyComplete();
  }

  @Test
  public void shouldSuccessOnRetriesFlux() throws JsonProcessingException {

    String mixinsStr = TestUtils.MAPPER.writeValueAsString(Mixin.values());

    mockResponseAfterSeveralAttempts(wireMockRule, 2, "testRetrying_success",
        "/icecream/mixins",
        aResponse().withStatus(SC_SERVICE_UNAVAILABLE).withHeader(RETRY_AFTER,
            "1"),
        aResponse().withStatus(SC_OK)
            .withHeader("Content-Type", "application/json")
            .withBody(mixinsStr));

    IcecreamServiceApi client = builder()
        .retryWhen(ReactiveRetryers.retryWithBackoff(3, 0))
        .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port());

    StepVerifier.create(client.getAvailableMixins())
        .expectNextSequence(Arrays.asList(Mixin.values()))
        .verifyComplete();
  }

  @Test
  public void shouldSuccessOnRetriesWoRetryAfter() throws JsonProcessingException {

    IceCreamOrder orderGenerated = new OrderGenerator().generate(1);
    String orderStr = TestUtils.MAPPER.writeValueAsString(orderGenerated);

    mockResponseAfterSeveralAttempts(wireMockRule, 2, "testRetrying_success",
        "/icecream/orders/1", aResponse().withStatus(SC_SERVICE_UNAVAILABLE),
        aResponse().withStatus(SC_OK)
            .withHeader("Content-Type", "application/json")
            .withBody(orderStr));

    IcecreamServiceApi client = builder()
        .retryWhen(ReactiveRetryers.retryWithBackoff(3, 0))
        .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port());

    StepVerifier.create(client.findOrder(1))
        .expectNextMatches(equalsComparingFieldByFieldRecursively(orderGenerated))
        .verifyComplete();
  }

  private static void mockResponseAfterSeveralAttempts(WireMockClassRule rule,
                                                       int failedAttemptsNo,
                                                       String scenario,
                                                       String url,
                                                       ResponseDefinitionBuilder failResponse,
                                                       ResponseDefinitionBuilder response) {
    String state = STARTED;
    for (int attempt = 0; attempt < failedAttemptsNo; attempt++) {
      String nextState = "attempt" + attempt;
      rule.stubFor(
          get(urlEqualTo(url))
              .inScenario(scenario).whenScenarioStateIs(state)
              .willReturn(failResponse).willSetStateTo(nextState));

      state = nextState;
    }

    rule.stubFor(get(urlEqualTo(url))
        .inScenario(scenario)
        .whenScenarioStateIs(state).willReturn(response));
  }

  @Test
  public void shouldFailAsNoMoreRetries() {

    String orderUrl = "/icecream/orders/1";

    wireMockRule.stubFor(get(urlEqualTo(orderUrl))
        .withHeader("Accept", equalTo("application/json"))
        .willReturn(aResponse().withStatus(503).withHeader(RETRY_AFTER, "1")));

    IcecreamServiceApi client = builder()
        .retryWhen(ReactiveRetryers.retry(3))
        .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port());

    StepVerifier.create(client.findOrder(1))
        .expectErrorMatches(throwable -> throwable instanceof RetryPublisherHttpClient.OutOfRetriesException
            && hasProperty("cause", isA(RetryableException.class)).matches(throwable))
        .verify();
  }

  @Test
  public void shouldFailAsNoMoreRetriesWithBackoff() {

    String orderUrl = "/icecream/orders/1";

    wireMockRule.stubFor(get(urlEqualTo(orderUrl))
        .withHeader("Accept", equalTo("application/json"))
        .willReturn(aResponse().withStatus(503).withHeader(RETRY_AFTER, "1")));

    IcecreamServiceApi client = builder()
        .retryWhen(ReactiveRetryers.retryWithBackoff(7, 5))
        .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port());

    StepVerifier.create(client.findOrder(1))
        .expectErrorMatches(throwable -> throwable instanceof RetryPublisherHttpClient.OutOfRetriesException
                && hasProperty("cause", isA(RetryableException.class)).matches(throwable))
        .verify();
  }

}