/*
 * Copyright (c) 2011, 2018 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// Contributors:
//     05/19/2010-2.1 ailitchev - Bug 244124 - Add Nested FetchGroup
//     09/21/2010-2.2 Frank Schwarz and ailitchev - Bug 325684 - QueryHints.BATCH combined with QueryHints.FETCH_GROUP_LOAD will cause NPE
package org.eclipse.persistence.testing.tests.jpa.fetchgroups;

import java.util.Collection;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.persistence.EntityManager;
import javax.persistence.Query;

import junit.framework.TestSuite;

import org.eclipse.persistence.config.QueryHints;
import org.eclipse.persistence.internal.helper.IdentityHashSet;
import org.eclipse.persistence.internal.jpa.EntityManagerImpl;
import org.eclipse.persistence.internal.queries.EntityFetchGroup;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.jpa.JpaHelper;
import org.eclipse.persistence.queries.AttributeGroup;
import org.eclipse.persistence.queries.FetchGroup;
import org.eclipse.persistence.queries.FetchGroupTracker;
import org.eclipse.persistence.queries.LoadGroup;
import org.eclipse.persistence.testing.models.jpa.advanced.Address;
import org.eclipse.persistence.testing.models.jpa.advanced.Department;
import org.eclipse.persistence.testing.models.jpa.advanced.Employee;
import org.eclipse.persistence.testing.models.jpa.advanced.PhoneNumber;
import org.eclipse.persistence.testing.models.jpa.advanced.Project;
import org.eclipse.persistence.testing.models.jpa.advanced.Employee.Gender;
import org.eclipse.persistence.testing.models.jpa.advanced.compositepk.Competency;

import org.junit.Test;

/**
 * @author dclarke
 * @since EclipseLink 2.1
 */
public class NestedFetchGroupTests extends BaseFetchGroupTests {

    public NestedFetchGroupTests() {
        super();
    }

    public NestedFetchGroupTests(String name) {
        super(name);
    }

    public static junit.framework.Test suite() {
        TestSuite suite = new TestSuite();
        suite.setName("NestedFetchGroupTests");

        suite.addTest(new NestedFetchGroupTests("testSetup"));
        suite.addTest(new NestedFetchGroupTests("dynamicFetchGroup_EmployeeAddress"));
        suite.addTest(new NestedFetchGroupTests("dynamicFetchGroup_Employee_NullAddress"));
        suite.addTest(new NestedFetchGroupTests("dynamicFetchGroup_EmployeeAddressNullPhone"));
        suite.addTest(new NestedFetchGroupTests("dynamicFetchGroup_EmployeeAddressEmptyPhone"));
        suite.addTest(new NestedFetchGroupTests("dynamicFetchGroup_EmployeeAddressEmptyPhoneLoad"));
        suite.addTest(new NestedFetchGroupTests("dynamicHierarchicalFetchGroup"));
        suite.addTest(new NestedFetchGroupTests("dynamicFetchGroup_ElementCollection"));
//**temp        suite.addTest(new NestedFetchGroupTests("dynamicHierarchicalFetchGroup_JOIN_FETCH"));
        suite.addTest(new NestedFetchGroupTests("dynamicHierarchicalFetchGroup_JOIN_FETCH_Copy"));
//**temp        suite.addTest(new NestedFetchGroupTests("managerDoubleNestedFetchGroupWithJoinFetch"));
        suite.addTest(new NestedFetchGroupTests("managerTripleNestedFetchGroupWithJoinFetch"));
        suite.addTest(new NestedFetchGroupTests("allNestedFetchGroupWithJoinFetch"));
        suite.addTest(new NestedFetchGroupTests("joinFetchDefaultFetchGroup"));
        suite.addTest(new NestedFetchGroupTests("joinFetchOutsideOfFetchGroup"));
        suite.addTest(new NestedFetchGroupTests("simpleNestedFetchGroupWithBatch"));
        suite.addTest(new NestedFetchGroupTests("simpleLoadGroup"));
        suite.addTest(new NestedFetchGroupTests("simpleFetchGroupLoadWithBatch"));

        return suite;
    }

    public void setUp() {
        super.setUp();

        defaultPhoneFG = new FetchGroup();
        defaultPhoneFG.addAttribute("number");
        phoneDescriptor.getFetchGroupManager().setDefaultFetchGroup(defaultPhoneFG);

        reprepareReadQueries(phoneDescriptor);
        reprepareReadQueries(employeeDescriptor);

        // We'll put a default FetchGroup on Phone
        assertNotNull(phoneDescriptor.getDefaultFetchGroup());
        assertNotNull(phoneDescriptor.getDescriptorQueryManager().getReadObjectQuery().getExecutionFetchGroup());
        assertTrue(phoneDescriptor.getFetchGroupManager().getFetchGroups().isEmpty());
    }

