/*
 * 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.gl.businessobject.lookup;

import org.kuali.kfs.gl.Constant;
import org.kuali.kfs.gl.businessobject.Balance;
import org.kuali.kfs.gl.service.BalanceService;
import org.kuali.kfs.krad.service.BusinessObjectService;
import org.kuali.kfs.sys.ConfigureContext;
import org.kuali.kfs.sys.KFSPropertyConstants;
import org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntry;
import org.kuali.kfs.sys.businessobject.lookup.LookupableSpringContext;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.kfs.sys.context.TestUtils;
import org.kuali.rice.core.api.util.type.KualiDecimal;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
 * This class contains the test cases that can be applied to the method in BalanceLookupableImpl class.
 */
@ConfigureContext
public class BalanceLookupableHelperServiceTest extends AbstractGeneralLedgerLookupableHelperServiceTestBase {

    private BalanceService balanceService;

    /**
     * Sets up the services needed to test balance lookups
     *
     * @see org.kuali.kfs.gl.businessobject.lookup.AbstractGeneralLedgerLookupableHelperServiceTestBase#setUp()
     */
    @Override
    protected void setUp() throws Exception {
        super.setUp();

        setBalanceService(SpringContext.getBean(BalanceService.class));
        lookupableHelperServiceImpl = LookupableSpringContext.getLookupableHelperService("glBalanceLookupableHelperService");
        lookupableHelperServiceImpl.setBusinessObjectClass(Balance.class);
    }

    /**
     * Covers the search results returned by BalanceLookupableHelperService
     *
     * @throws Exception thrown if an exception is encountered for any reason
     * @see org.kuali.module.gl.web.lookupable.AbstractGLLookupableTestBase#testGetSearchResults()
     */
    public void testGetSearchResults() throws Exception {
        testDataGenerator.generateTransactionData(pendingEntry);
        Balance balance = new Balance(pendingEntry);

        // test the search results before the specified entry is inserted into the database
        Map fieldValues = getLookupFieldValues(balance, true);
        List searchResults = lookupableHelperServiceImpl.getSearchResults(fieldValues);
        assertTrue(testDataGenerator.getMessageValue("noSuchRecord"), !contains(searchResults, balance));

        // add a new entry into database
        this.insertNewRecord(balance);

        // the new record can meet the search criteria.
        fieldValues = getLookupFieldValues(balance, true);
        searchResults = lookupableHelperServiceImpl.getSearchResults(fieldValues);
        assertTrue(testDataGenerator.getMessageValue("wrongRecordSize"), searchResults.size() == 1);
        assertTrue(testDataGenerator.getMessageValue("failToFindRecord"), contains(searchResults, balance));

        // add another entry into database
        Balance anotherBalance = new Balance(pendingEntry);
        anotherBalance.setAccountNumber(testDataGenerator.getPropertyValue("genericAccountNumber"));
        this.insertNewRecord(anotherBalance);

        // the new record cannot meet the search criteria.
        fieldValues = getLookupFieldValues(balance, true);
        searchResults = lookupableHelperServiceImpl.getSearchResults(fieldValues);
        assertTrue(testDataGenerator.getMessageValue("wrongRecordSize"), searchResults.size() == 1);
        assertTrue(testDataGenerator.getMessageValue("failToFindRecord"), !contains(searchResults, anotherBalance));

        // the new record cannot meet the search criteria.
        fieldValues = getLookupFieldValues(balance, false);
        searchResults = lookupableHelperServiceImpl.getSearchResults(fieldValues);
        assertTrue(testDataGenerator.getMessageValue("wrongRecordSize"), searchResults.size() >= 1);
        assertTrue(testDataGenerator.getMessageValue("failToFindRecord"), !contains(searchResults, anotherBalance));
    }

