/*
 * 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.optlockex.timestamp;

import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityTransaction;

import org.apache.openjpa.persistence.test.SingleEMFTestCase;

/*
 * Test create for JIRA OPENJPA-2476, see it for a very detailed
 * description of the issue.
 */
public class TestTimestampOptLockEx extends SingleEMFTestCase {

    @Override
    public void setUp() {
        // By default we'd round a Timestamp to the nearest millisecond on Oracle (see DBDictionary.datePrecision
        // and DBDictionary.setTimestamp) and nearest microsecond on DB2 (see DB2Dictionary.datePrecision and
        // DBDictionary.setTimestamp) when sending the value to the db...if we change datePrecision to 1, we round to
        // the nearest nanosecond.  On DB2 and Oracle, it appears the default precision is microseconds but it seems
        // DB2 truncates (no rounding) to microsecond for anything it is given with greater precision, whereas Oracle
        // rounds.  So in the case of DB2, this test will pass if datePrecision=1, but still fails on Oracle.
        // On the other hand, if we set the datePrecision to 1000000 and run against DB2, the test will fail.

        // This test requires datePrecision to be set to the same precision as the Timestamp column.
        // I've only been testing on Oracle and DB2 and not sure how other DBs treat a Timestamps precision
        // by default.  In VersionTSEntity I use a Timestamp(3) but this is not supported on, at least, Derby
        // and older versions of DB2...at this time I'll enable only on Oracle.
        setSupportedDatabases(org.apache.openjpa.jdbc.sql.OracleDictionary.class);
        if (isTestsDisabled()) {
            return;
        }

        // Set datePrecision=1000000 for Oracle since we are using Timestamp(3)....on Oracle
        // the default is 1000000 so we shouldn't need to set it, but lets set it to future
        // proof the test.
        super.setUp(DROP_TABLES, "openjpa.jdbc.DBDictionary", "datePrecision=1000000", VersionTSEntity.class);
    }

    public void testUpdate() {
        poplulate();
        //This loop is necessary since we need a timestamp which has been rounded up
        //by the database, or by OpenJPA such that the in-memory version of the Timestamp
        //varies from that which is in the database.
        for (int i = 0; i < 5000; i++) {
            EntityManager em = emf.createEntityManager();
            EntityTransaction tx = em.getTransaction();

            // Find an existing VersionTSEntity:
            // stored with microsecond precision, e.g. 2014-01-21 13:16:46.595428
            VersionTSEntity t = em.find(VersionTSEntity.class, 1);

            tx.begin();
            t.setSomeInt(t.getSomeInt() + 1);
            t = em.merge(t);

            tx.commit();
            // If this clear is removed the test works fine.
            em.clear();

            // Lets say at this point the 'in-memory' timestamp is: 2014-01-22 07:22:11.548778567.  What we
            // actually sent to the DB (via the previous merge) is by default rounded (see DBDictionary.setTimestamp)
            // to the nearest millisecond on Oracle (see DBDictionary.datePrecision) and nearest microsecond on
            // DB2 (see DB2Dictionary.datePrecision) when sending the value to the db.
            // Therefore, what we actually send to the db is: 2014-01-22 07:22:11.548779 (for DB2) or
            // 2014-01-22 07:22:11.549 (for Oracle).  Notice in either case we rounded up.

            // now, do a merge with the unchanged entity
            tx = em.getTransaction();
            tx.begin();

            t = em.merge(t); // this results in a select of VersionTSEntity

            //This 'fixes' the issue (but customer doesn't really want to add this):
            //em.refresh(t);

            // Here is where things get interesting.....an error will happen here when the timestamp
            // has been rounded up, as I'll explain:
            // As part of this merge/commit, we select the timestamp from the db to get its value
            // (see method ColumnVersionStrategy.checkVersion below), i.e:
            // 'SELECT t0.updateTimestamp FROM VersionTSEntity t0 WHERE t0.id = ?'.
            // We then compare the 'in-memory' timestamp to that which we got back from the DB, i.e. on
            // DB2 we compare:
            // in-mem:  2014-01-22 07:22:11.548778567
            // from db: 2014-01-22 07:22:11.548779
            // Because these do not 'compare' properly (the db version is greater), we throw the OptimisticLockEx!!
            // For completeness, lets look at an example where the timestamp is as follows after the above
            // update: 2014-01-22 07:22:11.548771234.  We would send to DB2
            // the following value: 2014-01-22 07:22:11.548771.  Then, as part of the very last merge/commit, we'd
            // compare:
            // in-mem:  2014-01-22 07:22:11.548771234
            // from db: 2014-01-22 07:22:11.548771
            // These two would 'compare' properly (the db version is lesser), as such we would not throw an
            // OptLockEx and the test works fine.
            tx.commit();
            em.close();
        }
    }

    public void poplulate(){
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        VersionTSEntity r = new VersionTSEntity();

        r.setId(1L);
        r.setSomeInt(0);
        em.persist(r);
        tx.commit();
        em.close();
    }
}