    @Test
    public void dynamicFetchGroup_EmployeeAddress() throws Exception {
        EntityManager em = createEntityManager();
        try {
            beginTransaction(em);

            Query query = em.createQuery("SELECT e FROM Employee e WHERE e.gender = :GENDER");
            query.setParameter("GENDER", Gender.Male);

            // Define the fields to be fetched on Employee
            FetchGroup fg = new FetchGroup();
            fg.addAttribute("id");
            fg.addAttribute("version");
            fg.addAttribute("firstName");
            fg.addAttribute("lastName");
            fg.addAttribute("address.city");
            fg.addAttribute("address.postalCode");

            // Configure the dynamic FetchGroup
            query.setHint(QueryHints.FETCH_GROUP, fg);

            List<Employee> emps = query.getResultList();

            assertNotNull(emps);
            for (Employee emp : emps) {
                FetchGroupTracker tracker = (FetchGroupTracker) emp;

                assertNotNull(tracker._persistence_getFetchGroup());

                // Verify specified fields plus mandatory ones are loaded
                assertTrue(tracker._persistence_isAttributeFetched("firstName"));
                assertTrue(tracker._persistence_isAttributeFetched("lastName"));
                assertTrue(tracker._persistence_isAttributeFetched("address"));
                FetchGroupTracker addrTracker = (FetchGroupTracker) emp.getAddress();
                assertTrue(addrTracker._persistence_isAttributeFetched("city"));
                assertTrue(addrTracker._persistence_isAttributeFetched("postalCode"));
                assertFalse(addrTracker._persistence_isAttributeFetched("street"));

                // Verify the other fields are not loaded
                assertFalse(tracker._persistence_isAttributeFetched("salary"));
                assertFalse(tracker._persistence_isAttributeFetched("startTime"));
                assertFalse(tracker._persistence_isAttributeFetched("endTime"));

                // Force the loading of lazy fields and verify
                emp.getSalary();

                assertTrue(tracker._persistence_isAttributeFetched("firstName"));
                assertTrue(tracker._persistence_isAttributeFetched("lastName"));
                assertTrue(tracker._persistence_isAttributeFetched("address"));
                assertTrue(tracker._persistence_isAttributeFetched("salary"));
                assertTrue(tracker._persistence_isAttributeFetched("startTime"));
                assertTrue(tracker._persistence_isAttributeFetched("endTime"));

                // Now we'll check the address uses the provided dynamic fetch-group
                addrTracker = (FetchGroupTracker) emp.getAddress();
                assertNotNull("Address does not have a FetchGroup", addrTracker._persistence_getFetchGroup());
                assertTrue(addrTracker._persistence_isAttributeFetched("city"));
                assertTrue(addrTracker._persistence_isAttributeFetched("postalCode"));
                assertFalse(addrTracker._persistence_isAttributeFetched("street"));
                assertFalse(addrTracker._persistence_isAttributeFetched("country"));

                // Now we'll check the phoneNumbers use of the default fetch group
                for (PhoneNumber phone : emp.getPhoneNumbers()) {
                    FetchGroupTracker phoneTracker = (FetchGroupTracker) phone;
                    assertNotNull("PhoneNumber does not have a FetchGroup", phoneTracker._persistence_getFetchGroup());
                    assertTrue(phoneTracker._persistence_isAttributeFetched("number"));
                    assertFalse(phoneTracker._persistence_isAttributeFetched("areaCode"));
                }
            }
        } finally {
            if (isTransactionActive(em)){
                rollbackTransaction(em);
            }
            closeEntityManager(em);
        }
    }

    @Test
    public void dynamicFetchGroup_Employee_NullAddress() throws Exception {
        EntityManager em = createEntityManager();
        try {
            beginTransaction(em);

            Query query = em.createQuery("SELECT e FROM Employee e WHERE e.gender = :GENDER");
            query.setParameter("GENDER", Gender.Male);

            // Define the fields to be fetched on Employee
            FetchGroup empGroup = new FetchGroup();
            empGroup.addAttribute("firstName");
            empGroup.addAttribute("lastName");
            empGroup.addAttribute("address");

            // Define the fields to be fetched on Address
            FetchGroup addressGroup = new FetchGroup();
            addressGroup.addAttribute("city");
            addressGroup.addAttribute("postalCode");

            empGroup.addAttribute("address");

            // Configure the dynamic FetchGroup
            query.setHint(QueryHints.FETCH_GROUP, empGroup);

            List<Employee> emps = query.getResultList();

            assertNotNull(emps);
            for (Employee emp : emps) {
                FetchGroupTracker tracker = (FetchGroupTracker) emp;

                assertNotNull(tracker._persistence_getFetchGroup());

                // Verify specified fields plus mandatory ones are loaded
                assertTrue(tracker._persistence_isAttributeFetched("id"));
                assertTrue(tracker._persistence_isAttributeFetched("firstName"));
                assertTrue(tracker._persistence_isAttributeFetched("lastName"));
                assertTrue(tracker._persistence_isAttributeFetched("version"));

                // Verify the other fields are not loaded
                assertFalse(tracker._persistence_isAttributeFetched("salary"));
                assertFalse(tracker._persistence_isAttributeFetched("startTime"));
                assertFalse(tracker._persistence_isAttributeFetched("endTime"));

                // Force the loading of lazy fields and verify
                emp.getSalary();

                assertTrue(tracker._persistence_isAttributeFetched("salary"));
                assertTrue(tracker._persistence_isAttributeFetched("startTime"));
                assertTrue(tracker._persistence_isAttributeFetched("endTime"));

                // Now we'll check the address uses the provided dynamic fetch-group
                FetchGroupTracker addrTracker = (FetchGroupTracker) emp.getAddress();
                assertNull("Address has an unexpected FetchGroup", addrTracker._persistence_getFetchGroup());

                // Now we'll check the phoneNumbers use of the default fetch group
                for (PhoneNumber phone : emp.getPhoneNumbers()) {
                    FetchGroupTracker phoneTracker = (FetchGroupTracker) phone;
                    assertNotNull("PhoneNumber does not have a FetchGroup", phoneTracker._persistence_getFetchGroup());
                    assertTrue(phoneTracker._persistence_isAttributeFetched("number"));
                    assertFalse(phoneTracker._persistence_isAttributeFetched("areaCode"));
                }
            }
        } finally {
            if (isTransactionActive(em)){
                rollbackTransaction(em);
            }
            closeEntityManager(em);
        }
    }

