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

import org.kuali.kfs.kns.service.DataDictionaryService;
import org.kuali.kfs.kns.service.TransactionalDocumentDictionaryService;
import org.kuali.kfs.krad.document.Document;
import org.kuali.kfs.krad.exception.DocumentAuthorizationException;
import org.kuali.kfs.krad.exception.ValidationException;
import org.kuali.kfs.krad.service.DocumentService;
import org.kuali.kfs.krad.service.PersistenceService;
import org.kuali.kfs.krad.util.GlobalVariables;
import org.kuali.kfs.sys.ConfigureContext;
import org.kuali.kfs.sys.DocumentTestUtils;
import org.kuali.kfs.sys.KFSKeyConstants;
import org.kuali.kfs.sys.businessobject.SourceAccountingLine;
import org.kuali.kfs.sys.businessobject.TargetAccountingLine;
import org.kuali.kfs.sys.context.KualiTestBase;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.kfs.sys.document.AccountingDocument;
import org.kuali.kfs.sys.document.AccountingDocumentTestUtils;
import org.kuali.kfs.sys.fixture.AccountingLineFixture;
import org.kuali.kfs.sys.fixture.UserNameFixture;
import org.kuali.rice.core.api.util.type.KualiDecimal;

import java.util.ArrayList;
import java.util.List;

import static org.kuali.kfs.sys.document.AccountingDocumentTestUtils.approveDocument;
import static org.kuali.kfs.sys.document.AccountingDocumentTestUtils.routeDocument;
import static org.kuali.kfs.sys.document.AccountingDocumentTestUtils.testGetNewDocument_byDocumentClass;
import static org.kuali.kfs.sys.fixture.AccountingLineFixture.LINE2;
import static org.kuali.kfs.sys.fixture.AccountingLineFixture.LINE3;
import static org.kuali.kfs.sys.fixture.UserNameFixture.khuntley;
import static org.kuali.kfs.sys.fixture.UserNameFixture.rorenfro;

/**
 * This class is used to test InternalBillingDocument.
 */
@ConfigureContext(session = khuntley)
public class InternalBillingDocumentTest extends KualiTestBase {
    protected static final Class<InternalBillingDocument> DOCUMENT_CLASS = InternalBillingDocument.class;

    protected Document getDocumentParameterFixture() throws Exception {
        return DocumentTestUtils.createDocument(SpringContext.getBean(DocumentService.class), InternalBillingDocument.class);
    }

    protected List<AccountingLineFixture> getTargetAccountingLineParametersFromFixtures() {
        List<AccountingLineFixture> list = new ArrayList<AccountingLineFixture>();
        list.add(LINE2);
        list.add(LINE3);
        list.add(LINE2);
        return list;
    }

    protected List<AccountingLineFixture> getSourceAccountingLineParametersFromFixtures() {
        List<AccountingLineFixture> list = new ArrayList<AccountingLineFixture>();
        list.add(LINE2);
        list.add(LINE3);
        list.add(LINE2);
        return list;
    }


    protected int getExpectedPrePeCount() {
        return 12;
    }

    @ConfigureContext(session = khuntley, shouldCommitTransactions = true)
    public final void testApprove_addAccessibleAccount_ChangingTotals() throws Exception {
        AccountingDocument retrieved;
        AccountingDocument original;
        String docId;

        // switch user to WESPRICE, build and route document with
        // accountingLines
        changeCurrentUser(getInitialUserName());
        original = buildDocument();
        routeDocument(original, SpringContext.getBean(DocumentService.class));
        docId = original.getDocumentNumber();

        // switch user to another user, add accountingLines for accounts not
        // controlled by this user
        changeCurrentUser(getTestUserName());
        retrieved = (AccountingDocument) SpringContext.getBean(DocumentService.class).getByDocumentHeaderId(docId);
        retrieved.addSourceAccountingLine(getSourceAccountingLineAccessibleAccount());
        retrieved.addTargetAccountingLine(getTargetAccountingLineAccessibleAccount());

        boolean failedAsExpected = false;
        try {
            approveDocument(retrieved, SpringContext.getBean(DocumentService.class));
        } catch (ValidationException e) {
            failedAsExpected = true;
        } catch (DocumentAuthorizationException dae) {
            // this means that the workflow status didn't change in time for the check, so this is
            // an expected exception
            failedAsExpected = true;
        }
        assertTrue("document should have failed validation: " + dumpMessageMapErrors(), failedAsExpected);
    }

