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

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.kuali.kfs.gl.businessobject.TrialBalanceReport;
import org.kuali.kfs.krad.util.ObjectUtils;
import org.kuali.kfs.sys.ConfigureContext;
import org.kuali.kfs.sys.context.KualiTestBase;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.kfs.sys.dataaccess.UnitTestSqlDao;
import org.kuali.rice.core.api.util.type.KualiDecimal;

import java.io.File;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;


/**
 * This class tests the changes made to generate the trial balance report
 * based on fiscal period codes along with pre-existing search parameters.
 * <p>
 * These tests also provide coverage for the related LookupableHelper and
 * DAO's, since if these classes don't function correctly, bad results
 * will manifest here.
 */
@ConfigureContext
public class TrialBalanceServiceTest extends KualiTestBase {

    /*
     * Enum to loop over for testing each period/chart-of-accounts code combo
     */
    private enum TrialBalanceInfo {

        /*
         * The thirteen periods as found in tech specs for TrialAccountBalance functionality.
         * These will also be initialized with a common fiscalYear and fiscalCoaCode.
         */
        Period_01("01", "1612056.16"), Period_02("02", "2607203.32"), Period_03("03", "4088938.24"), Period_04("04", "4748510.56"), Period_05("05", "5299554.38"),
        Period_06("06", "5718802.02"), Period_07("07", "6363696.74"), Period_08("08", "6899673.30"), Period_09("09", "7383177.34"), Period_10("10", "7788766.00"),
        Period_11("11", "8260399.64"), Period_12("12", "8908269.48"), Period_13("13", "8908269.48");

        // Magic Strings, common across relevant records in TrialBalanceServiceTest.DATA_FILE_PATH
        private static final String FISCAL_YEAR = "2014";
        private static final String FISCAL_CHART_OF_ACCOUNTS_CODE = "BA";

        // Instance Variables
        private String fiscalPeriodCode;
        private BigDecimal expectedBalance;


        /*
         * Initialize the enum for period and balance specific values.
         */
        private TrialBalanceInfo(final String code, final String expectedBalanceString) {
            this.fiscalPeriodCode = code;
            this.expectedBalance = new BigDecimal(expectedBalanceString).setScale(SCALE, BigDecimal.ROUND_HALF_UP);
        }

        public String getFiscalYear() {
            return FISCAL_YEAR;
        }

        public String getFiscalCoaCode() {
            return FISCAL_CHART_OF_ACCOUNTS_CODE;
        }

        public String getFiscalPeriodCode() {
            return this.fiscalPeriodCode;
        }

        public BigDecimal getExpectedBalance() {
            return this.expectedBalance;
        }

    }//enum


    /*
     * Enum used for switching of adding up credit OR debit fields
     * for a TrialBalanceReport; enables code re-use of getBalance(...)
     */
    private enum BalanceType {
        CREDIT, DEBIT
    }//enum


    // Magic value variables
    private static final String DATA_FILE_PATH = "org/kuali/kfs/gl/batch/fixture/gl_trllblnc.csv";
    private static final String DATA_FILE_DELIM = ",";
    private static final int COLUMN_COUNT = 25;
    private static final String DELETE_ALL_ENTRIES_SQL = "DELETE FROM GL_BALANCE_T";
    private static final String TIMESTAMP_FORMAT = "DD/MM/YYYY HH12:MI:SS PM"; // UnitTestSqlDao is hard coded to this value
    private static final int SCALE = 2; // Scale for BigDecimal instances

    // Note, the following is "newline'd" every five columns; also, be aware of the single quotes needed for varchar columns
    private static final String INSERT_SQL_FORMAT = "INSERT INTO GL_BALANCE_T ("
        + "UNIV_FISCAL_YR, FIN_COA_CD, ACCOUNT_NBR, SUB_ACCT_NBR, FIN_OBJECT_CD, "
        + "FIN_SUB_OBJ_CD, FIN_BALANCE_TYP_CD, FIN_OBJ_TYP_CD, ACLN_ANNL_BAL_AMT, FIN_BEG_BAL_LN_AMT, "
        + "CONTR_GR_BB_AC_AMT, MO1_ACCT_LN_AMT, MO2_ACCT_LN_AMT, MO3_ACCT_LN_AMT, MO4_ACCT_LN_AMT, "
        + "MO5_ACCT_LN_AMT, MO6_ACCT_LN_AMT, MO7_ACCT_LN_AMT, MO8_ACCT_LN_AMT, MO9_ACCT_LN_AMT, "
        + "MO10_ACCT_LN_AMT, MO11_ACCT_LN_AMT, MO12_ACCT_LN_AMT, MO13_ACCT_LN_AMT, TIMESTAMP)"
        + " VALUES (%s, '%s', '%s', '%s', '%s', "
        + "'%s', '%s', '%s', %s, %s, "
        + "%s, %s, %s, %s, %s, "
        + "%s, %s, %s, %s, %s, "
        + "%s, %s, %s, %s, %s)";

