/*
 * Copyright (c) 1998, 2021 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:
//     ailitchev - Uni-directional OneToMany
package org.eclipse.persistence.testing.tests.unidirectional;

import java.util.ArrayList;
import java.util.List;

import org.eclipse.persistence.expressions.Expression;
import org.eclipse.persistence.expressions.ExpressionBuilder;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.queries.Call;
import org.eclipse.persistence.queries.ReadAllQuery;
import org.eclipse.persistence.queries.ReadObjectQuery;
import org.eclipse.persistence.queries.SQLCall;
import org.eclipse.persistence.sessions.UnitOfWork;
import org.eclipse.persistence.tools.schemaframework.PopulationManager;
import org.eclipse.persistence.testing.framework.*;
import org.eclipse.persistence.testing.models.unidirectional.*;
import org.eclipse.persistence.testing.tests.writing.ComplexUpdateTest;

/**
 * This model tests reading/writing/deleting through using the employee demo.
 */
public class UnidirectionalEmployeeBasicTestModel extends TestModel {

    /**
     * Return the JUnit suite to allow JUnit runner to find it.
     * Unfortunately JUnit only allows suite methods to be static,
     * so it is not possible to generically do this.
     */
    public static junit.framework.TestSuite suite() {
        return new UnidirectionalEmployeeBasicTestModel();
    }

    public UnidirectionalEmployeeBasicTestModel() {
        setDescription("This model tests reading/writing/deleting using the unidirectionl employee demo.");
    }

    @Override
    public void addRequiredSystems() {
        addRequiredSystem(new EmployeeSystem());
    }

    @Override
    public void addTests() {
        addTest(getReadObjectTestSuite());
        addTest(getComplexUpdateObjectTestSuite());
        addTest(getInsertObjectTestSuite());
        addTest(getDeleteObjectTestSuite());
        addTest(getReadAllTestSuite());
    }

    public static TestSuite getDeleteObjectTestSuite() {
        TestSuite suite = new TestSuite();
        suite.setName("EmployeeDeleteObjectTestSuite");
        suite.setDescription("This suite tests the deletion of each object in the employee demo.");

        Class<Employee> employeeClass = org.eclipse.persistence.testing.models.unidirectional.Employee.class;
        PopulationManager manager = PopulationManager.getDefaultManager();

        suite.addTest(new DeleteObjectTest(manager.getObject(employeeClass, "0001")));
        suite.addTest(new DeleteObjectTest(manager.getObject(employeeClass, "0002")));
        suite.addTest(new DeleteObjectTest(manager.getObject(employeeClass, "0003")));
        suite.addTest(new DeleteObjectTest(manager.getObject(employeeClass, "0004")));
        suite.addTest(new DeleteObjectTest(manager.getObject(employeeClass, "0005")));

        suite.addTest(new TargetLockingTest_DeleteSource(true));
        suite.addTest(new TargetLockingTest_DeleteSource(false));

        return suite;
    }

    public static TestSuite getInsertObjectTestSuite() {
        TestSuite suite = new TestSuite();
        suite.setName("EmployeeUOWBasicInsertObjectTestSuite");
        suite.setDescription("This suite tests the insertion of each object in the employee demo using uow.");
        EmployeePopulator system = new EmployeePopulator();

        suite.addTest(new UnitOfWorkBasicInsertObjectTest(system.basicEmployeeExample1()));
        suite.addTest(new UnitOfWorkBasicInsertObjectTest(system.basicEmployeeExample2()));
        suite.addTest(new UnitOfWorkBasicInsertObjectTest(system.basicEmployeeExample3()));
        suite.addTest(new UnitOfWorkBasicInsertObjectTest(system.basicEmployeeExample4()));
        suite.addTest(new UnitOfWorkBasicInsertObjectTest(system.basicEmployeeExample5()));

        return suite;
    }

    public static TestSuite getReadAllTestSuite() {
        TestSuite suite = new TestSuite();
        suite.setName("EmployeeReadAllTestSuite");
        suite.setDescription("This suite tests the reading of all the objects of each class in the employee demo.");

        suite.addTest(new ReadAllTest(Employee.class, 12));

        suite.addTest(new ReadAllCallTest(Employee.class, 12, new SQLCall("SELECT VERSION, EMP_ID, L_NAME, F_NAME FROM UNIDIR_EMPLOYEE")));

        //Add new tests here ...
        Expression orderBy = new ExpressionBuilder().get("firstName").ascending();
        Call call = new SQLCall("SELECT VERSION, EMP_ID, L_NAME, F_NAME FROM UNIDIR_EMPLOYEE");
        suite.addTest(new ReadAllCallWithOrderingTest(Employee.class, 12, call, orderBy));

        suite.addTest(new JoinTest());
        suite.addTest(new JoinTest_SelectByFirstName());
        suite.addTest(new BatchReadingTest());
        suite.addTest(new BatchReadingTest_SelectByFirstName());

        return suite;
    }

