/*
 * 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.batch.exception;

import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;

import org.apache.openjpa.jdbc.conf.JDBCConfiguration;
import org.apache.openjpa.jdbc.sql.DBDictionary;
import org.apache.openjpa.jdbc.sql.OracleDictionary;
import org.apache.openjpa.jdbc.sql.PostgresDictionary;
import org.apache.openjpa.persistence.OpenJPAEntityManagerFactorySPI;
import org.apache.openjpa.persistence.test.AbstractPersistenceTestCase;
import org.apache.openjpa.util.ExceptionInfo;

//This test was created for OPENJPA-1550.  In this issue the user was
//not able to get the 'failed object' (the object causing the failure) when
//batch limit was -1 or a value greater than 1.  Also, they found that the
//'params' listed in the prepared statement were missing.  This test will set
//various batch limits and verify that with the fix to 1550, the correct
//'failed object' and prepared statement is returned.
public class TestBatchLimitException extends AbstractPersistenceTestCase {

    static Ent1 expectedFailedObject;
    static Ent1 expectedFailedObjectOracle;

    static boolean isOracle = false;
    static boolean isPostgres = false;

    final String expectedFailureMsg =
        "INSERT INTO Ent1 (pk, name) VALUES (?, ?) [params=(int) 200, (String) twohundred]";
    final String expectedFailureMsg18 =
        "INSERT INTO Ent1 (pk, name) VALUES (?, ?) [params=(int) 18, (String) name18]";
    String expectedFailureMsgOracle = expectedFailureMsg18;

    public EntityManagerFactory newEmf(String batchLimit) {
        OpenJPAEntityManagerFactorySPI emf =
            createEMF(Ent1.class,
                "openjpa.jdbc.SynchronizeMappings",
                "buildSchema(ForeignKeys=true)",
                "openjpa.jdbc.DBDictionary", batchLimit,
                "openjpa.ConnectionFactoryProperties", "PrintParameters=true",
                CLEAR_TABLES);

        assertNotNull("Unable to create EntityManagerFactory", emf);
        JDBCConfiguration conf = (JDBCConfiguration) emf.getConfiguration();
        DBDictionary dict = conf.getDBDictionaryInstance();
        isOracle = dict instanceof OracleDictionary;
        isPostgres = dict instanceof PostgresDictionary;
        return emf;
    }

    @Override
    public void setUp() {
        expectedFailedObject = null;
        expectedFailedObjectOracle = null;
    }

    // Test that we get the correct 'failed object' when we have a batchLimt
    // of X and Y rows, where X>Y. A duplicate row will be inserted
    // sometime within the Y rows. This will verify that we get the right
    // 'failed object' and message.
    public void testExceptionInFirstBatch() throws Throwable {
        EntityManagerFactory emf = newEmf("batchLimit=-1");
        EntityManager em = emf.createEntityManager();

        em.getTransaction().begin();
        em.persist(new Ent1(1, "one"));
        expectedFailedObject = new Ent1(200, "twohundred");
        em.persist(expectedFailedObject);
        em.persist(new Ent1(5, "five"));
        em.getTransaction().commit();
        em.close();

        EntityManager em2 = emf.createEntityManager();

        em2.getTransaction().begin();
        // special case, due to how Oracle returns all statements in the batch
        expectedFailedObjectOracle = new Ent1(18, "name18");
        expectedFailureMsgOracle = expectedFailureMsg18;
        em2.persist(expectedFailedObjectOracle);
        em2.persist(new Ent1(2, "two"));
        em2.persist(new Ent1(200, "twohundred"));
        em2.persist(new Ent1(3, "three"));
        em2.persist(new Ent1(1, "one"));
        em2.persist(new Ent1(5, "five"));

        try {
            em2.getTransaction().commit();
        } catch (Throwable excp) {
            verifyExDetails(excp);
        }
        finally {
            if (em2.getTransaction().isActive()) {
                em2.getTransaction().rollback();
            }
            em2.close();
            closeEMF(emf);
        }
    }

    // Test that we get the correct 'failed object' when there is only one
    // row in the batch. The 'batching' logic executes a different
    // statement when only one row is to be updated/inserted.
    public void testExceptionSingleBatchedRow() throws Throwable {
        EntityManagerFactory emf = newEmf("batchLimit=-1");
        EntityManager em = emf.createEntityManager();

        em.getTransaction().begin();
        expectedFailedObject = new Ent1(200, "twohundred");
        expectedFailedObjectOracle = expectedFailedObject;
        expectedFailureMsgOracle = expectedFailureMsg;
        em.persist(expectedFailedObject);
        em.getTransaction().commit();
        em.close();

        EntityManager em2 = emf.createEntityManager();

        em2.getTransaction().begin();
        em2.persist(new Ent1(200, "twohundred"));

        try {
            em2.getTransaction().commit();
        } catch (Throwable excp) {
            verifyExDetails(excp);
        }
        finally {
            if (em2.getTransaction().isActive()) {
                em2.getTransaction().rollback();
            }
            em2.close();
            closeEMF(emf);
        }
    }

    // Test that we get the correct 'failed object' and message when we
    // have a batchLimt of X and Y rows, where Y>X. In this case, the
    // batch is executed every time the batchLimt is hit. A duplicate
    // row will be inserted sometime after X (X+1, i.e right at the
    // boundary of the batch) to verify that we get the right
    // 'failed object' and msg no matter which batch a duplicate is
    // contained in. This test is important because as part of the
    // fix to OPENJPA-1510 we had to add extra logic to keep track
    // of which batch the 'failed object' was in, along with the
    // index into that batch.
    public void testExceptionInSecondBatch() throws Throwable {
        EntityManagerFactory emf = newEmf("batchLimit=9");
        EntityManager em = emf.createEntityManager();

        em.getTransaction().begin();
        expectedFailedObject = new Ent1(200, "twohundred");
        expectedFailedObjectOracle = expectedFailedObject;
        expectedFailureMsgOracle = expectedFailureMsg;
        em.persist(expectedFailedObject);
        em.getTransaction().commit();
        em.close();

        EntityManager em2 = emf.createEntityManager();

        em2.getTransaction().begin();

        // Put 9 objects/rows into the batch
        for (int i = 0; i < 9; i++) {
            em2.persist(new Ent1(i, "name" + i));
        }

        // Put the duplicate object/row as the first element in the second batch.
        em2.persist(new Ent1(200, "twohundred"));

        try {
            em2.getTransaction().commit();
        } catch (Throwable excp) {
            verifyExDetails(excp);
        }
        finally {
            if (em2.getTransaction().isActive()) {
                em2.getTransaction().rollback();
            }
            em2.close();
            closeEMF(emf);
        }
    }

    // Same as testRowsGreaterThanBatchLimit_boundaryCase, but the object to cause the failure
    // is in the middle of the second batch. testExceptioninSecondBatch puts
    // the failing object as the first element in the second batch, this test puts
    // it somewhere in the middle of the third batch. Again, we want to make sure our
    // indexing into the batch containing the 'failed object' is correct.
    public void testExceptionInThirdBatch() throws Throwable {
        final int batchLimit=9;
        EntityManagerFactory emf = newEmf("batchLimit="+batchLimit);
        EntityManager em = emf.createEntityManager();

        em.getTransaction().begin();
        expectedFailedObject = new Ent1(200, "twohundred");
        em.persist(expectedFailedObject);
        em.getTransaction().commit();
        em.close();

        EntityManager em2 = emf.createEntityManager();

        em2.getTransaction().begin();

        // Persist 21 objects/rows....as such we will have two 'full'
        // batches (9*2=18) and 3 (21-18=3) objects/rows in the 3rd batch.
        int i=0;
        for (; i < 2*batchLimit; i++) {
            em2.persist(new Ent1(i, "name" + i));
        }

        // manually create third batch, due to how Oracle returns all statements in the batch
        expectedFailedObjectOracle = new Ent1(i, "name" + i++);
        expectedFailureMsgOracle = expectedFailureMsg18;
        em2.persist(expectedFailedObjectOracle);    // 18
        em2.persist(new Ent1(i, "name" + i++));     // 19
        em2.persist(new Ent1(i, "name" + i++));     // 20
        em2.persist(new Ent1(i, "name" + i++));     // 21
        // Put the duplicate row in the 3rd batch.
        em2.persist(new Ent1(200, "twohundred"));
        // Put a few more objects into the batch.
        for (i = 22; i < 4*batchLimit; i++) {
            em2.persist(new Ent1(i, "name" + i));
        }

        try {
            em2.getTransaction().commit();
        } catch (Throwable excp) {
            verifyExDetails(excp);
        }
        finally {
            if (em2.getTransaction().isActive()) {
                em2.getTransaction().rollback();
            }
            em2.close();
            closeEMF(emf);
        }
    }

    // Similar to the previous two tests, but lets run the test with a large
    // batch with a failure, and then commit, then run large batches
    // again with failures again.....just want to make sure things are not in
    // some way 're-used' between the two commits as far as the indexes go.
    public void testSecondExceptionHasRightIndex() throws Throwable {
        final int batchLimit=9;

        testExceptionInThirdBatch();

        EntityManagerFactory emf = newEmf("batchLimit=9");
        EntityManager em = emf.createEntityManager();

        em.getTransaction().begin();

        for (int i = 4*batchLimit; i < 5*batchLimit; i++) {
            em.persist(new Ent1(i, "name" + i));
        }

        // manually capture start of batch, due to how Oracle returns all statements in the batch
        expectedFailedObjectOracle = new Ent1(200, "twohundred");
        expectedFailureMsgOracle = expectedFailureMsg;
        em.persist(expectedFailedObjectOracle);

        for (int i = 5*batchLimit; i < 7*batchLimit; i++) {
            em.persist(new Ent1(i, "name" + i));
        }

        try {
            em.getTransaction().commit();
        } catch (Throwable excp) {
            verifyExDetails(excp);
        }
        finally {
            if (em.getTransaction().isActive()) {
                em.getTransaction().rollback();
            }
            em.close();
            closeEMF(emf);
        }
    }

    public void testExceptionWithMultipleCommits() throws Throwable {
        EntityManagerFactory emf = newEmf("batchLimit=-1");
        EntityManager em = emf.createEntityManager();

        em.getTransaction().begin();
        em.persist(new Ent1(1, "one"));
        expectedFailedObject = new Ent1(200, "twohundred");
        em.persist(expectedFailedObject);
        em.persist(new Ent1(5, "five"));
        em.getTransaction().commit();
        em.close();

        EntityManager em2 = emf.createEntityManager();
        em2.getTransaction().begin();
        em2.persist(new Ent1(4, "four"));
        em2.persist(new Ent1(0, "zero"));
        em2.persist(new Ent1(2, "two"));
        em2.persist(new Ent1(3, "three"));
        em2.getTransaction().commit();

        em2.getTransaction().begin();
        // special case, due to how Oracle returns all statements in the batch
        expectedFailedObjectOracle = new Ent1(18, "name18");
        expectedFailureMsgOracle = expectedFailureMsg18;
        em2.persist(expectedFailedObjectOracle);
        em2.persist(new Ent1(6, "six"));
        em2.persist(new Ent1(200, "twohundred"));
        em2.persist(new Ent1(7, "seven"));

        try {
            em2.getTransaction().commit();
        } catch (Throwable excp) {
            verifyExDetails(excp);
        }
        finally {
            if (em2.getTransaction().isActive()) {
                em2.getTransaction().rollback();
            }
            em2.close();
            closeEMF(emf);
        }
    }

    // Verify that the resultant exception contains the correct 'failed object'
    // and exception message.
    public void verifyExDetails(Throwable excp) throws Throwable {
        // The exception should contain the 'failed object'
        verifyFailedObject(excp);
        // The second cause should contain the message which shows the failing prepared statement.
        Throwable cause = excp.getCause().getCause();
        verifyExMsg(cause.getMessage());
    }

    public void verifyFailedObject(Throwable excp) throws Throwable {
        if (excp instanceof ExceptionInfo) {
            ExceptionInfo e = (ExceptionInfo) excp;

            Ent1 failedObject = (Ent1) e.getFailedObject();

            assertNotNull("Failed object was null.", failedObject);
            if (!isOracle && !isPostgres) {
                assertEquals(expectedFailedObject, failedObject);
            } else if (isOracle) {
                // since Oracle 18 we see a different behaviour, so test both ways
                assertTrue(failedObject.equals(expectedFailedObject) || failedObject.equals(expectedFailedObjectOracle) );
            } else {
                // special case, as Oracle returns all statements in the batch
                assertEquals(expectedFailedObjectOracle, failedObject);
            }
        }
        else {
            throw excp;
        }
    }

    public void verifyExMsg(String msg) {
        assertNotNull("Exception message was null.", msg);
        if (!isOracle && !isPostgres) {
            assertTrue("Did not see expected text in message. Expected <" + expectedFailureMsg + "> but was " +
                msg, msg.contains(expectedFailureMsg));
        } else if (isOracle) {
            // since Oracle 18 we see a different behaviour, so test both ways
            assertTrue(msg.contains(expectedFailureMsg) || msg.contains(expectedFailureMsgOracle));
        } else {
            // special case, as Oracle returns all statements in the batch
            assertTrue("Did not see expected text in message. Expected <" + expectedFailureMsgOracle + "> but was " +
                msg, msg.contains(expectedFailureMsgOracle));
        }
    }
}

