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

import java.sql.Date;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Random;

import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceUnitUtil;

import org.apache.openjpa.persistence.OpenJPAEntityManager;
import org.apache.openjpa.persistence.OpenJPAEntityManagerFactorySPI;
import org.apache.openjpa.persistence.OpenJPAPersistence;
import org.apache.openjpa.persistence.test.SingleEMFTestCase;

public class TestPersistenceUnitUtil extends SingleEMFTestCase{

    @Override
    public void setUp() {
        setUp(CLEAR_TABLES, EagerEntity.class, LazyEmbed.class,
            LazyEntity.class, EagerEmbed.class, RelEntity.class,
            EagerEmbedRel.class, MapEntity.class,
            MapKeyEmbed.class, MapValEntity.class,
            OneToEntity.class, ToManyLazy.class, ToManyEager.class);
    }

    /*
     * Verifies an entity and its persistent attributes are in the proper
     * load state.
     */
    public void testIsLoadedEager() {
        verifyIsLoadedEagerState(true);
    }

    /*
     * Verifies an entity and its persistent attributes are in the proper
     * not loaded state.
     */
    public void testNotLoadedLazy() {
        verifyIsLoadedEagerState(false);
    }

    /*
     * Verifies an entity and its persistent attributes are in the proper
     * loaded state.
     */
    public void testIsLoadedLazy() {
        verifyIsLoadedLazyState(true);
    }

    /*
     * Verifies an entity and its persistent attributes are in the proper
     * NOT_LOADED state.
     */
    public void testNotLoadedEager() {
        verifyIsLoadedEagerState(false);
    }

    /**
     * Verifies the use of PersistenceUnitUtil with multiple PU's.
     */
    public void testMultiplePUs() {
        OpenJPAEntityManagerFactorySPI emf1 =
            (OpenJPAEntityManagerFactorySPI)OpenJPAPersistence.
            createEntityManagerFactory("PUtil1",
                "org/apache/openjpa/persistence/util/" +
                "persistence.xml");
        assertNotNull(emf1);

        OpenJPAEntityManagerFactorySPI emf2 =
            (OpenJPAEntityManagerFactorySPI)OpenJPAPersistence.
            createEntityManagerFactory("PUtil2",
                "org/apache/openjpa/persistence/util/" +
                "persistence.xml");

        assertNotNull(emf2);
        assertNotSame(emf, emf1);
        assertNotSame(emf1, emf2);

        PersistenceUnitUtil puu = emf.getPersistenceUnitUtil();
        PersistenceUnitUtil puu1 = emf1.getPersistenceUnitUtil();
        PersistenceUnitUtil puu2 = emf2.getPersistenceUnitUtil();

        assertNotSame(puu, puu1);
        assertNotSame(puu, puu2);
        assertNotSame(puu1, puu2);

        EntityManager em = emf.createEntityManager();
        EntityManager em1 = emf1.createEntityManager();
        EntityManager em2 = emf2.createEntityManager();

        verifyPULoadState(em, puu, puu1, puu2);
        verifyPULoadState(em1, puu1, puu, puu2);
        verifyPULoadState(em2, puu2, puu, puu1);

        em.close();
        em1.close();
        em2.close();

        if (emf1 != null) {
            emf1.close();
        }
        if (emf2 != null) {
            emf2.close();
        }

    }

    private void verifyPULoadState(EntityManager em,
        PersistenceUnitUtil...puu) {

        EagerEntity ee = createEagerEntity();
        assertEquals(false, puu[0].isLoaded(ee));
        assertEquals(false, puu[0].isLoaded(ee,
            "id"));
        assertEquals(false, puu[1].isLoaded(ee));
        assertEquals(false, puu[1].isLoaded(ee,
            "id"));
        assertEquals(false, puu[2].isLoaded(ee));
        assertEquals(false, puu[2].isLoaded(ee,
            "id"));

        em.getTransaction().begin();
        em.persist(ee);
        em.getTransaction().commit();

        assertEquals(true, puu[0].isLoaded(ee));
        assertEquals(true, puu[0].isLoaded(ee,
            "id"));
        assertEquals(false, puu[1].isLoaded(ee));
        assertEquals(false, puu[1].isLoaded(ee,
            "id"));
        assertEquals(false, puu[2].isLoaded(ee));
        assertEquals(false, puu[2].isLoaded(ee,
            "id"));
    }


