/*
 * The Kuali Financial System, a comprehensive financial management system for higher education.
 *
 * Copyright 2005-2016 The Kuali Foundation
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.kuali.kfs.sys;

import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.lang.StringUtils;
import org.kuali.kfs.krad.bo.PersistableBusinessObject;
import org.kuali.kfs.krad.service.BusinessObjectService;
import org.kuali.kfs.krad.service.PersistenceService;
import org.kuali.kfs.sys.context.SpringContext;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;

/**
 * provide with a set of utilities that can be used to prepare test data for unit testing. The core idea is to convert Java
 * properties into a list of specified business objects or search criteria.
 */
public class TestDataPreparator {
    public static final String DEFAULT_FIELD_NAMES = "fieldNames";
    public static final String DEFAULT_DELIMINATOR = "deliminator";

    /**
     * load properties from the given class path resource. The class path is different than the absolute path. If a resource is
     * located at /project/test/org/kuali/kfs/util/message.properties, then its class path is org/kuali/kfs/util/message.properties,
     * which is the fully-qualified Java package name plus the resource name.
     *
     * @param classPath the given class path of a resource
     * @return properties loaded from the given resource.
     */
    public static Properties loadPropertiesFromClassPath(String classPath) {
        Properties properties = new Properties();
        try {
            properties.load(TestDataPreparator.class.getClassLoader().getResourceAsStream(classPath));
        } catch (IOException e) {
            throw new RuntimeException("Invalid class path: " + classPath + e);
        }

        return properties;
    }

    /**
     * build a list of objects of type "clazz" from the test data provided by the given properties. The default fieldNames and
     * deliminator are used.
     *
     * @param clazz             the the specified object type
     * @param properties        the given properties that contain the test data
     * @param propertyKeyPrefix the test data with the given key prefix can be used to construct the return objects
     * @param numberOfData      the number of test data matching the search criteria
     * @return a list of objects of type "clazz" from the test data provided by the given properties
     */
    public static <T> List<T> buildTestDataList(Class<? extends T> clazz, Properties properties, String propertyKeyPrefix, int numberOfData) {
        String fieldNames = properties.getProperty(DEFAULT_FIELD_NAMES);
        String deliminator = properties.getProperty(DEFAULT_DELIMINATOR);
        return buildTestDataList(clazz, properties, propertyKeyPrefix, fieldNames, deliminator, numberOfData);
    }

    /**
     * build a list of objects of type "clazz" from the test data provided by the given properties
     *
     * @param clazz             the the specified object type
     * @param properties        the given properties that contain the test data
     * @param propertyKeyPrefix the test data with the given key prefix can be used to construct the return objects
     * @param fieldNames        the field names of the test data columns
     * @param deliminator       the deliminator that is used to separate the field from each other
     * @param numberOfData      the number of test data matching the search criteria
     * @return a list of objects of type "clazz" from the test data provided by the given properties
     */
    public static <T> List<T> buildTestDataList(Class<? extends T> clazz, Properties properties, String propertyKeyPrefix, String fieldNames, String deliminator, int numberOfData) {
        List<T> testDataList = new ArrayList<T>();
        for (int i = 1; i <= numberOfData; i++) {
            String propertyKey = propertyKeyPrefix + i;
            T testData = buildTestDataObject(clazz, properties, propertyKey, fieldNames, deliminator);
            testDataList.add(testData);
        }
        return testDataList;
    }

