/*
 * 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.openjpa.persistence.lockmgr;

import jakarta.persistence.EntityManager;
import jakarta.persistence.LockModeType;
import jakarta.persistence.TransactionRequiredException;

import org.apache.openjpa.jdbc.conf.JDBCConfiguration;
import org.apache.openjpa.jdbc.kernel.JDBCFetchConfigurationImpl;
import org.apache.openjpa.jdbc.sql.DB2Dictionary;
import org.apache.openjpa.jdbc.sql.DBDictionary;
import org.apache.openjpa.persistence.EntityManagerImpl;
import org.apache.openjpa.persistence.OpenJPAEntityManager;
import org.apache.openjpa.persistence.OpenJPAEntityManagerSPI;

/**
 * Test hints using EntityManager interface.
 */
public class TestEmLockMode extends SequencedActionsTest {
    private static String NON_SUPPORTED_OPTIMISTIC_SQL =
        "SELECT .* FROM LockEmployee .*";
    private static String NON_SUPPORTED_FOR_UPDATE_SQL = "" ; // append lock clause from dict
    private static String VERSION_UPDATE_SQL =
        "UPDATE LockEmployee SET version .* WHERE .*";
    private static String DB2_OPTIMISTIC_SQL =
        "SELECT .* FROM LockEmployee .* WHERE .*";
    private static String DB2_PESSIMISTIC_RS_SQL =
        "SELECT .* FROM LockEmployee .* WITH RS USE .*";
    private static String DB2_PESSIMISTIC_RR_SQL =
        "SELECT .* FROM LockEmployee .* WITH RR USE .*";

    @Override
    public void setUp() {
        setUp(LockEmployee.class, "openjpa.LockManager", "mixed");
        commonSetUp();
        NON_SUPPORTED_FOR_UPDATE_SQL = NON_SUPPORTED_OPTIMISTIC_SQL + " " + escapeRegex(getForUpdateClause()) + ".*";
    }

    private String escapeRegex(String clause) {
        // escape an update clause for use in a regex.
        // only handling ( ) for now
        String rval = clause.replace("(", "\\(");
        rval = rval.replace(")", "\\)");
        return rval;
    }

    /*
     * Test em.find(lockmode);
     */
    public void testFindLockModeIsolations() {
        EntityManager em = emf.createEntityManager();

        em.getTransaction().begin();

        commonTestFindLockModeIsolations(em, LockModeType.NONE, 1,
            DB2_OPTIMISTIC_SQL, 1, NON_SUPPORTED_OPTIMISTIC_SQL, 0, null);
        commonTestFindLockModeIsolations(em, LockModeType.READ, 1,
            DB2_OPTIMISTIC_SQL, 1, NON_SUPPORTED_OPTIMISTIC_SQL, 1,
            NON_SUPPORTED_OPTIMISTIC_SQL);
        commonTestFindLockModeIsolations(em, LockModeType.WRITE, 1,
            DB2_OPTIMISTIC_SQL, 1, NON_SUPPORTED_OPTIMISTIC_SQL, 1,
            VERSION_UPDATE_SQL);
        commonTestFindLockModeIsolations(em, LockModeType.OPTIMISTIC, 1,
            DB2_OPTIMISTIC_SQL, 1, NON_SUPPORTED_OPTIMISTIC_SQL, 1,
            NON_SUPPORTED_OPTIMISTIC_SQL);
        commonTestFindLockModeIsolations(em,
            LockModeType.OPTIMISTIC_FORCE_INCREMENT, 1, DB2_OPTIMISTIC_SQL, 1,
            NON_SUPPORTED_OPTIMISTIC_SQL, 1, VERSION_UPDATE_SQL);
        commonTestFindLockModeIsolations(em, LockModeType.PESSIMISTIC_READ, 2,
            DB2_PESSIMISTIC_RS_SQL, 2, NON_SUPPORTED_FOR_UPDATE_SQL, 1,
            NON_SUPPORTED_OPTIMISTIC_SQL);
        commonTestFindLockModeIsolations(em, LockModeType.PESSIMISTIC_WRITE, 2,
            DB2_PESSIMISTIC_RR_SQL, 2, NON_SUPPORTED_FOR_UPDATE_SQL, 1,
            NON_SUPPORTED_OPTIMISTIC_SQL);
        commonTestFindLockModeIsolations(em,
            LockModeType.PESSIMISTIC_FORCE_INCREMENT, 2,
            DB2_PESSIMISTIC_RR_SQL, 2, NON_SUPPORTED_FOR_UPDATE_SQL, 1,
            VERSION_UPDATE_SQL);

        em.getTransaction().rollback();
        em.close();
    }