    @ConfigureContext(session = khuntley, shouldCommitTransactions = true)
    public final void testApprove_addInaccessibleAccount_sourceLine() throws Exception {
        // switch user to WESPRICE, build and route document with
        // accountingLines
        AccountingDocument original;
        AccountingDocument retrieved;
        String docId;

        changeCurrentUser(getInitialUserName());
        original = buildDocument();
        routeDocument(original, SpringContext.getBean(DocumentService.class));
        docId = original.getDocumentNumber();

        // switch user to AHORNICK, add sourceAccountingLine for account not controlled by this user
        // (and add a balancing targetAccountingLine for an accessible account)
        changeCurrentUser(getTestUserName());
        retrieved = (AccountingDocument) SpringContext.getBean(DocumentService.class).getByDocumentHeaderId(docId);
        retrieved.addSourceAccountingLine(getSourceAccountingLineInaccessibleAccount());
        retrieved.addTargetAccountingLine(getTargetAccountingLineAccessibleAccount());

        // approve document, wait for failure b/c totals have changed
        boolean failedAsExpected = false;
        try {
            approveDocument(retrieved, SpringContext.getBean(DocumentService.class));
        } catch (ValidationException e) {
            failedAsExpected = true;
        } catch (DocumentAuthorizationException dae) {
            // this means that the workflow status didn't change in time for the check, so this is
            // an expected exception
            failedAsExpected = true;
        }
        assertTrue("document should have failed validation: " + dumpMessageMapErrors(), failedAsExpected);
    }

    @ConfigureContext(session = khuntley, shouldCommitTransactions = true)
    public final void testApprove_addInaccessibleAccount_targetLine() throws Exception {
        AccountingDocument retrieved;
        AccountingDocument original;
        String docId;

        // switch user to WESPRICE, build and route document with
        // accountingLines
        changeCurrentUser(getInitialUserName());
        original = buildDocument();
        routeDocument(original, SpringContext.getBean(DocumentService.class));
        docId = original.getDocumentNumber();

        // switch user to AHORNICK, add targetAccountingLine for accounts not
        // controlled by this user
        // (and add a balancing sourceAccountingLine for an accessible account)
        changeCurrentUser(getTestUserName());
        retrieved = (AccountingDocument) SpringContext.getBean(DocumentService.class).getByDocumentHeaderId(docId);
        retrieved.addTargetAccountingLine(getTargetAccountingLineInaccessibleAccount());
        retrieved.addSourceAccountingLine(getSourceAccountingLineAccessibleAccount());

        // approve document, wait for failure
        boolean failedAsExpected = false;
        try {
            approveDocument(retrieved, SpringContext.getBean(DocumentService.class));
        } catch (ValidationException e) {
            failedAsExpected = true;
        } catch (DocumentAuthorizationException dae) {
            // this means that the workflow status didn't change in time for the check, so this is
            // an expected exception
            failedAsExpected = true;
        }
        assertTrue("document should have failed validation: " + dumpMessageMapErrors(), failedAsExpected);
    }

    /**
     * This method...
     *
     * @throws Exception
     */
    @ConfigureContext(session = khuntley, shouldCommitTransactions = true)
    public final void testApprove_deleteAccessibleAccount() throws Exception {
        // switch user to WESPRICE, build and route document with
        // accountingLines
        AccountingDocument retrieved;
        AccountingDocument original;
        String docId;

        changeCurrentUser(getInitialUserName());
        original = buildDocument();
        routeDocument(original, SpringContext.getBean(DocumentService.class));
        docId = original.getDocumentNumber();

        // switch user to AHORNICK, delete sourceAccountingLine for accounts
        // controlled by this user
        // (and delete matching targetAccountingLine, for balance)
        changeCurrentUser(getTestUserName());
        retrieved = (AccountingDocument) SpringContext.getBean(DocumentService.class).getByDocumentHeaderId(docId);
        deleteSourceAccountingLine(retrieved, 0);
        deleteTargetAccountingLine(retrieved, 0);

        // approve document, wait for failure b/c totals have changed
        boolean failedAsExpected = false;
        try {
            approveDocument(retrieved, SpringContext.getBean(DocumentService.class));
        } catch (ValidationException e) {
            failedAsExpected = true;
        } catch (DocumentAuthorizationException dae) {
            // this means that the workflow status didn't change in time for the check, so this is
            // an expected exception
            failedAsExpected = true;
        }
        assertTrue("document should have failed validation: " + dumpMessageMapErrors(), failedAsExpected);
    }

