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

import org.apache.commons.lang.StringUtils;
import org.apache.cxf.helpers.FileUtils;
import org.kuali.kfs.coa.businessobject.OffsetDefinition;
import org.kuali.kfs.gl.GeneralLedgerConstants;
import org.kuali.kfs.gl.batch.service.AccountingCycleCachingService;
import org.kuali.kfs.gl.service.OriginEntryService;
import org.kuali.kfs.krad.service.PersistenceService;
import org.kuali.kfs.sys.ConfigureContext;
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.dataaccess.UnitTestSqlDao;
import org.kuali.kfs.sys.service.ConfigurableDateService;
import org.kuali.rice.core.api.config.property.ConfigurationService;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;

/**
 * OriginEntryTestBase...the uberpowerful base of a lot of GL tests.  Basically, this class provides
 * many convenience methods for writing tests that test against large batches of origin entries.
 */
@ConfigureContext
public abstract class OriginEntryTestBase extends KualiTestBase {
    private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(OriginEntryTestBase.class);

    protected ConfigurableDateService dateTimeService;
    protected PersistenceService persistenceService;
    protected UnitTestSqlDao unitTestSqlDao = null;
    protected ConfigurationService kualiConfigurationService = null;
    protected OriginEntryService originEntryService = null;
    protected AccountingCycleCachingService accountingCycleCachingService = null;

    protected Date date;
    protected String batchDirectory;
    protected String builtDirectory;

    protected String testingYear;
    protected String testingPeriodCode;

    /**
     * Constructs a OriginEntryTestBase instance
     */
    public OriginEntryTestBase() {
        super();
    }

    /**
     * Sets up this test base; that means getting some services from Spring and reseting the
     * enhancement flags.
     *
     * @see junit.framework.TestCase#setUp()
     */
    @Override
    protected void setUp() throws Exception {
        super.setUp();

        if (LOG.isDebugEnabled()) {
            LOG.debug("setUp() starting");
        }

        dateTimeService = SpringContext.getBean(ConfigurableDateService.class);
        date = dateTimeService.getCurrentDate();

        // Other objects needed for the tests
        persistenceService = SpringContext.getBean(PersistenceService.class, "persistenceServiceOjb");
        unitTestSqlDao = SpringContext.getBean(UnitTestSqlDao.class);
        kualiConfigurationService = SpringContext.getBean(ConfigurationService.class);
        originEntryService = SpringContext.getBean(OriginEntryService.class);

        batchDirectory = this.getBatchDirectoryName();
        buildBatchDirectory(batchDirectory);

        accountingCycleCachingService = SpringContext.getBean(AccountingCycleCachingService.class);
        accountingCycleCachingService.initialize();

        // Set all enhancements to off
        resetAllEnhancementFlags();

        testingYear = TestUtils.getFiscalYearForTesting().toString();
        testingPeriodCode = TestUtils.getPeriodCodeForTesting();
    }

    /**
     * Removes any build batch directory
     *
     * @see junit.framework.TestCase#tearDown()
     */
    @Override
    protected void tearDown() throws Exception {
        removeBatchDirectory(batchDirectory);

        if (accountingCycleCachingService != null) {
            accountingCycleCachingService.destroy();
        }
    }

    /**
     * get the name of the batch directory
     *
     * @return the name of the batch directory
     */
    protected String getBatchDirectoryName() {
        return SpringContext.getBean(ConfigurationService.class).getPropertyValueAsString("staging.directory") + "/gl/test_directory/originEntry";
    }

    /**
     * Recursively ensures that the whole of the path of the batch directory exists
     */
    protected void buildBatchDirectory(String batchDirectory) {
        String[] directoryPieces = batchDirectory.split("/");
        StringBuilder builtDirectorySoFar = new StringBuilder();
        StringBuilder directoryToRemoveSoFar = new StringBuilder();

        for (String directoryPiece : directoryPieces) {
            if (!StringUtils.isBlank(directoryPiece)) {
                builtDirectorySoFar.append('/');
                builtDirectorySoFar.append(directoryPiece);

                File dir = new File(builtDirectorySoFar.toString());
                if (!dir.exists()) {
                    directoryToRemoveSoFar.append('/');
                    directoryToRemoveSoFar.append(directoryPiece);
                    dir.mkdir();
                }
            }
        }

        builtDirectory = directoryToRemoveSoFar.toString();
    }

