/*
 * TestFetchPlan.java
 *
 * Created on October 16, 2006, 3:02 PM
 *
 * To change this template, choose Tools | Template Manager
 * and open the template in the editor.
 */
/*
 * 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.kernel;

import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.apache.openjpa.persistence.FetchPlan;
import org.apache.openjpa.persistence.OpenJPAEntityManager;
import org.apache.openjpa.persistence.OpenJPAQuery;
import org.apache.openjpa.persistence.kernel.common.apps.PCAddress;
import org.apache.openjpa.persistence.kernel.common.apps.PCCompany;
import org.apache.openjpa.persistence.kernel.common.apps.PCCountry;
import org.apache.openjpa.persistence.kernel.common.apps.PCDepartment;
import org.apache.openjpa.persistence.kernel.common.apps.PCDirectory;
import org.apache.openjpa.persistence.kernel.common.apps.PCEmployee;
import org.apache.openjpa.persistence.kernel.common.apps.PCFile;
import org.apache.openjpa.persistence.kernel.common.apps.PCPerson;

public class TestFetchPlan extends BaseKernelTest {

    static Object _rootDirId;
    static Object _rootCompanyId;

    static final int MAX_DEPTH = 5; // Maximum depth of the directories
    static final int MAX_CHILD = 3; // Maximum number of files/directory
    static final String quote = "\"";
    private static boolean firstTime = true;

    /**
     * Creates a new instance of TestFetchPlan
     */
    public TestFetchPlan() {
    }

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

    /**
     * Clears past data and creates new data for test.
     * Clear test data before and not <em>after</em> such that one can analyze
     * the database for test failures.
     */
    @Override
    public void setUp() throws Exception {
        if (firstTime) {
            firstTime = false;
            clearTestData();
            createTestData();
        }
    }

    /**
     * Create a directory tree of MAX_DEPTH with each directory having a single
     * directory and MAX_CHILD files.
     * Creates typical Employee-Department-Company-Address instances.
     *
     * @return the persitent identifier of the root directory.
     */
    void createTestData() {
        // create a tree of directories with files in them
        PCDirectory rootDir = new PCDirectory(getDirectoryName(0));
        PCDirectory parent = rootDir;
        for (int i = 1; i <= MAX_DEPTH; i++) {
            PCDirectory dir = new PCDirectory(getDirectoryName(i));
            parent.add(dir);

            for (int j = 0; j < MAX_CHILD; j++)
                parent.add(getFileName(j));

            parent = dir;
        }

        // create a graph
        //      | ---address-country
        //      |
        //  company-dept-employee-address-country
        //
        PCCountry country1 = new PCCountry("100", "Employee 1 Country");
        PCCountry country2 = new PCCountry("200", "Employee 2 Country");
        PCCountry ccountry = new PCCountry("300", "Company Country");

        PCCompany company = new PCCompany("Company");

        PCDepartment dept1 = new PCDepartment("Department1");
        PCDepartment dept2 = new PCDepartment("Department2");

        PCEmployee emp1 = new PCEmployee("Employee1");
        PCEmployee emp2 = new PCEmployee("Employee2");

        PCAddress addr1 = new PCAddress("Street1", "city1", country1);
        PCAddress addr2 = new PCAddress("Street2", "city2", country2);
        PCAddress caddr = new PCAddress("Street3", "city3", ccountry);

        dept1.addEmployee(emp1);
        dept2.addEmployee(emp2);

        company.addDepartment(dept1);
        company.addDepartment(dept2);

        company.setAddress(caddr);

        emp1.setAddress(addr1);
        emp2.setAddress(addr2);

        OpenJPAEntityManager pm = getPM();
        startTx(pm);
        pm.persist(rootDir);
        pm.persist(company);
//        _rootDirId     = pm.getObjectId(rootDir);
        _rootDirId = rootDir.getId();
        assertNotNull(_rootDirId);
//        _rootCompanyId = pm.getObjectId(company);
        _rootCompanyId = company.getId();
        assertNotNull(_rootCompanyId);
        endTx(pm);
        endEm(pm);
    }

    /**
     * Test that the single valued field (_parent) is not traversed when the
     * fecth group selects only the _name field.
     */
    public void testZeroRecursionDepthSingleValuedField() {
        genericTestForSingleValuedRecursiveField("name", 4, 0);
    }

    /**
     * Test that the single valued field (_parent) is traversed once and only
     * once when the fecth group selects the _parent field with recursion depth
     * of 1 (default).
     */
    public void testOneRecursionDepthSingleValuedField() {
        genericTestForSingleValuedRecursiveField("name+parent", 4, 1);
    }

    /**
     * Test that the single valued field (_parent) is traversed twice and only
     * twice when the fecth group selects the _parent field with recursion depth
     * of 2.
     */
    public void testTwoRecursionDepthSingleValuedField() {
        genericTestForSingleValuedRecursiveField("name+parent+grandparent",
            4, 2);
    }

    public void testThreeRecursionDepthSingleValuedField() {
        genericTestForSingleValuedRecursiveField
            ("name+parent+grandparent+greatgrandparent", 4, 3);
    }

    public void testInfiniteRecursionDepthSingleValuedField() {
        genericTestForSingleValuedRecursiveField("allparents", 4, -1);
    }

    /**
     * Generically tests recursive traversal of single-valued parent field.
     *
     * @param plan a plan that fetches L parents and no children
     * @param rd the recursion depth of directory from the root
     * @param fd the fetch depth = number of parents fetched
     */
    public void genericTestForSingleValuedRecursiveField(String plan, int rd,
        int fd) {
        PCDirectory result = queryDirectoryWithPlan(plan, rd, fd);

        checkParents(result, rd, fd);

        Object children = PCDirectory.reflect(result, "_children");
        assertNull(children);
    }

    /**
     * Query to obtain a single directory at the given depth.
     * The directory name is constructed by the depth it occurs (d0 for root,
     * d1 for depth 1 and so on).<BR>
     * Checks the result for for matching name and size of the result (must
     * be one).
     *
     * @param plan name of a fetch plan
     * @param depth depth of the directory to be queried
     * @return the selected directory.
     */
    PCDirectory queryDirectoryWithPlan(String plan, int rd, int fd) {
        OpenJPAEntityManager pm = getPM();
        pm.getFetchPlan().addFetchGroup(plan);
        if (fd != 0)
            pm.getFetchPlan().setMaxFetchDepth(fd);

//        String filter = "_name == " + quoted(getDirectoryName(rd));
//        OpenJPAQuery query = pm.createNativeQuery(filter,PCDirectory.class);
//        List result = (List) query.getResultList();

        String query = "SELECT o FROM PCDirectory o WHERE o._name = '" +
            getDirectoryName(rd) + "'";
        List fresult = ((OpenJPAQuery) pm.createQuery(query)).getResultList();

        assertEquals(1, fresult.size());
        PCDirectory dir = (PCDirectory) fresult.get(0);

        return dir;
    }

    /**
     * Asserts that
     * <LI> the given directory name matches the directory name at depth D.
     * <LI> the parents upto L recursion is not null and beyond is
     * null.
     *
     * @param result a directory to test
     * @param D depth at which this directory appears
     * @param L the number of live (fetched) parents. -1 denotes infinite
     */
    void checkParents(PCDirectory result, int D, int L) {

        assertEquals("ge", getDirectoryName(D),
            PCDirectory.reflect(result, "_name"));
        PCDirectory[] parents = getParents(result, D);
        int N = (L == -1) ? D : L;
        for (int i = 0; i < N; i++) {
            assertNotNull(i + "-th parent at depth " + D + " is null",
                parents[i]);
            assertEquals(getDirectoryName(D - i - 1),
                PCDirectory.reflect(parents[i], "_name"));
        }
        for (int i = N; i < D; i++)
            assertNull(i + "-th parent at depth " + D + " is not null " +
                parents[i], parents[i]);
    }

    /**
     * Gets an array of parents of the given directory. The zeroth element
     * is the parent of the given directory and (i+1)-th element is the
     * parent of the i-th element. Uses reflection to ensure that the
     * side-effect does not cause a database access for the field.
     *
     * @param dir a starting directory
     * @param depth depth to recurse. must be positive.
     */
    PCDirectory[] getParents(PCDirectory dir, int depth) {
        PCDirectory[] result = new PCDirectory[depth];
        PCDirectory current = dir;
        for (int i = 0; i < depth; i++) {
            result[i] = (PCDirectory) PCDirectory.reflect(current, "_parent");
            current = result[i];
        }
        return result;
    }

    /**
     * Checks that the first L elements of the given array is non-null and
     * the rest are null.
     *
     * @param depth
     */
    void assertNullParent(PCDirectory[] parents, int L) {
        for (int i = 0; i < L; i++)
            assertNotNull(parents[i]);
        for (int i = L; i < parents.length; i++)
            assertNull(parents[i]);
    }

    String getDirectoryName(int depth) {
        return "d" + depth;
    }

    String getFileName(int depth) {
        return "f" + depth;
    }

    String quoted(String s) {
        return quote + s + quote;
    }

    /**
     * Defines a fetch plan that has several fetch groups to traverse a chain
     * of relationships.
     * After getting the root by an extent query, checks (by reflection) that
     * all the relations in the chain are fetched.
     * The fetch depth is kept infinite, so what would be fetched is essentially
     * controlled by the fetch groups.
     */
    public void testRelationTraversal() {
        OpenJPAEntityManager pm = getPM();
        FetchPlan plan = pm.getFetchPlan();
        pm.getFetchPlan().setMaxFetchDepth(-1);
        plan.addFetchGroup("employee.department");
        plan.addFetchGroup("department.company");
        plan.addFetchGroup("company.address");
        plan.addFetchGroup("address.country");

        Iterator employees = pm.createExtent(PCEmployee.class, true).iterator();
        while (employees.hasNext()) {
            PCEmployee emp = (PCEmployee) employees.next();

            PCDepartment dept = (PCDepartment) PCEmployee.reflect(emp,
                "department");
            assertNotNull(dept);

            PCCompany company = (PCCompany) PCDepartment.reflect(dept,
                "company");
            assertNotNull(company);

            PCAddress addr = (PCAddress) PCCompany.reflect(company, "address");
            assertNotNull(addr);

            PCCountry country = (PCCountry) PCAddress.reflect(addr, "country");
            assertNotNull(country);
        }
    }

    /**
     * Defines a fetch plan that has several fetch groups to traverse a chain
     * of relationships but truncated at the last relation.
     * After getting the root by an extent query, checks (by reflection) that
     * all but the last relation in the chain are fetched.
     * The fetch depth is kept infinite, so what would be fetched is essentially
     * controlled by the fetch groups.
     */
    public void testRelationTraversalTruncated() {
        OpenJPAEntityManager pm = getPM();
        FetchPlan plan = pm.getFetchPlan();
        pm.getFetchPlan().setMaxFetchDepth(-1);
        plan.addFetchGroup("employee.department");
        plan.addFetchGroup("department.company");
        plan.addFetchGroup("company.address");

        Iterator employees = pm.createExtent(PCEmployee.class, true).iterator();
        while (employees.hasNext()) {
            PCEmployee emp = (PCEmployee) employees.next();

            PCDepartment dept = (PCDepartment) PCEmployee.reflect(emp,
                "department");
            assertNotNull(dept);

            PCCompany company = (PCCompany) PCDepartment.reflect(dept,
                "company");
            assertNotNull(company);

            PCAddress addr = (PCAddress) PCCompany.reflect(company, "address");
            assertNotNull(addr);

            PCCountry country = (PCCountry) PCAddress.reflect(addr, "country");
            assertNull(country);
        }
    }

    /**
     * Gets a Compnay object by getObjectById() method as opposed to query.
     * The active fetch groups should bring in the multi-valued relationships.
     * The address->country relationship can be reached in two alternate
     * paths -- one as company->address->country and the other is
     * company->department->employee->address->country.
     * Though active fetch groups allow both the paths -- the max fetch depth
     * is set such that the shorter path is taken but not the longer one.
     * Hence the company's address->country should be loaded but not the
     * employee's.
     */
    public void testRelationTraversalWithCompanyAsRoot() {
        OpenJPAEntityManager pm = getPM();
        FetchPlan plan = pm.getFetchPlan();

        plan.setMaxFetchDepth(2);
        plan.addFetchGroup("company.departments");
        plan.addFetchGroup("company.address");
        plan.addFetchGroup("department.employees");
        plan.addFetchGroup("person.address");
        plan.addFetchGroup("address.country");

        PCCompany company =
            (PCCompany) pm.find(PCCompany.class, _rootCompanyId);
        Set departments = (Set) PCCompany.reflect(company, "departments");
        assertNotNull("department is null", departments);
        assertEquals("exp. depart size is not 2", 2, departments.size());
        PCDepartment dept = (PCDepartment) departments.iterator().next();
        assertNotNull("dept is null", dept);
        Set employees = (Set) PCDepartment.reflect(dept, "employees");
        assertNotNull("employees is null", employees);
        assertEquals(1, employees.size());
        PCEmployee emp = (PCEmployee) employees.iterator().next();
        assertNotNull("emp is not null", emp);
        PCAddress eaddr = (PCAddress) PCPerson.reflect(emp, "address");
        PCAddress caddr = (PCAddress) PCCompany.reflect(company, "address");
        assertNull("eaddr is not null", eaddr);
        assertNotNull("caddr is null", caddr);
        PCCountry country = (PCCountry) PCAddress.reflect(caddr, "country");
        assertNotNull("country is null", country);
    }

    /**
     * Same as above but the root compnay instance is detached.
     */
    public void testDetachedRelationTraversalWithCompanyAsRoot() {
        OpenJPAEntityManager pm = getPM();
        FetchPlan plan = pm.getFetchPlan();
        pm.getFetchPlan().setMaxFetchDepth(2);
        plan.addFetchGroup("company.departments");
        plan.addFetchGroup("company.address");
        plan.addFetchGroup("department.employees");
        plan.addFetchGroup("person.address");
        plan.addFetchGroup("address.country");

        PCCompany company1 =
            (PCCompany) pm.find(PCCompany.class, _rootCompanyId);

        PCCompany company = (PCCompany) pm.detachCopy(company1);
        assertTrue("company is equal company1", company != company1);
        Set departments = (Set) PCCompany.reflect(company, "departments");
        assertNotNull("department is null", departments);
        assertEquals("department size is not 2", 2, departments.size());
        PCDepartment dept = (PCDepartment) departments.iterator().next();
        assertNotNull("dept is null", dept);
        Set employees = (Set) PCDepartment.reflect(dept, "employees");
        assertNotNull("employee is null", employees);
        assertEquals("employees size not 1", 1, employees.size());
        PCEmployee emp = (PCEmployee) employees.iterator().next();
        assertNotNull("emp is null", emp);
        PCAddress eaddr = (PCAddress) PCPerson.reflect(emp, "address");
        PCAddress caddr = (PCAddress) PCCompany.reflect(company, "address");
        assertNull("eaddr is not null", eaddr);
        assertNotNull("caddr is null", caddr);
        PCCountry country = (PCCountry) PCAddress.reflect(caddr, "country");
        assertNotNull("country is null", country);
    }

    public void testDefaultFetchGroup() {
        OpenJPAEntityManager pm = getPM();

        String squery =
            "SELECT DISTINCT o FROM PCEmployee o WHERE o.name = 'Employee1'";
        OpenJPAQuery q = pm.createQuery(squery);

        //FIXME jthomas
        PCEmployee person = (PCEmployee) q.getSingleResult();
        assertEquals("Exp. String is not employee1", "Employee1",
            PCPerson.reflect(person, "name"));
    }

    public void testDefaultFetchGroupExistsByDefault() {
        OpenJPAEntityManager pm = getPM();
        assertTrue("pm does not contain default fetchplan",
            pm.getFetchPlan().getFetchGroups().contains(
                FetchPlan.GROUP_DEFAULT));
    }

    public void testDefaultFetchGroupCanBeRemoved() {
        OpenJPAEntityManager pm = getPM();
        assertTrue("does not contain default fetchplan",
            pm.getFetchPlan().getFetchGroups().contains(
                FetchPlan.GROUP_DEFAULT));

        pm.getFetchPlan().removeFetchGroup(FetchPlan.GROUP_DEFAULT);
        assertFalse("does contain default fetchplan",
            pm.getFetchPlan().getFetchGroups().contains(
                FetchPlan.GROUP_DEFAULT));

        OpenJPAEntityManager pm2 = getPM();
        assertTrue("pm2 does not contain default fetchplan",
            pm2.getFetchPlan().getFetchGroups().contains(
                FetchPlan.GROUP_DEFAULT));
    }

    @Override
    public void tearDown() throws Exception {
        super.tearDown();
    }

    private void clearTestData() throws Exception {
//        OpenJPAEntityManagerFactory pmf =
//            (OpenJPAEntityManagerFactory) getEmf();
//        OpenJPAConfiguration conf=pmf.getConfiguration();
//
//        Class.forName(pmf.getConfiguration().getConnection2DriverName());
//        String url=conf.getConnection2URL();
//        String user=conf.getConnection2UserName();
//        String pass=conf.getConnection2Password();
//
//        Connection con = DriverManager.getConnection(
//                url,
//                user,
//                pass);
//        con.setAutoCommit(true);
//        con.prepareStatement("DELETE FROM PCDIRECTORY").executeUpdate();
//        con.prepareStatement("DELETE FROM PCFILE").executeUpdate();
//        con.prepareStatement("DELETE FROM PCPERSON").executeUpdate();
//        con.prepareStatement("DELETE FROM PCDEPARTMENT").executeUpdate();
//        con.prepareStatement("DELETE FROM PCCOMPANY").executeUpdate();
//        con.prepareStatement("DELETE FROM PCADDRESS").executeUpdate();
//        con.prepareStatement("DELETE FROM PCCOUNTRY").executeUpdate();

        deleteAll(PCDirectory.class);
        deleteAll(PCFile.class);
        deleteAll(PCPerson.class);
        deleteAll(PCDepartment.class);
        deleteAll(PCCompany.class);
        deleteAll(PCAddress.class);
        deleteAll(PCCountry.class);
        deleteAll(PCEmployee.class);
    }
}
