package reactivefeign.cloud;

import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder;
import com.github.tomakehurst.wiremock.junit.WireMockClassRule;
import com.netflix.client.*;
import com.netflix.client.config.CommonClientConfigKey;
import com.netflix.client.config.DefaultClientConfigImpl;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixCommandKey;
import com.netflix.hystrix.HystrixCommandProperties;
import com.netflix.hystrix.HystrixObservableCommand;
import com.netflix.loadbalancer.BaseLoadBalancer;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;
import feign.RequestLine;
import feign.RetryableException;
import org.junit.*;
import org.junit.rules.ExpectedException;
import reactivefeign.publisher.RetryPublisherHttpClient;
import reactor.core.publisher.Mono;

import java.util.stream.Stream;

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 java.util.Arrays.asList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.*;
import static reactivefeign.ReactiveRetryers.retry;

/**
 * @author Sergii Karpenko
 */
public class LoadBalancingReactiveHttpClientTest {

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

    @Rule
    public ExpectedException expectedException = ExpectedException.none();

    private static String serviceName = "LoadBalancingTargetTest-loadBalancingDefaultPolicyRoundRobin";

    @BeforeClass
    public static void setupServersList() throws ClientException {
        DefaultClientConfigImpl clientConfig = new DefaultClientConfigImpl();
        clientConfig.loadDefaultValues();
        clientConfig.setProperty(CommonClientConfigKey.NFLoadBalancerClassName, BaseLoadBalancer.class.getName());
        ILoadBalancer lb = ClientFactory.registerNamedLoadBalancerFromclientConfig(serviceName, clientConfig);
        lb.addServers(asList(new Server("localhost", server1.port()), new Server("localhost", server2.port())));
    }

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

    @Test
    public void shouldLoadBalanceRequests() {
        String body = "success!";
        mockSuccess(server1, body);
        mockSuccess(server2, body);

        TestInterface client = CloudReactiveFeign.<TestInterface>builder()
                .enableLoadBalancer()
                .setHystrixCommandSetterFactory(getSetterFactoryWithTimeoutDisabled())
                .target(TestInterface.class, "http://" + serviceName);

        String result1 = client.get().block();
        String result2 = client.get().block();

        assertThat(result1)
                .isEqualTo(result2)
                .isEqualTo(body);

        server1.verify(1, getRequestedFor(urlEqualTo("/")));
        server2.verify(1, getRequestedFor(urlEqualTo("/")));
    }

    @Test
    public void shouldFailAsPolicyWoRetries() {

        expectedException.expect(RuntimeException.class);
        expectedException.expectCause(allOf(isA(RetryPublisherHttpClient.OutOfRetriesException.class),
                hasProperty("cause", isA(RetryableException.class))));

        try {
            loadBalancingWithRetry(2, 0, 0);
        } catch (Throwable t) {
            assertThat(server1.getAllServeEvents().size() == 1
                    ^ server2.getAllServeEvents().size() == 1);
            throw t;
        }
    }

    @Test
    public void shouldRetryOnSameAndFail() {

        expectedException.expect(RuntimeException.class);
        expectedException.expectCause(allOf(isA(RetryPublisherHttpClient.OutOfRetriesException.class),
                                            hasProperty("cause", isA(RetryableException.class))));

        try {
            loadBalancingWithRetry(2, 1, 0);
        } catch (Throwable t) {
            assertThat(server1.getAllServeEvents().size() == 2
                    ^ server2.getAllServeEvents().size() == 2);
            throw t;
        }
    }

    @Test
    public void shouldRetryOnNextAndFail() {

        expectedException.expect(RuntimeException.class);
        expectedException.expectCause(isA(ClientException.class));

        try {
            loadBalancingWithRetry(2, 1, 1);
        } catch (Throwable t) {
            assertThat(server1.getAllServeEvents().size() == 2
                    && server2.getAllServeEvents().size() == 2);
            throw t;
        }
    }

    @Test
    public void shouldRetryOnSameAndSuccess() {

        loadBalancingWithRetry(2, 2, 0);

        assertThat(server1.getAllServeEvents().size() == 3
                ^ server2.getAllServeEvents().size() == 3);

    }

    private void loadBalancingWithRetry(int failedAttemptsNo, int retryOnSame, int retryOnNext) {
        String body = "success!";
        Stream.of(server1, server2).forEach(server -> {
            mockSuccessAfterSeveralAttempts(server, "/",
                    failedAttemptsNo, 503,
                    aResponse()
                            .withStatus(200)
                            .withHeader("Content-Type", "application/json")
                            .withBody(body));
        });

        RetryHandler retryHandler = new RequestSpecificRetryHandler(true, true,
                new DefaultLoadBalancerRetryHandler(0, retryOnNext, true), null);

        TestInterface client = CloudReactiveFeign.<TestInterface>builder()
                .retryWhen(retry(retryOnSame))
                .enableLoadBalancer(retryHandler)
                .setHystrixCommandSetterFactory(getSetterFactoryWithTimeoutDisabled())
                .target(TestInterface.class, "http://" + serviceName);

        String result = client.get().block();
        assertThat(result).isEqualTo(body);
    }

    private CloudReactiveFeign.SetterFactory getSetterFactoryWithTimeoutDisabled() {
        return (target, methodMetadata) -> {
			String groupKey = target.name();
			HystrixCommandKey commandKey = HystrixCommandKey.Factory.asKey(methodMetadata.configKey());
			return HystrixObservableCommand.Setter
					.withGroupKey(HystrixCommandGroupKey.Factory.asKey(groupKey))
					.andCommandKey(commandKey)
					.andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
							.withExecutionTimeoutEnabled(false)
					);
		};
    }

    static void mockSuccess(WireMockClassRule server, String body) {
        server.stubFor(get(urlEqualTo("/"))
                .willReturn(aResponse()
                        .withStatus(200)
                        .withHeader("Content-Type", "application/json")
                        .withBody(body)));
    }

    static void mockSuccessAfterSeveralAttempts(WireMockClassRule server, String url,
                                                int failedAttemptsNo, int errorCode, ResponseDefinitionBuilder response) {
        String state = STARTED;
        for (int attempt = 0; attempt < failedAttemptsNo; attempt++) {
            String nextState = "attempt" + attempt;
            server.stubFor(get(urlEqualTo(url))
                    .inScenario("testScenario")
                    .whenScenarioStateIs(state)
                    .willReturn(aResponse()
                            .withStatus(errorCode)
                            .withHeader("Retry-After", "1"))
                    .willSetStateTo(nextState));

            state = nextState;
        }

        server.stubFor(get(urlEqualTo(url))
                .inScenario("testScenario")
                .whenScenarioStateIs(state)
                .willReturn(response));
    }


    interface TestInterface {

        @RequestLine("GET /")
        Mono<String> get();
    }
}