    private void commonTestFindLockModeIsolations(EntityManager em,
        LockModeType lockMode, int expectedSupportSQLCount,
        String expectedSupportSQL, int expectedNonSupportSQLCount,
        String expectedNonSupportSQL, int expectedVersionUpdateCount,
        String expectedVersionUpdateSQL) {
        OpenJPAEntityManager oem = (OpenJPAEntityManager) em.getDelegate();
        JDBCFetchConfigurationImpl fConfig = (JDBCFetchConfigurationImpl)
            ((EntityManagerImpl) oem).getBroker().getFetchConfiguration();
        DBDictionary dict = ((JDBCConfiguration) ((OpenJPAEntityManagerSPI) oem)
            .getConfiguration()).getDBDictionaryInstance();

        em.clear();
        resetSQL();
        int beforeIsolation = fConfig.getIsolation();
        em.find(LockEmployee.class, 1, lockMode);
        if (dict.supportsIsolationForUpdate() &&
            dict instanceof DB2Dictionary) {
            assertEquals(expectedSupportSQLCount, getSQLCount());
            assertAllSQLInOrder(expectedSupportSQL);
        } else {
            assertEquals(expectedNonSupportSQLCount, getSQLCount());
            assertAllSQLInOrder(expectedNonSupportSQL);
        }

        resetSQL();
        em.flush();
        assertEquals(expectedVersionUpdateCount, getSQLCount());
        if (expectedVersionUpdateSQL != null)
            assertAllSQLInOrder(expectedVersionUpdateSQL);

        assertEquals(beforeIsolation, fConfig.getIsolation());
    }

    /*
     * Test em.refresh(lockmode);
     */
    public void testRefreshLockModeIsolations() {
        EntityManager em = emf.createEntityManager();

        em.getTransaction().begin();

        commonTestRefreshLockModeIsolations(em, LockModeType.NONE, 1,
            DB2_OPTIMISTIC_SQL, 1, NON_SUPPORTED_OPTIMISTIC_SQL, 0, null);
        commonTestRefreshLockModeIsolations(em, LockModeType.READ, 1,
            DB2_OPTIMISTIC_SQL, 1, NON_SUPPORTED_OPTIMISTIC_SQL, 1,
            NON_SUPPORTED_OPTIMISTIC_SQL);
        commonTestRefreshLockModeIsolations(em, LockModeType.WRITE, 1,
            DB2_OPTIMISTIC_SQL, 1, NON_SUPPORTED_OPTIMISTIC_SQL, 1,
            VERSION_UPDATE_SQL);
        commonTestRefreshLockModeIsolations(em, LockModeType.OPTIMISTIC, 1,
            DB2_OPTIMISTIC_SQL, 1, NON_SUPPORTED_OPTIMISTIC_SQL, 1,
            NON_SUPPORTED_OPTIMISTIC_SQL);
        commonTestRefreshLockModeIsolations(em,
            LockModeType.OPTIMISTIC_FORCE_INCREMENT, 1, DB2_OPTIMISTIC_SQL, 1,
            NON_SUPPORTED_OPTIMISTIC_SQL, 1, VERSION_UPDATE_SQL);
        commonTestRefreshLockModeIsolations(em, LockModeType.PESSIMISTIC_READ,
            2, DB2_PESSIMISTIC_RS_SQL, 2, NON_SUPPORTED_FOR_UPDATE_SQL, 1,
            NON_SUPPORTED_OPTIMISTIC_SQL);
        commonTestRefreshLockModeIsolations(em, LockModeType.PESSIMISTIC_WRITE,
            2, DB2_PESSIMISTIC_RR_SQL, 2, NON_SUPPORTED_FOR_UPDATE_SQL, 1,
            NON_SUPPORTED_OPTIMISTIC_SQL);
        commonTestRefreshLockModeIsolations(em,
            LockModeType.PESSIMISTIC_FORCE_INCREMENT, 2,
            DB2_PESSIMISTIC_RR_SQL, 2, NON_SUPPORTED_FOR_UPDATE_SQL, 1,
            VERSION_UPDATE_SQL);

        em.getTransaction().rollback();
        em.close();
    }

