/*
 * Copyright 2017-2019 the original author or authors.
 *
 * 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
 *
 *      https://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 io.r2dbc.spi.test;

import io.r2dbc.spi.Blob;
import io.r2dbc.spi.Clob;
import io.r2dbc.spi.Connection;
import io.r2dbc.spi.ConnectionFactory;
import io.r2dbc.spi.Result;
import io.r2dbc.spi.Statement;
import io.r2dbc.spi.ValidationDepth;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.support.AbstractLobCreatingPreparedStatementCallback;
import org.springframework.jdbc.support.lob.DefaultLobHandler;
import org.springframework.jdbc.support.lob.LobCreator;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;

import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import java.util.stream.IntStream;

import static org.junit.jupiter.api.Assertions.assertThrows;

/**
 * R2DBC TCK implementation to verify a driver.
 * <p>Functionality that is not supported by a driver should exclude the individual tests by using {@link Disabled @Disabled}.
 * Implementors need to provide the following methods:
 *
 * <ul>
 *     <li>{@link #getConnectionFactory()}: Provide the {@link ConnectionFactory} under test.</li>
 *     <li>{@link #getCreateTableWithAutogeneratedKey()}: Return the {@code CREATE TABLE} statement to create a table.</li>
 *     <li>{@link #getPlaceholder(int)}: Return an parameter placeholder for the {@code n}th argument. Arguments start at zero.</li>
 *     <li>{@link #getIdentifier(int)}: Return an identifier for the {@code n}th argument. The placeholder was obtained prior to this call via  {@link #getPlaceholder(int)}. Arguments start at zero
 *     <li>{@link #getJdbcOperations}: Return a handle to {@link JdbcOperations} configured to the same data source as {@link #getConnectionFactory()}. Used for test fixture setup/teardown.</li>
 * </ul>
 *
 * <p>The following customization hooks can be overridden to adapt the TCK to a specific database:
 * <ul>
 *    <li>{@link #getInsertIntoWithAutogeneratedKey()}: Return the {@code INSERT INTO} statement to insert data into a table supporting auto-generated keys.</li>
 *    <li>{@link #blobType()}: Returns the SQL type for a {@link Blob} column.</li>
 *    <li>{@link #clobType()}: Returns the SQL type for a {@link Clob} column.</li>
 * </ul>
 *
 * @param <T> type of parameter placeholder identifiers, see {@link #getIdentifier(int)}.
 */
public interface TestKit<T> {

    static <T> Mono<T> close(Connection connection) {
        return Mono.from(connection
            .close())
            .then(Mono.empty());
    }

    static <T> Mono<T> discard(Blob blob) {
        return Mono.from(blob
            .discard())
            .then(Mono.empty());
    }

    static <T> Mono<T> discard(Clob clob) {
        return Mono.from(clob
            .discard())
            .then(Mono.empty());
    }

    static Mono<List<Integer>> extractColumns(Result result) {
        return Flux.from(result
            .map((row, rowMetadata) -> row.get("value", Integer.class)))
            .collectList();
    }

    static Mono<Integer> extractRowsUpdated(Result result) {
        return Mono.from(result.getRowsUpdated());
    }

    /**
     * Returns a {@link ConnectionFactory} for the connected database.
     *
     * @return a {@link ConnectionFactory} for the connected database
     */
    ConnectionFactory getConnectionFactory();

    /**
     * Returns the {@code CREATE TABLE} statement for a table named {@code test}
     * with two columns: First one uses auto-generated keys, second one is named {@code value} of type {@code INT}.
     * <p>
     * Example:
     * <pre class="code">
     *     CREATE TABLE test ( id SERIAL,  value INTEGER );
     *  // or
     *     CREATE TABLE test ( id INTEGER IDENTITY,  value INTEGER );
     *     </pre>
     *
     * @return the {@code CREATE TABLE} statement
     */
    String getCreateTableWithAutogeneratedKey();

    /**
     * Returns the database-specific placeholder for a given substitution.
     *
     * @param index the zero-index number of the substitution
     * @return the database-specific placeholder for a given substitution
     */
    String getPlaceholder(int index);