    @Test
    public void dynamicFetchGroup_EmployeeAddressNullPhone() throws Exception {
        EntityManager em = createEntityManager();
        try {
            beginTransaction(em);

            Query query = em.createQuery("SELECT e FROM Employee e WHERE e.gender = :GENDER");
            query.setParameter("GENDER", Gender.Male);

            // Define the fields to be fetched on Employee
            FetchGroup empGroup = new FetchGroup();
            empGroup.addAttribute("firstName");
            empGroup.addAttribute("lastName");
            empGroup.addAttribute("address");
            empGroup.addAttribute("address.city");
            empGroup.addAttribute("address.postalCode");

            //empGroup.addAttribute("phoneNumbers").setUseDefaultFetchGroup(false);
            FetchGroup fullPhone = this.phoneDescriptor.getFetchGroupManager().createFullFetchGroup();
            // to preclude Employee from being loaded by phoneNumber.owner add it to the fetch group
            fullPhone.addAttribute("owner.id");
            empGroup.addAttribute("phoneNumbers", fullPhone);

            // Configure the dynamic FetchGroup
            query.setHint(QueryHints.FETCH_GROUP, empGroup);

            List<Employee> emps = query.getResultList();

            assertNotNull(emps);
            for (Employee emp : emps) {
                FetchGroupTracker tracker = (FetchGroupTracker) emp;

                assertNotNull(tracker._persistence_getFetchGroup());

                // Verify specified fields plus mandatory ones are loaded
                assertTrue(tracker._persistence_isAttributeFetched("id"));
                assertTrue(tracker._persistence_isAttributeFetched("firstName"));
                assertTrue(tracker._persistence_isAttributeFetched("lastName"));
                assertTrue(tracker._persistence_isAttributeFetched("version"));

                // Verify the other fields are not loaded
                assertFalse(tracker._persistence_isAttributeFetched("salary"));
                assertFalse(tracker._persistence_isAttributeFetched("startTime"));
                assertFalse(tracker._persistence_isAttributeFetched("endTime"));

                // Force the loading of lazy fields and verify
                emp.getSalary();

                assertTrue(tracker._persistence_isAttributeFetched("salary"));
                assertTrue(tracker._persistence_isAttributeFetched("startTime"));
                assertTrue(tracker._persistence_isAttributeFetched("endTime"));

                // Now we'll check the address uses the provided dynamic fetch-group
                FetchGroupTracker addrTracker = (FetchGroupTracker) emp.getAddress();
                assertNotNull("Address does not have a FetchGroup", addrTracker._persistence_getFetchGroup());
                assertTrue(addrTracker._persistence_isAttributeFetched("city"));
                assertTrue(addrTracker._persistence_isAttributeFetched("postalCode"));
                assertFalse(addrTracker._persistence_isAttributeFetched("street"));
                assertFalse(addrTracker._persistence_isAttributeFetched("country"));

                // Now we'll check the phoneNumbers use of the default fetch group
                for (PhoneNumber phone : emp.getPhoneNumbers()) {
                    FetchGroupTracker phoneTracker = (FetchGroupTracker) phone;
                    assertNull("PhoneNumber has a FetchGroup", phoneTracker._persistence_getFetchGroup());
                }
            }
        } finally {
            if (isTransactionActive(em)){
                rollbackTransaction(em);
            }
            closeEntityManager(em);
        }
    }

    @Test
    public void dynamicFetchGroup_ElementCollection(){
        EntityManager em = createEntityManager();
        AttributeGroup compt = new AttributeGroup(null, Competency.class, true);
        compt.addAttribute("description");
        AttributeGroup fg = new AttributeGroup(null, org.eclipse.persistence.testing.models.jpa.advanced.compositepk.Department.class, true);
        fg.addAttribute("competencies", compt);
        clearCache();
        Collection<org.eclipse.persistence.testing.models.jpa.advanced.compositepk.Department> results = em.createQuery("select d from Department d").setHint(QueryHints.FETCH_GROUP, fg.toFetchGroup()).getResultList();
        for (org.eclipse.persistence.testing.models.jpa.advanced.compositepk.Department dept : results){
            assertFalse("Collection fetched: scientists, fg ignored", ((FetchGroupTracker)dept)._persistence_isAttributeFetched("scientists"));
            assertFalse("Collection fetched: offices, fg ignored", ((FetchGroupTracker)dept)._persistence_isAttributeFetched("offices"));
            assertTrue("Collection not fetched: competencies, fg ignored", ((FetchGroupTracker)dept)._persistence_isAttributeFetched("competencies"));
            for (Competency embeded: dept.getCompetencies()){
                assertTrue("Element attribute not loaded: description, fg ignored", ((FetchGroupTracker)embeded)._persistence_isAttributeFetched("description"));
            }
            dept.getScientists().size();
            assertTrue("Collection not fetched: scientists, fg ignored", ((FetchGroupTracker)dept)._persistence_isAttributeFetched("scientists"));
            assertTrue("Collection not fetched: offices, fg ignored", ((FetchGroupTracker)dept)._persistence_isAttributeFetched("offices"));
            assertTrue("Collection not fetched: competencies, fg ignored", ((FetchGroupTracker)dept)._persistence_isAttributeFetched("competencies"));
            for (Competency embeded: dept.getCompetencies()){
                embeded.getRating();
                assertTrue("Element attribute not loaded: description, fg ignored", ((FetchGroupTracker)embeded)._persistence_isAttributeFetched("description"));
            }
        }
    }

