/*
 * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
 * agreements. See the NOTICE file distributed with this work for additional information regarding
 * copyright ownership. The ASF licenses this file to You 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 org.apache.geode.session.tests;

import java.io.IOException;
import java.net.URISyntaxException;
import java.util.function.Function;

import jakarta.servlet.http.HttpServletRequest;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.cookie.BasicCookieStore;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.cookie.BasicClientCookie;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.ParseException;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.net.URIBuilder;

import org.apache.geode.modules.session.CommandServlet;
import org.apache.geode.modules.session.QueryCommand;

/**
 * A simple http client that talks to a server running the session-testing-war.
 *
 * <p>
 * This client sends commands to the {@link CommandServlet} over http to modify session properties
 * and returns the results. The client has support for connecting to multiple servers and sending
 * the session cookie returned by one server to other servers, to emulate the behavior of a client
 * talking to the servers through a load balancer.
 *
 * <p>
 * The client currently only targets servers running on "localhost"
 *
 * <p>
 * To set the server this client is targeting, use {@link #setPort}.
 *
 * <p>
 * <b>Jakarta EE 10 Migration Changes:</b>
 * <ul>
 * <li>Apache HttpComponents 4.x → 5.x (required for Jakarta compatibility)</li>
 * <li>Package names: org.apache.http.* → org.apache.hc.client5.* and org.apache.hc.core5.*</li>
 * <li>API changes: StatusLine → getCode()/getReasonPhrase(), BasicHttpContext →
 * HttpClientContext</li>
 * <li>URI building: setHost() now requires separate setHost(host) and setPort(port) calls</li>
 * </ul>
 */
public class Client {
  private static final String HOST = "localhost";
  private int port = 8080;
  private String cookie;
  private final HttpClientContext context;

  private final URIBuilder reqURIBuild;
  private final CloseableHttpClient httpclient;

  public Client() {
    reqURIBuild = new URIBuilder();
    reqURIBuild.setScheme("http");

    httpclient = HttpClients.createDefault();
    context = HttpClientContext.create();

    cookie = null;
  }

  /**
   * Change the server that the client is targeting.
   *
   * @param port the port that the server is listening on
   */
  public void setPort(int port) {
    this.port = port;
  }

  /*
   * Get the value of a session attribute on the server
   */
  public Response get(String key) throws IOException, URISyntaxException {
    return get(key, true);
  }

  /*
   * Get the value of a session attribute on the server
   */
  public Response get(String key, boolean storeRespCookie) throws IOException, URISyntaxException {
    resetURI();
    reqURIBuild.setParameter("cmd", QueryCommand.GET.name());
    reqURIBuild.setParameter("param", key);

    return doRequest(new HttpGet(reqURIBuild.build()), storeRespCookie);
  }

  /*
   * Set the value of a session attribute on the server
   */
  public Response set(String key, String value) throws IOException, URISyntaxException {
    return set(key, value, true);
  }

  /*
   * Set the value of a session attribute on the server
   */
  public Response set(String key, String value, boolean storeRespCookie)
      throws IOException, URISyntaxException {
    resetURI();
    reqURIBuild.setParameter("cmd", QueryCommand.SET.name());
    reqURIBuild.setParameter("param", key);
    reqURIBuild.setParameter("value", value);

    return doRequest(new HttpGet(reqURIBuild.build()), storeRespCookie);
  }

  /*
   * Instantiate and execute a function in the server. Note that this function must be present in
   * the sesssion testing war.
   */
  public Response executionFunction(
      Class<? extends Function<HttpServletRequest, String>> functionClass)
      throws IOException, URISyntaxException {
    resetURI();
    reqURIBuild.setParameter("cmd", QueryCommand.FUNCTION.name());
    reqURIBuild.setParameter("function", functionClass.getName());

    return doRequest(new HttpGet(reqURIBuild.build()), true);
  }

  /**
   * Remove the session attribute on the server
   *
   * @param key - the session attribute to remove
   * @return the response to the request
   * @throws IOException if an exception is encountered while sending the request
   * @throws URISyntaxException if the generated string could not be parsed as a URI reference
   */
  public Response remove(String key) throws IOException, URISyntaxException {
    return remove(key, true);
  }

  /**
   * Remove the session attribute on the server
   *
   * @param key - the session attribute to remove
   * @param storeRespCookie - whether or not to store the session cookie of this request
   * @return the response to the request
   * @throws IOException if an exception is encountered while sending the request
   * @throws URISyntaxException if the generated string could not be parsed as a URI reference
   */
  public Response remove(String key, boolean storeRespCookie)
      throws URISyntaxException, IOException {
    resetURI();
    reqURIBuild.setParameter("cmd", QueryCommand.REMOVE.name());
    reqURIBuild.setParameter("param", key);

    return doRequest(new HttpGet(reqURIBuild.build()), storeRespCookie);
  }

  /*
   * Invalidate this clients session on the server
   */
  public Response invalidate() throws IOException, URISyntaxException {
    return invalidate(true);
  }