    /**
     * Removes any directories added as part of building the batch directory
     */
    protected void removeBatchDirectory(String batchDirectory) {
        String unbuiltDirectory = batchDirectory.substring(0, batchDirectory.length() - builtDirectory.length());

        String pathToUnbuild = new String(batchDirectory);
        while (!unbuiltDirectory.equals(pathToUnbuild)) {
            File pathToUnbuildFile = new File(pathToUnbuild);
            clearAllFilesInDirectory(pathToUnbuildFile);
            FileUtils.removeDir(pathToUnbuildFile);

            int lastSeperator = pathToUnbuild.lastIndexOf('/');
            pathToUnbuild = pathToUnbuild.substring(0, lastSeperator);
        }
    }

    /**
     * Removes all the files within the given directory
     *
     * @param dir the directory to delete files from
     */
    protected void clearAllFilesInDirectory(File dir) {
        for (File f : dir.listFiles()) {
            if (f.isDirectory()) {
                FileUtils.removeDir(f);
            } else {
                FileUtils.delete(f);
            }
        }
    }

    /**
     * An inner class to point to a specific entry in a group
     */
    protected class EntryHolder {
        private String baseFileName;
        private String transactionLine;

        /**
         * Constructs a OriginEntryTestBase.EntryHolder
         *
         * @param baseFileName    the group that the entry to point to is in
         * @param transactionLine the line number of the entry
         */
        public EntryHolder(String baseFileName, String transactionLine) {
            this.baseFileName = baseFileName;
            this.transactionLine = transactionLine;
        }

        /**
         * @return the base file name
         */
        public String getBaseFileName() {
            return this.baseFileName;
        }

        /**
         * @return the transaction line
         */
        public String getTransactionLine() {
            return this.transactionLine;
        }
    }

    /**
     * An inner class to filter only the files for this batch run
     */

    class BatchFilenameFilter implements FilenameFilter {

        /**
         * @see java.io.FilenameFilter#accept(java.io.File, java.lang.String)
         */
        @Override
        public boolean accept(File dir, String name) {
            return name.endsWith(GeneralLedgerConstants.BatchFileSystem.EXTENSION);
        }
    }

    /**
     * Given a type code and a bunch of transactions, creates a new backup file and adds all
     * the transactions to that file
     *
     * @param baseFileName the type code of the new file
     * @param transactions an array of String-formatted entries to save into the file
     */
    protected void loadInputTransactions(String fileName, String[] transactions) {
        final String fullFileName = batchDirectory + "/" + fileName + GeneralLedgerConstants.BatchFileSystem.EXTENSION;
        try {
            PrintStream ps = new PrintStream(fullFileName);
            for (int i = 0; i < transactions.length; i++) {
                ps.println(transactions[i]);
            }
            ps.close();
        } catch (FileNotFoundException fnfe) {
            throw new RuntimeException("Could not open file " + fullFileName);
        }
    }

    /**
     * Deletes everything in the expenditure transaction table
     */
    protected void clearExpenditureTable() {
        unitTestSqlDao.sqlCommand("delete from GL_EXPEND_TRN_MT");
    }

    /**
     * Deletes everything in the sufficient fund balance table
     */
    protected void clearSufficientFundBalanceTable() {
        unitTestSqlDao.sqlCommand("delete from GL_SF_BALANCES_T");
    }

    /**
     * Deletes all entries in the entry table with the given chart code and account number
     *
     * @param fin_coa_cd  the chart code of entries to delete
     * @param account_nbr the account number of entries to delete
     */
    protected void clearGlEntryTable(String fin_coa_cd, String account_nbr) {
        unitTestSqlDao.sqlCommand("delete from GL_ENTRY_T where fin_coa_cd = '" + fin_coa_cd + "' and account_nbr = '" + account_nbr + "'");
    }

    /**
     * Deletes everything in the gl reversal table
     */
    protected void clearReversalTable() {
        unitTestSqlDao.sqlCommand("delete from GL_REVERSAL_T");
    }

    /**
     * Deletes everything in the gl balance table
     */
    protected void clearGlBalanceTable() {
        unitTestSqlDao.sqlCommand("delete from GL_BALANCE_T");
    }

    /**
     * Deletes everything in the gl encumbrance table.
     */
    protected void clearEncumbranceTable() {
        unitTestSqlDao.sqlCommand("delete from GL_ENCUMBRANCE_T");
    }

    /**
     * Deletes everything in the gl account balance table
     */
    protected void clearGlAccountBalanceTable() {
        unitTestSqlDao.sqlCommand("delete from GL_ACCT_BALANCES_T");
    }

    /**
     * Deletes all files in the batch test directory
     */
    protected void clearBatchFiles() {
        //I didn't see a deleteAll method, so...
        for (File file : new File(batchDirectory).listFiles(new BatchFilenameFilter())) {
            file.delete();
        }
    }