    /**
     * Returns the bind identifier for a given substitution.
     *
     * @param index the zero-index number of the substitution
     * @return the bind identifier for a given substitution
     */
    T getIdentifier(int index);

    /**
     * Returns a {@link JdbcOperations} for the connected database.
     *
     * @return a {@link JdbcOperations} for the connected database
     */
    JdbcOperations getJdbcOperations();

    @Test
    default void autoCommitByDefault() {
        Mono.from(getConnectionFactory().create())
            .flatMapMany(connection -> Flux.just(connection.isAutoCommit())
                .concatWith(close(connection)))
            .as(StepVerifier::create)
            .expectNext(true).as("new connections are in auto-commit mode")
            .verifyComplete();
    }

    @Test
    default void changeAutoCommitCommitsTransaction() {
        Mono.from(getConnectionFactory().create())
            .flatMapMany(connection ->
                Flux.from(connection.setAutoCommit(false))
                    .thenMany(connection.beginTransaction())
                    .thenMany(connection.createStatement("INSERT INTO test VALUES(200)").execute())
                    .flatMap(Result::getRowsUpdated)
                    .thenMany(connection.setAutoCommit(true))
                    .thenMany(connection.createStatement("SELECT value FROM test").execute())
                    .flatMap(it -> it.map((row, metadata) -> row.get("value")))
                    .concatWith(close(connection))
            )
            .as(StepVerifier::create)
            .expectNext(200).as("autoCommit(true) committed the transaction. Expecting a value to be present")
            .verifyComplete();
    }

    @Test
    default void sameAutoCommitLeavesTransactionUnchanged() {
        Mono.from(getConnectionFactory().create())
            .flatMapMany(connection ->
                Flux.from(connection.setAutoCommit(false))
                    .thenMany(connection.beginTransaction())
                    .thenMany(connection.createStatement("INSERT INTO test VALUES(200)").execute())
                    .flatMap(Result::getRowsUpdated)
                    .thenMany(connection.setAutoCommit(false))
                    .thenMany(connection.rollbackTransaction())
                    .thenMany(connection.createStatement("SELECT value FROM test").execute())
                    .flatMap(it -> it.map((row, metadata) -> row.get("value")))
                    .concatWith(close(connection))
            )
            .as(StepVerifier::create)
            .verifyComplete();
    }

    @Test
    default void batch() {
        getJdbcOperations().execute("INSERT INTO test VALUES (100)");

        Mono.from(getConnectionFactory().create())
            .flatMapMany(connection -> Flux.from(connection

                .createBatch()
                .add("INSERT INTO test VALUES(200)")
                .add("SELECT value FROM test")
                .execute())

                .concatWith(close(connection)))
            .flatMap(Result::getRowsUpdated)
            .then()
            .as(StepVerifier::create)
            .verifyComplete();
    }

    @Test
    default void bindFails() {
        Mono.from(getConnectionFactory().create())
            .flatMap(connection -> {

                Statement statement = connection.createStatement(String.format("INSERT INTO test VALUES(%s)", getPlaceholder(0)));
                assertThrows(IllegalArgumentException.class, () -> statement.bind(0, null), "bind(0, null) should fail");
                assertThrows(IndexOutOfBoundsException.class, () -> statement.bind(99, ""), "bind(nonexistent-index, null) should fail");
                assertThrows(IllegalArgumentException.class, () -> bind(statement, getIdentifier(0), null), "bind(identifier, null) should fail");
                assertThrows(IllegalArgumentException.class, () -> bind(statement, getIdentifier(0), Class.class), "bind(identifier, Class.class) should fail");
                assertThrows(IllegalArgumentException.class, () -> statement.bind("unknown", ""), "bind(unknown-placeholder, \"\") should fail");
                return close(connection);
            })
            .as(StepVerifier::create)
            .verifyComplete();
    }

