package org.folio.okapi.service.impl;

import io.vertx.core.Vertx;
import io.vertx.core.json.JsonObject;
import io.vertx.junit5.Timeout;
import io.vertx.junit5.VertxExtension;
import io.vertx.junit5.VertxTestContext;
import java.io.IOException;
import org.assertj.core.api.WithAssertions;
import org.folio.okapi.common.OkapiLogger;
import org.folio.okapi.util.PgTestBase;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.testcontainers.containers.Container.ExecResult;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.MountableFile;

@Timeout(5000)
@ExtendWith(VertxExtension.class)
@Testcontainers(disabledWithoutDocker = true)
class PostgresHandleTest extends PgTestBase implements WithAssertions {

  static final String KEY_PATH = "/var/lib/postgresql/data/server.key";
  static final String CRT_PATH = "/var/lib/postgresql/data/server.crt";
  static final String CONF_PATH = "/var/lib/postgresql/data/postgresql.conf";
  static final String CONF_BAK_PATH = "/var/lib/postgresql/data/postgresql.conf.bak";
  static String serverCrt;

  static void exec(String... command) {
    try {
      ExecResult execResult = POSTGRESQL_CONTAINER.execInContainer(command);
      OkapiLogger.get().debug(() -> String.join(" ", command) + " " + execResult);
    } catch (InterruptedException|IOException|UnsupportedOperationException e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Append each entry to postgresql.conf and reload it into postgres.
   * Appending a key=value entry has precedence over any previous entries of the same key.
   */
  static void configure(String... configEntries) {
    exec("cp", "-p", CONF_BAK_PATH, CONF_PATH);  // start with unaltered config
    for (String configEntry : configEntries) {
      exec("sh", "-c", "echo '" + configEntry + "' >> " + CONF_PATH);
    }
    exec("su-exec", "postgres", "pg_ctl", "reload");
  }

  @BeforeAll
  static void beforeAll() {
    MountableFile serverKeyFile = MountableFile.forClasspathResource("server.key");
    MountableFile serverCrtFile = MountableFile.forClasspathResource("server.crt");
    POSTGRESQL_CONTAINER.copyFileToContainer(serverKeyFile, KEY_PATH);
    POSTGRESQL_CONTAINER.copyFileToContainer(serverCrtFile, CRT_PATH);
    exec("chown", "postgres.postgres", KEY_PATH, CRT_PATH);
    exec("chmod", "400", KEY_PATH, CRT_PATH);
    exec("cp", "-p", CONF_PATH, CONF_BAK_PATH);

    serverCrt = getResource("server.crt");
    OkapiLogger.get().debug(() -> config().encodePrettily());
  }

  @AfterAll
  static void afterAll() {
    configure();  // restore and reload original config
  }

  @Test
  void hostAndPort(Vertx vertx) {
    PostgresHandle postgresHandle = new PostgresHandle(vertx,
        new JsonObject().put("postgres_host", "example.com").put("postgres_port", "9876"));
    assertThat(postgresHandle.getOptions())
    .extracting("getHost", "getPort").containsExactly("example.com", 9876);
  }

  @Test
  void ignoreInvalidPortNumber(Vertx vertx) {
    PostgresHandle postgresHandle = new PostgresHandle(vertx,
        new JsonObject().put("postgres_port", "q"));
    assertThat(postgresHandle.getOptions())
    .extracting("getHost", "getPort").containsExactly("localhost", 5432);
  }

  static private JsonObject config() {
    return new JsonObject()
        .put("postgres_host", POSTGRESQL_CONTAINER.getHost())
        .put("postgres_port", POSTGRESQL_CONTAINER.getFirstMappedPort() + "")
        .put("postgres_database", POSTGRESQL_CONTAINER.getDatabaseName())
        .put("postgres_username", POSTGRESQL_CONTAINER.getUsername())
        .put("postgres_password", POSTGRESQL_CONTAINER.getPassword())
        .put("postgres_server_pem", serverCrt);
  }

  @Test
  @DisplayName("Basic connectivity test without encryption")
  @SuppressWarnings("java:S2699")  // suppress "Tests should include assertions"
  void connectWithoutSsl(Vertx vertx, VertxTestContext vtc) {
    configure("ssl = off");
    JsonObject config = config();
    config.remove("postgres_server_pem");
    new PostgresHandle(vertx, config).getConnection(vtc.completing());
  }

  @Test
  void rejectWithoutSsl(Vertx vertx, VertxTestContext vtc) {
    configure("ssl = off");
    new PostgresHandle(vertx, config()).getConnection(vtc.failing(fail -> vtc.completeNow()));
  }

  @Test
  void tlsv1_3(Vertx vertx, VertxTestContext vtc) {
    configure("ssl = on");
    new PostgresHandle(vertx, config()).getConnection(vtc.succeeding(connection -> vtc.verify(() -> {
      assertThat(connection.isSSL()).isTrue();
      String sql = "SELECT version FROM pg_stat_ssl WHERE pid = pg_backend_pid()";
      connection.query(sql, vtc.succeeding(rowset -> vtc.verify(() -> {
        assertThat(rowset.iterator().next().getString(0)).isEqualTo("TLSv1.3");
        vtc.completeNow();
      })));
    })));
  }

  @Test
  void rejectTlsv1_2(Vertx vertx, VertxTestContext vtc) {
    configure("ssl = on", "ssl_min_protocol_version = TLSv1.2", "ssl_max_protocol_version = TLSv1.2");
    new PostgresHandle(vertx, config()).getConnection(vtc.failing(fail -> vtc.completeNow()));
  }
}