    private void verifyIsLoadedEagerState(boolean loaded) {
        PersistenceUnitUtil puu = emf.getPersistenceUnitUtil();
        assertSame(emf, puu);
        EntityManager em = emf.createEntityManager();
        EagerEntity ee = createEagerEntity();

        // Vfy LoadState is false for the unmanaged entity
        assertEquals(false, puu.isLoaded(ee));
        assertEquals(false, puu.isLoaded(ee,
            "id"));

        em.getTransaction().begin();
        em.persist(ee);
        em.getTransaction().commit();
        em.clear();

        if (loaded)
            ee = em.find(EagerEntity.class, ee.getId());
        else
            ee = em.getReference(EagerEntity.class, ee.getId());

        assertEquals(loaded, puu.isLoaded(ee));
        assertEquals(loaded, puu.isLoaded(ee, "id"));
        assertEquals(loaded, puu.isLoaded(ee, "name"));
        assertEquals(loaded, puu.isLoaded(ee, "eagerEmbed"));
        assertEquals(false, puu.isLoaded(ee, "transField"));

        em.close();
    }

    private void verifyIsLoadedLazyState(boolean loaded) {
        PersistenceUnitUtil puu = emf.getPersistenceUnitUtil();
        assertSame(emf, puu);
        EntityManager em = emf.createEntityManager();
        LazyEntity le = createLazyEntity();

        // Vfy LoadState is false for the unmanaged entity
        assertEquals(false, puu.isLoaded(le));
        assertEquals(false, puu.isLoaded(le,"id"));

        em.getTransaction().begin();
        em.persist(le);
        em.getTransaction().commit();
        em.clear();

        // Use find or getReference based upon expected state
        if (loaded)
            le = em.find(LazyEntity.class, le.getId());
        else
            le = em.getReference(LazyEntity.class, le.getId());

        assertEquals(loaded, puu.isLoaded(le));
        assertEquals(loaded, puu.isLoaded(le, "id"));

        // Name is lazy fetch so it should not be loaded
        assertEquals(false, puu.isLoaded(le, "name"));
        assertEquals(loaded, puu.isLoaded(le, "lazyEmbed"));
        assertEquals(false, puu.isLoaded(le, "transField"));

        em.close();
    }

    /*
     * Verifies that an entity and attributes are considered loaded if they
     * are assigned by the application.
     */
    public void testIsApplicationLoaded() {
        PersistenceUnitUtil puu = emf.getPersistenceUnitUtil();
        assertSame(emf, puu);
        EntityManager em = emf.createEntityManager();
        EagerEntity ee = createEagerEntity();

        em.getTransaction().begin();
        em.persist(ee);
        em.getTransaction().commit();
        em.clear();

        ee = em.getReference(EagerEntity.class, ee.getId());
        assertNotNull(ee);
        assertEagerLoadState(puu, ee, false);

        ee.setName("AppEagerName");
        EagerEmbed emb = createEagerEmbed();
        ee.setEagerEmbed(emb);
        // Assert fields are loaded via application loading
        assertEagerLoadState(puu, ee, true);
        // Vfy the set values are applied to the entity
        assertEquals("AppEagerName", ee.getName());
        assertEquals(emb, ee.getEagerEmbed());

        em.close();
    }