    /**
     * This method includes the test cases applied to the pending entry option: Approved and All
     *
     * @throws Exception thrown if an exception is encountered for any reason
     */
    public void testPendingEntryOption() throws Exception {
        testDataGenerator.generateTransactionData(pendingEntry);
        pendingEntry.setUniversityFiscalPeriodCode("CB");
        KualiDecimal pendingAmount = pendingEntry.getTransactionLedgerEntryAmount().abs();
        Balance balance = new Balance(pendingEntry);

        // prepare the test data for the pending option: approved
        insertNewPendingEntry(pendingEntry);
        this.insertNewRecord(balance);

        // test if the approved pending entry has been combined with cash balance
        Map fieldValues = getLookupFieldValues(balance, true);
        fieldValues.put(Constant.PENDING_ENTRY_OPTION, Constant.APPROVED_PENDING_ENTRY);
        fieldValues.put(Constant.CONSOLIDATION_OPTION, Constant.CONSOLIDATION);
        List searchResults = lookupableHelperServiceImpl.getSearchResults(fieldValues);
        assertTrue(testDataGenerator.getMessageValue("wrongRecordSize"), searchResults.size() >= 1);

        Balance result = (Balance) searchResults.get(0);
        KualiDecimal approvedCGBeginningBalance = result.getContractsGrantsBeginningBalanceAmount().abs();
        // assertTrue(testDataGenerator.getMessageValue("incorrectAmount"), approvedCGBeginningBalance.equals(pendingAmount));

        // prepare the test data for the pending option: All
        GeneralLedgerPendingEntry nonapprovedPendingEntry = new GeneralLedgerPendingEntry();
        testDataGenerator.generateTransactionData(nonapprovedPendingEntry);
        nonapprovedPendingEntry.setUniversityFiscalPeriodCode("CB");
        nonapprovedPendingEntry.setFinancialDocumentApprovedCode(" ");

        String sequenceNumber = testDataGenerator.getProperties().getProperty("genericSquenceNumber");
        nonapprovedPendingEntry.setTransactionLedgerEntrySequenceNumber(new Integer(sequenceNumber));

        insertNewPendingEntry(nonapprovedPendingEntry);

        // test if the all pending entries have been combined with cash balance
        fieldValues = getLookupFieldValues(balance, true);
        fieldValues.put(Constant.PENDING_ENTRY_OPTION, Constant.ALL_PENDING_ENTRY);
        fieldValues.put(Constant.CONSOLIDATION_OPTION, Constant.CONSOLIDATION);

        searchResults = lookupableHelperServiceImpl.getSearchResults(fieldValues);
        assertTrue(testDataGenerator.getMessageValue("wrongRecordSize"), searchResults.size() == 1);

        Balance newResult = (Balance) searchResults.get(0);
        KualiDecimal allCGBeginningBalance = newResult.getContractsGrantsBeginningBalanceAmount().abs();
        // assertTrue(testDataGenerator.getMessageValue("incorrectAmount"),
        // !approvedCGBeginningBalance.equals(allCGBeginningBalance));
    }

    /**
     * This method tests if the orphan pending entries can be included in the search results
     *
     * @throws Exception thrown if an exception is encountered for any reason
     */
    public void testOrphanPendingEntry() throws Exception {
        testDataGenerator.generateTransactionData(pendingEntry);
        Balance balance = new Balance(pendingEntry);

        // test if the all pending entries have been combined into a balance record
        // TODO the pending entry cannot be retrived. It took one day to find the reason, but I didn't sovle that.
        getPendingEntryService().save(pendingEntry);
        insertNewRecord(balance);

        Map fieldValues = getLookupFieldValues(balance, true);
        fieldValues.put(Constant.PENDING_ENTRY_OPTION, Constant.ALL_PENDING_ENTRY);

        Map pendingEntryFieldValues = BusinessObjectFieldConverter.convertToTransactionFieldValues(fieldValues);
        pendingEntryFieldValues.remove(Constant.CONSOLIDATION_OPTION);
        pendingEntryFieldValues.remove(Constant.PENDING_ENTRY_OPTION);
        pendingEntryFieldValues.remove(Constant.AMOUNT_VIEW_OPTION);
        Iterator iterator = getPendingEntryService().findPendingLedgerEntriesForBalance(pendingEntryFieldValues, true);

        List searchResults = lookupableHelperServiceImpl.getSearchResults(fieldValues);
        assertTrue(testDataGenerator.getMessageValue("failToFindRecord"), contains(searchResults, balance));
        assertTrue(testDataGenerator.getMessageValue("wrongRecordSize"), searchResults.size() >= 1);
    }