    public static TestSuite getReadObjectTestSuite() {
        TestSuite suite = new TestSuite();
        suite.setName("EmployeeReadObjectTestSuite");
        suite.setDescription("This suite test the reading of each object in the employee demo.");

        Class<Employee> employeeClass = Employee.class;
        PopulationManager manager = PopulationManager.getDefaultManager();

        suite.addTest(new ReadObjectTest(manager.getObject(employeeClass, "0001")));
        suite.addTest(new ReadObjectTest(manager.getObject(employeeClass, "0002")));
        suite.addTest(new ReadObjectTest(manager.getObject(employeeClass, "0003")));
        suite.addTest(new ReadObjectTest(manager.getObject(employeeClass, "0004")));
        suite.addTest(new ReadObjectTest(manager.getObject(employeeClass, "0005")));

        Employee employee = (Employee)manager.getObject(employeeClass, "0001");
        suite.addTest(new ReadObjectCallTest(employeeClass, new SQLCall("SELECT VERSION, EMP_ID, L_NAME, F_NAME FROM UNIDIR_EMPLOYEE WHERE F_NAME = '"+employee.getFirstName()+"' AND L_NAME = '"+employee.getLastName()+"'")));
        employee = (Employee)manager.getObject(employeeClass, "0002");
        suite.addTest(new ReadObjectCallTest(employeeClass, new SQLCall("SELECT VERSION, EMP_ID, L_NAME, F_NAME FROM UNIDIR_EMPLOYEE WHERE F_NAME = '"+employee.getFirstName()+"' AND L_NAME = '"+employee.getLastName()+"'")));
        employee = (Employee)manager.getObject(employeeClass, "0003");
        suite.addTest(new ReadObjectCallTest(employeeClass, new SQLCall("SELECT VERSION, EMP_ID, L_NAME, F_NAME FROM UNIDIR_EMPLOYEE WHERE F_NAME = '"+employee.getFirstName()+"' AND L_NAME = '"+employee.getLastName()+"'")));

        return suite;
    }

    public static TestSuite getComplexUpdateObjectTestSuite() {
        TestSuite suite = new TestSuite();
        suite.setName("EmployeeComplexUpdateTestSuite");
        suite.setDescription("This suite tests the updating of each an employee by adding and/or removing managed employees and/or phones.");

        Class<Employee> employeeClass = Employee.class;
        PopulationManager manager = PopulationManager.getDefaultManager();
        Employee originalEmployee = (Employee)manager.getObject(employeeClass, "0001");
        Employee otherEmployee = (Employee)manager.getObject(employeeClass, "0002");

        // add a managed Employee from other Employee managed List; remove the first managed Employee.
        suite.addTest(new EmployeeComplexUpdateTest(originalEmployee, otherEmployee.getManagedEmployees().get(0), originalEmployee.getManagedEmployees().get(0)));
        // remove the first Phone.
        suite.addTest(new EmployeeComplexUpdateTest(originalEmployee, (Object)null, originalEmployee.getPhoneNumbers().get(0)));
        // add a managed Employee from other Employee managed List and new phone;
        // remove the first two managed Employees and the first Phone.
        Employee newEmployee = new Employee();
        newEmployee.setFirstName("New");
        PhoneNumber newPhoneNumber = new PhoneNumber("home", "001", "0000001");
        suite.addTest(new EmployeeComplexUpdateTest(originalEmployee,
                new Object[]{otherEmployee.getManagedEmployees().get(0), newEmployee, newPhoneNumber},
                new Object[]{originalEmployee.getManagedEmployees().get(0), originalEmployee.getManagedEmployees().get(1), originalEmployee.getPhoneNumbers().get(0)}));
        suite.addTest(new CascadeLockingTest());
        suite.addTest(new TargetLockingTest_AddRemoveTarget());

        return suite;
    }

