/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * 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
 *
 *     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 tests.java.sql;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Vector;
import java.util.logging.Logger;

import tests.support.DatabaseCreator;
import tests.support.Support_SQL;
import tests.support.ThreadPool;

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

@RunWith(JUnit4.class)
public class StressTest {
    Vector<Connection> vc = new Vector<Connection>();

    private static Connection conn;

    private static Statement statement;

    @Before
    public void setUp() throws Exception {
        Support_SQL.loadDriver();
        conn = Support_SQL.getConnection();
        statement = conn.createStatement();
        createTestTables();
        vc.clear();
    }

    @After
    public void tearDown() throws Exception {
        closeConnections();
        statement.close();
        conn.close();
    }

    private void createTestTables() {
        try {
            DatabaseMetaData meta = conn.getMetaData();
            ResultSet userTab = meta.getTables(null, null, null, null);

            while (userTab.next()) {
                String tableName = userTab.getString("TABLE_NAME");
                if (tableName.equals(DatabaseCreator.TEST_TABLE2)) {
                    statement.execute(DatabaseCreator.DROP_TABLE2);
                }
            }
            statement.execute(DatabaseCreator.CREATE_TABLE2);
        } catch (SQLException sql) {
            fail("Unexpected SQLException " + sql.toString());
        }
        return;
    }

    private void dropTestTables() {
        try {
            statement.execute(DatabaseCreator.DROP_TABLE2);
        } catch (SQLException sql) {
            fail("Unexpected SQLException " + sql.toString());
        }
        return;
    }

//    /**
//     * @see junit.framework.TestCase#setUp()
//     */
//    @Override
//    protected void setUp() throws Exception {
//        super.setUp();
//        vc.clear();
//    }
//
//    /**
//     * @see junit.framework.TestCase#tearDown()
//     */
//    @Override
//    protected void tearDown() throws Exception {
//        closeConnections();
//        statement.execute("DELETE FROM " + DatabaseCreator.TEST_TABLE2);
//        super.tearDown();
//    }

    /**
     * StressTest#testManyConnectionsUsingOneThread(). Create many
     *        connections to the DataBase using one thread.
     */
    @Test
    public void testManyConnectionsUsingOneThread() {
        try {
            int maxConnections = getConnectionNum();
            openConnections(maxConnections);
            assertEquals("Incorrect number of created connections",
                    maxConnections, vc.size());
        } catch (Exception e) {
            fail("Unexpected Exception " + e.toString());
        }
    }

    /**
     * StressTest#testManyConnectionsUsingManyThreads(). Create many
     *        connections to the DataBase using some threads.
     */
    @Test
    public void testManyConnectionsUsingManyThreads() {
        int numTasks = getConnectionNum();

        ThreadPool threadPool = new ThreadPool(numTasks);

        // run example tasks
        for (int i = 0; i < numTasks; i++) {
            threadPool.runTask(createTask(i));
        }
        // close the pool and wait for all tasks to finish.
        threadPool.join();
        assertEquals("Unable to create a connection", numTasks, vc.size());
        if (numTasks != Support_SQL.sqlMaxConnections) {
            try {
                // try to create connection n + 1
                Connection c = Support_SQL.getConnection();
                c.close();
                fail("It is possible to create more than " + numTasks
                        + "connections");
            } catch (SQLException sql) {
                // expected
            }
        }
    }

    /**
     * StressTest#testInsertOfManyRowsUsingOneThread(). Insert a lot of
     *        records to the Database using a maximum number of connections.
     */
    @Test
    public void testInsertOfManyRowsUsingOneThread() {

        Logger.global
                .info("java.sql stress test: single thread and many operations.");
        int maxConnections = getConnectionNum();
        Logger.global.info("Opening " + maxConnections + " to database "
                + Support_SQL.getFilename());
        openConnections(maxConnections);

        int tasksPerConnection = Support_SQL.sqlMaxTasks / maxConnections;
        Logger.global.info("TasksPerConnection =  " + Support_SQL.sqlMaxTasks
                + " by (maxConnections) " + maxConnections + " = "
                + tasksPerConnection);
        int pk = 1;
        for (int i = 0; i < vc.size(); ++i) {
            Logger.global.info(" creating " + tasksPerConnection
                    + "tasks for Connection " + i);
            Connection c = vc.elementAt(i);
            for (int j = 0; j < tasksPerConnection; ++j) {
                insertNewRecord(c, pk++);
            }
        }
        try {
            ResultSet rs = statement
                    .executeQuery("SELECT COUNT(*) as counter FROM "
                            + DatabaseCreator.TEST_TABLE2);
            assertTrue("RecordSet is empty", rs.next());
            assertEquals("Incorrect number of records", tasksPerConnection
                    * maxConnections, rs.getInt("counter"));
            rs.close();
        } catch (SQLException sql) {
            fail("Unexpected SQLException " + sql.toString());
        }

    }