    @Test
    public void dynamicFetchGroup_EmployeeAddressEmptyPhone() {

    }
    public void dynamicFetchGroup_EmployeeAddressEmptyPhoneLoad() {

    }
    void internal_dynamicFetchGroup_EmployeeAddressEmptyPhone(boolean shouldLoad) {
        EntityManager em = createEntityManager();
        try {
            beginTransaction(em);

            Query query = em.createQuery("SELECT e FROM Employee e WHERE e.gender = :GENDER");
            query.setParameter("GENDER", Gender.Male);

            // Define the fields to be fetched on Employee
            FetchGroup fg = new FetchGroup();
            fg.addAttribute("firstName");
            fg.addAttribute("lastName");
            fg.addAttribute("address.city");
            fg.addAttribute("address.postalCode");
            // to preclude Employee from being loaded by phoneNumber.owner add it to the fetch group
            FetchGroup ownerId = new FetchGroup();
            ownerId.addAttribute("owner.id");
            fg.addAttribute("phoneNumbers", ownerId);

            if(shouldLoad) {
                fg.setShouldLoad(true);
            }

            // Configure the dynamic FetchGroup
            query.setHint(QueryHints.FETCH_GROUP, fg);

            List<Employee> emps = query.getResultList();

            assertNotNull(emps);
            assertEquals(1 + (shouldLoad ? 0 : (emps.size() * 2)), getQuerySQLTracker(em).getTotalSQLSELECTCalls());

            for (Employee emp : emps) {
                FetchGroupTracker tracker = (FetchGroupTracker) emp;

                assertNotNull(tracker._persistence_getFetchGroup());

                // Verify specified fields plus mandatory ones are loaded
                assertTrue(tracker._persistence_isAttributeFetched("id"));
                assertTrue(tracker._persistence_isAttributeFetched("firstName"));
                assertTrue(tracker._persistence_isAttributeFetched("lastName"));
                assertTrue(tracker._persistence_isAttributeFetched("version"));

                // Verify the other fields are not loaded
                assertFalse(tracker._persistence_isAttributeFetched("salary"));
                assertFalse(tracker._persistence_isAttributeFetched("startTime"));
                assertFalse(tracker._persistence_isAttributeFetched("endTime"));

                // Force the loading of lazy fields and verify
                emp.getSalary();

                assertTrue(tracker._persistence_isAttributeFetched("salary"));
                assertTrue(tracker._persistence_isAttributeFetched("startTime"));
                assertTrue(tracker._persistence_isAttributeFetched("endTime"));

                // Now we'll check the address uses the provided dynamic fetch-group
                FetchGroupTracker addrTracker = (FetchGroupTracker) emp.getAddress();
                assertNotNull("Address does not have a FetchGroup", addrTracker._persistence_getFetchGroup());
                assertTrue(addrTracker._persistence_isAttributeFetched("city"));
                assertTrue(addrTracker._persistence_isAttributeFetched("postalCode"));
                assertFalse(addrTracker._persistence_isAttributeFetched("street"));
                assertFalse(addrTracker._persistence_isAttributeFetched("country"));

                // Now we'll check the phoneNumbers use of the default fetch group
                for (PhoneNumber phone : emp.getPhoneNumbers()) {
                    FetchGroupTracker phoneTracker = (FetchGroupTracker) phone;
                    assertNotNull("PhoneNumber does not have a FetchGroup", phoneTracker._persistence_getFetchGroup());
                    assertFalse(phoneTracker._persistence_isAttributeFetched("number"));
                    assertFalse(phoneTracker._persistence_isAttributeFetched("areaCode"));

                    phone.getNumber();

                    assertTrue(phoneTracker._persistence_isAttributeFetched("number"));
                    assertTrue(phoneTracker._persistence_isAttributeFetched("areaCode"));
                }
            }
        } finally {
            if (isTransactionActive(em)){
                rollbackTransaction(em);
            }
            closeEntityManager(em);
        }
    }

    @Test
    public void dynamicHierarchicalFetchGroup() throws Exception {

        EntityManager em = createEntityManager();

        Query query = em.createQuery("SELECT e FROM Employee e WHERE e.lastName LIKE :LNAME AND e.manager.lastName <> e.lastName");
        query.setParameter("LNAME", "%");

        // Define the fields to be fetched on Employee
        FetchGroup fg = new FetchGroup();
        fg.addAttribute("firstName");
        fg.addAttribute("lastName");
        fg.addAttribute("salary");
        fg.addAttribute("gender");
        fg.addAttribute("manager.firstName");
        fg.addAttribute("manager.lastName");
        fg.addAttribute("manager.salary");
        fg.addAttribute("manager.gender");
        fg.addAttribute("manager.manager.firstName");
        fg.addAttribute("manager.manager.lastName");
        fg.addAttribute("manager.manager.salary");
        fg.addAttribute("manager.manager.gender");
        query.setHint(QueryHints.FETCH_GROUP, fg);

        List<Employee> emps = query.getResultList();

        int numSelect = getQuerySQLTracker(em).getTotalSQLSELECTCalls();

        for (Employee emp : emps) {
            assertFetched(emp, fg);
        }
        assertEquals(numSelect, getQuerySQLTracker(em).getTotalSQLSELECTCalls());
    }

    @Test
    public void dynamicHierarchicalFetchGroup_JOIN_FETCH() throws Exception {
        internalDynamicHierarchicalFetchGroup_JOIN_FETCH(false);
    }

    @Test
    public void dynamicHierarchicalFetchGroup_JOIN_FETCH_Copy() throws Exception {
        internalDynamicHierarchicalFetchGroup_JOIN_FETCH(true);
    }