    /**
     * Tests adding/removing of target object to/from the source for UnidirectionalOneToManyMapping.
     * Derived from ComplexUpdateTest, this test expects an instance of Employee as original object,
     * as well as object (or array, or list) of objects to be added to the original objects as the second parameter,
     * as well as object (or array, or list) of objects to be removed from the original objects as the third parameter.
     * These objects should be of type either Employee or PhoneNumber.
     */
    static class EmployeeComplexUpdateTest extends ComplexUpdateTest {
        List<Employee> managedEmployeesToAdd = new ArrayList<Employee>();
        List<Employee> managedEmployeesToRemove = new ArrayList<Employee>();
        List<PhoneNumber> phonesToAdd = new ArrayList<PhoneNumber>();
        List<PhoneNumber> phonesToRemove = new ArrayList<PhoneNumber>();
        public EmployeeComplexUpdateTest(Employee originalEmployee, List objectsToAdd, Object objectToRemove) {
            this(originalEmployee, (objectsToAdd != null ? objectsToAdd.toArray() : null), (objectToRemove != null ? new Object[]{objectToRemove} : null));
        }
        public EmployeeComplexUpdateTest(Employee originalEmployee, Object objectToAdd, List objectsToRemove) {
            this(originalEmployee, (objectToAdd != null ? new Object[]{objectToAdd} : null), (objectsToRemove != null ? objectsToRemove.toArray() : null));
        }
        public EmployeeComplexUpdateTest(Employee originalEmployee, List objectsToAdd, List objectsToRemove) {
            this(originalEmployee, (objectsToAdd != null ? objectsToAdd.toArray() : null), (objectsToRemove != null ? objectsToRemove.toArray() : null));
        }
        public EmployeeComplexUpdateTest(Employee originalEmployee, Object objectToAdd, Object objectToRemove) {
            this(originalEmployee, (objectToAdd != null ? new Object[]{objectToAdd} : null), (objectToRemove != null ? new Object[]{objectToRemove} : null));
        }
        public EmployeeComplexUpdateTest(Employee originalEmployee, Object[] objectsToAdd, Object objectToRemove) {
            this(originalEmployee, objectsToAdd, (objectToRemove != null ? new Object[]{objectToRemove} : null));
        }
        public EmployeeComplexUpdateTest(Employee originalEmployee, Object objectToAdd, Object[] objectsToRemove) {
            this(originalEmployee, (objectToAdd != null ? new Object[]{objectToAdd} : null), objectsToRemove);
        }
        public EmployeeComplexUpdateTest(Employee originalEmployee, Object[] objectsToAdd, Object[] objectsToRemove) {
            super(originalEmployee);
            this.usesUnitOfWork = true;
            if(objectsToAdd != null) {
                for(int i=0; i < objectsToAdd.length; i++) {
                    Object objectToAdd = objectsToAdd[i];
                    if(objectToAdd instanceof Employee) {
                        if(!originalEmployee.getManagedEmployees().contains(objectToAdd)) {
                            managedEmployeesToAdd.add((Employee)objectToAdd);
                        } else {
                            throw new TestWarningException("OriginalEmployee: " + originalEmployee + " already manages employee to be added: " + objectToAdd);
                        }
                    } else {
                        // must be Phone
                        if(!originalEmployee.getPhoneNumbers().contains(objectToAdd)) {
                            phonesToAdd.add((PhoneNumber)objectToAdd);
                        } else {
                            throw new TestWarningException("OriginalEmployee: " + originalEmployee + " already has the phonee to be added: " + objectToAdd);
                        }
                    }
                }
            }
            if(objectsToRemove != null) {
                for(int i=0; i < objectsToRemove.length; i++) {
                    Object objectToRemove = objectsToRemove[i];
                    if(objectToRemove instanceof Employee) {
                        if(originalEmployee.getManagedEmployees().contains(objectToRemove)) {
                            managedEmployeesToRemove.add((Employee)objectToRemove);
                        } else {
                            throw new TestWarningException("OriginalEmployee: " + originalEmployee + " doesn't manage employee to be removed: " + objectToRemove);
                        }
                    } else {
                        // must be Phone
                        if(originalEmployee.getPhoneNumbers().contains(objectToRemove)) {
                            phonesToRemove.add((PhoneNumber)objectToRemove);
                        } else {
                            throw new TestWarningException("OriginalEmployee: " + originalEmployee + " doesn't have the phonee to be removed: " + objectToRemove);
                        }
                    }
                }
            }
            // generate a meaningful test name
            String employeeString = "";
            if(managedEmployeesToAdd.size() > 0 || managedEmployeesToRemove.size() > 0 ) {
                String addString = "";
                if(managedEmployeesToAdd.size() > 0) {
                    addString = "add "+ managedEmployeesToAdd.size();
                }
                String removeString = "";
                if(managedEmployeesToRemove.size() > 0) {
                    removeString = "remove "+ managedEmployeesToRemove.size();
                }
                employeeString = addString +(addString.length()>0 && removeString.length()>0 ? " and " : " ") + removeString + " Employees";
            }
            String phoneString = "";
            if(phonesToAdd.size() > 0 || phonesToRemove.size() > 0 ) {
                String addString = "";
                if(phonesToAdd.size() > 0) {
                    addString = "add "+ phonesToAdd.size();
                }
                String removeString = "";
                if(phonesToRemove.size() > 0) {
                    removeString = "remove "+ phonesToRemove.size();
                }
                phoneString = addString +(addString.length()>0 && removeString.length()>0 ? " and " : "") + removeString + " Phones";
            }
            setName("EmployeeComplexUpdateTest: " + employeeString +(employeeString.length()>0 && phoneString.length()>0 ? "; " : "")+ phoneString+";");
            setDescription("The test updates original Employee object: " +originalObject.toString()+ " from the database by adding and/or removing managedEmployees and/or PhoneNumbers and verifies that the object updated correctly.");
        }
        @Override
        public String getName() {
            String testName = super.getName();
            int lastIndex = testName.lastIndexOf(";");
            if(lastIndex > 0) {
                testName = testName.substring(0, lastIndex);
            }
            return testName;
        }
        @Override
        protected void changeObject() {
            UnitOfWork uow = (UnitOfWork)getSession();
            Employee cloneEmployee = (Employee)workingCopy;
            for(int i=0; i < managedEmployeesToAdd.size(); i++) {
                Employee cloneEmployeeToAdd = (Employee)uow.registerObject(managedEmployeesToAdd.get(i));
                cloneEmployee.addManagedEmployee(cloneEmployeeToAdd);
            }
            for(int i=0; i < managedEmployeesToRemove.size(); i++) {
                Employee cloneEmployeeToRemove = (Employee)uow.registerObject(managedEmployeesToRemove.get(i));
                cloneEmployee.removeManagedEmployee(cloneEmployeeToRemove);
            }
            for(int i=0; i < phonesToRemove.size(); i++) {
                PhoneNumber clonePhoneToRemove = (PhoneNumber)uow.registerObject(phonesToRemove.get(i));
                cloneEmployee.removePhoneNumber(clonePhoneToRemove);
            }
            for(int i=0; i < phonesToAdd.size(); i++) {
                PhoneNumber clonePhoneToAdd = (PhoneNumber)uow.registerObject(phonesToAdd.get(i));
                cloneEmployee.addPhoneNumber(clonePhoneToAdd);
            }
        }
        @Override
        protected void setup() {
            super.setup();

            for(int i=0; i < managedEmployeesToAdd.size(); i++) {
                Employee readEmployeeToAdd = (Employee)readObject(managedEmployeesToAdd.get(i));
                if(readEmployeeToAdd != null) {
                    managedEmployeesToAdd.set(i, readEmployeeToAdd);
                } else {
                    // it's a new object
                }
            }
            for(int i=0; i < managedEmployeesToRemove.size(); i++) {
                Employee readEmployeeToRemove = (Employee)readObject(managedEmployeesToRemove.get(i));
                if(readEmployeeToRemove != null) {
                    managedEmployeesToRemove.set(i, readEmployeeToRemove);
                } else {
                    throw new TestWarningException("Employee to be removed: " + managedEmployeesToRemove.get(i) + " is not in the db");
                }
            }
            for(int i=0; i < phonesToRemove.size(); i++) {
                PhoneNumber readPhoneToRemove = (PhoneNumber)readObject(phonesToRemove.get(i));
                if(readPhoneToRemove != null) {
                    phonesToRemove.set(i, readPhoneToRemove);
                } else {
                    throw new TestWarningException("Phone to be removed: " + phonesToRemove.get(i) + " is not in the db");
                }
            }
            for(int i=0; i < phonesToAdd.size(); i++) {
                PhoneNumber readPhoneToAdd = (PhoneNumber)readObject(phonesToAdd.get(i));
                if(readPhoneToAdd != null) {
                    phonesToAdd.set(i, readPhoneToAdd);
                } else {
                    // it's a new object
                }
            }
        }
        protected Object readObject(Object object) {
            ReadObjectQuery query = new ReadObjectQuery();
            query.setSelectionObject(object);
            return getSession().executeQuery(query);
        }
    }