    @Test
    default void bindNull() {
        Mono.from(getConnectionFactory().create())
            .flatMapMany(connection -> {
                Statement statement = connection
                    .createStatement(String.format("INSERT INTO test VALUES(%s)", getPlaceholder(0)));
                bindNull(statement, getIdentifier(0), Integer.class);
                return Flux.from(statement.add()
                    .execute())
                    .flatMap(TestKit::extractRowsUpdated)
                    .concatWith(close(connection));
            })
            .as(StepVerifier::create)
            .expectNextCount(1).as("rows inserted")
            .verifyComplete();
    }

    @Test
    default void bindNullFails() {
        Mono.from(getConnectionFactory().create())
            .flatMap(connection -> {

                Statement statement = connection.createStatement(String.format("INSERT INTO test VALUES(%s)", getPlaceholder(0)));
                assertThrows(IllegalArgumentException.class, () -> statement.bindNull(null, String.class), "bindNull(null, …) should fail");
                assertThrows(IllegalArgumentException.class, () -> bind(statement, getIdentifier(0), null), "bindNull(identifier, null) should fail");
                return close(connection);
            })
            .as(StepVerifier::create)
            .verifyComplete();
    }

    @Test
    default void blobInsert() {
        Mono.from(getConnectionFactory().create())
            .flatMapMany(connection -> {
                Statement statement = connection.createStatement(String.format("INSERT INTO blob_test VALUES (%s)", getPlaceholder(0)));
                bind(statement, getIdentifier(0), Blob.from(Mono.just(StandardCharsets.UTF_8.encode("test-value"))));
                return Flux.from(statement.execute())
                    .flatMap(TestKit::extractRowsUpdated)
                    .concatWith(close(connection));
            })
            .as(StepVerifier::create)
            .expectNextCount(1).as("rows inserted")
            .verifyComplete();
    }

    @Test
    default void blobSelect() {
        getJdbcOperations().execute("INSERT INTO blob_test VALUES (?)", new AbstractLobCreatingPreparedStatementCallback(new DefaultLobHandler()) {

            @Override
            protected void setValues(PreparedStatement ps, LobCreator lobCreator) throws SQLException {
                lobCreator.setBlobAsBytes(ps, 1, StandardCharsets.UTF_8.encode("test-value").array());
            }

        });

        // BLOB as ByteBuffer
        Mono.from(getConnectionFactory().create())
            .flatMapMany(connection -> Flux.from(connection

                .createStatement("SELECT * from blob_test")
                .execute())
                .flatMap(result -> result
                    .map((row, rowMetadata) -> row.get("value")))
                .cast(ByteBuffer.class)
                .map(buffer -> {
                    byte[] bytes = new byte[buffer.remaining()];
                    buffer.get(bytes);
                    return bytes;
                })
                .concatWith(close(connection)))
            .as(StepVerifier::create)
            .expectNextMatches(actual -> {
                ByteBuffer expected = StandardCharsets.UTF_8.encode("test-value");
                return Arrays.equals(expected.array(), actual);
            })
            .verifyComplete();

        // BLOB as Blob
        Mono.from(getConnectionFactory().create())
            .flatMapMany(connection -> Flux.from(connection

                .createStatement("SELECT * from blob_test")
                .execute())
                .flatMap(result -> result
                    .map((row, rowMetadata) -> row.get("value", Blob.class)))
                .flatMap(blob -> Flux.from(blob.stream())
                    .reduce(ByteBuffer::put)
                    .concatWith(discard(blob)))

                .concatWith(close(connection)))
            .as(StepVerifier::create)
            .expectNextMatches(actual -> {
                ByteBuffer expected = StandardCharsets.UTF_8.encode("test-value");
                return Arrays.equals(expected.array(), actual.array());
            })
            .verifyComplete();
    }

    /**
     * Customization hook: Returns the SQL type for a {@link Blob} column.
     *
     * @return the SQL type for a {@link Blob} column.
     */
    default String blobType() {
        return "BLOB";
    }