    public void testPCMapEager() {
        PersistenceUnitUtil puu = emf.getPersistenceUnitUtil();
        EntityManager em = emf.createEntityManager();

        MapValEntity mve = new MapValEntity();
        mve.setIntVal(10);
        MapKeyEmbed mke = new MapKeyEmbed();
        mke.setFirstName("Jane");
        mke.setLastName("Doe");

        MapEntity me = new MapEntity();

        assertEquals(false, puu.isLoaded(me));
        assertEquals(false, puu.isLoaded(me,
            "mapValEntity"));
        assertEquals(false, puu.isLoaded(me,
            "mapEntities"));

        assertEquals(false, puu.isLoaded(mve));

        // Create a circular ref
        me.setMapValEntity(mve);
        mve.setMapEntity(me);

        HashMap<MapKeyEmbed, MapValEntity> hm =
            new HashMap<>();

        hm.put(mke, mve);
        me.setMapEntities(hm);

        em.getTransaction().begin();
        em.persist(me);
        em.getTransaction().commit();

        assertEquals(true, puu.isLoaded(me));
        assertEquals(true, puu.isLoaded(me,
            "mapValEntity"));
        assertEquals(true, puu.isLoaded(me,
            "mapEntities"));

        assertEquals(true, puu.isLoaded(mve));

        em.close();
    }

    /*
     * Verify load state is not loaded for null relationships or relationships
     * set to null.
     */
    public void testSetNullLazyRelationship() {

        PersistenceUnitUtil puu = emf.getPersistenceUnitUtil();
        EntityManager em = emf.createEntityManager();

        try {
            OneToEntity ote = new OneToEntity();
            assertFalse(puu.isLoaded(ote, "toManyLazy"));
            em.getTransaction().begin();
            em.persist(ote);
            em.getTransaction().commit();
            em.clear();
            ote = em.find(OneToEntity.class, ote.getId());
            // Field is lazy and not immediately loaded by the application
            assertFalse(puu.isLoaded(ote, "toManyLazy"));
            // Force load the lazy field
            ote.getToManyLazy();
            assertTrue(puu.isLoaded(ote, "toManyLazy"));

            OneToEntity ote2 = new OneToEntity();
            em.getTransaction().begin();
            em.persist(ote2);
            em.getTransaction().commit();
            // Field gets set to loaded upon commit
            assertTrue(puu.isLoaded(ote2, "toManyLazy"));
            em.clear();
            ote2 = em.find(OneToEntity.class, ote2.getId());

            // Field is lazy and not immediately loaded by the application
            assertFalse(puu.isLoaded(ote2, "toManyLazy"));

            // Load by application
            List<ToManyLazy> tmes = new ArrayList<>();
            for (int i = 0; i < 5; i++) {
                tmes.add(new ToManyLazy("ToMany" + i));
            }
            em.getTransaction().begin();
            ote2.setToManyLazy(tmes);
            // App loaded before commit
            assertTrue(puu.isLoaded(ote2, "toManyLazy"));
            em.getTransaction().commit();
            // Still loaded after commit
            assertTrue(puu.isLoaded(ote2, "toManyLazy"));

            // Set to null - still loaded per spec.
            em.getTransaction().begin();
            ote2.setToManyLazy(null);
            // Considered loaded before commit
            assertTrue(puu.isLoaded(ote2, "toManyLazy"));
            em.getTransaction().commit();
            //Loaded after commit
            assertTrue(puu.isLoaded(ote2, "toManyLazy"));
        }
        finally {
            if (em.getTransaction().isActive()) {
                em.getTransaction().rollback();
            }
        }
        em.close();
    }

