// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2012-2014 Monty Program Ab
// Copyright (c) 2015-2021 MariaDB Corporation Ab

package org.mariadb.jdbc.integration;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.FileAppender;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import javax.net.ssl.SSLException;
import javax.security.auth.x500.X500Principal;
import javax.sql.PooledConnection;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.Test;
import org.mariadb.jdbc.Common;
import org.mariadb.jdbc.Connection;
import org.mariadb.jdbc.MariaDbPoolDataSource;
import org.mariadb.jdbc.Statement;
import org.mariadb.jdbc.client.tls.HostnameVerifier;
import org.slf4j.LoggerFactory;

public class LoggingTest extends Common {

  @Test
  void basicLogging() throws Exception {
    Assumptions.assumeTrue(isMariaDBServer());
    File tempFile = File.createTempFile("log", ".tmp");

    Logger logger = (Logger) LoggerFactory.getLogger("org.mariadb.jdbc");
    Level initialLevel = logger.getLevel();
    logger.setLevel(Level.TRACE);
    logger.setAdditive(false);
    logger.detachAndStopAllAppenders();

    LoggerContext context = new LoggerContext();
    FileAppender<ILoggingEvent> fa = new FileAppender<>();
    fa.setName("FILE");
    fa.setImmediateFlush(true);
    PatternLayoutEncoder pa = new PatternLayoutEncoder();
    pa.setPattern("%r %5p %c [%t] - %m%n");
    pa.setContext(context);
    pa.start();
    fa.setEncoder(pa);

    fa.setFile(tempFile.getPath());
    fa.setAppend(true);
    fa.setContext(context);
    fa.start();

    logger.addAppender(fa);

    try (Connection conn = createCon()) {
      Statement stmt = conn.createStatement();
      stmt.execute("SELECT 1");
    }
    try (Connection conn = createCon("useCompression=true")) {
      Statement stmt = conn.createStatement();
      stmt.execute("SELECT 1");
    }

    MariaDbPoolDataSource ds =
        new MariaDbPoolDataSource(
            mDefUrl + "&sessionVariables=wait_timeout=1&maxIdleTime=2&testMinRemovalDelay=2");
    Thread.sleep(4000);
    PooledConnection pc = ds.getPooledConnection();
    pc.getConnection().isValid(1);
    pc.close();
    ds.close();
    try {
      String contents = new String(Files.readAllBytes(Paths.get(tempFile.getPath())));
      String defaultRequest =
          "+--------------------------------------------------+\n"
              + "|  0  1  2  3  4  5  6  7   8  9  a  b  c  d  e  f |\n"
              + "+--------------------------------------------------+------------------+\n"
              + "| 23 00 00 00 03 53 45 54  20 53 45 53 53 49 4F 4E | #....SET SESSION |\n"
              + "| 20 54 52 41 4E 53 41 43  54 49 4F 4E 20 52 45 41 |  TRANSACTION REA |\n"
              + "| 44 20 57 52 49 54 45                             | D WRITE          |\n"
              + "+--------------------------------------------------+------------------+";
      if (!"maxscale".equals(System.getenv("srv")) && !"skysql-ha".equals(System.getenv("srv"))) {
        Assertions.assertTrue(
            contents.contains(defaultRequest)
                || contents.contains(defaultRequest.replace("\r\n", "\n")),
            contents);
      }
      String selectOne =
          "+--------------------------------------------------+\n"
              + "|  0  1  2  3  4  5  6  7   8  9  a  b  c  d  e  f |\n"
              + "+--------------------------------------------------+------------------+\n"
              + "| 09 00 00 00 03 53 45 4C  45 43 54 20 31          | .....SELECT 1    |\n"
              + "+--------------------------------------------------+------------------+\n";
      Assertions.assertTrue(
          contents.contains(selectOne) || contents.contains(selectOne.replace("\r\n", "\n")),
          contents);
      String rowResult =
          "+--------------------------------------------------+\n"
              + "|  0  1  2  3  4  5  6  7   8  9  a  b  c  d  e  f |\n"
              + "+--------------------------------------------------+------------------+\n"
              + "| 02 00 00 03 01 31                                | .....1           |\n"
              + "+--------------------------------------------------+------------------+\n";
      String rowResultWithEof =
          "+--------------------------------------------------+\n"
              + "|  0  1  2  3  4  5  6  7   8  9  a  b  c  d  e  f |\n"
              + "+--------------------------------------------------+------------------+\n"
              + "| 02 00 00 04 01 31                                | .....1           |\n"
              + "+--------------------------------------------------+------------------+\n";
      if ("maxscale".equals(System.getenv("srv")) || "skysql-ha".equals(System.getenv("srv"))) {
        Assertions.assertTrue(
            contents.contains(rowResultWithEof)
                || contents.contains(rowResultWithEof.replace("\r\n", "\n")),
            contents);
      } else {
        Assertions.assertTrue(
            contents.contains(rowResult) || contents.contains(rowResult.replace("\r\n", "\n")),
            contents);
      }

      Assertions.assertTrue(
          contents.contains(
              "pool MariaDB-pool new physical connection created (total:1, active:0, pending:0)"),
          contents);
      Assertions.assertTrue(
          contents.contains("pool MariaDB-pool connection removed due to inactivity"), contents);

      logger.setLevel(initialLevel);
      logger.detachAppender(fa);
    } catch (IOException e) {
      e.printStackTrace();
      Assertions.fail();
    }
  }