    /**
     * @tests
     * TODO(b/169673091): Investigate why it could occasionally take 10min rather than 10s to
     * finish.
     */
    @Test
    public void testInsertOfManyRowsUsingManyThreads() {
        Logger.global.info("java.sql stress test: multiple threads and many operations.");

        int numConnections = getConnectionNum();
        int tasksPerConnection = Support_SQL.sqlMaxTasks / numConnections;

        Logger.global.info("Opening "+numConnections+" to database "+Support_SQL.getFilename());

        ThreadPool threadPool = new ThreadPool(numConnections);

        for (int i = 0; i < numConnections; ++i) {
            Logger.global.info(" creating "+tasksPerConnection+ " tasks for Connection "+i);
            threadPool.runTask(insertTask(numConnections, i));
        }
        // close the pool and wait for all tasks to finish.
        threadPool.join();
        Logger.global.info("All threads joined");
        assertEquals("Unable to create a connection", numConnections, vc.size());

        try {
            ResultSet rs = statement
                    .executeQuery("SELECT COUNT(*) as counter FROM "
                            + DatabaseCreator.TEST_TABLE2);
            assertTrue("RecordSet is empty", rs.next());
            Logger.global.info("Counting statement returned");

            assertEquals("Incorrect number of records", tasksPerConnection
                    * numConnections, rs.getInt("counter"));
            rs.close();
            Logger.global.info("ResultSet closed");
        } catch (SQLException sql) {
            fail("Unexpected SQLException " + sql.toString());

        }

    }

    private int getConnectionNum() {
        int num = Support_SQL.sqlMaxConnections;
        try {
            int mc = conn.getMetaData().getMaxConnections();
            if (mc != 0) {
                if (num != mc) {
                    System.err.println("Will be used no more than " + mc
                            + " connections to the DataBase");
                }
                num = mc;
            }
        } catch (SQLException sql) {
            fail("Unexpected SQLException " + sql.toString());
        }
        return num;
    }

    private void openConnections(int maxConnections) {
        int i = 0;
        try {
            for (; i < maxConnections; ++i) {
                Connection c = Support_SQL.getConnection();
                if (c == null) {
                    assertEquals("Unable to create a connection",
                            maxConnections, i);
                }
                vc.add(c);
            }
        } catch (SQLException sql) {
            assertEquals("Unable to create a connection", maxConnections, i);
        }
        return;
    }

    private void closeConnections() {
        int i = 0;
        try {
            for (; i < vc.size(); ++i) {
                vc.elementAt(i).close();
            }
        } catch (SQLException sql) {
            assertEquals("Unable to close a connection", vc.size(), i);
        }
        return;
    }

    private Runnable createTask(final int taskID) {
        return new Runnable() {
            public void run() {
                try {
                    Connection c = Support_SQL.getConnection();
                    if (c == null) {
                        return;
                    }
                    synchronized (this) {
                        vc.add(c);
                    }
                } catch (SQLException sql) {
                    // nothing to do
                }
            }
        };
    }

    private Runnable insertTask(final int numConnections, final int taskID) {
        return new Runnable() {
            public void run() {
                try {
                    Connection c = Support_SQL.getConnection();
                    if (c == null) {
                        return;
                    }
                    synchronized (this) {
                        vc.add(c);
                    }
                    int tasksPerConnection = Support_SQL.sqlMaxTasks
                            / numConnections;
                    for (int i = 0; i < tasksPerConnection; ++i) {
                        insertNewRecord(c, (i + 1) + tasksPerConnection
                                * taskID);
                    }
                } catch (SQLException sql) {
                    // do nothing
                }
            }
        };
    }

    private void insertNewRecord(Connection c, int pk) {
        String query = "INSERT INTO " + DatabaseCreator.TEST_TABLE2
                + "(finteger, ftext, fcharacter, fdecimal, fnumeric,"
                + " fsmallint, ffloat, freal, fdouble, fdate, ftime)"
                + " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
        try {
            PreparedStatement ps = c.prepareStatement(query);
            ps.setInt(1, pk);
            ps.setString(2, "text");
            ps.setString(3, "chr");
            ps.setFloat(4, 0.1f);
            ps.setFloat(5, 0.2f);
            ps.setShort(6, (short) 3);
            ps.setFloat(7, 0.4f);
            ps.setDouble(8, 0.5);
            ps.setDouble(9, 0.6);
            ps.setDate(10, new java.sql.Date(System.currentTimeMillis()));
            ps.setTime(11, new java.sql.Time(System.currentTimeMillis()));
            ps.execute();
            ps.close();
        } catch (SQLException sql) {
            fail("Unexpected SQLException " + sql.toString());
        }
        return;
    }
}
