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

package io.opentelemetry.instrumentation.apachehttpclient.v4_3;

import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest;
import io.opentelemetry.instrumentation.testing.junit.http.HttpClientResult;
import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions;
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URI;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.client.ResponseHandler;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicHttpRequest;
import org.apache.http.protocol.BasicHttpContext;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.TestInstance;

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public abstract class AbstractApacheHttpClientTest {

  protected abstract InstrumentationExtension testing();

  protected abstract CloseableHttpClient createClient(boolean readTimeout);

  private CloseableHttpClient client;
  private CloseableHttpClient clientWithReadTimeout;

  @BeforeAll
  void setUp() {
    client = createClient(false);
    clientWithReadTimeout = createClient(true);
  }

  @AfterAll
  void tearDown() throws Exception {
    client.close();
    clientWithReadTimeout.close();
  }

  CloseableHttpClient getClient(URI uri) {
    if (uri.toString().contains("/read-timeout")) {
      return clientWithReadTimeout;
    }
    return client;
  }

  @Nested
  class ApacheClientHostRequestTest extends AbstractHttpClientTest<BasicHttpRequest> {

    @Override
    public BasicHttpRequest buildRequest(String method, URI uri, Map<String, String> headers) {
      // also testing with an absolute path below
      return configureRequest(new BasicHttpRequest(method, fullPathFromUri(uri)), headers);
    }

    @Override
    public int sendRequest(
        BasicHttpRequest request, String method, URI uri, Map<String, String> headers)
        throws Exception {
      return getResponseCode(
          getClient(uri)
              .execute(new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme()), request));
    }

    @Override
    public void sendRequestWithCallback(
        BasicHttpRequest request,
        String method,
        URI uri,
        Map<String, String> headers,
        HttpClientResult httpClientResult) {
      try {
        getClient(uri)
            .execute(
                new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme()),
                request,
                responseCallback(httpClientResult));
      } catch (Throwable t) {
        httpClientResult.complete(t);
      }
    }

    @Override
    protected void configure(HttpClientTestOptions.Builder optionsBuilder) {
      configureTest(optionsBuilder);
    }
  }

  @Nested
  class ApacheClientHostRequestContextTest extends AbstractHttpClientTest<BasicHttpRequest> {

    @Override
    public BasicHttpRequest buildRequest(String method, URI uri, Map<String, String> headers) {
      // also testing with an absolute path below
      return configureRequest(new BasicHttpRequest(method, fullPathFromUri(uri)), headers);
    }

    @Override
    public int sendRequest(
        BasicHttpRequest request, String method, URI uri, Map<String, String> headers)
        throws Exception {
      return getResponseCode(
          getClient(uri)
              .execute(
                  new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme()),
                  request,
                  new BasicHttpContext()));
    }

    @Override
    public void sendRequestWithCallback(
        BasicHttpRequest request,
        String method,
        URI uri,
        Map<String, String> headers,
        HttpClientResult httpClientResult) {
      try {
        getClient(uri)
            .execute(
                new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme()),
                request,
                responseCallback(httpClientResult),
                new BasicHttpContext());
      } catch (Throwable t) {
        httpClientResult.complete(t);
      }
    }

    @Override
    protected void configure(HttpClientTestOptions.Builder optionsBuilder) {
      configureTest(optionsBuilder);
    }
  }

  @Nested
  class ApacheClientHostAbsoluteUriRequestTest extends AbstractHttpClientTest<BasicHttpRequest> {

    @Override
    public BasicHttpRequest buildRequest(String method, URI uri, Map<String, String> headers) {
      return configureRequest(new BasicHttpRequest(method, uri.toString()), headers);
    }

    @Override
    public int sendRequest(
        BasicHttpRequest request, String method, URI uri, Map<String, String> headers)
        throws Exception {
      return getResponseCode(
          getClient(uri)
              .execute(new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme()), request));
    }

    @Override
    public void sendRequestWithCallback(
        BasicHttpRequest request,
        String method,
        URI uri,
        Map<String, String> headers,
        HttpClientResult httpClientResult) {
      try {
        getClient(uri)
            .execute(
                new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme()),
                request,
                responseCallback(httpClientResult));
      } catch (Throwable t) {
        httpClientResult.complete(t);
      }
    }

    @Override
    protected void configure(HttpClientTestOptions.Builder optionsBuilder) {
      configureTest(optionsBuilder);
    }
  }

  @Nested
  class ApacheClientHostAbsoluteUriRequestContextTest
      extends AbstractHttpClientTest<BasicHttpRequest> {

    @Override
    public BasicHttpRequest buildRequest(String method, URI uri, Map<String, String> headers) {
      return configureRequest(new BasicHttpRequest(method, uri.toString()), headers);
    }

    @Override
    public int sendRequest(
        BasicHttpRequest request, String method, URI uri, Map<String, String> headers)
        throws Exception {
      return getResponseCode(
          getClient(uri)
              .execute(
                  new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme()),
                  request,
                  new BasicHttpContext()));
    }

    @Override
    public void sendRequestWithCallback(
        BasicHttpRequest request,
        String method,
        URI uri,
        Map<String, String> headers,
        HttpClientResult httpClientResult) {
      try {
        getClient(uri)
            .execute(
                new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme()),
                request,
                responseCallback(httpClientResult),
                new BasicHttpContext());
      } catch (Throwable t) {
        httpClientResult.complete(t);
      }
    }

    @Override
    protected void configure(HttpClientTestOptions.Builder optionsBuilder) {
      configureTest(optionsBuilder);
    }
  }

  @Nested
  class ApacheClientUriRequestTest extends AbstractHttpClientTest<HttpUriRequest> {

    @Override
    public HttpUriRequest buildRequest(String method, URI uri, Map<String, String> headers) {
      // also testing with an absolute path below
      return configureRequest(new HttpUriRequest(method, uri), headers);
    }

    @Override
    public int sendRequest(
        HttpUriRequest request, String method, URI uri, Map<String, String> headers)
        throws Exception {
      return getResponseCode(getClient(uri).execute(request));
    }

    @Override
    public void sendRequestWithCallback(
        HttpUriRequest request,
        String method,
        URI uri,
        Map<String, String> headers,
        HttpClientResult httpClientResult) {
      try {
        getClient(uri).execute(request, responseCallback(httpClientResult));
      } catch (Throwable t) {
        httpClientResult.complete(t);
      }
    }

    @Override
    protected void configure(HttpClientTestOptions.Builder optionsBuilder) {
      configureTest(optionsBuilder);
    }
  }

  @Nested
  class ApacheClientUriRequestContextTest extends AbstractHttpClientTest<HttpUriRequest> {

    @Override
    public HttpUriRequest buildRequest(String method, URI uri, Map<String, String> headers) {
      // also testing with an absolute path below
      return configureRequest(new HttpUriRequest(method, uri), headers);
    }

    @Override
    public int sendRequest(
        HttpUriRequest request, String method, URI uri, Map<String, String> headers)
        throws Exception {
      return getResponseCode(getClient(uri).execute(request, new BasicHttpContext()));
    }

    @Override
    public void sendRequestWithCallback(
        HttpUriRequest request,
        String method,
        URI uri,
        Map<String, String> headers,
        HttpClientResult httpClientResult) {
      try {
        getClient(uri).execute(request, responseCallback(httpClientResult), new BasicHttpContext());
      } catch (Throwable t) {
        httpClientResult.complete(t);
      }
    }

    @Override
    protected void configure(HttpClientTestOptions.Builder optionsBuilder) {
      configureTest(optionsBuilder);
    }
  }

  static <T extends HttpRequest> T configureRequest(T request, Map<String, String> headers) {
    request.addHeader("user-agent", "apachehttpclient");
    headers.forEach((key, value) -> request.setHeader(new BasicHeader(key, value)));
    return request;
  }

  static int getResponseCode(HttpResponse response) {
    try {
      if (response.getEntity() != null && response.getEntity().getContent() != null) {
        response.getEntity().getContent().close();
      }
    } catch (IOException e) {
      throw new UncheckedIOException(e);
    }
    return response.getStatusLine().getStatusCode();
  }

  static ResponseHandler<HttpResponse> responseCallback(HttpClientResult httpClientResult) {
    return response -> {
      try {
        httpClientResult.complete(getResponseCode(response));
      } catch (Throwable t) {
        httpClientResult.complete(t);
        return response;
      }
      return response;
    };
  }

  static void configureTest(HttpClientTestOptions.Builder optionsBuilder) {
    optionsBuilder.setUserAgent("apachehttpclient");
    optionsBuilder.setResponseCodeOnRedirectError(302);
    optionsBuilder.enableTestReadTimeout();
    optionsBuilder.setHttpAttributes(
        endpoint -> {
          Set<AttributeKey<?>> attributes =
              new HashSet<>(HttpClientTestOptions.DEFAULT_HTTP_ATTRIBUTES);
          attributes.add(SemanticAttributes.HTTP_SCHEME);
          attributes.add(SemanticAttributes.HTTP_TARGET);
          return attributes;
        });
  }

  static String fullPathFromUri(URI uri) {
    StringBuilder builder = new StringBuilder();
    if (uri.getPath() != null) {
      builder.append(uri.getPath());
    }

    if (uri.getQuery() != null) {
      builder.append('?');
      builder.append(uri.getQuery());
    }

    if (uri.getFragment() != null) {
      builder.append('#');
      builder.append(uri.getFragment());
    }
    return builder.toString();
  }
}