  @Test
  void certLogging() throws Exception {
    File tempFile = File.createTempFile("log", ".tmp");

    Logger logger = (Logger) LoggerFactory.getLogger("org.mariadb.jdbc");
    Level initialLevel = logger.getLevel();
    logger.setLevel(Level.TRACE);
    logger.setAdditive(false);
    logger.detachAndStopAllAppenders();

    LoggerContext context = new LoggerContext();
    FileAppender<ILoggingEvent> fa = new FileAppender<>();
    fa.setName("FILE");
    fa.setImmediateFlush(true);
    PatternLayoutEncoder pa = new PatternLayoutEncoder();
    pa.setPattern("%r %5p %c [%t] - %m%n");
    pa.setContext(context);
    pa.start();
    fa.setEncoder(pa);

    fa.setFile(tempFile.getPath());

    fa.setAppend(true);
    fa.setContext(context);
    fa.start();

    logger.addAppender(fa);

    String certString =
        ""
            + "-----BEGIN CERTIFICATE-----\n"
            + "MIIDfDCCAmSgAwIBAgIURZJQVOWv+oaj+MLlHWc1B0TnOaowDQYJKoZIhvcNAQEL\n"
            + "BQAwUjELMAkGA1UEBhMCQ04xCzAJBgNVBAgMAkdEMQswCQYDVQQHDAJTWjESMBAG\n"
            + "A1UECgwJQWNtZSxJbmMuMRUwEwYDVQQDDAxBY21lIFJvb3QgQ0EwIBcNMjEwMzMw\n"
            + "MDkwODAxWhgPMjEyMTAzMDYwOTA4MDFaMFMxCzAJBgNVBAYTAkNOMQswCQYDVQQI\n"
            + "DAJHRDELMAkGA1UEBwwCU1oxEjAQBgNVBAoMCUFjbWUsSW5jLjEWMBQGA1UEAwwN\n"
            + "Ki5tYXJpYWRiLm9yZzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANAJ\n"
            + "xqbqTGmwO5n3kVd6QJPRSh+0M1HIQacyM/tkE7jLw3725/KtknuwuFbPpxKyTCLC\n"
            + "IoNx4yaBbmx783OPP3pokXTWiMdrVZdLltBNamNzekNFN4YhR5oN479M5cKgrk94\n"
            + "Ud+ql0NN5FscrSQ0fSdJf0idJMqThro1MJVp9rp5cdCba6/lKyDbdOybe5f7rmrg\n"
            + "+37J+src67+rqwVT8ZwZgLTGDf4X9OSIzyw6+PCWYWr89aurrOuOyqA3QqXVRZa/\n"
            + "IxOMHIdzXMgLN6+HduwdZ+DNv1NPT2MDlRQvOnDop3NoEVKWekOTv50LbKRgWTYO\n"
            + "TK/dfcsDpZmdyHv7pb8CAwEAAaNHMEUwQwYDVR0RBDwwOoIVbG9jYWxob3N0Lmxv\n"
            + "Y2FsZG9tYWlugglsb2NhbGhvc3SHBH8AAAGHECABDbg5AjRoAAAAAAAABEMwDQYJ\n"
            + "KoZIhvcNAQELBQADggEBAHsiJz9cpmL8BTa/o10S+pmap3iOnYYuJT0llCRLJ+Ji\n"
            + "msO2niyIwqCJHMLcEABCENJt0HDOEKlnunVgc+X/6K8DnPrYhfWQbYI/dwUBoSIQ\n"
            + "siK/yKW0q+S+YjCVpNMA3iMfhJ9Qe9LDO+xdCBhzplgrV8YwG+J2FUNbZfvl5cML\n"
            + "TjKLWrWo9dgZyH/7mjwryRzswfUfr/lRARCyrMotaXfYmjPjwTSRc0aPGrEjs3ns\n"
            + "WMtimgh7Zw3Tbxc51miz9CRy767lq/9BGTdeBLmW0EXssIJb9uO0Ht3C/Pqy0ojk\n"
            + "8e1eYtofjTsqWHZ1s2LhtT0HvXdL6BnWP9GWc/zxiKM=\n"
            + "-----END CERTIFICATE-----\n";
    CertificateFactory cf = CertificateFactory.getInstance("X.509");
    X509Certificate cert =
        (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(certString.getBytes()));

    assertEquals(
        new X500Principal("CN=*.mariadb.org, O=\"Acme,Inc.\", L=SZ, ST=GD, C=CN"),
        cert.getSubjectX500Principal());
    HostnameVerifier.verify("localhost", cert, -1);
    HostnameVerifier.verify("localhost.localdomain", cert, -1);
    verifyExceptionEqual(
        "local.host",
        cert,
        "DNS host \"local.host\" doesn't correspond to certificate "
            + "CN \"*.mariadb.org\" and SAN[{DNS:\"localhost.localdomain\"},{DNS:\"localhost\"},{IP:\"127.0.0.1\"},{IP:\"2001:db8:3902:3468:0:0:0:443\"}]");

    HostnameVerifier.verify("127.0.0.1", cert, -1);
    verifyExceptionEqual(
        "127.0.0.2",
        cert,
        "IPv4 host \"127.0.0.2\" doesn't correspond to certificate "
            + "CN \"*.mariadb.org\" and SAN[{DNS:\"localhost.localdomain\"},{DNS:\"localhost\"},{IP:\"127.0.0.1\"},{IP:\"2001:db8:3902:3468:0:0:0:443\"}]");

    HostnameVerifier.verify("2001:db8:3902:3468:0:0:0:443", cert, -1);
    verifyExceptionEqual(
        "2001:db8:1::",
        cert,
        "IPv6 host \"2001:db8:1::\" doesn't correspond to certificate "
            + "CN \"*.mariadb.org\" and SAN[{DNS:\"localhost.localdomain\"},{DNS:\"localhost\"},{IP:\"127.0.0.1\"},{IP:\"2001:db8:3902:3468:0:0:0:443\"}]");
    try {
      String contents = new String(Files.readAllBytes(Paths.get(tempFile.getPath())));

      assertTrue(
          contents.contains(
              "DNS verification of hostname : type=DNS value=localhost.localdomain to local.host"));
      assertTrue(
          contents.contains(
              "IPv4 verification of hostname : type=IP value=127.0.0.1 to 127.0.0.2"));
      assertTrue(
          contents.contains(
              "IPv6 verification of hostname : type=IP value=2001:db8:3902:3468:0:0:0:443 to 2001:db8:1::"));

      logger.setLevel(initialLevel);
      logger.detachAppender(fa);
    } catch (IOException e) {
      e.printStackTrace();
      Assertions.fail();
    }
  }

  private void verifyExceptionEqual(String host, X509Certificate cert, String exceptionMessage) {
    Exception e =
        Assertions.assertThrows(SSLException.class, () -> HostnameVerifier.verify(host, cert, -1));
    Assertions.assertTrue(
        e.getMessage().contains(exceptionMessage), "real message:" + e.getMessage());
  }
}