    @Test
    default void clobInsert() {
        Mono.from(getConnectionFactory().create())
            .flatMapMany(connection -> {
                Statement statement = connection.createStatement(String.format("INSERT INTO clob_test VALUES (%s)", getPlaceholder(0)));
                bind(statement, getIdentifier(0), Clob.from(Mono.just("test-value")));
                return Flux.from(statement.execute())
                    .flatMap(Result::getRowsUpdated)
                    .concatWith(close(connection));
            })
            .as(StepVerifier::create)
            .expectNextCount(1).as("rows inserted")
            .verifyComplete();
    }

    @Test
    default void clobSelect() {
        getJdbcOperations().execute("INSERT INTO clob_test VALUES (?)", new AbstractLobCreatingPreparedStatementCallback(new DefaultLobHandler()) {

            @Override
            protected void setValues(PreparedStatement ps, LobCreator lobCreator) throws SQLException {
                lobCreator.setClobAsString(ps, 1, "test-value");
            }

        });

        // CLOB defaults to String
        Mono.from(getConnectionFactory().create())
            .flatMapMany(connection -> Flux.from(connection

                .createStatement("SELECT * from clob_test")
                .execute())
                .flatMap(result -> result
                    .map((row, rowMetadata) -> row.get("value")))

                .concatWith(close(connection)))
            .as(StepVerifier::create)
            .expectNext("test-value").as("value from select")
            .verifyComplete();

        // CLOB consume as Clob
        Mono.from(getConnectionFactory().create())
            .flatMapMany(connection -> Flux.from(connection

                .createStatement("SELECT * from clob_test")
                .execute())
                .flatMap(result -> result
                    .map((row, rowMetadata) -> row.get("value", Clob.class)))
                .flatMap(clob -> Flux.from(clob.stream())
                    .reduce(new StringBuilder(), StringBuilder::append)
                    .map(StringBuilder::toString)
                    .concatWith(discard(clob)))

                .concatWith(close(connection)))
            .as(StepVerifier::create)
            .expectNext("test-value").as("value from select")
            .verifyComplete();
    }

    /**
     * Customization hook: Returns the SQL type for a {@link Clob} column.
     *
     * @return the SQL type for a {@link Clob} column.
     */
    default String clobType() {
        return "CLOB";
    }

    @Test
    default void columnMetadata() {
        getJdbcOperations().execute("INSERT INTO test_two_column VALUES (100, 'hello')");

        Mono.from(getConnectionFactory().create())
            .flatMapMany(connection -> Flux.from(connection

                .createStatement("SELECT col1 AS value, col2 AS value FROM test_two_column")
                .execute())
                .flatMap(result -> {
                    return result.map((row, rowMetadata) -> {
                        Collection<String> columnNames = rowMetadata.getColumnNames();
                        return Arrays.asList(rowMetadata.getColumnMetadata("value").getName(), rowMetadata.getColumnMetadata("VALUE").getName(), columnNames.contains("value"), columnNames.contains(
                            "VALUE"));
                    });
                })
                .flatMapIterable(Function.identity())
                .concatWith(close(connection)))
            .as(StepVerifier::create)
            .expectNext("value").as("Column label col1")
            .expectNext("value").as("Column label col1 (get by uppercase)")
            .expectNext(true).as("getColumnNames.contains(value)")
            .expectNext(true).as("getColumnNames.contains(VALUE)")
            .verifyComplete();
    }

    @Test
    default void compoundStatement() {
        getJdbcOperations().execute("INSERT INTO test VALUES (100)");

        Mono.from(getConnectionFactory().create())
            .flatMapMany(connection -> Flux.from(connection

                .createStatement("SELECT value FROM test; SELECT value FROM test")
                .execute())
                .flatMap(TestKit::extractColumns)

                .concatWith(close(connection)))
            .as(StepVerifier::create)
            .expectNext(Collections.singletonList(100)).as("value from first select")
            .expectNext(Collections.singletonList(100)).as("value from second select")
            .verifyComplete();
    }

