/*
 * 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.batch.service.impl;

import org.kuali.kfs.coa.businessobject.ObjectCode;
import org.kuali.kfs.gl.businessobject.Balance;
import org.kuali.kfs.gl.businessobject.BalanceHistory;
import org.kuali.kfs.gl.businessobject.Entry;
import org.kuali.kfs.gl.businessobject.EntryHistory;
import org.kuali.kfs.gl.businessobject.OriginEntryInformation;
import org.kuali.kfs.gl.dataaccess.LedgerBalanceBalancingDao;
import org.kuali.kfs.gl.dataaccess.LedgerBalancingDao;
import org.kuali.kfs.gl.dataaccess.LedgerEntryBalancingDao;
import org.kuali.kfs.gl.dataaccess.LedgerEntryHistoryBalancingDao;
import org.kuali.kfs.krad.bo.PersistableBusinessObjectBase;
import org.kuali.kfs.krad.service.BusinessObjectService;
import org.kuali.kfs.krad.util.ObjectUtils;
import org.kuali.kfs.sys.KFSConstants;
import org.kuali.kfs.sys.context.KualiTestBase;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.kfs.sys.context.TestUtils;
import org.kuali.kfs.sys.service.UniversityDateService;
import org.kuali.kfs.sys.service.impl.ReportWriterTextServiceImpl;
import org.kuali.rice.core.api.datetime.DateTimeService;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

/**
 * Is a Base class for GL and LD BalancingService test cases
 */
public abstract class BalancingServiceImplTestBase extends KualiTestBase {
    private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(BalancingServiceImplTestBase.class);

    protected static final String CHART_OF_ACCOUNTS_CODE = "BL";
    protected static final String FINANCIAL_OBJECT_CODE = "5772";

    protected BalancingServiceBaseImpl<Entry, Balance> balancingService;
    protected LedgerBalancingDao ledgerBalancingDao;
    protected LedgerEntryHistoryBalancingDao ledgerEntryHistoryBalancingDao;
    protected BusinessObjectService businessObjectService;
    protected DateTimeService dateTimeService;
    protected UniversityDateService universityDateService;

    protected Integer startUniversityFiscalYear;
    protected Integer obsoleteUniversityFiscalYear;
    protected String[] INPUT_TRANSACTIONS = this.getInputTransactions();

    @Override
    protected void setUp() throws Exception {
        super.setUp();

        dateTimeService = SpringContext.getBean(DateTimeService.class);
        universityDateService = SpringContext.getBean(UniversityDateService.class);

        // Due to how WrappingBatchService works we need to manually initialize report writing
        ((ReportWriterTextServiceImpl) balancingService.reportWriterService).initialize();

        // Make the last two of INPUT_TRANSACTIONS out of date range
        Integer currentFiscalYear = universityDateService.getCurrentFiscalYear();
        startUniversityFiscalYear = currentFiscalYear - balancingService.getPastFiscalYearsToConsider();
        obsoleteUniversityFiscalYear = startUniversityFiscalYear - 1;
        for (int i = 0; i < INPUT_TRANSACTIONS.length; i++) {
            if (i < INPUT_TRANSACTIONS.length - 2) {
                INPUT_TRANSACTIONS[i] = currentFiscalYear + INPUT_TRANSACTIONS[i];
            } else {
                // Want the last two to be out of range
                INPUT_TRANSACTIONS[i] = obsoleteUniversityFiscalYear + INPUT_TRANSACTIONS[i];
            }
        }

        // And to make sure that the posters don't complain we need to generate data in the obsolete fiscal year
        this.generateObsoleteYearData(obsoleteUniversityFiscalYear);
    }

    @Override
    protected void tearDown() throws Exception {
        // KualiTestBase cleans added files up. Hence we need to make sure BalancingService doesn't use cache when looking for files
        balancingService.clearPosterFileCache();

        // Due to how WrappingBatchService works we need to manually destroy report writing
        ((ReportWriterTextServiceImpl) balancingService.reportWriterService).destroy();

        super.tearDown();
    }