    /**
     * build an object of type "clazz" from the test data provided by the given properties
     *
     * @param clazz       the the specified object type
     * @param properties  the given properties that contain the test data
     * @param propertyKey the test data with the given key
     * @param fieldNames  the field names of the test data columns
     * @param deliminator the deliminator that is used to separate the field from each other
     * @return an object of type "clazz" from the test data provided by the given properties
     */
    public static <T> T buildTestDataObject(Class<? extends T> clazz, Properties properties, String propertyKey, String fieldNames, String deliminator) {
        T testData = null;
        try {
            testData = clazz.newInstance();
            ObjectUtil.populateBusinessObject(testData, properties, propertyKey, fieldNames, deliminator);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return testData;
    }

    /**
     * build an object of type "clazz" from the test data provided by the given properties
     *
     * @param clazz       the the specified object type
     * @param properties  the given properties that contain the test data
     * @param propertyKey the test data with the given key
     * @return an object of type "clazz" from the test data provided by the given properties
     */
    public static <T> T buildTestDataObject(Class<? extends T> clazz, Properties properties, String propertyKey) {
        String fieldNames = properties.getProperty(DEFAULT_FIELD_NAMES);
        String deliminator = properties.getProperty(DEFAULT_DELIMINATOR);

        T testData = null;
        try {
            testData = clazz.newInstance();
            ObjectUtil.populateBusinessObject(testData, properties, propertyKey, fieldNames, deliminator);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return testData;
    }

    /**
     * build a list of objects of type "clazz" from the expected results provided by the given properties. The default fieldNames
     * and deliminator are used.
     *
     * @param clazz             the the specified object type. The instance of this type should be comparable through overriding Object.equals()
     * @param properties        the given properties that contain the expected results
     * @param propertyKeyPrefix the expected results with the given key prefix can be used to construct the return objects
     * @param numberOfData      the number of the expected results matching the search criteria
     * @return a list of objects of type "clazz" from the expected results provided by the given properties
     */
    public static <T> List<T> buildExpectedValueList(Class<? extends T> clazz, Properties properties, String propertyKeyPrefix, int numberOfData) {
        String fieldNames = properties.getProperty(DEFAULT_FIELD_NAMES);
        String deliminator = properties.getProperty(DEFAULT_DELIMINATOR);
        return buildExpectedValueList(clazz, properties, propertyKeyPrefix, fieldNames, deliminator, numberOfData);
    }

    /**
     * build a list of objects of type "clazz" from the expected results provided by the given properties
     *
     * @param clazz             the the specified object type. The instance of this type should be comparable through overriding Object.equals()
     * @param properties        the given properties that contain the expected results
     * @param propertyKeyPrefix the expected results with the given key prefix can be used to construct the return objects
     * @param fieldNames        the field names of the expected results columns
     * @param deliminator       the deliminator that is used to separate the field from each other
     * @param numberOfData      the number of the expected results matching the search criteria
     * @return a list of objects of type "clazz" from the expected results provided by the given properties
     */
    public static <T> List<T> buildExpectedValueList(Class<? extends T> clazz, Properties properties, String propertyKeyPrefix, String fieldNames, String deliminator, int numberOfData) {
        List<T> expectedDataList = new ArrayList<T>();
        for (int i = 1; i <= numberOfData; i++) {
            String propertyKey = propertyKeyPrefix + i;
            try {
                T expectedData = TestDataPreparator.buildTestDataObject(clazz, properties, propertyKey, fieldNames, deliminator);

                if (!expectedDataList.contains(expectedData)) {
                    expectedDataList.add(expectedData);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return expectedDataList;
    }

    /**
     * build the cleanup criteria for "clazz" from the given properties. The default fieldNames and deliminator are used.
     *
     * @param clazz       the the specified object type.
     * @param properties  the given properties that contain the cleanup criteria fields and values
     * @param propertyKey the given property whose value provides the cleanup criteria values
     * @return the cleanup criteria for "clazz" from the given properties
     */
    public static <T> Map<String, Object> buildCleanupCriteria(Class<? extends T> clazz, Properties properties, String propertyKey) {
        String fieldNames = properties.getProperty(DEFAULT_FIELD_NAMES);
        String deliminator = properties.getProperty(DEFAULT_DELIMINATOR);
        return buildCleanupCriteria(clazz, properties, propertyKey, fieldNames, deliminator);
    }

    /**
     * build the cleanup criteria for "clazz" from the given properties.
     *
     * @param clazz       the the specified object type.
     * @param properties  the given properties that contain the cleanup criteria fields and values
     * @param propertyKey the given property whose value provides the cleanup criteria values
     * @param fieldNames  the field names of the cleanup columns
     * @param deliminator the deliminator that is used to separate the field from each other
     * @return the cleanup criteria for "clazz" from the given properties
     */
    public static <T> Map<String, Object> buildCleanupCriteria(Class<? extends T> clazz, Properties properties, String propertyKey, String fieldNames, String deliminator) {
        T instanceOfClazz = TestDataPreparator.buildTestDataObject(clazz, properties, propertyKey, fieldNames, deliminator);
        return ObjectUtil.buildPropertyMap(instanceOfClazz, Arrays.asList(StringUtils.split(fieldNames, deliminator)));
    }

    /**
     * persist the given data object if it is not in the persistent store
     *
     * @param dataObject the given data object
     * @return return the data object persisted into the data store
     */
    public static <T extends PersistableBusinessObject> T persistDataObject(T dataObject) {
        T existingDataObject = (T) getBusinessObjectService().retrieve(dataObject);
        if (existingDataObject == null) {
            getBusinessObjectService().save(dataObject);
            getPersistenceService().retrieveNonKeyFields(dataObject);
            return dataObject;
        } else {
            ObjectUtil.buildObject(existingDataObject, dataObject);
        }

        return existingDataObject;
    }

    /**
     * persist the given data object if it is not in the persistent store
     *
     * @param dataObject the given data object
     * @return return the data object persisted into the data store
     */
    public static <T extends PersistableBusinessObject> void persistDataObject(List<T> dataObjects) {
        for (T dataObject : dataObjects) {
            persistDataObject(dataObject);
        }
    }

    /**
     * remove the existing data from the database so that they cannot affact the test results
     */
    public static <T extends PersistableBusinessObject> void doCleanUpWithoutReference(Class<T> clazz, Properties properties, String propertykey, String fieldNames, String deliminator) throws Exception {
        Map<String, Object> fieldValues = buildFieldValues(clazz, properties, propertykey, fieldNames, deliminator);
        getBusinessObjectService().deleteMatching(clazz, fieldValues);
    }

    /**
     * remove the existing data from the database so that they cannot affact the test results
     */
    public static <T extends PersistableBusinessObject> void doCleanUpWithReference(Class<T> clazz, Properties properties, String propertykey, String fieldNames, String deliminator) throws Exception {
        List<T> dataObjects = findMatching(clazz, properties, propertykey, fieldNames, deliminator);

        for (T object : dataObjects) {
            getBusinessObjectService().delete(object);
        }
    }

    /**
     * remove the existing data from the database so that they cannot affact the test results
     */
    public static <T extends PersistableBusinessObject> List<T> findMatching(Class<T> clazz, Properties properties, String propertykey, String fieldNames, String deliminator) throws Exception {
        Map<String, Object> fieldValues = buildFieldValues(clazz, properties, propertykey, fieldNames, deliminator);
        return (List<T>) getBusinessObjectService().findMatching(clazz, fieldValues);
    }

    /**
     * build the field name and value pairs from the given properties in the specified properties file
     */
    public static <T> Map<String, Object> buildFieldValues(Class<T> clazz, Properties properties, String propertykey, String fieldNames, String deliminator) throws Exception {
        T cleanup = clazz.newInstance();
        ObjectUtil.populateBusinessObject(cleanup, properties, propertykey, fieldNames, deliminator);
        return ObjectUtil.buildPropertyMap(cleanup, Arrays.asList(StringUtils.split(fieldNames, deliminator)));
    }

    /**
     * test if the given object is in the given collection. The given key fields can be used for the comparison.
     *
     * @return true if the given object is in the given collection; otherwise, false
     */
    public static <T> boolean contains(List<T> collection, T object, List<String> keyFields) {
        for (T objectInCollection : collection) {
            boolean contains = ObjectUtil.equals(objectInCollection, object, keyFields);

            if (contains) {
                return true;
            }
        }

        return false;
    }

    /**
     * test if the given two collections contain the exactly same elements. The given key fields can be used for the comparison.
     *
     * @return true if the given two collections contain the exactly same elements; otherwise, false
     */
    public static <T> boolean hasSameElements(List<T> collection1, List<T> collection2, List<String> keyFields) {
        if (collection1 == collection2) {
            return true;
        }

        if (collection1 == null || collection2 == null) {
            return false;
        }

        if (collection1.size() != collection2.size()) {
            return false;
        }

        for (T object : collection2) {
            boolean contains = contains(collection1, object, keyFields);
            if (!contains) {
                return false;
            }
        }

        return true;
    }

    /**
     * Generates transaction data for a business object from properties
     *
     * @param businessObject the transction business object
     * @return the transction business object with data
     * @throws Exception thrown if an exception is encountered for any reason
     */
    public static <T> T buildTestDataObject(Class<? extends T> clazz, Properties properties) {
        T testData = null;

        try {
            testData = clazz.newInstance();

            Iterator propsIter = properties.keySet().iterator();
            while (propsIter.hasNext()) {
                String propertyName = (String) propsIter.next();
                String propertyValue = (String) properties.get(propertyName);

                // if searchValue is empty and the key is not a valid property ignore
                if (StringUtils.isBlank(propertyValue) || !(PropertyUtils.isWriteable(testData, propertyName))) {
                    continue;
                }

                String propertyType = PropertyUtils.getPropertyType(testData, propertyName).getSimpleName();
                Object finalPropertyValue = ObjectUtil.valueOf(propertyType, propertyValue);

                if (finalPropertyValue != null) {
                    PropertyUtils.setProperty(testData, propertyName, finalPropertyValue);
                }
            }
        } catch (Exception e) {
            throw new RuntimeException("Cannot build a test data object with the given data. " + e);
        }

        return testData;
    }

    /**
     * get an instant of BusinessObjectService
     *
     * @return an instant of BusinessObjectService
     */
    private static BusinessObjectService getBusinessObjectService() {
        return SpringContext.getBean(BusinessObjectService.class);
    }

    /**
     * get an instant of PersistenceService
     *
     * @return an instant of PersistenceService
     */
    private static PersistenceService getPersistenceService() {
        return SpringContext.getBean(PersistenceService.class);
    }
}