    @Test
    default void createStatementFails() {

        Mono.from(getConnectionFactory().create())
            .flatMap(connection -> {
                assertThrows(IllegalArgumentException.class, () -> connection.createStatement(null));

                return close(connection);
            })
            .as(StepVerifier::create)
            .verifyComplete();
    }

    @BeforeEach
    default void createTable() {
        getJdbcOperations().execute("CREATE TABLE test ( value INTEGER )");
        getJdbcOperations().execute("CREATE TABLE test_two_column ( col1 INTEGER, col2 VARCHAR(100) )");
        getJdbcOperations().execute(String.format("CREATE TABLE blob_test ( value %s )", blobType()));
        getJdbcOperations().execute(String.format("CREATE TABLE clob_test ( value %s )", clobType()));
    }

    @AfterEach
    default void dropTable() {
        getJdbcOperations().execute("DROP TABLE test");
        getJdbcOperations().execute("DROP TABLE test_two_column");
        getJdbcOperations().execute("DROP TABLE blob_test");
        getJdbcOperations().execute("DROP TABLE clob_test");
    }

    @Test
    default void duplicateColumnNames() {
        getJdbcOperations().execute("INSERT INTO test_two_column VALUES (100, 'hello')");

        Mono.from(getConnectionFactory().create())
            .flatMapMany(connection -> Flux.from(connection

                .createStatement("SELECT col1 AS value, col2 AS value FROM test_two_column")
                .execute())

                .flatMap(result -> result
                    .map((row, rowMetadata) -> Arrays.asList(row.get("value"), row.get("VALUE"))))
                .flatMapIterable(Function.identity())

                .concatWith(close(connection)))
            .as(StepVerifier::create)
            .expectNext(100).as("value from col1")
            .expectNext(100).as("value from col1 (upper case)")
            .verifyComplete();
    }

    @Test
    default void prepareStatement() {
        Mono.from(getConnectionFactory().create())
            .flatMapMany(connection -> {
                Statement statement = connection.createStatement(String.format("INSERT INTO test VALUES(%s)", getPlaceholder(0)));

                IntStream.range(0, 10)
                    .forEach(i -> bind(statement, getIdentifier(0), i).add());

                return Flux.from(statement
                    .execute())
                    .flatMap(TestKit::extractRowsUpdated)
                    .concatWith(close(connection));
            })
            .as(StepVerifier::create)
            .expectNextCount(10).as("values from insertions")
            .verifyComplete();
    }

    @Test
    default void prepareStatementWithIncompleteBatchFails() {
        Mono.from(getConnectionFactory().create())
            .flatMapMany(connection -> {
                Statement statement = connection.createStatement(String.format("INSERT INTO test VALUES(%s,%s)", getPlaceholder(0), getPlaceholder(1)));

                bind(statement, getIdentifier(0), 0);

                assertThrows(IllegalStateException.class, statement::add);
                return close(connection);
            })
            .as(StepVerifier::create)
            .verifyComplete();
    }

    @Test
    default void prepareStatementWithIncompleteBindingFails() {
        Mono.from(getConnectionFactory().create())
            .flatMapMany(connection -> {
                Statement statement = connection.createStatement(String.format("INSERT INTO test VALUES(%s,%s)", getPlaceholder(0), getPlaceholder(1)));

                bind(statement, getIdentifier(0), 0);

                assertThrows(IllegalStateException.class, statement::execute);
                return close(connection);
            })
            .as(StepVerifier::create)
            .verifyComplete();
    }

    /**
     * Customization hook: Returns the {@code INSERT INTO} statement for a table which created
     * by {@link #getCreateTableWithAutogeneratedKey}, only contains the
     * data for second column {@code value} which should be {@literal 100}.
     * <p>
     * Example:
     * <pre class="code">
     *     INSERT INTO test VALUES(100);
     * // or
     *     INSERT INTO test VALUES(DEFAULT,100);
     * </pre>
     *
     * @return the {@code INSERT INTO} statement
     */
    default String getInsertIntoWithAutogeneratedKey() {
        return "INSERT INTO test VALUES(100)";
    }

