/*
 * 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.internal.jta;

import static org.apache.geode.distributed.ConfigurationProperties.CACHE_XML_FILE;
import static org.apache.geode.test.util.ResourceUtils.getResource;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.catchThrowable;

import java.io.File;
import java.nio.charset.Charset;
import java.sql.SQLException;
import java.time.Duration;
import java.util.Properties;
import java.util.regex.Pattern;

import javax.naming.Context;
import javax.sql.DataSource;
import javax.transaction.xa.XAException;

import jakarta.transaction.SystemException;
import jakarta.transaction.UserTransaction;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;

import org.apache.geode.cache.Cache;
import org.apache.geode.cache.CacheFactory;
import org.apache.geode.internal.process.ProcessUtils;

/**
 * Fixed up (best as possible) and re-enabled JTA tests from ExceptionsDUnitTest. Treat these as
 * characterization tests because I think some of this behavior is not strictly correct.
 */
public class TransactionTimeoutExceptionIntegrationTest {

  private static final Pattern NEW_DB_PATTERN = Pattern.compile("newDB");
  private static final int DATA_SOURCE_POOL_SIZE = 2;
  private static final int BLOCKING_TIMEOUT_SECONDS = 6;
  private static final int IDLE_TIMEOUT_SECONDS = 600;
  private static final int LOGIN_TIMEOUT_SECONDS = 2;
  private static final int TRANSACTION_TIMEOUT_SECONDS = 2;

  private Cache cache;

  @Rule
  public TemporaryFolder temporaryFolder = new TemporaryFolder();

  @Before
  public void setUp() throws Exception {
    String derbySystemHome = temporaryFolder.newFolder("derby").getAbsolutePath();
    System.setProperty("derby.system.home", derbySystemHome);

    String newDB = "newDB_" + ProcessUtils.identifyPid();

    String input =
        IOUtils.toString(getResource("TransactionTimeoutExceptionIntegrationTest_cachejta.xml"),
            Charset.defaultCharset());
    input = NEW_DB_PATTERN.matcher(input).replaceAll(newDB);
    String output = modifyCacheJtaXml(input, newDB);
    File cacheJtaXmlFile = temporaryFolder.newFile("cachejta.xml");

    FileUtils.writeStringToFile(cacheJtaXmlFile, output, Charset.defaultCharset());

    Properties props = new Properties();
    props.setProperty(CACHE_XML_FILE, cacheJtaXmlFile.getAbsolutePath());

    cache = new CacheFactory(props).create();
  }

  @After
  public void tearDown() throws Exception {
    cache.close();
  }

  @Test
  public void testBlockingTimeOut() throws Exception {
    Context jndiContext = cache.getJNDIContext();
    DataSource dataSource = (DataSource) jndiContext.lookup("java:/XAPooledDataSource");
    UserTransaction utx = (UserTransaction) jndiContext.lookup("java:/UserTransaction");
    utx.begin();
    dataSource.getConnection();

    // sleep longer than the blocking timeout (nothing exposed to await on)
    Thread.sleep(Duration.ofSeconds(BLOCKING_TIMEOUT_SECONDS * 2).toMillis());

    Throwable thrown = catchThrowable(utx::commit);

    // NOTE: XAException is double-wrapped in SystemException (probably unintentional)
    assertThat(thrown)
        .isInstanceOf(SystemException.class);
    assertThat(thrown.getCause())
        .isInstanceOf(SystemException.class)
        .hasCauseInstanceOf(XAException.class)
        .hasMessageContaining("No current connection");
  }

  @Test
  public void testLoginTimeOut() throws Exception {
    Context jndiContext = cache.getJNDIContext();
    DataSource dataSource = (DataSource) jndiContext.lookup("java:/XAPooledDataSource");
    for (int i = 0; i < DATA_SOURCE_POOL_SIZE; i++) {
      dataSource.getConnection();
    }

    Throwable thrown = catchThrowable(dataSource::getConnection);

    assertThat(thrown)
        .isInstanceOf(SQLException.class)
        .hasMessageContaining("Login time-out exceeded");
  }