    // Services
    private TrialBalanceService trialBalanceService;
    private UnitTestSqlDao unitTestSqlDao;


    /**
     * This method sets up several services and intializes
     * the DB to only contain records from file.
     *
     * @see junit.framework.TestCase#setUp()
     */
    @Override
    public void setUp() throws Exception {
        super.setUp();
        trialBalanceService = SpringContext.getBean(TrialBalanceService.class);
        unitTestSqlDao = SpringContext.getBean(UnitTestSqlDao.class);
        initDbFromFile();
    }


    /**
     * This method tests the service's results for finding a trial balance
     * based on search parameters encapsulated by the TrialBalanceInfo enum.
     */
    public void testFindTrialBalance() {
        for (TrialBalanceInfo trialBalanceInfo : TrialBalanceInfo.values()) {
            // Perform query through the new code
            List<TrialBalanceReport> trialBalanceReports = getTrialBalanceReports(trialBalanceInfo);

            // Assert the credit balance is correct
            BigDecimal expectedBalance = trialBalanceInfo.getExpectedBalance();
            BigDecimal calculatedCreditBalance = getCreditBalance(trialBalanceReports);
            String msg = String.format("Unexpected balance for period '%s'; expected balance of '%s', but found credit balance of '%s'.", trialBalanceInfo.getFiscalPeriodCode(), expectedBalance, calculatedCreditBalance);
            assertTrue(msg, expectedBalance.compareTo(calculatedCreditBalance) == 0);

            // Assert the debit balance is correct
            BigDecimal calculatedDebitBalance = getDebitBalance(trialBalanceReports);
            msg = String.format("Unexpected balance for period '%s'; expected balance of '%s', but found debit balance of '%s'.", trialBalanceInfo.getFiscalPeriodCode(), expectedBalance, calculatedDebitBalance);
            assertTrue(msg, expectedBalance.compareTo(calculatedDebitBalance) == 0);
        }
    }


    /**
     * Test to ensure report pdf is generated on the file system
     */
    public void testGenerateReportForExtractProcess() {
        File reportFile = null;
        try {
            Collection dataSource = getTrialBalanceReports(TrialBalanceInfo.Period_13);
            String reportFilePath = trialBalanceService.generateReportForExtractProcess(dataSource, TrialBalanceInfo.Period_13.getFiscalYear(), TrialBalanceInfo.Period_13.getFiscalPeriodCode());
            reportFile = new File(reportFilePath);

            // Assert file exists
            String msg = String.format("Expected report file does not exist, should be found at '%s'.", reportFilePath);
            assertTrue(msg, reportFile.exists());

            // Assert file is non-zero
            long fileSize = reportFile.length();
            msg = String.format("Expected '%s' to be a non-zero size.", fileSize);
            assertTrue(msg, fileSize > 0);

        } finally {
            // If anything goes wrong, we still need to clean up, not done in
            // tearDown(), since this is the only test that creates a file
            FileUtils.deleteQuietly(reportFile);
        }
    }


    /*
     * This method clears all records from the GL_BALANCE_T table,
     * pulls records from disk, and inserts the records into DB.
     *
     * In such a way, we know for sure the state of the DB while
     * testing.
     */
    private void initDbFromFile() {
        unitTestSqlDao.sqlCommand(DELETE_ALL_ENTRIES_SQL);
        List<String> insertStatements = getInsertStatements();
        for (String insertStatement : insertStatements) {
            unitTestSqlDao.sqlCommand(insertStatement);
        }
    }