    @ConfigureContext(session = khuntley, shouldCommitTransactions = true)
    public final void testApprove_updateAccessibleAccount() throws Exception {
        // switch user to WESPRICE, build and route document with
        // accountingLines
        AccountingDocument retrieved;
        AccountingDocument original;
        String docId;

        changeCurrentUser(getInitialUserName());
        original = buildDocument();
        routeDocument(original, SpringContext.getBean(DocumentService.class));
        docId = original.getDocumentNumber();

        // switch user to RORENFRO, update sourceAccountingLine for accounts
        // controlled by this user
        // (and delete update targetAccountingLine, for balance)
        changeCurrentUser(getTestUserName());
        retrieved = (AccountingDocument) SpringContext.getBean(DocumentService.class).getByDocumentHeaderId(docId);

        // make sure totals don't change
        KualiDecimal originalSourceLineAmt = retrieved.getSourceAccountingLine(0).getAmount();
        KualiDecimal originalTargetLineAmt = retrieved.getTargetAccountingLine(0).getAmount();
        KualiDecimal newSourceLineAmt = originalSourceLineAmt.divide(new KualiDecimal(2));
        KualiDecimal newTargetLineAmt = originalTargetLineAmt.divide(new KualiDecimal(2));

        // update existing lines with new amount which equals orig divided by 2
        updateSourceAccountingLine(retrieved, 0, newSourceLineAmt.toString());
        updateTargetAccountingLine(retrieved, 0, newTargetLineAmt.toString());

        // add in new lines and change amounts
        TargetAccountingLine newTargetLine = getTargetAccountingLineAccessibleAccount();
        newTargetLine.setAmount(newTargetLineAmt);
        SourceAccountingLine newSourceLine = getSourceAccountingLineAccessibleAccount();
        newSourceLine.setAmount(newSourceLineAmt);
        retrieved.addTargetAccountingLine(newTargetLine);
        retrieved.addSourceAccountingLine(newSourceLine);

        try {
            approveDocument(retrieved, SpringContext.getBean(DocumentService.class));
        } catch (DocumentAuthorizationException dae) {
            // this means that the workflow status didn't change in time for the check, so this is
            // an expected exception
        } catch (ValidationException ex) {
            fail("Business Rules Failed: " + dumpMessageMapErrors());
        }
    }

    @ConfigureContext(session = khuntley, shouldCommitTransactions = true)
    public final void testApprove_updateInaccessibleAccount_sourceLine() throws Exception {
        AccountingDocument retrieved;
        AccountingDocument original;
        String docId;

        // switch user to WESPRICE, build and route document with
        // accountingLines
        changeCurrentUser(getInitialUserName());
        original = buildDocument();
        routeDocument(original, SpringContext.getBean(DocumentService.class));
        docId = original.getDocumentNumber();

        // switch user to AHORNICK, update sourceAccountingLines for accounts
        // not controlled by this user
        // (and update matching accessible targetLine, for balance)
        changeCurrentUser(getTestUserName());
        retrieved = (AccountingDocument) SpringContext.getBean(DocumentService.class).getByDocumentHeaderId(docId);
        updateSourceAccountingLine(retrieved, 1, "3.14");
        updateTargetAccountingLine(retrieved, 0, "3.14");

        // clear the OJB cache, otherwise when the original is retrieved later, it matches the updated doc and the
        // system doesn't think an update occurred.
        SpringContext.getBean(PersistenceService.class, "persistenceServiceOjb").clearCache();

        // approve document, wait for failure
        boolean failedAsExpected = false;
        try {
            approveDocument(retrieved, SpringContext.getBean(DocumentService.class));
        } catch (ValidationException e) {
            failedAsExpected = GlobalVariables.getMessageMap().containsMessageKey(KFSKeyConstants.ERROR_ACCOUNTINGLINE_INACCESSIBLE_UPDATE);
        }
        assertTrue("document should have failed validation: " + dumpMessageMapErrors(), failedAsExpected);
    }