    /**
     * Tests cascade locking of the source Employee object for adding, modifying and removing the target object (Employee and Phone).
     * The version of he source should be always incremented on either adding or removing any target.
     * Also currently source's version incremented on changing of the privately owned target (Phone),
     * but on change of non private target (managed Employee) stays unchanged.
     */
    static class CascadeLockingTest extends TransactionalTestCase {
        long version[] = new long[7];
        long versionExpected[] = new long[] {1, 2, 2, 3, 4, 5, 6};
        public CascadeLockingTest() {
            super();
            setName("CascadeLockingPolicyTest");
            setDescription("Tests optimistic lock cascading for UnidirectionalOneToManyMapping");
        }
        @Override
        public void setup() {
            super.setup();
            for(int i=0; i<version.length; i++) {
                version[i] = 0;
            }
        }
        @Override
        public void test() {
            // setup
            Employee manager = new Employee();
            manager.setFirstName("Manager");
            Employee employee = new Employee();
            employee.setFirstName("Employee");

            UnitOfWork uow = getSession().acquireUnitOfWork();
            uow.registerObject(manager);
            uow.registerObject(employee);
            uow.commit();

            version[0] = getSession().getDescriptor(Employee.class).getOptimisticLockingPolicy().getWriteLockValue(manager, manager.getId(), getAbstractSession());

            // test1 - add managed employee, manager's version should increment.
            uow = getSession().acquireUnitOfWork();
            Employee managerClone = (Employee)uow.registerObject(manager);
            Employee employeeClone = (Employee)uow.registerObject(employee);
            managerClone.addManagedEmployee(employeeClone);
            uow.commit();
            version[1] = getSession().getDescriptor(Employee.class).getOptimisticLockingPolicy().getWriteLockValue(manager, manager.getId(), getAbstractSession());

            // test2 - alter managed employee, manager's version should NOT increment.
            uow = getSession().acquireUnitOfWork();
            employeeClone = (Employee)uow.registerObject(employee);
            employeeClone.setFirstName("Altered");
            uow.commit();
            version[2] = getSession().getDescriptor(Employee.class).getOptimisticLockingPolicy().getWriteLockValue(manager, manager.getId(), getAbstractSession());

            // test3- remove managed employee, manager's version should increment.
            uow = getSession().acquireUnitOfWork();
            managerClone = (Employee)uow.registerObject(manager);
            employeeClone = (Employee)uow.registerObject(employee);
            managerClone.removeManagedEmployee(employeeClone);
            uow.commit();
            version[3] = getSession().getDescriptor(Employee.class).getOptimisticLockingPolicy().getWriteLockValue(manager, manager.getId(), getAbstractSession());

            PhoneNumber phone = new PhoneNumber("home", "613", "1111111");

            // test4 - add phone, manager's version should increment.
            uow = getSession().acquireUnitOfWork();
            managerClone = (Employee)uow.registerObject(manager);
            PhoneNumber phoneClone = (PhoneNumber)uow.registerObject(phone);
            managerClone.addPhoneNumber(phoneClone);
            uow.commit();
            version[4] = getSession().getDescriptor(Employee.class).getOptimisticLockingPolicy().getWriteLockValue(manager, manager.getId(), getAbstractSession());

            // test5- alter phone, manager's version should increment.
            uow = getSession().acquireUnitOfWork();
            phoneClone = (PhoneNumber)uow.registerObject(phone);
            phoneClone.setType("work");
            uow.commit();
            version[5] = getSession().getDescriptor(Employee.class).getOptimisticLockingPolicy().getWriteLockValue(manager, manager.getId(), getAbstractSession());

            // test6- remove phone, manager's version should increment.
            uow = getSession().acquireUnitOfWork();
            managerClone = (Employee)uow.registerObject(manager);
            phoneClone = (PhoneNumber)uow.registerObject(phone);
            managerClone.removePhoneNumber(phoneClone);
            uow.commit();
            version[6] = getSession().getDescriptor(Employee.class).getOptimisticLockingPolicy().getWriteLockValue(manager, manager.getId(), getAbstractSession());
        }
        @Override
        public void verify() {
            int numTestsFailed = 0;
            String errorMsg = "";
            for(int i=0; i<version.length; i++) {
                if(version[i] + numTestsFailed != versionExpected[i]) {
                    numTestsFailed++;
                    errorMsg += "test" + i +" failed; ";
                }
            }
            if(numTestsFailed > 0) {
                throw new TestErrorException(errorMsg);
            }
        }
    }
    static class BatchReadingTest extends TestCase {
        boolean shouldPrintDebugOutput = false;
        public BatchReadingTest() {
            setName("EmployeeBatchReadingTest - no selection criteria");
            setDescription("Tests batch reading of Employees with batch expression managedEmployees.phoneNumbers");
        }
        void setSelectionCriteria(ReadAllQuery query) {
        }
        @Override
        public void test() {
            // clear cache
            getSession().getIdentityMapAccessor().initializeAllIdentityMaps();
            // create batch read query, set its selectionCriteria
            ReadAllQuery query = new ReadAllQuery(Employee.class);
            setSelectionCriteria(query);
            // before adding batch read attributes clone the query to create control query
            ReadAllQuery controlQuery = (ReadAllQuery)query.clone();
            // add batch read attributes
            Expression managedEmployees = query.getExpressionBuilder().get("managedEmployees");
            Expression managedEmployeesPhoneNumbers = managedEmployees.get("phoneNumbers");
            query.addBatchReadAttribute(managedEmployeesPhoneNumbers);
            // execute the query
            List employees = (List)getSession().executeQuery(query);
            if(employees.isEmpty()) {
                throw new TestProblemException("No Employees were read");
            }
            // need to instantiate only a single Phone on a single managed Employee to trigger sql that reads data from the db for all.
            // still need to trigger all the indirections - but (except the first one) they are not accessing the db
            // (the data is already cached in the value holders).
            printDebug("Trigger batch reading results");
            boolean isConnected = true;
            for(int i=0; i < employees.size(); i++) {
                Employee manager = (Employee)employees.get(i);
                if(!manager.getManagedEmployees().isEmpty()) {
                    printDebug("Manager = " + manager);
                    for(int j=0; j < manager.getManagedEmployees().size(); j++) {
                        Employee emp = (Employee)manager.getManagedEmployees().get(j);
                        printDebug("     " + emp);
                        for(int k = 0; k < emp.getPhoneNumbers().size(); k++) {
                            if(isConnected) {
                                // need to instantiate only a single Phone on a single managed Employee to trigger sql that reads data from the db for all.
                                // to ensure that no other sql is issued close connection.
                                ((AbstractSession)getSession()).getAccessor().closeConnection();
                                isConnected = false;
                            }
                            PhoneNumber phone = (PhoneNumber)emp.getPhoneNumbers().get(k);
                            printDebug("          " + phone);
                        }
                    }
                } else {
                    printDebug(manager.toString());
                }
            }
            if(!isConnected) {
                // reconnect connection
                ((AbstractSession)getSession()).getAccessor().reestablishConnection((AbstractSession)getSession());
            }
            printDebug("");

            // obtain control results
            // clear cache
            getSession().getIdentityMapAccessor().initializeAllIdentityMaps();
            // execute control query
            List controlEmployees = (List)getSession().executeQuery(controlQuery);
            // instantiate all value holders that the batch query expected to instantiate
            printDebug("Trigger control results");
            for(int i=0; i < controlEmployees.size(); i++) {
                Employee manager = (Employee)controlEmployees.get(i);
                if(!manager.getManagedEmployees().isEmpty()) {
                    printDebug("Manager = " + manager);
                    for(int j=0; j < manager.getManagedEmployees().size(); j++) {
                        Employee emp = (Employee)manager.getManagedEmployees().get(j);
                        printDebug("     " + emp);
                        for(int k = 0; k < emp.getPhoneNumbers().size(); k++) {
                            PhoneNumber phone = (PhoneNumber)emp.getPhoneNumbers().get(k);
                            printDebug("          " + phone);
                        }
                    }
                } else {
                    printDebug(manager.toString());
                }
            }

            // compare results
            String errorMsg = JoinedAttributeTestHelper.compareCollections(employees, controlEmployees, getSession().getClassDescriptor(Employee.class), ((AbstractSession)getSession()));
            if(errorMsg.length() > 0) {
                throw new TestErrorException(errorMsg);
            }
        }
        void printDebug(String msg) {
            if(shouldPrintDebugOutput) {
                System.out.println(msg);
            }
        }
    }
    static class BatchReadingTest_SelectByFirstName extends BatchReadingTest {
        public BatchReadingTest_SelectByFirstName() {
            super();
            setName("EmployeeBatchReadingTest - select by first name");
        }
        @Override
        void setSelectionCriteria(ReadAllQuery query) {
            query.setSelectionCriteria(query.getExpressionBuilder().get("firstName").like("J%"));
        }
    }
    static class JoinTest extends TestCase {
        public JoinTest() {
            super();
            setName("JoinTest - no selection criteria");
            setDescription("Tests reading of Employees with join expressions anyOf(managedEmployees) and anyOf(managedEmployees).anyOf(phoneNumbers)");
        }
        void setSelectionCriteria(ReadAllQuery query) {
        }
        @Override
        public void test() {
            ReadAllQuery query = new ReadAllQuery();
            query.setReferenceClass(Employee.class);
            setSelectionCriteria(query);

            ReadAllQuery controlQuery = (ReadAllQuery)query.clone();

            Expression employees = query.getExpressionBuilder().anyOf("managedEmployees");
            query.addJoinedAttribute(employees);
            Expression phones = employees.anyOf("phoneNumbers");
            query.addJoinedAttribute(phones);

            String errorMsg = JoinedAttributeTestHelper.executeQueriesAndCompareResults(controlQuery, query, (AbstractSession)getSession());
            if(errorMsg.length() > 0) {
                throw new TestErrorException(errorMsg);
            }
        }
    }
    static class JoinTest_SelectByFirstName extends JoinTest {
        public JoinTest_SelectByFirstName() {
            super();
            setName("JoinTest - select by first name");
        }
        @Override
        void setSelectionCriteria(ReadAllQuery query) {
            query.setSelectionCriteria(query.getExpressionBuilder().get("firstName").like("J%"));
        }
    }