  @Test
  public void testTransactionTimeOut() throws Exception {
    Context jndiContext = cache.getJNDIContext();
    DataSource dataSource = (DataSource) jndiContext.lookup("java:/XAPooledDataSource");
    dataSource.getConnection();
    UserTransaction utx = (UserTransaction) jndiContext.lookup("java:/UserTransaction");
    utx.begin();
    utx.setTransactionTimeout(TRANSACTION_TIMEOUT_SECONDS);

    // sleep longer than the transaction timeout (nothing exposed to await on)
    Thread.sleep(Duration.ofSeconds(TRANSACTION_TIMEOUT_SECONDS * 2).toMillis());

    Throwable thrown = catchThrowable(utx::commit);

    assertThat(thrown)
        .isInstanceOf(IllegalStateException.class)
        .hasMessageContaining("Transaction is null, cannot commit a null transaction");
  }

  private String modifyCacheJtaXml(String jtaConfig, String newDB) {
    String begin = "<jndi-binding type=\"XAPooledDataSource\"";
    String end = "</jndi-binding>";
    String jndiBinding =
        "<jndi-binding type=\"XAPooledDataSource\" " +
            "jndi-name=\"XAPooledDataSource\" " +
            "jdbc-driver-class=\"org.apache.derby.jdbc.EmbeddedDriver\" " +
            "init-pool-size=\"" + DATA_SOURCE_POOL_SIZE + "\" " +
            "max-pool-size=\"" + DATA_SOURCE_POOL_SIZE + "\" " +
            "idle-timeout-seconds=\"" + IDLE_TIMEOUT_SECONDS + "\" " +
            "blocking-timeout-seconds=\"" + BLOCKING_TIMEOUT_SECONDS + "\" " +
            "login-timeout-seconds=\"" + LOGIN_TIMEOUT_SECONDS + "\" " +
            "conn-pooled-datasource-class=\"org.apache.derby.jdbc.EmbeddedConnectionPoolDataSource\" "
            +
            "xa-datasource-class=\"org.apache.derby.jdbc.EmbeddedXADataSource\" " +
            "user-name=\"mitul\" " +
            "password=\"83f0069202c571faf1ae6c42b4ad46030e4e31c17409e19a\" " +
            "connection-url=\"jdbc:derby:" + newDB + ";create=true\" >";
    String configProperty =
        "<config-property>" +
            "<config-property-name>description</config-property-name>" +
            "<config-property-type>java.lang.String</config-property-type>" +
            "<config-property-value>hi</config-property-value>" +
            "</config-property>" +
            "<config-property>" +
            "<config-property-name>user</config-property-name>" +
            "<config-property-type>java.lang.String</config-property-type>" +
            "<config-property-value>jeeves</config-property-value>" +
            "</config-property>" +
            "<config-property>" +
            "<config-property-name>password</config-property-name>" +
            "<config-property-type>java.lang.String</config-property-type>" +
            "<config-property-value>" +
            "83f0069202c571faf1ae6c42b4ad46030e4e31c17409e19a" +
            "</config-property-value>" +
            "</config-property>" +
            "<config-property>" +
            "<config-property-name>databaseName</config-property-name>" +
            "<config-property-type>java.lang.String</config-property-type>" +
            "<config-property-value>" + newDB + "</config-property-value>" +
            "</config-property>" +
            System.lineSeparator();

    String modifiedConfig = jndiBinding + configProperty;

    int indexOfBegin = jtaConfig.indexOf(begin);
    int indexOfEnd = jtaConfig.indexOf(end, indexOfBegin);

    return new StringBuilder(jtaConfig).replace(indexOfBegin, indexOfEnd, modifiedConfig)
        .toString();
  }
}