    public void testBasicGetters() {
        // Here we test a set of basic getters. There really isn't much that should be done here because they vary between GL and Labor plus
        // these are pretty simple methods. Just checking that they return values should be enough.
        assertNotNull(balancingService.getPastFiscalYearsToConsider());
        assertNotNull(balancingService.getComparisonFailuresToPrintPerReport());

        // These should at minimum return Entry and Balance plus Histories for each GL and Labor
        assertNotNull(balancingService.getShortTableLabel((Entry.class).getSimpleName()));
        assertNotNull(balancingService.getShortTableLabel((EntryHistory.class).getSimpleName()));
        assertNotNull(balancingService.getShortTableLabel((Balance.class).getSimpleName()));
        assertNotNull(balancingService.getShortTableLabel((BalanceHistory.class).getSimpleName()));

        // Whether files are ready or not depends on if the poster ran before this. We are just executing the code here and not checking return value.
        balancingService.getPosterInputFile();
        balancingService.getPosterErrorOutputFile();
    }

    public void testGetOriginEntry() {
        LOG.debug("Basic test that getting GL or Labor OriginEntryInformation works. Since the parsing method isn't part of Balancing we don't run a full test.");
        OriginEntryInformation originEntry = balancingService.getOriginEntry(INPUT_TRANSACTIONS[0], 0);
        assertNotNull(originEntry);
        assertEquals(TestUtils.getFiscalYearForTesting().intValue(), originEntry.getUniversityFiscalYear().intValue());
    }

    public void testIsFilesReady() {
        // Delete the files since we don't know whether the poster run before this test case or not
        TestUtils.deleteFilesInDirectory(this.getBatchFileDirectoryName());

        LOG.debug("No file data present scenario");
        assertFalse(balancingService.isFilesReady());

        LOG.debug("Files ready scenario");
        this.postTestCaseEntries(INPUT_TRANSACTIONS);
        assertTrue(balancingService.isFilesReady());
    }

    public abstract void testRunBalancingPopulateData();

    public abstract void testRunBalancingDeleteObsoleteUniversityFiscalYearData();

    public abstract void testRunBalancingHistoryUpdate();

    /**
     * Fix Test
     */
    public void PATCHFIX_testRunBalancingComparisionFailure() {
        // Execute exactly the same as testRunBalancingPopulateData. This serves to populate the tables
        this.testRunBalancingPopulateData();

        // Now run balancing again, in the same accounting cycle
        assertTrue(balancingService.runBalancing());

        // This caused comparison failures because the update was run twice for the same accounting cycle, verify that was the case
        this.assertCompareHistoryFailure();
    }


    /**
     * Test helper since PosterService and LaborPosterService are exclusive interfaces.
     */
    protected abstract void postTestCaseEntries(String[] inputTransactions);

    /**
     * Test helper to get GL or Labor sample transactions. This isn't in fixtures because these are written to a file, not used as objects
     */
    protected abstract String[] getInputTransactions();

    /**
     * Test helper to compare history data to live data, expecting comparison success
     */
    protected void assertCompareHistorySuccess() {
        assertEquals(0, balancingService.compareEntryHistory().intValue());
        assertEquals(0, balancingService.compareBalanceHistory().intValue());

        Map<String, Integer> countCustomComparisionFailures = balancingService.customCompareHistory();

        if (ObjectUtils.isNotNull(countCustomComparisionFailures)) {
            for (Iterator<String> names = countCustomComparisionFailures.keySet().iterator(); names.hasNext(); ) {
                String name = names.next();
                assertEquals(0, countCustomComparisionFailures.get(name).intValue());
            }
        }
    }

