package demo;

import static org.assertj.core.api.Assertions.assertThat;

import java.io.IOException;
import java.net.http.HttpClient;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.function.Supplier;

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.client.JdkClientHttpRequestFactory;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.support.RestClientAdapter;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.support.WebClientAdapter;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;

import reactor.core.publisher.Flux;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class HttpClientsComparisonTest {

    @LocalServerPort int port;

    static final Path OUT = Path.of("cap");

    @BeforeAll
    static void clean() throws IOException {
        Files.createDirectories(OUT);
        try (var s = Files.list(OUT)) { s.forEach(p -> p.toFile().delete()); }
    }

    static void say(String file, String line) throws IOException {
        System.out.println(line);
        Files.writeString(OUT.resolve(file), line + System.lineSeparator(), StandardOpenOption.CREATE, StandardOpenOption.APPEND);
    }

    String base() { return "http://localhost:" + port; }

    static long ms(Runnable r) {
        long t0 = System.nanoTime();
        r.run();
        return (System.nanoTime() - t0) / 1_000_000;
    }

    @Test
    void sameCallFourWays() throws Exception {
        String f = "01-same-call.txt";
        RestClient rc = RestClient.builder().baseUrl(base()).build();
        WebClient wc = WebClient.builder().baseUrl(base()).build();

        HttpClientsUser a = rc.get().uri("/users/{id}", 7).retrieve().body(HttpClientsUser.class);
        HttpClientsUser b = wc.get().uri("/users/{id}", 7).retrieve().bodyToMono(HttpClientsUser.class).block();
        HttpClientsUserApi viaRest = HttpServiceProxyFactory.builderFor(RestClientAdapter.create(rc)).build()
                .createClient(HttpClientsUserApi.class);
        HttpClientsUserApi viaWeb = HttpServiceProxyFactory.builderFor(WebClientAdapter.create(wc)).build()
                .createClient(HttpClientsUserApi.class);

        say(f, "RestClient                    -> " + a);
        say(f, "WebClient (.block())          -> " + b);
        say(f, "interface over RestClient     -> " + viaRest.byId(7));
        say(f, "interface over WebClient      -> " + viaWeb.byId(7));
        assertThat(a).isEqualTo(b).isEqualTo(viaRest.byId(7)).isEqualTo(viaWeb.byId(7));
    }

    @Test
    void tenSlowCalls() throws Exception {
        String f = "02-ten-slow-calls.txt";
        RestClient rc = RestClient.builder().baseUrl(base()).build();
        WebClient wc = WebClient.builder().baseUrl(base()).build();
        Supplier<HttpClientsUser> one = () -> rc.get().uri("/slow?ms=200").retrieve().body(HttpClientsUser.class);
        one.get(); // warm-up
        wc.get().uri("/slow?ms=1").retrieve().bodyToMono(HttpClientsUser.class).block();

        long sequential = ms(() -> { for (int i = 0; i < 10; i++) one.get(); });

        long pool4 = ms(() -> {
            try (ExecutorService ex = Executors.newFixedThreadPool(4)) {
                futures(ex, one, 10);
            }
        });

        long virtual = ms(() -> {
            try (ExecutorService ex = Executors.newVirtualThreadPerTaskExecutor()) {
                futures(ex, one, 10);
            }
        });

        long reactive = ms(() -> Flux.range(0, 10)
                .flatMap(i -> wc.get().uri("/slow?ms=200").retrieve().bodyToMono(HttpClientsUser.class))
                .collectList().block());

        say(f, "10 x 200 ms calls, one after another (RestClient)      : " + sequential + " ms");
        say(f, "10 x 200 ms calls, RestClient on 4 platform threads    : " + pool4 + " ms");
        say(f, "10 x 200 ms calls, RestClient on virtual threads       : " + virtual + " ms");
        say(f, "10 x 200 ms calls, WebClient with Flux.flatMap         : " + reactive + " ms");
        assertThat(sequential).isGreaterThan(1900);
        assertThat(pool4).isBetween(550L, 1200L);
        assertThat(virtual).isLessThan(700);
        assertThat(reactive).isLessThan(700);
    }

    static void futures(ExecutorService ex, Supplier<HttpClientsUser> call, int n) {
        List<Future<HttpClientsUser>> fs = new ArrayList<>();
        for (int i = 0; i < n; i++) fs.add(ex.submit(call::get));
        for (var fu : fs) {
            try { fu.get(); } catch (Exception e) { throw new RuntimeException(e); }
        }
    }

    @Test
    void errorsDiffer() throws Exception {
        String f = "03-errors.txt";
        RestClient rc = RestClient.builder().baseUrl(base()).build();
        WebClient wc = WebClient.builder().baseUrl(base()).build();
        HttpClientsUserApi viaRest = HttpServiceProxyFactory.builderFor(RestClientAdapter.create(rc)).build()
                .createClient(HttpClientsUserApi.class);
        HttpClientsUserApi viaWeb = HttpServiceProxyFactory.builderFor(WebClientAdapter.create(wc)).build()
                .createClient(HttpClientsUserApi.class);

        say(f, "-- HTTP 404 --");
        say(f, "RestClient                : " + thrown(() -> rc.get().uri("/users/404").retrieve().body(HttpClientsUser.class)));
        say(f, "WebClient                 : " + thrown(() -> wc.get().uri("/users/404").retrieve().bodyToMono(HttpClientsUser.class).block()));
        say(f, "interface over RestClient : " + thrown(() -> viaRest.byId(404)));
        say(f, "interface over WebClient  : " + thrown(() -> viaWeb.byId(404)));
        say(f, "-- HTTP 500 --");
        say(f, "RestClient                : " + thrown(() -> rc.get().uri("/fail").retrieve().body(String.class)));
        say(f, "WebClient                 : " + thrown(() -> wc.get().uri("/fail").retrieve().bodyToMono(String.class).block()));
        say(f, "-- nobody listening on the port --");
        RestClient deadRc = RestClient.builder().baseUrl("http://localhost:1").build();
        WebClient deadWc = WebClient.builder().baseUrl("http://localhost:1").build();
        say(f, "RestClient                : " + thrown(() -> deadRc.get().uri("/x").retrieve().body(String.class)));
        say(f, "WebClient                 : " + thrown(() -> deadWc.get().uri("/x").retrieve().bodyToMono(String.class).block()));

        String[] transport = new String[1];
        RestClient probe = RestClient.builder().baseUrl(base())
                .requestInterceptor((req, body, exec) -> {
                    transport[0] = "unknown";
                    for (var fld : req.getClass().getDeclaredFields()) {
                        if (org.springframework.http.client.ClientHttpRequestFactory.class.isAssignableFrom(fld.getType())) {
                            try {
                                fld.setAccessible(true);
                                transport[0] = fld.get(req).getClass().getSimpleName();
                            } catch (ReflectiveOperationException ignored) { }
                        }
                    }
                    return exec.execute(req, body);
                }).build();
        probe.get().uri("/users/7").retrieve().body(HttpClientsUser.class);
        say(f, "default RestClient transport    : " + transport[0]);

        say(f, "-- one handler for both, via onStatus --");
        say(f, "RestClient onStatus       : " + thrown(() -> rc.get().uri("/users/404").retrieve()
                .onStatus(HttpStatusCode::is4xxClientError, (req, res) -> { throw new IllegalArgumentException("user not found"); })
                .body(HttpClientsUser.class)));
        say(f, "WebClient onStatus        : " + thrown(() -> wc.get().uri("/users/404").retrieve()
                .onStatus(HttpStatusCode::is4xxClientError, res -> reactor.core.publisher.Mono.error(new IllegalArgumentException("user not found")))
                .bodyToMono(HttpClientsUser.class).block()));
    }

    static String thrown(Runnable r) {
        try {
            r.run();
            return "no exception";
        } catch (Exception e) {
            String cause = e.getCause() == null ? "" : "  (cause: " + e.getCause().getClass().getSimpleName() + ")";
            String msg = e.getMessage() == null ? "" : e.getMessage().split("\n")[0];
            if (msg.length() > 60) msg = msg.substring(0, 60) + "...";
            String type = e.getClass().getName().substring(e.getClass().getName().lastIndexOf('.') + 1);
            return type + cause + (e instanceof IllegalArgumentException ? ": " + msg : "");
        }
    }

    @Test
    void timeouts() throws Exception {
        String f = "04-timeouts.txt";
        RestClient defaultRc = RestClient.builder().baseUrl(base()).build();
        defaultRc.get().uri("/slow?ms=1").retrieve().body(HttpClientsUser.class); // warm-up: first call pays for class loading
        long[] took = new long[1];
        took[0] = ms(() -> defaultRc.get().uri("/slow?ms=1500").retrieve().body(HttpClientsUser.class));
        say(f, "RestClient, default settings, 1500 ms response : waited " + took[0] + " ms and got the answer");

        JdkClientHttpRequestFactory factory = new JdkClientHttpRequestFactory(
                HttpClient.newBuilder().connectTimeout(Duration.ofMillis(300)).build());
        factory.setReadTimeout(Duration.ofMillis(300));
        RestClient limited = RestClient.builder().baseUrl(base()).requestFactory(factory).build();
        long[] t2 = new long[1];
        String r = null;
        long t0 = System.nanoTime();
        try {
            limited.get().uri("/slow?ms=1500").retrieve().body(HttpClientsUser.class);
        } catch (Exception e) {
            r = e.getClass().getSimpleName() + " (cause: " + e.getCause().getClass().getSimpleName() + ")";
        }
        t2[0] = (System.nanoTime() - t0) / 1_000_000;
        say(f, "RestClient, read timeout 300 ms                 : gave up after ~" + (t2[0] / 100 * 100) + " ms with " + r);

        WebClient wc = WebClient.builder().baseUrl(base()).build();
        t0 = System.nanoTime();
        String w = null;
        try {
            wc.get().uri("/slow?ms=1500").retrieve().bodyToMono(HttpClientsUser.class).timeout(Duration.ofMillis(300)).block();
        } catch (Exception e) {
            w = e.getClass().getSimpleName() + " (cause: " + e.getCause().getClass().getSimpleName() + ")";
        }
        long t3 = (System.nanoTime() - t0) / 1_000_000;
        say(f, "WebClient, Mono.timeout(300 ms)                 : gave up after ~" + (t3 / 100 * 100) + " ms with " + w);
        assertThat(took[0]).isGreaterThan(1400);
        assertThat(t2[0]).isLessThan(900);
        assertThat(t3).isLessThan(900);
    }
}