    /**
     * Check all the entries in the file against the data passed in EntryHolder[]. If any of them are different, assert an
     * error.
     *
     * @param fileCount       the expected number of files
     * @param requiredEntries an array of expected String-formatted entries to check against
     */
    protected void assertOriginEntries(int fileCount, EntryHolder[] requiredEntries) {
        //we do not need to call clearCache() since no dao and jdbc calls mixted in this method.
        //refer to KFSMI-7637
        // persistenceService.clearCache();

        File[] files = new File(batchDirectory).listFiles(new BatchFilenameFilter());
        assertEquals("Number of groups is wrong: " + Arrays.toString(files), fileCount, files.length);

        List<EntryHolder> sortedEntryTransactions = new ArrayList<EntryHolder>();
        for (File file : files) {
            BufferedReader br = null;
            try {
                br = new BufferedReader(new FileReader(file));
                String type = file.getName().replaceFirst(GeneralLedgerConstants.BatchFileSystem.EXTENSION, "");
                // FIXME have to do something to bring types and filenames into sync, and probably have to remove separator character too
                String line = null;

                while ((line = br.readLine()) != null) {
                    sortedEntryTransactions.add(new EntryHolder(type, line));
                }
            } catch (FileNotFoundException fnfe) {
                throw new RuntimeException(fnfe);
            } catch (IOException ioe) {
                throw new RuntimeException(ioe);
            }
        }

        // now, sort the lines here to avoid any DB sorting issues
        //in the origin entry group version, this would sort the groups in order of creation, but in the files version,
        //it will sort the files in alphabetical order by type; entries within the groups (files) should be sorted the same
        //I don't foresee a problem with this...
        Comparator<EntryHolder> entryHolderComparator = new Comparator<EntryHolder>() {
            @Override
            public int compare(EntryHolder o1, EntryHolder o2) {
                int groupCompareResult = o1.baseFileName.compareTo(o2.baseFileName);
                if (groupCompareResult == 0) {
                    return o1.transactionLine.compareTo(o2.transactionLine);
                } else {
                    return groupCompareResult;
                }
            }
        };
        Collections.sort(sortedEntryTransactions, entryHolderComparator);
        Arrays.sort(requiredEntries, entryHolderComparator);

        // This is for debugging purposes - change to true for output
        System.err.println("Files:");
        for (File file : files) {
            System.err.println("F:" + file.getName());
        }

        System.err.println("Transactions:");
        for (EntryHolder element : sortedEntryTransactions) {
            System.err.println("L:" + element.baseFileName + " " + element.transactionLine);
        }
        System.err.println("Expected Transactions:");
        for (EntryHolder element : requiredEntries) {
            System.err.println("L:" + element.baseFileName + " " + element.transactionLine);
        }

        assertEquals("Wrong number of transactions in Origin Entry\nExpected: " + requiredEntries + "\nActual: " + sortedEntryTransactions, requiredEntries.length, sortedEntryTransactions.size());

        int count = 0;
        for (EntryHolder foundTransaction : sortedEntryTransactions) {

            // Check file type
            assertEquals("Group for transaction " + count + " is wrong", requiredEntries[count].baseFileName, foundTransaction.baseFileName);

            // Check transaction - this is done this way so that Anthill prints the two transactions to make
            // resolving the issue easier.

            String expected = requiredEntries[count].transactionLine.substring(0, 173);// trim();
            String found = foundTransaction.transactionLine.substring(0, 173);// trim();

            if (!found.equals(expected)) {
                System.err.println("Expected transaction: " + expected);
                System.err.println("Found transaction:    " + found);

                assertEquals("Transaction " + count + " doesn't match expected output", expected, found);
            }
            count++;
        }
    }

    protected static Object[] FLEXIBLE_OFFSET_ENABLED_FLAG = {OffsetDefinition.class, KFSConstants.SystemGroupParameterNames.FLEXIBLE_OFFSET_ENABLED_FLAG};

    /**
     * Resets the flexible offset and flexible claim on cash parameters, so that processes running as unit tests have consistent behaviors
     *
     * @throws Exception if the parameters could not be reset for some reason
     */
    protected void resetAllEnhancementFlags() throws Exception {
        setApplicationConfigurationFlag((Class<?>) FLEXIBLE_OFFSET_ENABLED_FLAG[0], (String) FLEXIBLE_OFFSET_ENABLED_FLAG[1], false);
    }

    /**
     * Resets a parameter for the sake of the unit test
     *
     * @param componentClass the module class of the parameter
     * @param name           the name of the parameter to reset
     * @param value          the new value for the parameter
     * @throws Exception thrown if some vague thing goes wrong
     */
    protected void setApplicationConfigurationFlag(Class<?> componentClass, String name, boolean value) throws Exception {
        TestUtils.setSystemParameter(componentClass, name, value ? "Y" : "N");
    }

}
