/*
 * 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.karaf.main.lock;

import static org.easymock.EasyMock.*;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

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 org.apache.felix.utils.properties.Properties;

import org.apache.karaf.main.util.BootstrapLogManager;
import org.easymock.EasyMock;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

public abstract class BaseJDBCLockTest {

    DefaultJDBCLock lock;
    Properties props;
    String user = "root";
    String password = "";
    String driver;
    String url;
    String tableName = "LOCK_TABLE";
    String clustername = "karaf_cluster";
    int timeout = 10;
    String momentDatatype = "BIGINT";
    String nodeDatatype = "VARCHAR(20)";
    String createTableStmtSuffix = "";

    Connection connection;
    DatabaseMetaData metaData;
    ResultSet resultSet;
    PreparedStatement preparedStatement;
    Statement statement;

    abstract DefaultJDBCLock createLock(Properties props);

    @BeforeClass
    public static void setUpTestSuite() {
        Properties properties = new Properties();
        properties.put("karaf.bootstrap.log", "target/karaf.log");
        BootstrapLogManager.setProperties(properties);
    }

    @Before
    public void setUp() throws Exception {
        connection = EasyMock.createNiceMock(Connection.class);
        metaData = EasyMock.createMock(DatabaseMetaData.class);
        resultSet = EasyMock.createMock(ResultSet.class);
        preparedStatement = EasyMock.createMock(PreparedStatement.class);
        statement = EasyMock.createMock(Statement.class);

        props = new Properties();
        props.put("karaf.lock.jdbc.url", url);
        props.put("karaf.lock.jdbc.driver", driver);
        props.put("karaf.lock.jdbc.user", user);
        props.put("karaf.lock.jdbc.password", password);
        props.put("karaf.lock.jdbc.table", tableName);
        props.put("karaf.lock.jdbc.clustername", clustername);
        props.put("karaf.lock.jdbc.timeout", Integer.toString(timeout));
    }

    @Test
    public void initShouldCreateTheSchemaIfItNotExists() throws Exception {
        expect(connection.isClosed()).andReturn(false);
        connection.setAutoCommit(false);
        expect(connection.getMetaData()).andReturn(metaData).anyTimes();
        expect(metaData.getTables(isNull(), isNull(), anyString(), aryEq(new String[]{"TABLE"}))).andReturn(resultSet);
        expect(resultSet.next()).andReturn(false);
        resultSet.close();
        expectLastCall().atLeastOnce();
        expect(connection.isClosed()).andReturn(false);
        expect(connection.createStatement()).andReturn(statement);
        expect(statement.execute("CREATE TABLE " + tableName + " (MOMENT " + momentDatatype + ", NODE " + nodeDatatype + ")" + createTableStmtSuffix)).andReturn(false);
        expect(statement.execute("INSERT INTO " + tableName + " (MOMENT, NODE) VALUES (1, '" + clustername + "')")).andReturn(false);
        statement.close();
        connection.commit();

        replay(connection, metaData, statement, preparedStatement, resultSet);

        lock = createLock(props);

        verify(connection, metaData, statement, preparedStatement, resultSet);
    }

    @Test
    public void initShouldNotCreateTheSchemaIfItAlreadyExists() throws Exception {
        connection.setAutoCommit(false);
        expect(connection.getMetaData()).andReturn(metaData);
        expect(metaData.getTables(isNull(), isNull(), anyString(), aryEq(new String[]{"TABLE"}))).andReturn(resultSet);
        expect(resultSet.next()).andReturn(true);
        resultSet.close();
        expectLastCall().atLeastOnce();
        replay(connection, metaData, statement, preparedStatement, resultSet);

        lock = createLock(props);

        verify(connection, metaData, statement, preparedStatement, resultSet);
    }

    @Test
    public void lockShouldReturnTrueItTheTableIsNotLocked() throws Exception {
        initShouldNotCreateTheSchemaIfItAlreadyExists();
        reset(connection, metaData, statement, preparedStatement, resultSet);

        expect(connection.isClosed()).andReturn(false);
        expect(connection.prepareStatement("SELECT * FROM " + tableName + " FOR UPDATE")).andReturn(preparedStatement);
        preparedStatement.setQueryTimeout(10);
        expect(preparedStatement.execute()).andReturn(true);
        preparedStatement.close();
        expect(connection.isClosed()).andReturn(false);
        expect(connection.prepareStatement("UPDATE " + tableName + " SET MOMENT = 1")).andReturn(preparedStatement);
        preparedStatement.setQueryTimeout(10);
        expect(preparedStatement.executeUpdate()).andReturn(1);
        preparedStatement.close();

        replay(connection, metaData, statement, preparedStatement, resultSet);

        boolean lockAcquired = lock.lock();

        verify(connection, metaData, statement, preparedStatement, resultSet);
        assertTrue(lockAcquired);
    }

    @Test
    public void lockShouldReturnFalseIfAnotherRowIsLocked() throws Exception {
        initShouldNotCreateTheSchemaIfItAlreadyExists();
        reset(connection, metaData, statement, preparedStatement, resultSet);

        expect(connection.isClosed()).andReturn(false);
        expect(connection.prepareStatement("SELECT * FROM " + tableName + " FOR UPDATE")).andReturn(preparedStatement);
        preparedStatement.setQueryTimeout(10);
        expect(preparedStatement.execute()).andReturn(true);
        preparedStatement.close();
        expect(connection.isClosed()).andReturn(false);
        expect(connection.prepareStatement("UPDATE " + tableName + " SET MOMENT = 1")).andReturn(preparedStatement);
        preparedStatement.setQueryTimeout(10);
        expect(preparedStatement.executeUpdate()).andThrow(new SQLException());
        preparedStatement.close();

        replay(connection, metaData, statement, preparedStatement, resultSet);

        boolean lockAcquired = lock.lock();

        verify(connection, metaData, statement, preparedStatement, resultSet);
        assertFalse(lockAcquired);
    }

    @Test
    public void lockShouldReturnFalseIfTheRowIsAlreadyLocked() throws Exception {
        initShouldNotCreateTheSchemaIfItAlreadyExists();
        reset(connection, metaData, statement, preparedStatement, resultSet);

        expect(connection.isClosed()).andReturn(false);
        expect(connection.prepareStatement("SELECT * FROM " + tableName + " FOR UPDATE")).andReturn(preparedStatement);
        preparedStatement.setQueryTimeout(10);
        expect(preparedStatement.execute()).andReturn(true);
        preparedStatement.close();
        expect(connection.isClosed()).andReturn(false);
        expect(connection.prepareStatement("UPDATE " + tableName + " SET MOMENT = 1")).andReturn(preparedStatement);
        preparedStatement.setQueryTimeout(10);
        expect(preparedStatement.executeUpdate()).andThrow(new SQLException());
        preparedStatement.close();

        replay(connection, metaData, statement, preparedStatement, resultSet);

        boolean lockAcquired = lock.lock();

        verify(connection, metaData, statement, preparedStatement, resultSet);
        assertFalse(lockAcquired);
    }

    @Test
    public void release() throws Exception {
        initShouldNotCreateTheSchemaIfItAlreadyExists();
        reset(connection, metaData, statement, preparedStatement, resultSet);

        expect(connection.isClosed()).andReturn(false);
        expect(connection.isClosed()).andReturn(false);
        expect(connection.isClosed()).andReturn(false);
        connection.rollback();
        connection.close();

        replay(connection, metaData, statement, preparedStatement, resultSet);

        lock.release();

        verify(connection, metaData, statement, preparedStatement, resultSet);
    }

    @Test
    public void releaseShouldSucceedForAnAlreadyClosedConnection() throws Exception {
        initShouldNotCreateTheSchemaIfItAlreadyExists();
        reset(connection, metaData, statement, preparedStatement, resultSet);

        expect(connection.isClosed()).andReturn(true);

        replay(connection, metaData, statement, preparedStatement, resultSet);

        lock.release();

        verify(connection, metaData, statement, preparedStatement, resultSet);
    }

    @Test
    public void releaseShouldSucceedForANullConnectionReference() throws Exception {
        initShouldNotCreateTheSchemaIfItAlreadyExists();
        reset(connection, metaData, statement, preparedStatement, resultSet);

        lock.lockConnection = null;

        replay(connection, metaData, statement, preparedStatement, resultSet);

        lock.release();

        verify(connection, metaData, statement, preparedStatement, resultSet);
    }

    @Test
    public void isAliveShouldReturnTrueIfItHoldsTheLock() throws Exception {
        initShouldNotCreateTheSchemaIfItAlreadyExists();
        reset(connection, metaData, statement, preparedStatement, resultSet);

        expect(connection.isClosed()).andReturn(false);
        expect(connection.isClosed()).andReturn(false);
        expect(connection.prepareStatement("UPDATE " + tableName + " SET MOMENT = 1")).andReturn(preparedStatement);
        preparedStatement.setQueryTimeout(10);
        expect(preparedStatement.executeUpdate()).andReturn(1);
        preparedStatement.close();

        replay(connection, metaData, statement, preparedStatement, resultSet);

        boolean alive = lock.isAlive();

        verify(connection, metaData, statement, preparedStatement, resultSet);
        assertTrue(alive);
    }

    @Test
    public void isAliveShouldReturnFalseIfTheConnectionIsClosed() throws Exception {
        initShouldNotCreateTheSchemaIfItAlreadyExists();
        reset(connection, metaData, statement, preparedStatement, resultSet);

        expect(connection.isClosed()).andReturn(true);

        replay(connection, metaData, statement, preparedStatement, resultSet);

        boolean alive = lock.isAlive();

        verify(connection, metaData, statement, preparedStatement, resultSet);
        assertFalse(alive);
    }

    @Test
    public void isAliveShouldReturnFalseIfTheConnectionIsNull() throws Exception {
        initShouldNotCreateTheSchemaIfItAlreadyExists();
        reset(connection, metaData, statement, preparedStatement, resultSet);

        lock.lockConnection = null;

        replay(connection, metaData, statement, preparedStatement, resultSet);

        boolean alive = lock.isAlive();

        verify(connection, metaData, statement, preparedStatement, resultSet);
        assertFalse(alive);
    }

    @Test
    public void isAliveShouldReturnFalseIfItNotHoldsTheLock() throws Exception {
        initShouldNotCreateTheSchemaIfItAlreadyExists();
        reset(connection, metaData, statement, preparedStatement, resultSet);

        expect(connection.isClosed()).andReturn(false);
        expect(connection.isClosed()).andReturn(false);
        expect(connection.prepareStatement("UPDATE " + tableName + " SET MOMENT = 1")).andReturn(preparedStatement);
        preparedStatement.setQueryTimeout(10);
        expect(preparedStatement.executeUpdate()).andThrow(new SQLException());
        preparedStatement.close();

        replay(connection, metaData, statement, preparedStatement, resultSet);

        boolean alive = lock.isAlive();

        verify(connection, metaData, statement, preparedStatement, resultSet);
        assertFalse(alive);
    }

    @Test
    public void lockShouldReturnFalseIfTableIsEmpty() throws Exception {
        initShouldNotCreateTheSchemaIfItAlreadyExists();
        reset(connection, metaData, statement, preparedStatement, resultSet);

        expect(connection.isClosed()).andReturn(false);
        expect(connection.prepareStatement("SELECT * FROM " + tableName + " FOR UPDATE")).andReturn(preparedStatement);
        preparedStatement.setQueryTimeout(10);
        expect(preparedStatement.execute()).andReturn(true);
        preparedStatement.close();
        expect(connection.isClosed()).andReturn(false);
        expect(connection.prepareStatement("UPDATE " + tableName + " SET MOMENT = 1")).andReturn(preparedStatement);
        preparedStatement.setQueryTimeout(10);
        expect(preparedStatement.executeUpdate()).andReturn(0);
        preparedStatement.close();

        replay(connection, metaData, statement, preparedStatement, resultSet);

        boolean lockAcquired = lock.lock();

        verify(connection, metaData, statement, preparedStatement, resultSet);
        assertFalse(lockAcquired);
    }
}