    /**
     * Base class for TargetLockingTest_AddRemoveTarget and TargetLockingTest_DeleteSource.
     * and mapping's shouldIncrementTargetLockValueOnAddOrRemoveTarget flag is set to true (default setting)
     * adding/removing target to/from source causes target's version to increment.
     */
    static class TargetLockingTest extends TestCase {
        Employee employee[];

        public TargetLockingTest() {
            super();
        }
        long getVersion(Employee emp) {
            return getSession().getDescriptor(Employee.class).getOptimisticLockingPolicy().getWriteLockValue(emp, emp.getId(), getAbstractSession());
        }
        @Override
        public void reset() {
            UnitOfWork uow = getSession().acquireUnitOfWork();
            for(int i=0; i<employee.length; i++) {
                if(employee[i] != null) {
                    uow.deleteObject(employee[i]);
                }
            }
            uow.commit();
        }
    }
    /**
     * If target descriptor of UnidirectionalOneToMany mapping has optimistic locking policy,
     * and mapping's shouldIncrementTargetLockValueOnAddOrRemoveTarget flag is set to true (default setting)
     * adding/removing target to/from source causes target's version to increment.
     */
    static class TargetLockingTest_AddRemoveTarget extends TargetLockingTest {

        public TargetLockingTest_AddRemoveTarget() {
            super();
            setName("TargetLockingTest_AddRemoveTarget");
            setDescription("Tests target optimistic locking for UnidirectionalOneToManyMapping when targets are added to and removed from the source.");
        }
        @Override
        public void setup() {
            // create 5 Employees.
            employee = new Employee[5];

            employee[0] = new Employee();
            employee[0].setFirstName("Manager");

            // 1 and 2 have manager 0.
            employee[1] = new Employee();
            employee[1].setFirstName("Employee_1");
            employee[0].addManagedEmployee(employee[1]);

            employee[2] = new Employee();
            employee[2].setFirstName("Employee_2");
            employee[0].addManagedEmployee(employee[2]);

            // 3 and 4 don't have a manager.
            employee[3] = new Employee();
            employee[3].setFirstName("Employee_3");

            employee[4] = new Employee();
            employee[4].setFirstName("Employee_4");

            // insert all the Employees into the db.
            UnitOfWork uow = getSession().acquireUnitOfWork();
            for(int i=0; i<employee.length; i++) {
                uow.registerObject(employee[i]);
            }
            uow.commit();
        }
        @Override
        public void test() {
            UnitOfWork uow = getSession().acquireUnitOfWork();
            Employee managerClone = (Employee)uow.registerObject(employee[0]);
            // remove all managed Employees (1 and 2)
            managerClone.getManagedEmployees().clear();
            // add to managed list new Employees (3 and 4)
            Employee employee3Clone = (Employee)uow.registerObject(employee[3]);
            Employee employee4Clone = (Employee)uow.registerObject(employee[4]);
            managerClone.addManagedEmployee(employee3Clone);
            managerClone.addManagedEmployee(employee4Clone);
            // after commit the  versions of all Employees should be changed.
            uow.commit();
        }
        @Override
        public void verify() {
            long version[] = new long[employee.length];
            String errorMsg = "";
            for(int i=0; i<employee.length; i++) {
                version[i] = getVersion(employee[i]);
                if(version[i] != 2) {
                    errorMsg += "in the cache version["+i+"] = "+version[i]+" (2 was expected); ";
                }
            }
            // make sure that versions in the db are correct, too.
            for(int i=0; i<employee.length; i++) {
                employee[i] = (Employee)getSession().refreshObject(employee[i]);
                version[i] = getVersion(employee[i]);
                if(version[i] != 2) {
                    errorMsg += "in the db version["+i+"] = "+version[i]+" (2 was expected); ";
                }
            }
            if(errorMsg.length() > 0) {
                throw new TestErrorException(errorMsg);
            }
        }
    }
    /**
     * If target descriptor of UnidirectionalOneToMany mapping has optimistic locking policy,
     * and mapping's shouldIncrementTargetLockValueOnDeleteSource flag is set to true (default setting)
     * the deleting the source source causes all targets' versions to increment.
     * Note that in this case the indirection is triggered to make sure that the proper targets' versions used.
     */
    static class TargetLockingTest_DeleteSource extends TargetLockingTest {
        boolean isIndirectionTriggered;