    private void commonTestRefreshLockModeIsolations(EntityManager em,
        LockModeType lockMode, int expectedSupportSQLCount,
        String expectedSupportSQL, int expectedNonSupportSQLCount,
        String expectedNonSupportSQL, int expectedVersionUpdateCount,
        String expectedVersionUpdateSQL) {
        OpenJPAEntityManager oem = (OpenJPAEntityManager) em.getDelegate();
        JDBCFetchConfigurationImpl fConfig = (JDBCFetchConfigurationImpl)
            ((EntityManagerImpl) oem).getBroker().getFetchConfiguration();
        DBDictionary dict = ((JDBCConfiguration) ((OpenJPAEntityManagerSPI) oem)
            .getConfiguration()).getDBDictionaryInstance();

        em.clear();
        LockEmployee employee = em.find(LockEmployee.class, 1);
        resetSQL();
        int beforeIsolation = fConfig.getIsolation();
        em.refresh(employee, lockMode);
        if (dict.supportsIsolationForUpdate() &&
            dict instanceof DB2Dictionary) {
            assertEquals(expectedSupportSQLCount, getSQLCount());
            assertAllSQLInOrder(expectedSupportSQL);
        } else {
            assertEquals(expectedNonSupportSQLCount, getSQLCount());
            assertAllSQLInOrder(expectedNonSupportSQL);
        }

        resetSQL();
        em.flush();
        assertEquals(expectedVersionUpdateCount, getSQLCount());
        if (expectedVersionUpdateSQL != null)
            assertAllSQLInOrder(expectedVersionUpdateSQL);

        assertEquals(beforeIsolation, fConfig.getIsolation());
    }

    /*
     * Test em.lock(lockmode);
     */
    public void testLockLockModeIsolations() {
        EntityManager em = emf.createEntityManager();

        em.getTransaction().begin();

        commonTestLockLockModeIsolations(em, LockModeType.NONE, 0, null, 0,
            null, 0, null);
        commonTestLockLockModeIsolations(em, LockModeType.READ, 0, null, 0,
            null, 1, NON_SUPPORTED_OPTIMISTIC_SQL);
        commonTestLockLockModeIsolations(em, LockModeType.WRITE, 0, null, 0,
            null, 1, VERSION_UPDATE_SQL);
        commonTestLockLockModeIsolations(em, LockModeType.OPTIMISTIC, 0, null,
            0, null, 1, NON_SUPPORTED_OPTIMISTIC_SQL);
        commonTestLockLockModeIsolations(em,
            LockModeType.OPTIMISTIC_FORCE_INCREMENT, 0, null, 0, null, 1,
            VERSION_UPDATE_SQL);
        commonTestLockLockModeIsolations(em, LockModeType.PESSIMISTIC_READ, 2,
            DB2_PESSIMISTIC_RS_SQL, 2, NON_SUPPORTED_FOR_UPDATE_SQL, 1,
            NON_SUPPORTED_OPTIMISTIC_SQL);
        commonTestLockLockModeIsolations(em, LockModeType.PESSIMISTIC_WRITE, 2,
            DB2_PESSIMISTIC_RR_SQL, 2, NON_SUPPORTED_FOR_UPDATE_SQL, 1,
            NON_SUPPORTED_OPTIMISTIC_SQL);
        commonTestLockLockModeIsolations(em,
            LockModeType.PESSIMISTIC_FORCE_INCREMENT, 2,
            DB2_PESSIMISTIC_RR_SQL, 2, NON_SUPPORTED_FOR_UPDATE_SQL, 1,
            VERSION_UPDATE_SQL);

        em.getTransaction().rollback();
        em.close();
    }

