/*
 * Copyright 2022 Google LLC
 *
 * 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 com.google.android.libraries.mobiledatadownload.testing;

import android.net.Uri;
import android.util.Log;
import com.google.common.base.Optional;
import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.http.Header;
import org.apache.http.HttpException;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.entity.FileEntity;
import org.apache.http.impl.DefaultConnectionReuseStrategy;
import org.apache.http.impl.DefaultHttpResponseFactory;
import org.apache.http.impl.DefaultHttpServerConnection;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.CoreConnectionPNames;
import org.apache.http.params.HttpParams;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.BasicHttpProcessor;
import org.apache.http.protocol.HttpContext;
import org.apache.http.protocol.HttpRequestHandler;
import org.apache.http.protocol.HttpRequestHandlerRegistry;
import org.apache.http.protocol.HttpService;

/** TestHttpServer is a simple http server that listens to http requests on a single thread. */
public final class TestHttpServer {

  private static final String TAG = "TestHttpServer";
  private static final String TEST_HOST = "localhost";

  private static final String HEAD_REQUEST_METHOD = "HEAD";
  private static final String ETAG_HEADER = "ETag";
  private static final String IF_NONE_MATCH_HEADER = "If-None-Match";
  private static final String BINARY_CONTENT_TYPE = "application/binary";
  private static final String PROTO_CONTENT_TYPE = "application/x-protobuf";
  private static final String TEXT_CONTENT_TYPE = "text/plain";

  private final HttpParams httpParams = new BasicHttpParams();
  private final HttpService httpService;
  private final HttpRequestHandlerRegistry registry;
  private final AtomicBoolean finished = new AtomicBoolean();

  private Thread serverThread;
  private ServerSocket serverSocket;
  // 0 means user didn't specify a port number and will use automatically assigned port.
  private final int userDesignatedPort;

  public TestHttpServer() {
    this(0);
  }

  public TestHttpServer(int portNumber) {
    userDesignatedPort = portNumber;
    httpParams.setBooleanParameter(CoreConnectionPNames.TCP_NODELAY, true);
    registry = new HttpRequestHandlerRegistry();

    httpService =
        new HttpService(
            new BasicHttpProcessor(),
            new DefaultConnectionReuseStrategy(),
            new DefaultHttpResponseFactory());
    httpService.setHandlerResolver(registry);
    httpService.setParams(httpParams);
  }
  /** Registers a handler for an endpoint pattern. */
  public void registerHandler(String pattern, HttpRequestHandler handler) {
    registry.register(pattern, handler);
  }

  /** Registers a handler that binds onto a text file for an endpoint pattern. */
  public void registerTextFile(String pattern, String filepath) {
    registerFile(pattern, filepath, TEXT_CONTENT_TYPE, /* eTagOptional= */ Optional.absent());
  }

  /** Registers a handler that binds onto a file for an endpoint pattern. */
  public void registerBinaryFile(String pattern, String filepath) {
    registerFile(pattern, filepath, BINARY_CONTENT_TYPE, /* eTagOptional= */ Optional.absent());
  }

  /**
   * Registers a handler that binds onto a proto file for an endpoint pattern with the specified
   * ETag.
   */
  public void registerProtoFileWithETag(String pattern, String filepath, String eTag) {
    registerFile(pattern, filepath, PROTO_CONTENT_TYPE, Optional.of(eTag));
  }

  private void registerFile(
      String pattern, String filepath, String contentType, Optional<String> eTagOptional) {
    registerHandler(
        pattern,
        (httpRequest, httpResponse, httpContext) -> {
          if (eTagOptional.isPresent()) {
            String eTag = eTagOptional.get();
            httpResponse.addHeader(ETAG_HEADER, eTag);
            setHttpStatusCode(httpRequest, httpResponse, eTag);
            if (httpResponse.getStatusLine().getStatusCode() == HttpStatus.SC_NOT_MODIFIED
                || HEAD_REQUEST_METHOD.equals(httpRequest.getRequestLine().getMethod())) {
              return;
            }
          } else { // The ETag is not present.
            httpResponse.setStatusCode(HttpStatus.SC_OK);
          }
          File file = new File(filepath);
          httpResponse.setEntity(new FileEntity(file, contentType));
        });
  }

  /** Starts the test http server and returns the prefix of the test url. */
  public Uri.Builder startServer() throws IOException {
    serverSocket =
        new ServerSocket(
            /* port= */ userDesignatedPort, /* backlog= */ 0, InetAddress.getByName(TEST_HOST));
    serverThread =
        new Thread(
            () -> {
              try {
                while (!finished.get()) {
                  Socket socket = serverSocket.accept();
                  handleRequest(socket);
                }
              } catch (IOException e) {
                Log.e(TAG, "Exception: " + e);
              }
            });
    serverThread.start();
    return getTestUrlPrefix();
  }

  public void stopServer() {
    try {
      finished.set(true);
      serverSocket.close();
      serverThread.join();
    } catch (IOException | InterruptedException e) {
      Log.e(TAG, "Exception when stopping server: " + e);
    }
  }

  private void handleRequest(Socket socket) {
    DefaultHttpServerConnection connection = new DefaultHttpServerConnection();
    try {
      connection.bind(socket, httpParams);
      HttpContext httpContext = new BasicHttpContext();
      httpService.handleRequest(connection, httpContext);
    } catch (IOException | HttpException e) {
      Log.e(TAG, "Unexpected exception while processing request " + e);
    } finally {
      try {
        connection.shutdown();
      } catch (IOException e) {
        // Ignore.
      }
    }
  }

  private Uri.Builder getTestUrlPrefix() {
    String authority = TEST_HOST + ":" + serverSocket.getLocalPort();
    return new Uri.Builder().scheme("http").encodedAuthority(authority);
  }

  private static void setHttpStatusCode(
      HttpRequest httpRequest, HttpResponse httpResponse, String eTag) {
    Header[] headers = httpRequest.getAllHeaders();
    // We use `If-None-Match` header and ETag to detect whether the file has been changed since the
    // last sync. If the ETag from client matches the one at server, the file is not changed and
    // HttpStatus.SC_NOT_MODIFIED is returned; otherwise, the file is changed and HttpStatus.SC_OK
    // is returned.
    for (Header header : headers) {
      // Find the `If-None-Match` header.
      if (!IF_NONE_MATCH_HEADER.equals(header.getName())) {
        continue;
      }
      httpResponse.setStatusCode(
          eTag.equals(header.getValue()) ? HttpStatus.SC_NOT_MODIFIED : HttpStatus.SC_OK);
      return;
    }
    httpResponse.setStatusCode(HttpStatus.SC_OK);
  }
}