    void internalDynamicHierarchicalFetchGroup_JOIN_FETCH(boolean useCopy) throws Exception {

        EntityManager em = createEntityManager();
        try {
            beginTransaction(em);

            Query query = em.createQuery("SELECT e FROM Employee e JOIN FETCH e.manager WHERE e.lastName LIKE :LNAME AND e.manager.lastName <> e.lastName");
            query.setParameter("LNAME", "%");

            // Define the fields to be fetched on Employee
            FetchGroup fg = new FetchGroup();
            fg.addAttribute("firstName");
            fg.addAttribute("lastName");
            fg.addAttribute("manager.firstName");
            fg.addAttribute("manager.salary");
            fg.addAttribute("manager.manager");
            query.setHint(QueryHints.FETCH_GROUP, fg);

            // applied to the selected Employee who is not a manager of some other selected Employee
            FetchGroup employeeFG = new EntityFetchGroup(new String[]{"id", "version", "firstName", "lastName", "manager"});
            // applied to the manager of a selected Employee who is not selected as an Employee
            FetchGroup managerFG = new EntityFetchGroup(new String[]{"id", "version", "firstName", "salary", "manager"});
            // applied to the object which is both selected as an Employee and the manager of another selected Employee
            FetchGroup employeeManagerFG = employeeDescriptor.getFetchGroupManager().flatUnionFetchGroups(employeeFG, managerFG, false);

            // used in useCopy case only
            FetchGroup employeeManagerManagerFG = null;
            if(useCopy) {
                employeeManagerManagerFG = employeeDescriptor.getFetchGroupManager().flatUnionFetchGroups(new EntityFetchGroup("manager"), employeeDescriptor.getFetchGroupManager().getNonReferenceEntityFetchGroup(), false);
            }

            /*
             * These are the first names of Employees involved; --> means "managed by".
             * All the employees here are returned by the query except Jill and Sarah-loo (they got no manager).
             *
             * Sarah ------>Bob -------> John ----> Jim-bob ---> Jill ---> null
             * Charles -----^   Marius ----^
             *
             * Nancy ------> Sarah-loo ---> null
             *
             * Sarah, Charles, Nancy should have employeeFG;
             * Sarah-loo - managerFG;
             * Bob, Marius - employeeManagerFG;
             * John, Jim-bob should have a union of three fetch groups: {firstName,lastName,manager}, {firstName,salary,manager}, {manager}
             * Jill should have a union of two groups:  {firstName,salary,manager}, {manager}
             * The result for all three of them is the same:
             *   in read case (useCopy == false) it should be null (no fetch group), because defaultFetchGroup is null;
             *   in copy case (useCopy == true) it should be a union of "manager" and all non relational attributes (NonReferenceEntityFetchGroup).
             * That's how leaf reference attribute is treated:
             *   default fetch group for read;
             *   NonReferenceEntityFetchGroup (see FetchGroupManager) for copy.
             * In this test defaultFetchGroup (null) / NonReferenceEntityFetchGroup comes from {manager},
             *   in useCopy == true case additional manager comes from another fetch group (they all contain manager).
             */

            List<Employee> emps = query.getResultList();

            if(useCopy) {
                /*for(Employee emp : emps) {
                    int idHashCode =  System.identityHashCode(emp);
                    System.out.println(emp.getFirstName() + '\t' + idHashCode);
                }*/
                emps = (List)JpaHelper.getEntityManager(em).copy(emps, fg);
            }

            // Sets of managed Employees keyed by their manager
            Map<Employee, Set<Employee>> managedEmployeesByManager = new IdentityHashMap();
            for (Employee emp : emps) {
                Employee manager = emp.getManager();
                Set<Employee> managedEmployees = managedEmployeesByManager.get(manager);
                if(managedEmployees == null) {
                    managedEmployees = new IdentityHashSet();
                    managedEmployeesByManager.put(manager, managedEmployees);
                }
                managedEmployees.add(emp);
            }

            for (Employee emp : emps) {
                Set<Employee> managedEmployees = managedEmployeesByManager.get(emp);
                Employee manager = emp.getManager();
                if(managedEmployees == null) {
                    // employee is NOT a manager of any of the selected employees:
                    assertFetched(emp, employeeFG);

                    Set<Employee> managedByManagerEmployees = managedEmployeesByManager.get(manager);
                    // indicates whether one of manager's managed employees is a manager itself
                    boolean isManagersManager = false;
                    for(Employee managedEmp : managedByManagerEmployees) {
                        if(managedEmployeesByManager.containsKey(managedEmp)) {
                            isManagersManager = true;
                            break;
                        }
                    }
                    if(isManagersManager) {
                        if(useCopy) {
                            // for at least one of the selected employees manager is manager's manager:
                            //   someSelectedEmp.getManager().getManager() == manager
                            // That means for someSelectedEmp emp is defined by {manager.manager} FetchGroup's item,
                            // which means NonReferenceEntityFetchGroup (only non-reference attributes + pk)
                            // for another employee it's just a manager - which means it should include "manager":
                            // employeeManagerManagerFG is the union of these two EntityFetchGroups.
                            assertFetched(manager, employeeManagerManagerFG);
                        } else {
                            // for at least one of the selected employees manager is manager's manager:
                            //   someSelectedEmp.getManager().getManager() == manager
                            // That means for someSelectedEmp emp is defined by {manager.manager} FetchGroup's item,
                            // which means no fetch group should be used.
                            assertNoFetchGroup(manager);
                        }
                    } else {
                        // it's not manager's manager
                        if(emps.contains(manager)) {
                            // it's a manager of one of the selected Employees, and selected itself.
                            assertFetched(manager, employeeManagerFG);
                        } else {
                            // it's a manager of one of the selected Employees, but not selected itself.
                            assertFetched(manager, managerFG);
                        }
                    }
                } else {
                    // employee is a manager of at least one of the selected employees
                    // indicates whether one of emp's managed employees is a manager itself
                    boolean isManagersManager = false;
                    for(Employee managedEmp : managedEmployees) {
                        if(managedEmployeesByManager.containsKey(managedEmp)) {
                            isManagersManager = true;
                            break;
                        }
                    }

                    if(isManagersManager) {
                        if(useCopy) {
                            // for at least one of the selected employees manager is manager's manager:
                            //   someSelectedEmp.getManager().getManager() == manager
                            // That means for someSelectedEmp emp is defined by {manager.manager} FetchGroup's item,
                            // which means NonReferenceEntityFetchGroup (only non-reference attributes + pk)
                            // for another employee it's just a manager - which means it should include "manager":
                            // employeeManagerManagerFG is the union of these two EntityFetchGroups.
                            assertFetched(emp, employeeManagerManagerFG);
                        } else {
                            // for at least one of the selected employees emp is manager's manager:
                            //   someSelectedEmp.getManager().getManager() == emp
                            // That means for someSelectedEmp emp is defined by {manager.manager} FetchGroup's item,
                            // which means no fetch group should be used.
                            assertNoFetchGroup(emp);
                        }
                    } else {
                        // it's selected employee, manager of some selected employee, but not manager's manager
                        assertFetched(emp, employeeManagerFG);
                    }

                    if(useCopy) {
                        // for at least one of the selected employees manager is manager's manager:
                        //   someSelectedEmp.getManager().getManager() == manager
                        // That means for someSelectedEmp emp is defined by {manager.manager} FetchGroup's item,
                        // which means NonReferenceEntityFetchGroup (only non-reference attributes + pk)
                        // for another employee it's just a manager - which means it should include "manager":
                        // employeeManagerManagerFG is the union of these two EntityFetchGroups.
                        assertFetched(manager, employeeManagerManagerFG);
                    } else {
                        // for at least one of the selected employees manager is manager's manager:
                        //   someSelectedEmp.getManager().getManager() == manager
                        // That means for someSelectedEmp emp.getManager() is defined by {manager.manager} FetchGroup's item,
                        // which means no fetch group should be used.
                        assertNoFetchGroup(manager);
                    }
                }
            }
        } finally {
            if (isTransactionActive(em)){
                rollbackTransaction(em);
            }
            closeEntityManager(em);
        }
    }