    @Test
    default void returnGeneratedValues() {

        getJdbcOperations().execute("DROP TABLE test");
        getJdbcOperations().execute(getCreateTableWithAutogeneratedKey());

        Mono.from(getConnectionFactory().create())
            .flatMapMany(connection -> {
                Statement statement = connection.createStatement(getInsertIntoWithAutogeneratedKey());

                statement.returnGeneratedValues();

                return Flux.from(statement
                    .execute())
                    .concatWith(close(connection)).flatMap(it -> it.map((row, rowMetadata) -> row.get(0)));
            })
            .as(StepVerifier::create)
            .expectNextCount(1)
            .verifyComplete();
    }

    @Test
    default void returnGeneratedValuesFails() {

        Mono.from(getConnectionFactory().create())
            .flatMapMany(connection -> {
                Statement statement = connection.createStatement("INSERT INTO test");

                assertThrows(IllegalArgumentException.class, () -> statement.returnGeneratedValues((String[]) null));
                return close(connection);
            })
            .as(StepVerifier::create)
            .verifyComplete();
    }

    @Test
    default void savePoint() {
        getJdbcOperations().execute("INSERT INTO test VALUES (100)");

        Mono.from(getConnectionFactory().create())
            .flatMapMany(connection -> Mono.from(connection

                .beginTransaction())
                .<Object>thenMany(Flux.from(connection.createStatement("SELECT value FROM test")
                    .execute())
                    .flatMap(TestKit::extractColumns))

                .concatWith(Flux.defer(() -> {
                    Statement statement = connection.createStatement(String.format("INSERT INTO test VALUES (%s)", getPlaceholder(0)));
                    bind(statement, getIdentifier(0), 200);
                    return statement.execute();
                })
                    .flatMap(TestKit::extractRowsUpdated))
                .concatWith(Flux.from(connection.createStatement("SELECT value FROM test")
                    .execute())
                    .flatMap(TestKit::extractColumns))

                .concatWith(connection.createSavepoint("test_savepoint"))
                .concatWith(Flux.defer(() -> {
                    Statement statement = connection.createStatement(String.format("INSERT INTO test VALUES (%s)", getPlaceholder(0)));
                    bind(statement, getIdentifier(0), 300);
                    return statement.execute();
                })
                    .flatMap(TestKit::extractRowsUpdated))
                .concatWith(Flux.from(connection.createStatement("SELECT value FROM test")
                    .execute())
                    .flatMap(TestKit::extractColumns))

                .concatWith(connection.rollbackTransactionToSavepoint("test_savepoint"))
                .concatWith(Flux.from(connection.createStatement("SELECT value FROM test")
                    .execute())
                    .flatMap(TestKit::extractColumns))

                .concatWith(close(connection)))
            .as(StepVerifier::create)
            .expectNext(Collections.singletonList(100)).as("value from select")
            .expectNext(1).as("rows inserted")
            .expectNext(Arrays.asList(100, 200)).as("values from select")
            .expectNext(1).as("rows inserted")
            .expectNext(Arrays.asList(100, 200, 300)).as("values from select")
            .expectNext(Arrays.asList(100, 200)).as("values from select")
            .verifyComplete();
    }

    @Test
    default void savePointStartsTransaction() {
        Mono.from(getConnectionFactory().create())
            .flatMapMany(connection -> Mono.from(connection
                .createSavepoint("test_savepoint"))
                .then(Mono.fromSupplier(connection::isAutoCommit))
                .concatWith(close(connection)))
            .as(StepVerifier::create)
            .expectNext(false).as("createSavepoint starts a transaction")
            .verifyComplete();
    }