    /**
     * This method includes the test cases applied to the consolidation option: Consolidate and Detail
     *
     * @throws Exception thrown if an exception is encountered for any reason
     */
    public void testConsolidationOption() throws Exception {
        // ensure the transaction data does not exist in enty table. Otherwise, execption may be raised
        testDataGenerator.generateTransactionData(pendingEntry);
        Balance balanceOne = new Balance(pendingEntry);
        this.insertNewRecord(balanceOne);

        // get the number of the search results before adding the second record into database
        Map fieldValues = getLookupFieldValues(balanceOne, true);
        fieldValues.put(Constant.CONSOLIDATION_OPTION, Constant.CONSOLIDATION);

        List searchResults = lookupableHelperServiceImpl.getSearchResults(fieldValues);
        int numOfFirstResults = searchResults.size();

        String subAccountNumber = testDataGenerator.getPropertyValue("genericSubAccountNumber");
        pendingEntry.setSubAccountNumber(subAccountNumber);
        Balance balanceTwo = new Balance(pendingEntry);
        this.insertNewRecord(balanceTwo);

        // test if the second record is consolidated with others
        fieldValues = getLookupFieldValues(balanceOne, true);
        fieldValues.put(Constant.CONSOLIDATION_OPTION, Constant.CONSOLIDATION);

        searchResults = lookupableHelperServiceImpl.getSearchResults(fieldValues);
        int numOfSecondResults = searchResults.size();

        // test if the search results appear in details
        fieldValues = getLookupFieldValues(balanceOne, false);
        fieldValues.put(Constant.CONSOLIDATION_OPTION, Constant.DETAIL);

        searchResults = lookupableHelperServiceImpl.getSearchResults(fieldValues);
        int numOfThirdResults = searchResults.size();
        assertTrue(testDataGenerator.getMessageValue("wrongRecordSize"), numOfSecondResults < numOfThirdResults);
    }