   @Test
   public void managerDoubleNestedFetchGroupWithJoinFetch() {
       managerNestedFetchGroupWithJoinFetch(true);
   }

   @Test
   public void managerTripleNestedFetchGroupWithJoinFetch() {
       managerNestedFetchGroupWithJoinFetch(false);
   }

   void managerNestedFetchGroupWithJoinFetch(boolean isDouble) {
        EntityManager em = createEntityManager();
        try {
            beginTransaction(em);

            Query query = em.createQuery("SELECT e FROM Employee e WHERE e.manager.manager IS NOT NULL");
            FetchGroup managerFG = new FetchGroup();
            if(isDouble) {
                // Double
                managerFG.addAttribute("manager.manager");
            } else {
                // Triple
                managerFG.addAttribute("manager.manager.manager");
            }

            query.setHint(QueryHints.FETCH_GROUP, managerFG);
            query.setHint(QueryHints.LEFT_FETCH, "e.manager");

            assertNotNull(getFetchGroup(query));
            assertSame(managerFG, getFetchGroup(query));

            List<Employee> employees = query.getResultList();

            int nSql;
            if(isDouble) {
                // In this case the number of generated sqls is unpredictable.
                // Additional sql generated for every object that
                // has been first fetched as manager.manager
                // and then is selected as an employee - getting its manger
                // performed without fetch group therefore triggering reading of the whole object
                nSql = getQuerySQLTracker(em).getTotalSQLSELECTCalls();
            } else {
                assertEquals(1, getQuerySQLTracker(em).getTotalSQLSELECTCalls());
                nSql = 1;
            }

            Employee emp = employees.get(0);
            assertFetched(emp, managerFG);

            // manager (if not null) is instantiated by the fetch group, before emp.getManager call.
            Employee manager = emp.getManager();
            assertEquals(nSql, getQuerySQLTracker(em).getTotalSQLSELECTCalls());
            assertFetched(manager, managerFG);

            // instantiates the whole object
            emp.getLastName();
            nSql++;
            assertNoFetchGroup(emp);
            assertEquals(nSql, getQuerySQLTracker(em).getTotalSQLSELECTCalls());

            assertFetched(manager, managerFG);
            // instantiates the whole object
            manager.getLastName();
            nSql++;
            assertNoFetchGroup(manager);
            assertEquals(nSql, getQuerySQLTracker(em).getTotalSQLSELECTCalls());

            nSql++;
            for (PhoneNumber phone : emp.getPhoneNumbers()) {
                assertFetched(phone, this.defaultPhoneFG);
                phone.getAreaCode();
                nSql++;
                assertNoFetchGroup(phone);
            }
            assertEquals(nSql, getQuerySQLTracker(em).getTotalSQLSELECTCalls());

            nSql++;
            for (PhoneNumber phone : manager.getPhoneNumbers()) {
                assertFetched(phone, this.defaultPhoneFG);
                phone.getAreaCode();
                nSql++;
                assertNoFetchGroup(phone);
            }
            assertEquals(nSql, getQuerySQLTracker(em).getTotalSQLSELECTCalls());
        } finally {
            if (isTransactionActive(em)){
                rollbackTransaction(em);
            }
            closeEntityManager(em);
        }
    }

