/*
 * 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.module.ar.document;

import org.kuali.kfs.krad.UserSession;
import org.kuali.kfs.krad.exception.ValidationException;
import org.kuali.kfs.krad.service.BusinessObjectService;
import org.kuali.kfs.krad.service.DocumentService;
import org.kuali.kfs.krad.util.GlobalVariables;
import org.kuali.kfs.krad.util.MessageMap;
import org.kuali.kfs.module.ar.ArConstants;
import org.kuali.kfs.module.ar.businessobject.AccountsReceivableDocumentHeader;
import org.kuali.kfs.module.ar.businessobject.CashControlDetail;
import org.kuali.kfs.module.ar.businessobject.CustomerInvoiceDetail;
import org.kuali.kfs.module.ar.businessobject.InvoicePaidApplied;
import org.kuali.kfs.module.ar.businessobject.NonAppliedHolding;
import org.kuali.kfs.module.ar.businessobject.NonInvoiced;
import org.kuali.kfs.module.ar.document.service.CashControlDocumentService;
import org.kuali.kfs.module.ar.document.service.CustomerInvoiceDocumentTestUtil;
import org.kuali.kfs.module.ar.document.service.PaymentApplicationDocumentService;
import org.kuali.kfs.module.ar.fixture.CustomerInvoiceDetailFixture;
import org.kuali.kfs.module.ar.fixture.CustomerInvoiceDocumentFixture;
import org.kuali.kfs.sys.ConfigureContext;
import org.kuali.kfs.sys.DocumentTestUtils;
import org.kuali.kfs.sys.businessobject.ChartOrgHolder;
import org.kuali.kfs.sys.context.KualiTestBase;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.kfs.sys.fixture.UserNameFixture;
import org.kuali.kfs.sys.service.FinancialSystemUserService;
import org.kuali.kfs.sys.service.UniversityDateService;
import org.kuali.rice.core.api.datetime.DateTimeService;
import org.kuali.rice.core.api.util.type.KualiDecimal;
import org.kuali.rice.kew.api.exception.WorkflowException;

import java.sql.Date;
import java.util.ArrayList;
import java.util.List;

import static org.kuali.kfs.sys.fixture.UserNameFixture.khuntley;

@ConfigureContext(session = khuntley)
public class PaymentApplicationDocumentTest extends KualiTestBase {
    static protected final String ANNOTATION = "PaymentApplicationDocument testing";
    static protected final String TESTING = "PaymentApplicationDocument testing";
    protected DocumentService documentService;
    protected BusinessObjectService businessObjectService;
    protected CashControlDocumentService cashControlDocumentService;
    protected PaymentApplicationDocumentService paymentApplicationDocumentService;
    protected DateTimeService dateTimeService;
    protected UniversityDateService universityDateService;

    @Override
    protected void setUp() throws Exception {
        super.setUp();
        documentService = SpringContext.getBean(DocumentService.class);
        businessObjectService = SpringContext.getBean(BusinessObjectService.class);
        cashControlDocumentService = SpringContext.getBean(CashControlDocumentService.class);
        paymentApplicationDocumentService = SpringContext.getBean(PaymentApplicationDocumentService.class);
        dateTimeService = SpringContext.getBean(DateTimeService.class);
        universityDateService = SpringContext.getBean(UniversityDateService.class);
    }

    // Test that payment application documents are created for each cash control detail
    public void testCreatedUponApprovingCashControlDocument() throws Exception {
        List<CashControlDetailSpec> specs = new ArrayList<CashControlDetailSpec>();
        specs.add(new CashControlDetailSpec("ABB2", "9999", new Date(System.currentTimeMillis()), new KualiDecimal(1000)));

        CashControlDocument cashControlDocument = createAndSaveNewCashControlDocument(specs);

        for (CashControlDetail detail : cashControlDocument.getCashControlDetails()) {
            assertNotNull("Reference financial document should not have been null on line: " + detail, detail.getReferenceFinancialDocument());
        }
    }


    public void testExactlyApplyingSucceeds() throws Exception {
        // Set our customer up to owe $1
        CustomerInvoiceDetailFixture[] invoiceDetailFixtures =
            new CustomerInvoiceDetailFixture[]{CustomerInvoiceDetailFixture.ONE_DOLLAR_INVOICE_DETAIL};

        // Receive a payment of $1 from the Customer.
        CashControlDetailSpec[] cashControlDetailSpecs = new CashControlDetailSpec[]{
            CashControlDetailSpec.specFor(new KualiDecimal(1))
        };

        KualiDecimal[] amountsToApply = new KualiDecimal[]{
            new KualiDecimal(1)
        };

        assertTrue(applyFundsToPaymentApplication(invoiceDetailFixtures, cashControlDetailSpecs, amountsToApply, null, null));
    }

    // FIXME TODO This should succeed when saving but fail when submitting.
    public void testUnderApplyingFailsWithoutUnapplied() throws Exception {
        // Set our customer up to owe $10
        CustomerInvoiceDetailFixture[] invoiceDetailFixtures =
            new CustomerInvoiceDetailFixture[]{CustomerInvoiceDetailFixture.TEN_DOLLAR_INVOICE_DETAIL};

        // Receive a payment of $10 from the Customer.
        CashControlDetailSpec[] cashControlDetailSpecs = new CashControlDetailSpec[]{
            CashControlDetailSpec.specFor(new KualiDecimal(10))
        };

        KualiDecimal[] amountsToApply = new KualiDecimal[]{
            new KualiDecimal(1)
        };

        assertTrue(applyFundsToPaymentApplication(invoiceDetailFixtures, cashControlDetailSpecs, amountsToApply, null, null));

        assertFalse(applyFundsToPaymentApplication(invoiceDetailFixtures, cashControlDetailSpecs, amountsToApply, null, null, true, false));
    }

    public void testUnderApplyingSucceedsWithUnapplied() throws Exception {
        // Set our customer up to owe $10
        CustomerInvoiceDetailFixture[] invoiceDetailFixtures =
            new CustomerInvoiceDetailFixture[]{CustomerInvoiceDetailFixture.TEN_DOLLAR_INVOICE_DETAIL};

        // Receive a payment of $10 from the Customer.
        CashControlDetailSpec[] cashControlDetailSpecs = new CashControlDetailSpec[]{
            CashControlDetailSpec.specFor(new KualiDecimal(10))
        };

        KualiDecimal[] amountsToApply = new KualiDecimal[]{
            new KualiDecimal(1)
        };

        KualiDecimal[] unappliedAmounts = new KualiDecimal[]{
            new KualiDecimal(9)
        };

        assertTrue(applyFundsToPaymentApplication(invoiceDetailFixtures, cashControlDetailSpecs, amountsToApply, null, null));
    }

    public void testOverApplyingFails() throws Exception {

        // Set our customer up to owe $1
        CustomerInvoiceDetailFixture[] invoiceDetailFixtures =
            new CustomerInvoiceDetailFixture[]{CustomerInvoiceDetailFixture.ONE_DOLLAR_INVOICE_DETAIL};

        // Receive a payment of $2 from the Customer.
        CashControlDetailSpec[] cashControlDetailSpecs = new CashControlDetailSpec[]{
            CashControlDetailSpec.specFor(new KualiDecimal(2))
        };

        KualiDecimal[] amountsToApply = new KualiDecimal[]{
            new KualiDecimal(2)
        };

        assertFalse(applyFundsToPaymentApplication(invoiceDetailFixtures, cashControlDetailSpecs, amountsToApply, null, null));
    }

    public void testAddingNonInvoicedLinesSucceedsWhenBalanced() throws Exception {

        // Set our customer up to owe $10
        CustomerInvoiceDetailFixture[] invoiceDetailFixtures =
            new CustomerInvoiceDetailFixture[]{CustomerInvoiceDetailFixture.TEN_DOLLAR_INVOICE_DETAIL};

        // Receive a payment of $2 from the Customer.
        CashControlDetailSpec[] cashControlDetailSpecs = new CashControlDetailSpec[]{
            CashControlDetailSpec.specFor(new KualiDecimal(2))
        };

        NonInvoiced[] nonInvoiceds = new NonInvoiced[]{
            buildNonInvoiced("BL", "0212002", "0795", new KualiDecimal(2))
        };

        assertTrue(applyFundsToPaymentApplication(invoiceDetailFixtures, cashControlDetailSpecs, null, null, nonInvoiceds));
    }

    // ------
    // ------ Utility methods. Non test methods.
    // ------

    @Override
    protected void changeCurrentUser(UserNameFixture sessionUser) throws Exception {
        GlobalVariables.setUserSession(new UserSession(sessionUser.toString()));
    }

    protected CashControlDocument createAndSaveNewCashControlDocument(CashControlDetailSpec[] specs) throws WorkflowException {
        List<CashControlDetailSpec> _specs = new ArrayList<CashControlDetailSpec>();
        for (CashControlDetailSpec spec : specs) {
            _specs.add(spec);
        }
        return createAndSaveNewCashControlDocument(_specs);
    }

    protected CashControlDocument createAndSaveNewCashControlDocument(List<CashControlDetailSpec> specs) throws WorkflowException {
        CashControlDocument cashControlDocument = createNewCashControlDocument(specs);
        documentService.saveDocument(cashControlDocument);
        return cashControlDocument;
    }

    protected CashControlDocument createNewCashControlDocument(CashControlDetailSpec[] specs) throws WorkflowException {
        List<CashControlDetailSpec> _specs = new ArrayList<CashControlDetailSpec>();
        for (CashControlDetailSpec spec : specs) {
            _specs.add(spec);
        }
        return createNewCashControlDocument(_specs);
    }

    protected CashControlDocument createNewCashControlDocument(List<CashControlDetailSpec> specs) throws WorkflowException {
        CashControlDocument cashControlDocument = DocumentTestUtils.createDocument(SpringContext.getBean(DocumentService.class), CashControlDocument.class);// documentService.getNewDocument(CashControlDocument.class);
        cashControlDocument.getDocumentHeader().setDocumentDescription(TESTING);
        cashControlDocument.setUniversityFiscalYear(universityDateService.getCurrentFiscalYear());
        cashControlDocument.setCustomerPaymentMediumCode("CK");

        AccountsReceivableDocumentHeader arDocumentHeader = cashControlDocument.getAccountsReceivableDocumentHeader();
        arDocumentHeader.setDocumentNumber(cashControlDocument.getDocumentNumber());

        // Set the processing chart and org
        UserSession userSession = GlobalVariables.getUserSession();
        ChartOrgHolder organization = SpringContext.getBean(FinancialSystemUserService.class).getPrimaryOrganization(userSession.getPerson(), ArConstants.AR_NAMESPACE_CODE);
        arDocumentHeader.setProcessingChartOfAccountCode(organization.getChartOfAccountsCode());
        arDocumentHeader.setProcessingOrganizationCode(organization.getOrganizationCode());

        MessageMap e = GlobalVariables.getMessageMap();
        int errorCount = e.getNumberOfPropertiesWithErrors();
        try {
            documentService.saveDocument(cashControlDocument);
        } catch (Exception t) {
            fail("Document save failed: " + t.getClass().getName() + " : " + t.getMessage() + "\n" + dumpMessageMapErrors() + "\n" + cashControlDocument);
        }
        for (CashControlDetailSpec spec : specs) {
            CashControlDetail cashControlDetail = buildCashControlDetail(cashControlDocument, spec);
            cashControlDocumentService.addNewCashControlDetail(TESTING, cashControlDocument, cashControlDetail);
            PaymentApplicationDocument paymentApplicationDocument = cashControlDocumentService.createAndSavePaymentApplicationDocument(TESTING, cashControlDocument, cashControlDetail);
            cashControlDetail.setReferenceFinancialDocumentNumber(paymentApplicationDocument.getDocumentNumber());
        }
        return cashControlDocument;
    }

    protected CashControlDetail buildCashControlDetail(CashControlDocument cashControlDocument, CashControlDetailSpec spec) {
        CashControlDetail cashControlDetail = new CashControlDetail();
        cashControlDetail.setCashControlDocument(cashControlDocument);
        cashControlDetail.setDocumentNumber(cashControlDocument.getDocumentNumber());
        cashControlDetail.setCustomerNumber(spec.customerNumber);
        cashControlDetail.setCustomerPaymentMediumIdentifier(spec.customerPaymentMediumIdentifier);
        cashControlDetail.setCustomerPaymentDate(spec.customerPaymentDate);
        cashControlDetail.setFinancialDocumentLineAmount(spec.financialDocumentLineAmount);
        return cashControlDetail;
    }

    public InvoiceAndCashControlDocumentPair createCashControlDocument(CustomerInvoiceDetailFixture[] invoiceDetailFixtures, CashControlDetailSpec[] cashControlDetailSpecs) throws WorkflowException {

        // Create an invoice
        CustomerInvoiceDocument invoice =
            CustomerInvoiceDocumentTestUtil.submitNewCustomerInvoiceDocumentAndReturnIt(
                CustomerInvoiceDocumentFixture.BASE_CIDOC_WITH_CUSTOMER, invoiceDetailFixtures, null);

        // Create a cash control document as well.
        CashControlDocument cashControlDocument = createAndSaveNewCashControlDocument(cashControlDetailSpecs);

        return new InvoiceAndCashControlDocumentPair(invoice, cashControlDocument);
    }

    /**
     * This method will apply funds and save the payment application documents.
     * It will not route or approve the documents.
     *
     * @param invoiceDetailFixtures
     * @param cashControlDetailSpecs
     * @param amountsToApply
     * @param unappliedAmounts
     * @param nonInvoiceds
     * @return
     * @throws Exception
     */
    public boolean applyFundsToPaymentApplication(CustomerInvoiceDetailFixture[] invoiceDetailFixtures, CashControlDetailSpec[] cashControlDetailSpecs, KualiDecimal[] amountsToApply, KualiDecimal[] unappliedAmounts, NonInvoiced[] nonInvoiceds) throws Exception {
        return applyFundsToPaymentApplication(invoiceDetailFixtures, cashControlDetailSpecs, amountsToApply, unappliedAmounts, nonInvoiceds, false, false);
    }

    /**
     * This method will create save, route and approve payment application documents as specified.
     *
     * @param invoiceDetailFixtures
     * @param cashControlDetailSpecs
     * @param amountsToApply
     * @param unappliedAmounts
     * @param nonInvoiceds
     * @param routeDocument
     * @param approveDocument
     * @return
     * @throws Exception
     */
    public boolean applyFundsToPaymentApplication(CustomerInvoiceDetailFixture[] invoiceDetailFixtures, CashControlDetailSpec[] cashControlDetailSpecs, KualiDecimal[] amountsToApply, KualiDecimal[] unappliedAmounts, NonInvoiced[] nonInvoiceds, boolean routeDocument, boolean approveDocument) throws Exception {
        // Verify that we have an amount to apply to each invoice detail.
        if (null != amountsToApply && cashControlDetailSpecs.length != amountsToApply.length) {
            throw new Exception("The number of cash control detail specs must equal the number of amounts to apply.");
        }
        if (null != unappliedAmounts && cashControlDetailSpecs.length != unappliedAmounts.length) {
            throw new Exception("The number of cash control detail specs must equal the number of unapplied amounts.");
        }
        if (null != nonInvoiceds && cashControlDetailSpecs.length != nonInvoiceds.length) {
            throw new Exception("The number of cash control detail specs must equal the number of non-invoiced lines.");
        }

        // Create the invoice and cash control document we need to be able to test the payment application document.
        InvoiceAndCashControlDocumentPair pair = createCashControlDocument(invoiceDetailFixtures, cashControlDetailSpecs);
        CashControlDocument cashControlDocument = pair.cashControlDocument;
        CustomerInvoiceDocument invoice = pair.invoiceDocument;
        assertNotNull("invoiceDocument of the InvoiceAndCashControlDocumentPair must not be null", invoice);

        // Get convenient handles to the various relevant details.
        List<CashControlDetail> cashControlDetails = cashControlDocument.getCashControlDetails();
        List<CustomerInvoiceDetail> customerInvoiceDetails = invoice.getSourceAccountingLines();

        // Pick a sample invoice detail that we can apply payments to.
        CustomerInvoiceDetail sampleInvoiceDetail = customerInvoiceDetails.iterator().next();

        // Now try to apply too much money in receivables (cash control details) against the the outstanding balance (sample invoice detail)

        // counter allows us to match amounts to apply with specific invoice details
        int counter = 0;

        // aggregateOperationSucceeds allows us to measure the success of all operations in aggregate
        boolean aggregateOperationSucceeds = true;

        // Each cash control detail has a reference to a payment application document which records
        // the receipt of funds. To test the payment application document we modify these payment application
        // documents according to the arguments passed into this method.
        for (CashControlDetail cashControlDetail : cashControlDetails) {

            // payments are credit against the customer balance via the payment application document referenced from the cash control document.
            PaymentApplicationDocument paymentApplicationDocument = cashControlDetail.getReferenceFinancialDocument();

            // ------ Set invoice paid applieds

            // Make sure we've got an amount to apply directly to this document
            if (null != amountsToApply && counter < amountsToApply.length && null != amountsToApply[counter]) {
                // Create a new applied payment
                // Applying one big payment is just as good as applying a bunch of smaller payments from a testing perspective
                InvoicePaidApplied invoicePaidApplied = new InvoicePaidApplied();

                // set the document number for the invoice paid applied to the payment application document number.
                invoicePaidApplied.setDocumentNumber(paymentApplicationDocument.getDocumentNumber());

                // Set the invoice paid applied ref doc number to the document number for the customer invoice document
                invoicePaidApplied.setFinancialDocumentReferenceInvoiceNumber(invoice.getDocumentNumber());

                // Apply this payment to the sample invoice detail
                invoicePaidApplied.setInvoiceItemNumber(sampleInvoiceDetail.getInvoiceItemNumber());

                // Apply too much money (double the amount owed)
                // sampleInvoiceDetail.getAmount().multiply(new KualiDecimal(2))
                invoicePaidApplied.setInvoiceItemAppliedAmount(amountsToApply[counter]);

                invoicePaidApplied.setUniversityFiscalYear(universityDateService.getCurrentFiscalYear());
                invoicePaidApplied.setUniversityFiscalPeriodCode(universityDateService.getCurrentUniversityDate().getUniversityFiscalAccountingPeriod());
                invoicePaidApplied.setPaidAppliedItemNumber(cashControlDetailSpecs.length);

                // if there was not another invoice paid applied already created for the current detail then invoicePaidApplied will not be null
                if (invoicePaidApplied != null) {

                    // add it to the payment application document list of applied payments
                    paymentApplicationDocument.getInvoicePaidApplieds().add(invoicePaidApplied);

                    // set the new applied amount for the customer invoice detail
                    //sampleInvoiceDetail.setAmountToBeApplied(invoicePaidApplied.getInvoiceItemAppliedAmount());
                }
            }

            // ------ Set the unapplied amount

            // Make sure we've got an unapplied amount specified for this detail
            if (null != unappliedAmounts && counter < unappliedAmounts.length && null != unappliedAmounts[counter]) {
                NonAppliedHolding nonAppliedHolding = new NonAppliedHolding();
                nonAppliedHolding.setFinancialDocumentLineAmount(unappliedAmounts[counter]);
                nonAppliedHolding.setReferenceFinancialDocumentNumber(paymentApplicationDocument.getDocumentNumber());
                paymentApplicationDocument.setNonAppliedHolding(nonAppliedHolding);
            }

            // ------ Set the non-invoiced amount

            int nonInvoicedLineCounter = 1;
            if (null != nonInvoiceds && counter < nonInvoiceds.length && null != nonInvoiceds[counter]) {
                NonInvoiced nonInvoiced = nonInvoiceds[counter];
                nonInvoiced.setFinancialDocumentPostingYear(paymentApplicationDocument.getPostingYear());
                nonInvoiced.setDocumentNumber(paymentApplicationDocument.getDocumentNumber());
                nonInvoiced.setFinancialDocumentLineNumber(nonInvoicedLineCounter++);
                paymentApplicationDocument.getNonInvoiceds().add(nonInvoiced);
            }

            // Try to save the document
            try {
                documentService.saveDocument(paymentApplicationDocument);
            } catch (ValidationException validationException) {
                // Indicate a failures
                aggregateOperationSucceeds &= false;
            }

            // Try to route the document
            if (routeDocument) {
                try {
                    documentService.routeDocument(paymentApplicationDocument, "Unit tests", new ArrayList());
                } catch (ValidationException validationException) {
                    // Indicate a failures
                    aggregateOperationSucceeds &= false;
                }
            }

            // Try to approve the document
            if (approveDocument) {
                try {
                    documentService.approveDocument(paymentApplicationDocument, "Unit tests", new ArrayList());
                } catch (ValidationException validationException) {
                    // Indicate a failures
                    aggregateOperationSucceeds &= false;
                }
            }
        }

        return aggregateOperationSucceeds;
    }

    protected NonInvoiced buildNonInvoiced(String chartOfAccountsCode, String accountNumber, String financialObjectCode, KualiDecimal financialDocumentLineAmount) {
        NonInvoiced nonInvoiced = new NonInvoiced();
        nonInvoiced.setChartOfAccountsCode(chartOfAccountsCode);
        nonInvoiced.setAccountNumber(accountNumber);
        nonInvoiced.setFinancialObjectCode(financialObjectCode);
        nonInvoiced.setFinancialDocumentLineAmount(financialDocumentLineAmount);
        return nonInvoiced;
    }

    protected class InvoiceAndCashControlDocumentPair {
        CashControlDocument cashControlDocument;
        CustomerInvoiceDocument invoiceDocument;

        InvoiceAndCashControlDocumentPair() {
        }

        ;

        InvoiceAndCashControlDocumentPair(CustomerInvoiceDocument invoice, CashControlDocument cashControl) {
            this.invoiceDocument = invoice;
            this.cashControlDocument = cashControl;
        }
    }

    protected class NonInvoicedLineSpec {
        String chartCode;
        String accountNumber;
        String objectCode;
        KualiDecimal amount;

        NonInvoicedLineSpec() {
        }

        NonInvoicedLineSpec(String chartCode, String accountNumber, String objectCode, KualiDecimal amount) {
            this.chartCode = chartCode;
            this.accountNumber = accountNumber;
            this.objectCode = objectCode;
            this.amount = amount;
        }

    }

}