    /**
     * Test helper to compare history data to live data, expecting comparison failure
     */
    protected void assertCompareHistoryFailure() {
        assertNotSame(0, balancingService.compareEntryHistory().intValue());
        assertNotSame(0, balancingService.compareBalanceHistory().intValue());

        Map<String, Integer> countCustomComparisionFailures = balancingService.customCompareHistory();

        if (ObjectUtils.isNotNull(countCustomComparisionFailures)) {
            for (Iterator<String> names = countCustomComparisionFailures.keySet().iterator(); names.hasNext(); ) {
                String name = names.next();
                assertNotSame(0, countCustomComparisionFailures.get(name).intValue());
            }
        }
    }

    /**
     * Test helper to generate obsolete fiscal year data if it does not exist. This method is messy. What it is doing is to check if the entries exist in
     * case there was an FY rollover but data wasn't updated yet. If not, it makes copies of the current. This corresponds to the entries getInputTransactions
     *
     * @param obsoleteYear
     */
    protected void generateObsoleteYearData(Integer obsoleteYear) {
        Map<String, Object> obsoleteChartOfAccountFieldValues = new HashMap<String, Object>();
        obsoleteChartOfAccountFieldValues.put(KFSConstants.UNIVERSITY_FISCAL_YEAR_PROPERTY_NAME, obsoleteYear);
        obsoleteChartOfAccountFieldValues.put(KFSConstants.CHART_OF_ACCOUNTS_CODE_PROPERTY_NAME, CHART_OF_ACCOUNTS_CODE);
        obsoleteChartOfAccountFieldValues.put(KFSConstants.FINANCIAL_OBJECT_CODE_PROPERTY_NAME, FINANCIAL_OBJECT_CODE);
        ObjectCode obsoleteYearObjectCode = (ObjectCode) businessObjectService.findByPrimaryKey(ObjectCode.class, obsoleteChartOfAccountFieldValues);
        if (ObjectUtils.isNull(obsoleteYearObjectCode)) {
            Map<String, Object> fieldValues = new HashMap<String, Object>();
            fieldValues.put(KFSConstants.UNIVERSITY_FISCAL_YEAR_PROPERTY_NAME, TestUtils.getFiscalYearForTesting());
            fieldValues.put(KFSConstants.CHART_OF_ACCOUNTS_CODE_PROPERTY_NAME, CHART_OF_ACCOUNTS_CODE);
            fieldValues.put(KFSConstants.FINANCIAL_OBJECT_CODE_PROPERTY_NAME, FINANCIAL_OBJECT_CODE);
            ObjectCode objectCode = (ObjectCode) businessObjectService.findByPrimaryKey(ObjectCode.class, fieldValues);
            objectCode.setUniversityFiscalYear(obsoleteYear);
            objectCode.setVersionNumber(null);
            objectCode.setObjectId(null);
            businessObjectService.save(objectCode);
        }
    }

    /**
     * Test helper to expose protected method on BalancingService. Quick and dirty way to make the test case easy to write and avoid exposing (making public) production code unnecessarily.
     */
    protected int getHistoryCount(Integer fiscalYear, Class<? extends PersistableBusinessObjectBase> persistentClass) {
        return balancingService.getHistoryCount(fiscalYear, persistentClass);
    }

    /**
     * Test helper to expose protected method on BalancingService. Quick and dirty way to make the test case easy to write and avoid exposing (making public) production code unnecessarily.
     */
    protected LedgerEntryBalancingDao getLedgerEntryBalancingDao() {
        return balancingService.ledgerEntryBalancingDao;
    }

    /**
     * Test helper to expose protected method on BalancingService. Quick and dirty way to make the test case easy to write and avoid exposing (making public) production code unnecessarily.
     */
    protected LedgerBalanceBalancingDao getLedgerBalanceBalancingDao() {
        return balancingService.ledgerBalanceBalancingDao;
    }

    /**
     * Test helper to expose protected method on BalancingService. Quick and dirty way to make the test case easy to write and avoid exposing (making public) production code unnecessarily.
     */
    protected String getBatchFileDirectoryName() {
        return balancingService.batchFileDirectoryName;
    }
}