    public void testSetNullEagerRelationship() {

        PersistenceUnitUtil puu = emf.getPersistenceUnitUtil();
        EntityManager em = emf.createEntityManager();

        try {
            OneToEntity ote = new OneToEntity();
            assertFalse(puu.isLoaded(ote, "toManyEager"));
            em.getTransaction().begin();
            em.persist(ote);
            em.getTransaction().commit();
            em.clear();
            ote = em.find(OneToEntity.class, ote.getId());
            // Field is eager and is immediately loaded by the application
            assertTrue(puu.isLoaded(ote, "toManyEager"));

            OneToEntity ote2 = new OneToEntity();
            em.getTransaction().begin();
            em.persist(ote2);
            // Field is null by default, but after persist, it is treated as loaded.
            assertTrue(puu.isLoaded(ote2, "toManyEager"));
            em.getTransaction().commit();
            // Field gets set to loaded upon commit
            assertTrue(puu.isLoaded(ote2, "toManyEager"));
            em.clear();
            ote2 = em.find(OneToEntity.class, ote2.getId());

            // Field is eager and is immediately loaded by the application
            assertTrue(puu.isLoaded(ote2, "toManyEager"));

            // Load by application
            List<ToManyEager> tmes = new ArrayList<>();
            for (int i = 0; i < 5; i++) {
                tmes.add(new ToManyEager("ToMany" + i));
            }
            em.getTransaction().begin();
            ote2.setToManyEager(tmes);
            // App loaded before commit
            assertTrue(puu.isLoaded(ote2, "toManyEager"));
            em.getTransaction().commit();
            // Still loaded after commit
            assertTrue(puu.isLoaded(ote2, "toManyEager"));

            // Set to null - still loaded per spec.
            em.getTransaction().begin();
            ote2.setToManyEager(null);
            // Entity is considered loaded before commit
            assertTrue(puu.isLoaded(ote2));
            // Attribute is considered loaded before commit
            assertTrue(puu.isLoaded(ote2, "toManyEager"));
            em.getTransaction().commit();
            //Loaded after commit
            assertTrue(puu.isLoaded(ote2, "toManyEager"));
        }
        finally {
            if (em.getTransaction().isActive()) {
                em.getTransaction().rollback();
            }
        }
        em.close();
    }

    public void testBasicTypeNotLoaded() {
        PersistenceUnitUtil puu = emf.getPersistenceUnitUtil();
        EntityManager em = emf.createEntityManager();
        EagerEntity ee = createEagerEntity();
        int id = ee.getId();

        em.getTransaction().begin();
        em.persist(ee);
        em.getTransaction().commit();
        em.clear();
        // name is not eagerly loaded, only eagerEmbed is eagerly loaded
        OpenJPAEntityManager kem = OpenJPAPersistence.cast(em);
        kem.getFetchPlan().resetFetchGroups().removeFetchGroup("default")
            .addField(EagerEntity.class, "eagerEmbed");
        ee = em.find(EagerEntity.class, id);
        assertEquals(true, puu.isLoaded(ee));
    }

    private EagerEntity createEagerEntity() {
        EagerEntity ee = new EagerEntity();
        ee.setId(new Random().nextInt());
        ee.setName("EagerEntity");
        EagerEmbed emb = createEagerEmbed();
        ee.setEagerEmbed(emb);
        return ee;
    }

    private EagerEmbed createEagerEmbed() {
        EagerEmbed emb = new EagerEmbed();
        emb.setEndDate(new Date(System.currentTimeMillis()));
        emb.setStartDate(new Date(System.currentTimeMillis()));
        return emb;
    }

    private LazyEntity createLazyEntity() {
        LazyEntity le = new LazyEntity();
        le.setId(new Random().nextInt());
        le.setName("LazyEntity");
        LazyEmbed emb = new LazyEmbed();
        emb.setEndDate(new Date(System.currentTimeMillis()));
        emb.setStartDate(new Date(System.currentTimeMillis()));
        le.setLazyEmbed(emb);
        RelEntity re = new RelEntity();
        re.setName("My ent");
        ArrayList<RelEntity> rel = new ArrayList<>();
        rel.add(new RelEntity());
        return le;
    }

    private void assertEagerLoadState(PersistenceUnitUtil pu, Object ent,
        boolean state) {
        assertEquals(state, pu.isLoaded(ent));
        assertEquals(state, pu.isLoaded(ent, "id"));
        assertEquals(state, pu.isLoaded(ent, "name"));
        assertEquals(state, pu.isLoaded(ent, "eagerEmbed"));
        assertEquals(false, pu.isLoaded(ent, "transField"));
    }
}