        public TargetLockingTest_DeleteSource(boolean isIndirectionTriggered) {
            super();
            this.isIndirectionTriggered = isIndirectionTriggered;
            setName("TargetLockingTest_DeleteSource");
            if(isIndirectionTriggered) {
                setName(getName() + "_IndirectionTriggered");
            } else {
                setName(getName() + "_IndirectionNotTriggered");
            }
            setDescription("Tests target optimistic locking for UnidirectionalOneToManyMapping when the source is deleted.");
        }
        @Override
        public void setup() {
            // create 3 Employees.
            employee = new Employee[3];

            employee[0] = new Employee();
            employee[0].setFirstName("Manager");

            // 1 and 2 have manager 0.
            employee[1] = new Employee();
            employee[1].setFirstName("Employee_1");
            employee[0].addManagedEmployee(employee[1]);

            employee[2] = new Employee();
            employee[2].setFirstName("Employee_2");
            employee[0].addManagedEmployee(employee[2]);

            // insert all the Employees into the db.
            UnitOfWork uow = getSession().acquireUnitOfWork();
            for(int i=0; i<employee.length; i++) {
                uow.registerObject(employee[i]);
            }
            uow.commit();

            getSession().getIdentityMapAccessor().initializeAllIdentityMaps();
        }
        @Override
        public void test() {
            employee[0] = (Employee)getSession().readObject(employee[0]);
            if(isIndirectionTriggered) {
                // that triggers indirection.
                employee[0].getManagedEmployees().size();
            }

            UnitOfWork uow = getSession().acquireUnitOfWork();
            Employee managerClone = (Employee)uow.deleteObject(employee[0]);
            // after commit 0 is deleted and the versions of 1 and 2 should be changed.
            uow.commit();
            // set the deleted Employee to null so that reset method won't attempt to delete it again.
            employee[0] = null;
        }
        @Override
        public void verify() {
            long version[] = new long[employee.length];
            String errorMsg = "";
            for(int i=1; i<employee.length; i++) {
                version[i] = getVersion(employee[i]);
                if(version[i] != 2) {
                    errorMsg += "in the cache version["+i+"] = "+version[i]+" (2 was expected); ";
                }
            }
            // make sure that versions in the db are correct, too.
            for(int i=1; i<employee.length; i++) {
                employee[i] = (Employee)getSession().refreshObject(employee[i]);
                version[i] = getVersion(employee[i]);
                if(version[i] != 2) {
                    errorMsg += "in the db version["+i+"] = "+version[i]+" (2 was expected); ";
                }
            }
            if(errorMsg.length() > 0) {
                throw new TestErrorException(errorMsg);
            }
        }
    }
}