    /*
     * This method takes a static file, splits it by newline,
     * splits each line by comma into tokens, then formats
     * the tokens into an SQL insert statement, and returns
     * the list of resulting sql insert strings.
     */
    private List<String> getInsertStatements() {
        List<String> lines = null;
        try {
            lines = IOUtils.readLines(getClass().getClassLoader().getResourceAsStream(DATA_FILE_PATH));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        List<String> insertStatements = new ArrayList<String>();

        for (String line : lines) {
            String[] tokens = line.split(DATA_FILE_DELIM, -1);

            // Be DB agnostic with different timestamp functions
            String timeStampString = tokens[24];
            String timeStampSql = getTimestampSql(timeStampString);

            String sqlInsert = String.format(INSERT_SQL_FORMAT,
                tokens[0], tokens[1], tokens[2], tokens[3], tokens[4],
                tokens[5], tokens[6], tokens[7], tokens[8], tokens[9],
                tokens[10], tokens[11], tokens[12], tokens[13], tokens[14],
                tokens[15], tokens[16], tokens[17], tokens[18], tokens[19],
                tokens[20], tokens[21], tokens[22], tokens[23], timeStampSql);
            insertStatements.add(sqlInsert);
        }

        return insertStatements;
    }


    /*
     * Build DB platform-specific date function sql, e.g.:
     *     MySql  -> STR_TO_DATE('07/05/2014 07:49:19 AM', '%d/%m/%Y %r')
     *     Oracle -> TO_DATE('07/05/2014 07:49:19 AM', 'DD/MM/YYYY HH12:MI:SS PM')
     */
    private String getTimestampSql(String timeStampString) {
        StringBuffer sb = new StringBuffer();
        sb.append(unitTestSqlDao.getDbPlatform().getStrToDateFunction());
        sb.append("('");
        sb.append(timeStampString);
        sb.append("', ");
        sb.append(unitTestSqlDao.getDbPlatform().getDateFormatString(TIMESTAMP_FORMAT));
        sb.append(")");

        return sb.toString();
    }


    /*
     * This call cascades down the related service, LookupableHelper, and DAO.
     * The passed in TrialBalanceInfo determines which reports are returned.
     */
    private List<TrialBalanceReport> getTrialBalanceReports(TrialBalanceInfo trialBalanceInfo) {
        List objects = trialBalanceService.findTrialBalance(trialBalanceInfo.getFiscalYear(),
            trialBalanceInfo.getFiscalCoaCode(),
            trialBalanceInfo.getFiscalPeriodCode());
        List<TrialBalanceReport> trialBalanceReports = new ArrayList<TrialBalanceReport>();
        for (Object o : objects) {
            TrialBalanceReport trialBalanceReport = (TrialBalanceReport) o;
            trialBalanceReports.add(trialBalanceReport);
        }
        return trialBalanceReports;
    }


    /*
     * This method adds up all non-null credits from the passed in TrialBalanceReport
     * and returns the total sum.
     */
    private BigDecimal getCreditBalance(List<TrialBalanceReport> trialBalanceReports) {
        return getBalance(trialBalanceReports, BalanceType.CREDIT);
    }


    /*
     * This method adds up all non-null debits from the passed in TrialBalanceReport
     * and returns the total sum.
     */
    private BigDecimal getDebitBalance(List<TrialBalanceReport> trialBalanceReports) {
        return getBalance(trialBalanceReports, BalanceType.DEBIT);
    }


    /*
     * Helper method that adds up debit/credit acroos collection of TrialBalanceReport
     */
    private BigDecimal getBalance(List<TrialBalanceReport> trialBalanceReports, BalanceType balanceType) {
        BigDecimal balance = BigDecimal.ZERO.setScale(SCALE, BigDecimal.ROUND_HALF_UP);

        for (TrialBalanceReport trialBalanceReport : trialBalanceReports) {
            KualiDecimal testBalance;
            if (balanceType == BalanceType.CREDIT) {
                testBalance = trialBalanceReport.getCreditAmount();
            } else {
                // balanceType == BalanceType.DEBIT
                testBalance = trialBalanceReport.getDebitAmount();
            }

            if (ObjectUtils.isNotNull(testBalance)) {
                balance = balance.add(testBalance.bigDecimalValue().setScale(SCALE, BigDecimal.ROUND_HALF_UP));
            }
        }//for

        return balance;
    }

}
