/*
 * Copyright The OpenTelemetry Authors
 * SPDX-License-Identifier: Apache-2.0
 */

package io.opentelemetry.javaagent.instrumentation.netty.v4_0.client;

import static io.opentelemetry.api.trace.SpanKind.CLIENT;
import static io.opentelemetry.api.trace.SpanKind.INTERNAL;
import static io.opentelemetry.api.trace.SpanKind.SERVER;
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo;
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.catchThrowable;

import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpVersion;
import io.opentelemetry.instrumentation.test.utils.PortUtils;
import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension;
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestServer;
import io.opentelemetry.semconv.NetworkAttributes;
import io.opentelemetry.semconv.ServerAttributes;
import java.net.URI;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

class Netty40ConnectionSpanTest {

  @RegisterExtension
  static final InstrumentationExtension testing = AgentInstrumentationExtension.create();

  private static HttpClientTestServer server;
  private static final EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
  private static final Bootstrap bootstrap = buildBootstrap();

  @BeforeAll
  static void setupSpec() {
    server = new HttpClientTestServer(testing.getOpenTelemetry());
    server.start();
  }

  @AfterAll
  static void cleanupSpec() throws InterruptedException, ExecutionException, TimeoutException {
    eventLoopGroup.shutdownGracefully();
    server.stop().get(10, TimeUnit.SECONDS);
  }

  static Bootstrap buildBootstrap() {
    Bootstrap bootstrap = new Bootstrap();
    bootstrap
        .group(eventLoopGroup)
        .channel(NioSocketChannel.class)
        .handler(
            new ChannelInitializer<SocketChannel>() {
              @Override
              protected void initChannel(@NotNull SocketChannel socketChannel) throws Exception {
                ChannelPipeline pipeline = socketChannel.pipeline();
                pipeline.addLast(new HttpClientCodec());
              }
            });

    return bootstrap;
  }

  @Test
  void successfulRequest() throws Exception {
    // when
    URI uri = URI.create("http://localhost:" + server.httpPort() + "/success");

    DefaultFullHttpRequest request = buildRequest("GET", uri, new HashMap<>());
    int responseCode = testing.runWithSpan("parent", () -> sendRequest(request, uri));

    // then
    assertThat(responseCode).isEqualTo(200);

    testing.waitAndAssertTraces(
        trace ->
            trace.hasSpansSatisfyingExactly(
                span -> span.hasName("parent").hasKind(INTERNAL).hasNoParent(),
                span -> {
                  span.hasName("CONNECT").hasKind(INTERNAL).hasParent(trace.getSpan(0));
                  span.hasAttributesSatisfyingExactly(
                      equalTo(NetworkAttributes.NETWORK_TRANSPORT, "tcp"),
                      equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"),
                      equalTo(ServerAttributes.SERVER_ADDRESS, uri.getHost()),
                      equalTo(ServerAttributes.SERVER_PORT, uri.getPort()),
                      equalTo(NetworkAttributes.NETWORK_PEER_PORT, uri.getPort()),
                      equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1"));
                },
                span -> span.hasName("GET").hasKind(CLIENT).hasParent(trace.getSpan(0)),
                span ->
                    span.hasName("test-http-server").hasKind(SERVER).hasParent(trace.getSpan(2))));
  }

  @Test
  void failedRequest() throws Exception {
    // when
    URI uri = URI.create("http://localhost:" + PortUtils.UNUSABLE_PORT);

    DefaultFullHttpRequest request = buildRequest("GET", uri, new HashMap<>());
    Throwable thrown =
        catchThrowable(() -> testing.runWithSpan("parent", () -> sendRequest(request, uri)));

    // then
    testing.waitAndAssertTraces(
        trace ->
            trace.hasSpansSatisfyingExactly(
                span -> span.hasName("parent").hasKind(INTERNAL).hasNoParent().hasException(thrown),
                span -> {
                  span.hasName("CONNECT").hasKind(INTERNAL).hasParent(trace.getSpan(0));
                  span.hasAttributesSatisfying(equalTo(NetworkAttributes.NETWORK_TRANSPORT, "tcp"));
                  satisfies(
                      NetworkAttributes.NETWORK_TYPE,
                      val ->
                          val.satisfiesAnyOf(
                              v -> assertThat(val).isNull(), v -> assertThat(v).isEqualTo("ipv4")));
                  span.hasAttributesSatisfying(
                      equalTo(ServerAttributes.SERVER_ADDRESS, uri.getHost()),
                      equalTo(ServerAttributes.SERVER_PORT, uri.getPort()));
                  satisfies(
                      NetworkAttributes.NETWORK_PEER_PORT,
                      val ->
                          val.satisfiesAnyOf(
                              v -> assertThat(val).isNull(),
                              v -> assertThat(v).isEqualTo(uri.getPort())));
                  satisfies(
                      NetworkAttributes.NETWORK_PEER_ADDRESS,
                      val ->
                          val.satisfiesAnyOf(
                              v -> assertThat(val).isNull(),
                              v -> assertThat(v).isEqualTo("127.0.0.1")));
                }));
  }

  private static DefaultFullHttpRequest buildRequest(
      String method, URI uri, Map<String, String> headers) {
    DefaultFullHttpRequest request =
        new DefaultFullHttpRequest(
            HttpVersion.HTTP_1_1, HttpMethod.valueOf(method), uri.getPath(), Unpooled.EMPTY_BUFFER);
    HttpHeaders.setHost(request, uri.getHost() + ":" + uri.getPort());
    headers.forEach((k, v) -> request.headers().set(k, v));
    return request;
  }

  private static int sendRequest(DefaultFullHttpRequest request, URI uri)
      throws InterruptedException, ExecutionException, TimeoutException {
    Channel channel = bootstrap.connect(uri.getHost(), uri.getPort()).sync().channel();
    CompletableFuture<Integer> result = new CompletableFuture<Integer>();
    channel.pipeline().addLast(new ClientHandler(result));
    channel.writeAndFlush(request).get();
    return result.get(20, TimeUnit.SECONDS);
  }
}