   @Test
   public void allNestedFetchGroupWithJoinFetch() {
        EntityManager em = createEntityManager();
        try {
            beginTransaction(em);

            // select employees who are neither managers nor team leaders
            Query query = em.createQuery("SELECT e FROM Employee e WHERE NOT EXISTS(SELECT p.id FROM Project p WHERE p.teamLeader = e) AND NOT EXISTS(SELECT e2.id FROM Employee e2 WHERE e2.manager = e)");
            FetchGroup employeeFG = new FetchGroup("employee");
            employeeFG.addAttribute("lastName");

            employeeFG.addAttribute("address.country");
            employeeFG.addAttribute("address.city");
            query.setHint(QueryHints.LEFT_FETCH, "e.address");

            employeeFG.addAttribute("phoneNumbers");
            query.setHint(QueryHints.LEFT_FETCH, "e.phoneNumbers");

            employeeFG.addAttribute("projects.name");

            employeeFG.addAttribute("projects.teamLeader.firstName");
            //employeeFG.addAttribute("projects.teamLeader.address.street");
            //employeeFG.addAttribute("projects.teamLeader.address.postalCode");
            employeeFG.addAttribute("projects.teamLeader.phoneNumbers.owner");
            employeeFG.addAttribute("projects.teamLeader.phoneNumbers.type");
            employeeFG.addAttribute("projects.teamLeader.phoneNumbers.areaCode");
            query.setHint(QueryHints.LEFT_FETCH, "e.projects.teamLeader.phoneNumbers");

            employeeFG.addAttribute("manager.firstName");
            //employeeFG.addAttribute("manager.address.street");
            //employeeFG.addAttribute("manager.address.postalCode");
            employeeFG.addAttribute("manager.phoneNumbers.owner");
            employeeFG.addAttribute("manager.phoneNumbers.type");
            employeeFG.addAttribute("manager.phoneNumbers.areaCode");
            query.setHint(QueryHints.LEFT_FETCH, "e.manager.phoneNumbers");

            // department attribute defined with JoinFetchType.OUTER
            employeeFG.addAttribute("department.name");

            query.setHint(QueryHints.FETCH_GROUP, employeeFG);

            List<Employee> employees = query.getResultList();
            assertEquals(1, getQuerySQLTracker(em).getTotalSQLSELECTCalls());

            for(Employee emp :employees) {
                assertFetched(emp, employeeFG);

                Address address = emp.getAddress();
                if(address != null) {
                    assertFetched(address, employeeFG.getGroup("address"));
                }

                for (PhoneNumber phone : emp.getPhoneNumbers()) {
                    assertFetched(phone, defaultPhoneFG);
                }

                for (Project project : emp.getProjects()) {
                    assertFetched(project, employeeFG.getGroup("projects"));
                    Employee teamLeader = project.getTeamLeader();
                    if(teamLeader != null) {
                        assertFetched(teamLeader, employeeFG.getGroup("projects.teamLeader"));
                        for (PhoneNumber phone : teamLeader.getPhoneNumbers()) {
                            assertFetched(phone, employeeFG.getGroup("projects.teamLeader.phoneNumbers"));
                        }
                    }
                }

                Employee manager = emp.getManager();
                if(manager != null) {
                    assertFetched(manager, employeeFG.getGroup("manager"));
                    for (PhoneNumber phone : manager.getPhoneNumbers()) {
                        assertFetched(phone, employeeFG.getGroup("manager.phoneNumbers"));
                    }
                }

                Department department = emp.getDepartment();
                if(department != null) {
                    assertFetched(department, employeeFG.getGroup("department"));
                }
            }
            assertEquals(1, getQuerySQLTracker(em).getTotalSQLSELECTCalls());
        } finally {
            if (isTransactionActive(em)){
                rollbackTransaction(em);
            }
            closeEntityManager(em);
        }
    }

   @Test
   public void joinFetchDefaultFetchGroup() throws Exception {
        EntityManager em = createEntityManager();

        Query query = em.createQuery("SELECT e FROM Employee e");
        query.setHint(QueryHints.LEFT_FETCH, "e.phoneNumbers");

        List<Employee> employees = query.getResultList();

        for(Employee emp : employees) {
            assertNoFetchGroup(emp);
            for (PhoneNumber phone : emp.getPhoneNumbers()) {
                assertFetched(phone, defaultPhoneFG);
            }
        }
    }

   @Test
   public void joinFetchOutsideOfFetchGroup() throws Exception {
        EntityManager em = createEntityManager();

        Query query = em.createQuery("SELECT e FROM Employee e");
        // Define the fields to be fetched on Employee
        FetchGroup fg = new FetchGroup();
        fg.addAttribute("firstName");
        fg.addAttribute("lastName");
        query.setHint(QueryHints.FETCH_GROUP, fg);
        query.setHint(QueryHints.LEFT_FETCH, "e.address");

        List<Employee> employees = query.getResultList();
    }