    @ConfigureContext(session = khuntley, shouldCommitTransactions = true)
    public final void testApprove_updateInaccessibleAccount_targetLine() throws Exception {
        // switch user to WESPRICE, build and route document with
        // accountingLines
        AccountingDocument retrieved;
        AccountingDocument original;
        String docId;

        changeCurrentUser(getInitialUserName());
        original = buildDocument();
        routeDocument(original, SpringContext.getBean(DocumentService.class));
        docId = original.getDocumentNumber();

        // switch user to AHORNICK, update targetAccountingLine for accounts
        // not controlled by this user
        // (and update matching accessible sourceLine, for balance)
        changeCurrentUser(getTestUserName());
        retrieved = (AccountingDocument) SpringContext.getBean(DocumentService.class).getByDocumentHeaderId(docId);
        updateTargetAccountingLine(retrieved, 1, "2.81");
        updateSourceAccountingLine(retrieved, 0, "2.81");

        // clear the OJB cache, otherwise when the original is retrieved later, it matches the updated doc and the
        // system doesn't think an update occurred.
        SpringContext.getBean(PersistenceService.class, "persistenceServiceOjb").clearCache();

        // approve document, wait for failure
        boolean failedAsExpected = false;
        try {
            approveDocument(retrieved, SpringContext.getBean(DocumentService.class));
        } catch (ValidationException e) {
            failedAsExpected = GlobalVariables.getMessageMap().containsMessageKey(KFSKeyConstants.ERROR_ACCOUNTINGLINE_INACCESSIBLE_UPDATE);
        } catch (DocumentAuthorizationException dae) {
            // this means that the workflow status didn't change in time for the check, so this is
            // an expected exception
            failedAsExpected = true;
        }
        assertTrue("Test did not fail as expected: " + dumpMessageMapErrors(), failedAsExpected);
    }


    public final void testAddAccountingLine() throws Exception {
        List<SourceAccountingLine> sourceLines = generateSouceAccountingLines();
        List<TargetAccountingLine> targetLines = generateTargetAccountingLines();
        int expectedSourceTotal = sourceLines.size();
        int expectedTargetTotal = targetLines.size();
        AccountingDocumentTestUtils.testAddAccountingLine(DocumentTestUtils.createDocument(SpringContext.getBean(DocumentService.class), DOCUMENT_CLASS), sourceLines, targetLines, expectedSourceTotal, expectedTargetTotal);
    }


    public final void testGetNewDocument() throws Exception {
        testGetNewDocument_byDocumentClass(DOCUMENT_CLASS, SpringContext.getBean(DocumentService.class));
    }

    public final void testConvertIntoCopy_copyDisallowed() throws Exception {
        AccountingDocumentTestUtils.testConvertIntoCopy_copyDisallowed(buildDocument(), SpringContext.getBean(DataDictionaryService.class));

    }

    public final void testConvertIntoErrorCorrection_documentAlreadyCorrected() throws Exception {
        AccountingDocumentTestUtils.testConvertIntoErrorCorrection_documentAlreadyCorrected(buildDocument(), SpringContext.getBean(TransactionalDocumentDictionaryService.class));
    }

    public final void testConvertIntoErrorCorrection_errorCorrectionDisallowed() throws Exception {
        AccountingDocumentTestUtils.testConvertIntoErrorCorrection_errorCorrectionDisallowed(buildDocument(), SpringContext.getBean(DataDictionaryService.class));
    }