    @Test
    default void transactionCommit() {
        getJdbcOperations().execute("INSERT INTO test VALUES (100)");

        Mono.from(getConnectionFactory().create())
            .flatMapMany(connection -> Mono.from(connection

                .beginTransaction())
                .<Object>thenMany(Flux.from(connection.createStatement("SELECT value FROM test")
                    .execute())
                    .flatMap(TestKit::extractColumns))

                .concatWith(Flux.defer(() -> {
                    Statement statement = connection.createStatement(String.format("INSERT INTO test VALUES (%s)", getPlaceholder(0)));
                    bind(statement, getIdentifier(0), 200);
                    return statement.execute();
                })
                    .flatMap(TestKit::extractRowsUpdated))
                .concatWith(Flux.from(connection.createStatement("SELECT value FROM test")
                    .execute())
                    .flatMap(TestKit::extractColumns))

                .concatWith(connection.commitTransaction())
                .concatWith(Flux.from(connection.createStatement("SELECT value FROM test")
                    .execute())
                    .flatMap(TestKit::extractColumns))

                .concatWith(close(connection)))
            .as(StepVerifier::create)
            .expectNext(Collections.singletonList(100)).as("value from select")
            .expectNext(1).as("rows inserted")
            .expectNext(Arrays.asList(100, 200)).as("values from select")
            .expectNext(Arrays.asList(100, 200)).as("values from select")
            .verifyComplete();
    }

    @Test
    default void transactionRollback() {
        getJdbcOperations().execute("INSERT INTO test VALUES (100)");

        Mono.from(getConnectionFactory().create())
            .flatMapMany(connection -> Mono.from(connection

                .beginTransaction())
                .<Object>thenMany(Flux.from(connection.createStatement("SELECT value FROM test")
                    .execute())
                    .flatMap(TestKit::extractColumns))

                .concatWith(Flux.defer(() -> {
                    Statement statement = connection.createStatement(String.format("INSERT INTO test VALUES (%s)", getPlaceholder(0)));
                    bind(statement, getIdentifier(0), 200);
                    return statement.execute();
                })
                    .flatMap(TestKit::extractRowsUpdated))
                .concatWith(Flux.from(connection.createStatement("SELECT value FROM test")
                    .execute())
                    .flatMap(TestKit::extractColumns))

                .concatWith(connection.rollbackTransaction())
                .concatWith(Flux.from(connection.createStatement("SELECT value FROM test")
                    .execute())
                    .flatMap(TestKit::extractColumns))

                .concatWith(close(connection)))
            .as(StepVerifier::create)
            .expectNext(Collections.singletonList(100)).as("value from select")
            .expectNext(1).as("rows inserted")
            .expectNext(Arrays.asList(100, 200)).as("values from select")
            .expectNext(Collections.singletonList(100)).as("value from select")
            .verifyComplete();
    }

    @Test
    default void validate() {

        Mono.from(getConnectionFactory().create())
            .flatMapMany(connection -> Flux.concat(connection.validate(ValidationDepth.LOCAL),
                connection.validate(ValidationDepth.REMOTE),
                connection.close(),
                connection.validate(ValidationDepth.LOCAL),
                connection.validate(ValidationDepth.REMOTE)))
            .as(StepVerifier::create)
            .expectNext(true).as("successful local validation")
            .expectNext(true).as("successful remote validation")
            .expectNext(false).as("failed local validation after close")
            .expectNext(false).as("failed remote validation after close")
            .verifyComplete();
    }

    static Statement bind(Statement statement, Object identifier, Object value) {
        Assert.requireNonNull(identifier, "Identifier must not be null");
        if (identifier instanceof String) {
            return statement.bind((String) identifier, value);
        }
        if (identifier instanceof Integer) {
            return statement.bind((Integer) identifier, value);
        }
        throw new IllegalArgumentException(String.format("Identifier %s must be a String or Integer. Was: %s", identifier, identifier.getClass().getName()));
    }

    static Statement bindNull(Statement statement, Object identifier, Class<?> type) {
        Assert.requireNonNull(identifier, "Identifier must not be null");
        if (identifier instanceof String) {
            return statement.bindNull((String) identifier, type);
        }
        if (identifier instanceof Integer) {
            return statement.bindNull((Integer) identifier, type);
        }
        throw new IllegalArgumentException(String.format("Identifier %s must be a String or Integer. Was: %s", identifier, identifier.getClass().getName()));
    }

}