   @Test
   public void simpleNestedFetchGroupWithBatch() {
       EntityManager em = createEntityManager();
        try {
            beginTransaction(em);

            Query query = em.createQuery("SELECT e FROM Employee e");

            // Define the fields to be fetched on Employee
            FetchGroup employeeFG = new FetchGroup();
            employeeFG.setShouldLoad(true);
            employeeFG.addAttribute("firstName");
            employeeFG.addAttribute("lastName");
            employeeFG.addAttribute("address.country");
            employeeFG.addAttribute("address.city");

            FetchGroup phonesFG = defaultPhoneFG.clone();
            // to preclude PhoneNumber from triggering owner's full read
            phonesFG.addAttribute("owner.id");
            employeeFG.addAttribute("phoneNumbers", phonesFG);

            FetchGroup projectsFG = new FetchGroup("projects");
            projectsFG.addAttribute("name");
            projectsFG.addAttribute("name");
            // to preclude Project from triggering full read of the referenced Employee(s)
            projectsFG.addAttribute("teamMembers.id");
            projectsFG.addAttribute("teamLeader.id");
            employeeFG.addAttribute("projects", projectsFG);

            query.setHint(QueryHints.FETCH_GROUP, employeeFG);

            query.setHint(QueryHints.BATCH, "e.address");
            query.setHint(QueryHints.BATCH, "e.phoneNumbers");
            query.setHint(QueryHints.BATCH, "e.projects");

            // A single sql will be used to read all Project subclasses.
            query.setHint(QueryHints.INHERITANCE_OUTER_JOIN, "true");

            List<Employee> employees = query.getResultList();

            // Employee, Address, PhoneNumbers, Projects - an sql per class.
            // Address, PhoneNumbers and Projects are already loaded because
            // employeeFG.shouldLoad is set to true.
            assertEquals(4, getQuerySQLTracker(em).getTotalSQLSELECTCalls());

            // verify fetch groups
            for(Employee emp : employees) {
                assertFetched(emp, employeeFG);

                Address address = emp.getAddress();
                if(address != null) {
                         assertFetched(address, employeeFG.getGroup("address"));
                }

                for (PhoneNumber phone : emp.getPhoneNumbers()) {
                         assertFetched(phone, phonesFG);
                }

                for (Project project : emp.getProjects()) {
                         assertFetched(project, projectsFG);
                }
            }

            // Now let's access an attribute outside of the fetch group.
            // That triggers loading of the whole object.
            for(Employee emp : employees) {
                emp.getSalary();
                assertNoFetchGroup(emp);

                Address address = emp.getAddress();
                if(address != null) {
                   address.getStreet();
                   assertNoFetchGroup(address);
                }

                for (PhoneNumber phone : emp.getPhoneNumbers()) {
                    phone.getAreaCode();
                    assertNoFetchGroup(phone);
                }

                for (Project project : emp.getProjects()) {
                    project.getDescription();
                    assertNoFetchGroup(project);
                }
            }
        } finally {
            if (isTransactionActive(em)){
                rollbackTransaction(em);
            }
            closeEntityManager(em);
        }
   }

   @Test
    public void simpleLoadGroup() {
        EntityManager em = createEntityManager();
        try {
            beginTransaction(em);

             Query query = em.createQuery("SELECT e FROM Employee e WHERE e.gender = :GENDER");
             query.setParameter("GENDER", Gender.Female);
             List<Employee> employees = query.getResultList();

             LoadGroup group = new LoadGroup();
             group.addAttribute("address");
             group.addAttribute("phoneNumbers");
             group.addAttribute("manager.projects");
             AbstractSession session = (AbstractSession)((EntityManagerImpl)em.getDelegate()).getActiveSession();
             session.load(employees, group, session.getClassDescriptor(Employee.class), false);

             int numSelectBefore = getQuerySQLTracker(em).getTotalSQLSELECTCalls();

             // All indirections specified in the plan should have been already triggered.
             for(Employee emp : employees) {
                 emp.getAddress();
                 emp.getPhoneNumbers().size();
                 if(emp.getManager() != null) {
                     emp.getManager().getProjects().size();
                 }
             }

             int numSelectAfter = getQuerySQLTracker(em).getTotalSQLSELECTCalls();
             assertEquals(numSelectBefore, numSelectAfter);
        } finally {
            if (isTransactionActive(em)){
                rollbackTransaction(em);
            }
            closeEntityManager(em);
        }
    }

   @Test
   // Bug 325684 - QueryHints.BATCH combined with QueryHints.FETCH_GROUP_LOAD will cause NPE
    public void simpleFetchGroupLoadWithBatch() {
        EntityManager em = createEntityManager();
        try {
            beginTransaction(em);

             FetchGroup projectGroup = new FetchGroup();
             projectGroup.addAttribute("name");

             FetchGroup employeeGroup = new FetchGroup();
             employeeGroup.addAttribute("firstName");
             employeeGroup.addAttribute("lastName");
             employeeGroup.addAttribute("projects", projectGroup);

             Query query = em.createQuery("SELECT e FROM Employee e WHERE e.gender = :GENDER");
             query.setParameter("GENDER", Gender.Female);
             query.setHint(QueryHints.FETCH_GROUP, employeeGroup);
             query.setHint(QueryHints.FETCH_GROUP_LOAD, "true");
             query.setHint(QueryHints.BATCH, "e.projects");
             List<Employee> employees = query.getResultList();

             int numSelectBefore = getQuerySQLTracker(em).getTotalSQLSELECTCalls();

             // All indirections specified in the plan should have been already triggered.
             for (Employee e : employees) {
                 e.getProjects().size();
             }

             int numSelectAfter = getQuerySQLTracker(em).getTotalSQLSELECTCalls();
             assertEquals(numSelectBefore, numSelectAfter);
        } finally {
            if (isTransactionActive(em)){
                rollbackTransaction(em);
            }
            closeEntityManager(em);
        }
    }
}