    /**
     * This method includes the test cases applied to the amount view option: Monthly and Accumulate
     *
     * @throws Exception thrown if an exception is encountered for any reason
     */
    public void testAmountViewOption() throws Exception {
        try {
            testDataGenerator.generateTransactionData(pendingEntry);
            Balance balanceOne = new Balance(pendingEntry);

            this.insertNewRecord(balanceOne);

            // get the amount of the second month
            Map fieldValues = getLookupFieldValues(balanceOne, true);
            fieldValues.put(Constant.AMOUNT_VIEW_OPTION, Constant.MONTHLY);

            List searchResults = lookupableHelperServiceImpl.getSearchResults(fieldValues);
            assertTrue(testDataGenerator.getMessageValue("wrongRecordSize"), searchResults.size() == 1);
            assertTrue(testDataGenerator.getMessageValue("failToFindRecord"), contains(searchResults, balanceOne));

            KualiDecimal month2Amount = ((Balance) searchResults.get(0)).getMonth2Amount();
            assertTrue(testDataGenerator.getMessageValue("incorrectAmount"), month2Amount.equals(KualiDecimal.ZERO));

            // get the acculated amount of the second month
            fieldValues = getLookupFieldValues(balanceOne, true);
            fieldValues.put(Constant.AMOUNT_VIEW_OPTION, Constant.ACCUMULATE);

            searchResults = lookupableHelperServiceImpl.getSearchResults(fieldValues);
            assertTrue(testDataGenerator.getMessageValue("wrongRecordSize"), searchResults.size() == 1);
            assertTrue(testDataGenerator.getMessageValue("failToFindRecord"), contains(searchResults, balanceOne));

            KualiDecimal accumulatedMonth2Amount = ((Balance) searchResults.get(0)).getMonth2Amount();

            // test if the acculated amount is greater than or equal to the monthly amount
            assertTrue(testDataGenerator.getMessageValue("incorrectAmount"), accumulatedMonth2Amount.isGreaterEqual(month2Amount));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * This method includes the test cases applied to the consolidation option: Consolidate and Detail
     *
     * @throws Exception thrown if an exception is encountered for any reason
     */
    public void testPerformance() throws Exception {
        long threshlod = 60000;

        // get the number of the search results before adding the second record into database
        Balance balance = new Balance();
        balance.setAccountNumber("1031400");
        balance.setUniversityFiscalYear(TestUtils.getFiscalYearForTesting());
        balance.setChartOfAccountsCode("BL");

        Map fieldValues = getLookupFieldValues(balance, true);
        fieldValues.put(Constant.CONSOLIDATION_OPTION, Constant.CONSOLIDATION);

        long startTime = System.currentTimeMillis();
        List searchResults = lookupableHelperServiceImpl.getSearchResults(fieldValues);
        long endTime = System.currentTimeMillis();
        long duration = endTime - startTime;

        System.out.println(duration + "ms");
        assertTrue("Too slow", duration < threshlod);

        // test if the search results appear in details
        fieldValues = getLookupFieldValues(balance, false);
        fieldValues.put(Constant.CONSOLIDATION_OPTION, Constant.DETAIL);

        startTime = System.currentTimeMillis();
        searchResults = lookupableHelperServiceImpl.getSearchResults(fieldValues);
        endTime = System.currentTimeMillis();
        duration = endTime - startTime;

        System.out.println(duration + "ms");
        assertTrue("Too slow", duration < threshlod);
    }

    /**
     * Returns the lookup fields to test in the search results
     *
     * @param isExtended true if extended fields should be included, false if they should not be included
     * @return a List of field names to check
     * @see org.kuali.module.gl.web.lookupable.AbstractGLLookupableTestBase#getLookupFields(boolean)
     */
    public List getLookupFields(boolean isExtended) {
        List lookupFields = new ArrayList();

        lookupFields.add(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR);
        lookupFields.add(KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE);
        lookupFields.add(KFSPropertyConstants.ACCOUNT_NUMBER);
        lookupFields.add(KFSPropertyConstants.BALANCE_TYPE_CODE);

        lookupFields.add(Constant.CONSOLIDATION_OPTION);
        lookupFields.add(Constant.PENDING_ENTRY_OPTION);
        lookupFields.add(Constant.AMOUNT_VIEW_OPTION);

        // include the extended fields
        if (isExtended) {
            lookupFields.add(KFSPropertyConstants.SUB_ACCOUNT_NUMBER);
            lookupFields.add(KFSPropertyConstants.OBJECT_CODE);
            lookupFields.add(KFSPropertyConstants.SUB_OBJECT_CODE);
            lookupFields.add(KFSPropertyConstants.OBJECT_TYPE_CODE);
        }
        return lookupFields;
    }

    /**
     * This method inserts a new balance record into database
     *
     * @param balance the given balance
     */
    protected void insertNewRecord(Balance balance) {
        try {
            SpringContext.getBean(BusinessObjectService.class).save(balance);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * Gets the balanceService attribute.
     *
     * @return Returns the balanceService.
     */
    public BalanceService getBalanceService() {
        return balanceService;
    }

    /**
     * Sets the balanceService attribute value.
     *
     * @param balanceService The balanceService to set.
     */
    public void setBalanceService(BalanceService balanceService) {
        this.balanceService = balanceService;
    }
}