    private void commonTestLockLockModeIsolations(EntityManager em,
        LockModeType lockMode, int expectedSupportSQLCount,
        String expectedSupportSQL, int expectedNonSupportSQLCount,
        String expectedNonSupportSQL, int expectedVersionUpdateCount,
        String expectedVersionUpdateSQL) {
        OpenJPAEntityManager oem = (OpenJPAEntityManager) em.getDelegate();
        JDBCFetchConfigurationImpl fConfig = (JDBCFetchConfigurationImpl)
            ((EntityManagerImpl) oem).getBroker().getFetchConfiguration();
        DBDictionary dict = ((JDBCConfiguration) ((OpenJPAEntityManagerSPI) oem)
            .getConfiguration()).getDBDictionaryInstance();

        em.clear();
        LockEmployee employee = em.find(LockEmployee.class, 1);
        resetSQL();
        int beforeIsolation = fConfig.getIsolation();
        em.lock(employee, lockMode);
        if (dict.supportsIsolationForUpdate() &&
            dict instanceof DB2Dictionary) {
            assertEquals(expectedSupportSQLCount, getSQLCount());
            if (expectedSupportSQL != null)
                assertAllSQLInOrder(expectedSupportSQL);
        } else {
            assertEquals(expectedNonSupportSQLCount, getSQLCount());
            if (expectedNonSupportSQL != null)
                assertAllSQLInOrder(expectedNonSupportSQL);
        }

        resetSQL();
        em.flush();
        assertEquals(expectedVersionUpdateCount, getSQLCount());
        if (expectedVersionUpdateSQL != null)
            assertAllSQLInOrder(expectedVersionUpdateSQL);

        assertEquals(beforeIsolation, fConfig.getIsolation());
    }

    /*
     * Test em.getLockMode();
     */
    public void testGetLockMode() {
        EntityManager em = emf.createEntityManager();

        LockEmployee employee = em.find(LockEmployee.class, 1);
        try {
            em.getLockMode(employee);
            fail("Expecting TransactionRequiredException.");
        } catch (TransactionRequiredException tre) {
        } catch (Exception e){
            fail("Expecting TransactionRequiredException.");
        }

        em.getTransaction().begin();
        try {
            assertEquals("getLockMode only allows in transaction.",LockModeType.NONE, em.getLockMode(employee));
        } catch (Exception e){
            fail("Do not expecting any exception.");
        }
        em.getTransaction().rollback();

        em.clear();
        em.getTransaction().begin();
        try {
            // getLockMode on a detached entity;
            em.getLockMode(employee);
            fail("Expecting IllegalArgumentException for getLockMode on a detached entity in an active transaction.");
        } catch (IllegalArgumentException iae) {
        } catch (Exception e){
            fail("Expecting IllegalArgumentException for getLockMode on a detached entity in an active transaction.");
        }
        em.getTransaction().rollback();

        em.getTransaction().begin();
        try {
            employee = em.find(LockEmployee.class, 1, LockModeType.PESSIMISTIC_WRITE);
            assertEquals("Test getLockMode on non-NONE lock mode type.", LockModeType.PESSIMISTIC_WRITE, em
                    .getLockMode(employee));
        } catch (Exception e){
            fail("Do not expecting any exception.");
        }
        em.getTransaction().rollback();

        em.close();
    }
}