    @ConfigureContext(session = khuntley, shouldCommitTransactions = true)
    public final void testConvertIntoErrorCorrection() throws Exception {
        AccountingDocumentTestUtils.testConvertIntoErrorCorrection(buildDocument(), getExpectedPrePeCount(), SpringContext.getBean(DocumentService.class), SpringContext.getBean(TransactionalDocumentDictionaryService.class));
    }

    @ConfigureContext(session = khuntley, shouldCommitTransactions = true)
    public final void testRouteDocument() throws Exception {
        AccountingDocumentTestUtils.testRouteDocument(buildDocument(), SpringContext.getBean(DocumentService.class));
    }

    @ConfigureContext(session = khuntley, shouldCommitTransactions = true)
    public final void testSaveDocument() throws Exception {
        AccountingDocumentTestUtils.testSaveDocument(buildDocument(), SpringContext.getBean(DocumentService.class));
    }

    @ConfigureContext(session = khuntley, shouldCommitTransactions = true)
    public final void testConvertIntoCopy() throws Exception {
        AccountingDocumentTestUtils.testConvertIntoCopy(buildDocument(), SpringContext.getBean(DocumentService.class), getExpectedPrePeCount());
    }


    // test util methods
    protected List<SourceAccountingLine> generateSouceAccountingLines() throws Exception {
        List<SourceAccountingLine> sourceLines = new ArrayList<SourceAccountingLine>();
        // set accountinglines to document
        for (AccountingLineFixture sourceFixture : getSourceAccountingLineParametersFromFixtures()) {
            sourceLines.add(sourceFixture.createSourceAccountingLine());
        }

        return sourceLines;
    }

    protected List<TargetAccountingLine> generateTargetAccountingLines() throws Exception {
        List<TargetAccountingLine> targetLines = new ArrayList<TargetAccountingLine>();
        for (AccountingLineFixture targetFixture : getTargetAccountingLineParametersFromFixtures()) {
            targetLines.add(targetFixture.createTargetAccountingLine());
        }

        return targetLines;
    }

    protected InternalBillingDocument buildDocument() throws Exception {
        // put accounting lines into document parameter for later
        InternalBillingDocument document = (InternalBillingDocument) getDocumentParameterFixture();

        // set accountinglines to document
        for (AccountingLineFixture sourceFixture : getSourceAccountingLineParametersFromFixtures()) {
            sourceFixture.addAsSourceTo(document);
        }

        for (AccountingLineFixture targetFixture : getTargetAccountingLineParametersFromFixtures()) {
            targetFixture.addAsTargetTo(document);
        }

        return document;
    }

    protected void updateSourceAccountingLine(AccountingDocument document, int index, String newAmount) {
        SourceAccountingLine sourceLine = document.getSourceAccountingLine(index);
        sourceLine.setAmount(new KualiDecimal(newAmount));
    }

    protected void updateTargetAccountingLine(AccountingDocument document, int index, String newAmount) {
        TargetAccountingLine targetLine = document.getTargetAccountingLine(index);
        targetLine.setAmount(new KualiDecimal(newAmount));
    }


    protected void deleteSourceAccountingLine(AccountingDocument document, int index) {
        List sourceLines = document.getSourceAccountingLines();
        sourceLines.remove(index);
        document.setSourceAccountingLines(sourceLines);
    }

    protected void deleteTargetAccountingLine(AccountingDocument document, int index) {
        List targetLines = document.getTargetAccountingLines();
        targetLines.remove(index);
        document.setTargetAccountingLines(targetLines);
    }

    protected UserNameFixture getInitialUserName() {
        return khuntley; // replace rjweiss with khuntley
    }

    protected UserNameFixture getTestUserName() {
        return rorenfro;
    }

    protected SourceAccountingLine getSourceAccountingLineAccessibleAccount() throws Exception {
        return LINE2.createSourceAccountingLine();
    }

    protected TargetAccountingLine getTargetAccountingLineInaccessibleAccount() throws Exception {
        return LINE3.createTargetAccountingLine();
    }

    protected TargetAccountingLine getTargetAccountingLineAccessibleAccount() throws Exception {
        return LINE2.createTargetAccountingLine();
    }

    protected SourceAccountingLine getSourceAccountingLineInaccessibleAccount() throws Exception {
        return LINE3.createSourceAccountingLine();
    }

}

