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

import java.beans.ExceptionListener;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.beans.XMLDecoder;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

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

import junit.framework.AssertionFailedError;

/**
 * Generic test case that will be extended by a concrete company
 * model subclass.
 *
 * @author  Marc Prud'hommeaux
 */
public abstract class CompanyModelTest
    extends SingleEMTestCase {

    private static Map<Class,Class> factoryClasses;
    private Map<Class,Class> impls;

    @Override
    public void setUp() {
        // make a map of the implementations based on the class names in
        // the current package of the test subclass
        impls = new HashMap<>();
        impls.put(IAddress.class, localClass("Address"));
        impls.put(ICompany.class, localClass("Company"));
        impls.put(ICustomer.class, localClass("Customer"));
        impls.put(IPerson.class, localClass("Person"));
        impls.put(IEmployee.class, localClass("Employee"));
        impls.put(IFullTimeEmployee.class, localClass("FullTimeEmployee"));
        impls.put(ILineItem.class, localClass("LineItem"));
        impls.put(IProductOrder.class, localClass("ProductOrder"));
        impls.put(IPartTimeEmployee.class, localClass("PartTimeEmployee"));
        impls.put(IProduct.class, localClass("Product"));

        super.setUp(impls.values().toArray(new Class[impls.size()]));
        checkModel();
    }

    @Override
    public void tearDown() throws Exception {
        impls.clear();
        impls = null;
        factoryClasses = null;
        super.tearDown();
    }

    private Class localClass(String name) {
        String pkg = getClass().getPackage().getName();
        try {
            return Class.forName(pkg + "." + name);
        } catch (Exception e) {
            throw new IllegalStateException(e);
        }
    }

    /**
     * Runs through basic queries against all of the properties of all
     * of the known persistent classes. We're just checking here to
     * make sure the queries can be executed without problem. Queries
     * should always return all known instances in the database.
     */
    public void testBasicQueries() throws Exception {
        for (Class c : impls.values()) {
            for (PropertyDescriptor pd :
                Introspector.getBeanInfo(c).getPropertyDescriptors()) {

                if (pd.getWriteMethod() == null) // ignore read-only
                    continue;

                Set<String> queries = new TreeSet<>();
                getBasicQueries(queries, pd, "x.");

                StringBuilder str = new StringBuilder();

                // execute the individual queries
                for (String query : queries) {
                    find(c, "where " + query);
                    str.append(str.length() > 0 ? " or " : "").append(query);
                }

                // now execute all the queries combined
                find(c, "where " + str);
            }
        }
    }

    void getBasicQueries(Set<String> queries, PropertyDescriptor pd,
        String prefix) throws Exception {

        // max level of field traversal: 3
        // ### if (prefix.split("\\.").length > 3)
        if (prefix.split("\\.").length > 2)
            return;

        Class type = pd.getPropertyType();

        String name = prefix + pd.getName();

        if (!queries.add(name + " is not null"))
            return;

        queries.add(name + " is null");

        if (type.isAssignableFrom(Number.class) || type == int.class ||
            type == double.class || type == float.class ||
            type == long.class || type == short.class) {
            queries.add(name + " = 0");
            queries.add(name + " <> 0");
            queries.add(name + " > 0");
            queries.add(name + " < 0");
            queries.add(name + " >= 0");
            queries.add(name + " <= 0");
            queries.add("sqrt(" + name + ") <> 0");
            queries.add("abs(" + name + ") <> 0");
            // queries.add("mod(" + name + ", 5) <> 0");
        }

        if (type.isAssignableFrom(Collection.class)) {
            queries.add(name + " IS EMPTY");
            queries.add(name + " IS NOT EMPTY");
            queries.add("size(" + name + ") <> 0");
        }

        if (type.isAssignableFrom(String.class)) {
            queries.add("lower(" + name + ") = 'x'");
            queries.add("upper(" + name + ") = 'x'");
            queries.add("concat(" + name + ", " + name + ") = 'x'");
            queries.add("substring(" + name + ", 1, 2) = 'x'");
            queries.add("length(" + name + ") > 0");
            queries.add("locate(" + name + ", 'x', 1) > 0");
            queries.add("trim(leading ' ' from " + name + ") = 'x'");
        }

        if (type.isAssignableFrom(Date.class)) {
            queries.add(name + " <> CURRENT_TIMESTAMP");
        }

        // relation is an entity ... add all the relations
        if (impls.containsKey(type) || impls.containsValue(type)) {
            for (PropertyDescriptor desc :
                Introspector.getBeanInfo(type).getPropertyDescriptors()) {

                if (desc.getWriteMethod() == null) // ignore read-only
                    continue;

                // prevent recursion
                if (name.endsWith("." + desc.getName() + "."))
                    continue;

                getBasicQueries(queries, desc, name + ".");
            }
        }
    }

    void checkModel() {
        try {
            verifyModel();
        } catch (AssertionFailedError e) {
            // clear all existing instances
            clear(emf, impls.values().toArray(new Class[impls.size()]));

            // since the factory method needs to be static, we need to store
            // the classes statically
            factoryClasses = impls;
            try {
                final List<Exception> exceptions = new LinkedList<>();
                XMLDecoder decoder = new XMLDecoder(CompanyModelTest.class.
                    getResourceAsStream("companies.xml"));
                decoder.setExceptionListener(new ExceptionListener() {
                    @Override
                    public void exceptionThrown(Exception e) {
                        exceptions.add(e);
                    }
                });
                Collection obs = (Collection) decoder.readObject();

                if (exceptions.size() > 0) {
                    throw new IllegalStateException(exceptions.get(0));
                }

                assertNotNull(obs);

                persist(obs.toArray());
                decoder.close();
            } finally {
                factoryClasses = null;
            }
        }

        verifyModel();
    }

    int queryCount(Class c, String query, Object... params) {
        return find(c, query, params).size();
    }

    int queryCount(Class c) {
        return find(c, null).size();
    }

    Class impl(Class c) {
        return impls.get(c);
    }

    void verifyModel() {
        assertEquals(2, queryCount(impl(ICompany.class)));
        assertEquals(11, queryCount(impl(IAddress.class)));
        assertEquals(3, queryCount(impl(IProduct.class)));
        assertEquals(2, queryCount(impl(IProductOrder.class)));
        assertEquals(3, queryCount(impl(ILineItem.class)));
        assertEquals(1, queryCount(impl(IPartTimeEmployee.class)));
        assertEquals(3, queryCount(impl(IFullTimeEmployee.class)));
        assertEquals(4, queryCount(impl(ICustomer.class)));

        assertEquals(3, queryCount(impl(IAddress.class),
            "where x.state = 'CA'"));

        assertEquals(1, queryCount(impl(ICompany.class),
            "where size(x.employees) = 4"));
        assertEquals(1, queryCount(impl(ICompany.class),
            "where size(x.employees) = 0"));

        assertEquals(2, queryCount(impl(ICustomer.class),
            "where size(x.orders) = 1"));

        assertEquals(1, queryCount(impl(IProductOrder.class),
            "where x.shippedDate is null"));
        assertEquals(1, queryCount(impl(IProductOrder.class),
            "where x.shippedDate is not null"));

        assertEquals(1, queryCount(impl(IEmployee.class),
            "where x.manager is null"));
        assertEquals(2, queryCount(impl(IEmployee.class),
            "where x.manager.manager is null"));
        assertEquals(1, queryCount(impl(IEmployee.class),
            "where x.manager.manager.manager is null"));

        assertEquals(2, queryCount(impl(IPerson.class),
            "where x.firstName like ?1 and x.lastName like ?1", "M%"));
        assertEquals(1, queryCount(impl(IPerson.class),
            "where x.homeAddress.state = 'CA'"));
    }

    /**
     * Factory method that is called from the serialized XML.
     */
    public static Object create(Class intf)
        throws InstantiationException, IllegalAccessException {
        return factoryClasses.get(intf).newInstance();
    }
}