  /*
   * Invalidate this clients session on the server
   */
  public Response invalidate(boolean storeRespCookie) throws IOException, URISyntaxException {
    resetURI();
    reqURIBuild.setParameter("cmd", QueryCommand.INVALIDATE.name());
    reqURIBuild.setParameter("param", "null");

    return doRequest(new HttpGet(reqURIBuild.build()), storeRespCookie);
  }

  /**
   * Set the maximum inactive interval for this client's session on the server.
   *
   * If this time interval elapses without activity on the session, the session will expire.
   *
   * @param time - Time in seconds until the session should expire
   * @return the response to the request
   * @throws IOException if an exception is encountered while sending the request
   * @throws URISyntaxException if the generated string could not be parsed as a URI reference
   */
  public Response setMaxInactive(int time) throws IOException, URISyntaxException {
    return setMaxInactive(time, true);
  }

  /**
   * Set the maximum inactive interval for this client's session on the server.
   *
   * If this time interval elapses without activity on the session, the session will expire.
   *
   * @param time - Time in seconds until the session should expire
   * @param storeRespCookie whether to retain the value of a "Set-Cookie" header returned in the
   *        response
   * @return the response to the request
   * @throws IOException if an exception is encountered while sending the request
   * @throws URISyntaxException if the generated string could not be parsed as a URI reference
   */
  public Response setMaxInactive(int time, boolean storeRespCookie)
      throws IOException, URISyntaxException {
    resetURI();
    reqURIBuild.setParameter("cmd", QueryCommand.SET_MAX_INACTIVE.name());
    reqURIBuild.setParameter("value", Integer.toString(time));

    return doRequest(new HttpGet(reqURIBuild.build()), storeRespCookie);
  }

  private void resetURI() {
    reqURIBuild.setHost(HOST);
    reqURIBuild.setPort(port);
    reqURIBuild.clearParameters();
  }

  /**
   * Sends a request to the server and returns a custom response object with the result
   *
   * @param storeRespCookie if true, retain the value of a "Set-Cookie" header returned in the
   *        response.
   */
  private Response doRequest(HttpGet req, boolean storeRespCookie) throws IOException {
    addCookieHeader(req);

    CloseableHttpResponse resp = httpclient.execute(req, context);

    boolean isNew = true;
    String reqCookie = getCookieHeader(resp);
    if (reqCookie == null) {
      isNew = false;
      reqCookie = cookie;
    } else if (storeRespCookie) {
      cookie = reqCookie;
    }

    // HttpClient 5.x: StatusLine replaced with getCode() and getReasonPhrase()
    int statusCode = resp.getCode();
    if (statusCode != 200) {
      throw new IOException("Http request to " + HOST + ":" + port + " failed. Status: "
          + statusCode + " " + resp.getReasonPhrase());
    }

    String responseBody;
    try {
      responseBody = EntityUtils.toString(resp.getEntity());
    } catch (ParseException e) {
      throw new IOException("Failed to parse response entity", e);
    }

    Response response = new Response(reqCookie, responseBody, isNew);
    resp.close();
    return response;
  }

  private void addCookieHeader(HttpGet req) {
    // Set the cookie header
    if (cookie != null) {
      BasicClientCookie cookie = new BasicClientCookie("JSESSIONID", this.cookie);
      cookie.setDomain(HOST);
      cookie.setPath("/");

      BasicCookieStore cookieStore = new BasicCookieStore();
      cookieStore.addCookie(cookie);

      context.setAttribute(HttpClientContext.COOKIE_STORE, cookieStore);
    }
  }

  private String getCookieHeader(CloseableHttpResponse resp) {
    Header lastHeader = resp.getLastHeader("Set-Cookie");

    if (lastHeader == null) {
      return null;
    }
    // HttpClient 5.x: Parse Set-Cookie header value directly
    // Format: JSESSIONID=value; Path=/; ...
    String headerValue = lastHeader.getValue();
    int semicolonIndex = headerValue.indexOf(';');
    if (semicolonIndex > 0) {
      headerValue = headerValue.substring(0, semicolonIndex);
    }
    int equalsIndex = headerValue.indexOf('=');
    if (equalsIndex > 0) {
      return headerValue.substring(equalsIndex + 1);
    }
    return headerValue;
  }

  /**
   * A response received from the server.
   *
   * Currently contains the text value of the response body, as well as the session cookie.
   */
  public class Response {
    private final String sessionCookie;
    private final String response;
    private final boolean isNew;

    public Response(String sessionCookie, String response, boolean isNew) {
      this.sessionCookie = sessionCookie;
      this.response = response;
      this.isNew = isNew;
    }

    /*
     * The session cookie associated with this client.
     */
    public String getSessionCookie() {
      return sessionCookie;
    }

    /*
     * The String value of the response body.
     */
    public String getResponse() {
      return response;
    }

    /**
     * @return true if this response contained a "Set-Cookie" header, indicating that the server is
     *         setting a new value for the session cookie.
     */
    public boolean isNew() {
      return isNew;
    }

    @Override
    public String toString() {
      return "Response{" + "sessionCookie='" + sessionCookie + '\'' + ", response='" + response
          + '\'' + '}';
    }
  }
}
