/*
 * 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.commons.dbcp2;

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

import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;

import javax.sql.DataSource;

import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

/**
 * This test *must* execute before all other tests to be effective as it tests
 * the initialisation of DriverManager.
 * Based on the test case for DBCP-212 written by Marcos Sanz
 */
public class TestDriverManagerConnectionFactory {

    private static final String KEY_JDBC_DRIVERS = "jdbc.drivers";

    @BeforeAll
    public static void beforeClass() {
        System.setProperty(KEY_JDBC_DRIVERS, "org.apache.commons.dbcp2.TesterDriver");
    }

    @AfterAll
    public static void afterClass() {
        System.clearProperty(KEY_JDBC_DRIVERS);
    }

    @Test
    public void testDriverManagerInitWithEmptyProperties() throws Exception {
        final ConnectionFactory connectionFactory = new DriverManagerConnectionFactory(
                "jdbc:apache:commons:testdriver;user=foo;password=bar");
        connectionFactory.createConnection();
    }

    @Test
    public void testDriverManagerInitWithProperties() throws Exception {
        testDriverManagerInit(true);
    }

    @Test
    public void testDriverManagerInitWithCredentials() throws Exception {
        testDriverManagerInit(false);
    }

    @Test
    public void testDriverManagerWithoutUser() {
        final DriverManagerConnectionFactory cf = new DriverManagerConnectionFactory("jdbc:apache:commons:testdriver", null, "pass");
        assertThrows(IndexOutOfBoundsException.class, cf::createConnection); // thrown by TestDriver due to missing user
    }

    @Test
    public void testDriverManagerWithoutPassword() {
        final DriverManagerConnectionFactory cf = new DriverManagerConnectionFactory("jdbc:apache:commons:testdriver", "user", (char[]) null);
        assertThrows(SQLException.class, cf::createConnection); // thrown by TestDriver due to invalid password
    }

    @Test
    public void testDriverManagerWithoutCredentials() {
        final DriverManagerConnectionFactory cf = new DriverManagerConnectionFactory("jdbc:apache:commons:testdriver", null,  (char[]) null);
        assertThrows(ArrayIndexOutOfBoundsException.class, cf::createConnection); // thrown by TestDriver due to missing user
    }

    @Test
    public void testDriverManagerCredentialsInUrl() throws SQLException {
        final DriverManagerConnectionFactory cf = new DriverManagerConnectionFactory("jdbc:apache:commons:testdriver;user=foo;password=bar", null,  (char[]) null);
        cf.createConnection();
    }

    public void testDriverManagerInit(final boolean withProperties) throws Exception {
        final GenericObjectPoolConfig<PoolableConnection> config = new GenericObjectPoolConfig<>();
        config.setMaxTotal(10);
        config.setMaxIdle(0);
        final Properties properties = new Properties();
        // The names "user" and "password" are specified in java.sql.DriverManager.getConnection(String, String, String)
        properties.put("user", "foo");
        properties.put("password", "bar");
        final ConnectionFactory connectionFactory = withProperties ?
                new DriverManagerConnectionFactory("jdbc:apache:commons:testdriver", properties) :
                new DriverManagerConnectionFactory("jdbc:apache:commons:testdriver", "foo", "bar");
        final PoolableConnectionFactory poolableConnectionFactory =
            new PoolableConnectionFactory(connectionFactory, null);
        poolableConnectionFactory.setDefaultReadOnly(Boolean.FALSE);
        poolableConnectionFactory.setDefaultAutoCommit(Boolean.TRUE);

        final GenericObjectPool<PoolableConnection> connectionPool =
                new GenericObjectPool<>(poolableConnectionFactory, config);
        poolableConnectionFactory.setPool(connectionPool);
        final PoolingDataSource<PoolableConnection> dataSource =
                new PoolingDataSource<>(connectionPool);

        final ConnectionThread[] connectionThreads = new ConnectionThread[10];
        final Thread[] threads = new Thread[10];

        for (int i = 0; i < 10; i++) {
            connectionThreads[i] = new ConnectionThread(dataSource);
            threads[i] = new Thread(connectionThreads[i]);
        }
        for (int i = 0; i < 10; i++) {
            threads[i].start();
        }
        for (int i = 0; i < 10; i++) {
            while (threads[i].isAlive()){//JDK1.5: getState() != Thread.State.TERMINATED) {
                Thread.sleep(100);
            }
            if (!connectionThreads[i].getResult()) {
                fail("Exception during getConnection(): " + connectionThreads[i]);
            }
        }
    }

    private static final class ConnectionThread implements Runnable {
        private final DataSource ds;
        private volatile boolean result = true;

        private ConnectionThread(final DataSource ds) {
            this.ds = ds;
        }

        @Override
        public void run() {
            Connection conn = null;
            try {
                conn = ds.getConnection();
            } catch (final Exception e) {
                e.printStackTrace();
                result = false;
            } finally {
                if (conn != null) {
                    try {
                        conn.close();
                    } catch (final Exception e) {
                        e.printStackTrace();
                        result = false;
                    }
                }
            }
        }

        public boolean getResult() {
            return result;
        }

        @Override
        public String toString() {
            return "ConnectionThread [ds=" + ds + ", result=" + result + "]";
        }
    }

}