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

import org.kuali.kfs.module.cab.CabConstants;
import org.kuali.kfs.module.cab.businessobject.PurchasingAccountsPayableActionHistory;
import org.kuali.kfs.module.cab.businessobject.PurchasingAccountsPayableDocument;
import org.kuali.kfs.module.cab.businessobject.PurchasingAccountsPayableItemAsset;
import org.kuali.kfs.module.cab.businessobject.PurchasingAccountsPayableLineAssetAccount;
import org.kuali.kfs.module.cab.fixture.PurchasingAccountsPayableDocumentFixture;
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.fixture.UserNameFixture;
import org.kuali.rice.core.api.util.type.KualiDecimal;

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


public class PurApLineServiceTest extends KualiTestBase {

    private PurApLineService purApLineService;
    private List<PurchasingAccountsPayableDocument> purApDocuments;
    private PurchasingAccountsPayableItemAsset percentItemAsset;
    private PurchasingAccountsPayableDocument preqDocumentWithSingleItemSingleAccount;
    private PurchasingAccountsPayableDocument preqDocumentWithTwoItemsSingleAccount;
    private PurchasingAccountsPayableDocument cmDocumentWithSingleItemTwoAccounts;

    private String ERROR_MERGE_OBJECT_SUB_TYPES_DIFFERENT = "objectSubTypes are different for Merge";
    private String ERROR_MERGE_OBJECT_SUB_TYPES_SAME = "objectSubTypes are the same for Merge";
    private String ERROR_ALLOCATE_OBJECT_SUB_TYPES_DIFFERENT = "objectSubTypes are different for Allocate";
    private String ERROR_ALLOCATE_OBJECT_SUB_TYPES_SAME = "objectSubTypes are the same for Allocate";
    private String ERROR_PROCESS_SPLIT_SINGLE_ACCOUNT = "process split error happens when one item has one account";
    private String ERROR_PROCESS_SPLIT_MULTIPLE_ACCOUNT = "process split error happens when one item has multiple accounts";
    private String ERROR_PROCESS_MERGE = "process merge error";
    private String ERROR_PROCESS_ALLOCATE = "process allocate error";

    @ConfigureContext(session = UserNameFixture.khuntley, shouldCommitTransactions = false)
    @Override
    protected void setUp() throws Exception {
        super.setUp();
        purApLineService = SpringContext.getBean(PurApLineService.class);

        prepareTestDataRecords();
    }

    private void prepareTestDataRecords() throws Exception {
        // create 2 PREQ documents and 1 CM document matching the same PO. the first PREQ document has two line items and all the
        // other document has one line item in each document. Each item has one accounting line associated with.
        purApDocuments = PurchasingAccountsPayableDocumentFixture.createPurApDocuments();
        preqDocumentWithTwoItemsSingleAccount = purApDocuments.get(0);
        preqDocumentWithSingleItemSingleAccount = purApDocuments.get(1);
        cmDocumentWithSingleItemTwoAccounts = purApDocuments.get(2);
        percentItemAsset = preqDocumentWithTwoItemsSingleAccount.getPurchasingAccountsPayableItemAssets().get(0);
    }

    public void testProcessPercentPayment() throws Exception {
        List<PurchasingAccountsPayableActionHistory> actionsTaken = new ArrayList<PurchasingAccountsPayableActionHistory>();
        purApLineService.processPercentPayment(percentItemAsset, actionsTaken);
        assertEquals(percentItemAsset.getAccountsPayableItemQuantity(), new KualiDecimal(1));
        assertEquals(percentItemAsset.getTotalCost(), getTotalCost(percentItemAsset));
        assertEquals(percentItemAsset.getUnitCost(), getTotalCost(percentItemAsset));
        // check action taken history
        assertEquals(actionsTaken.size(), 1);
        assertEquals(actionsTaken.get(0).getActionTypeCode(), CabConstants.Actions.PERCENT_PAYMENT);

    }

    public void testProcessPercentPayment_noAction() throws Exception {
        percentItemAsset.setAccountsPayableItemQuantity(new KualiDecimal(2));
        List<PurchasingAccountsPayableActionHistory> actionsTaken = new ArrayList<PurchasingAccountsPayableActionHistory>();
        purApLineService.processPercentPayment(percentItemAsset, actionsTaken);
        assertEquals(percentItemAsset.getAccountsPayableItemQuantity(), new KualiDecimal(2));
        // check action taken history
        assertEquals(actionsTaken.size(), 0);
    }

    private KualiDecimal getTotalCost(PurchasingAccountsPayableItemAsset item) {
        KualiDecimal totalCost = KualiDecimal.ZERO;
        for (PurchasingAccountsPayableLineAssetAccount account : item.getPurchasingAccountsPayableLineAssetAccounts()) {
            totalCost = totalCost.add(account.getItemAccountTotalAmount());
        }
        return totalCost;
    }

    public void testMergeLinesHasDifferentObjectSubTypes_True() {
        List<PurchasingAccountsPayableItemAsset> items = new ArrayList<PurchasingAccountsPayableItemAsset>();

        items.add(purApDocuments.get(0).getPurchasingAccountsPayableItemAssets().get(1));
        items.add(purApDocuments.get(1).getPurchasingAccountsPayableItemAssets().get(0));

        assertTrue(ERROR_MERGE_OBJECT_SUB_TYPES_DIFFERENT, purApLineService.mergeLinesHasDifferentObjectSubTypes(items));
    }

    public void testMergeLinesHasDifferentObjectSubTypes_False() {
        List<PurchasingAccountsPayableItemAsset> items = new ArrayList<PurchasingAccountsPayableItemAsset>();

        items.add(purApDocuments.get(0).getPurchasingAccountsPayableItemAssets().get(0));
        items.add(purApDocuments.get(0).getPurchasingAccountsPayableItemAssets().get(1));

        assertFalse(ERROR_MERGE_OBJECT_SUB_TYPES_SAME, purApLineService.mergeLinesHasDifferentObjectSubTypes(items));
    }

    public void testAllocateLinesHasDifferentObjectSubTypes_Ture() {
        List<PurchasingAccountsPayableItemAsset> items = new ArrayList<PurchasingAccountsPayableItemAsset>();

        items.add(purApDocuments.get(0).getPurchasingAccountsPayableItemAssets().get(0));
        items.add(purApDocuments.get(0).getPurchasingAccountsPayableItemAssets().get(1));

        PurchasingAccountsPayableItemAsset allocateSourceItem = purApDocuments.get(1).getPurchasingAccountsPayableItemAssets().get(0);
        assertTrue(ERROR_ALLOCATE_OBJECT_SUB_TYPES_DIFFERENT, purApLineService.allocateLinesHasDifferentObjectSubTypes(items, allocateSourceItem));
    }

    public void testAllocateLinesHasDifferentObjectSubTypes_False() {
        List<PurchasingAccountsPayableItemAsset> items = new ArrayList<PurchasingAccountsPayableItemAsset>();

        items.add(purApDocuments.get(0).getPurchasingAccountsPayableItemAssets().get(0));
        items.add(purApDocuments.get(2).getPurchasingAccountsPayableItemAssets().get(0));

        PurchasingAccountsPayableItemAsset allocateSourceItem = purApDocuments.get(0).getPurchasingAccountsPayableItemAssets().get(1);
        assertFalse(ERROR_ALLOCATE_OBJECT_SUB_TYPES_SAME, purApLineService.allocateLinesHasDifferentObjectSubTypes(items, allocateSourceItem));
    }

    public void testProcessSplit_ItemHasSingleAccount() {
        List<PurchasingAccountsPayableActionHistory> actionsTaken = new ArrayList<PurchasingAccountsPayableActionHistory>();

        PurchasingAccountsPayableItemAsset splitItemAsset = preqDocumentWithSingleItemSingleAccount.getPurchasingAccountsPayableItemAssets().get(0);
        int oldItemSize = preqDocumentWithSingleItemSingleAccount.getPurchasingAccountsPayableItemAssets().size();
        KualiDecimal oldQuantity = splitItemAsset.getAccountsPayableItemQuantity();
        KualiDecimal oldTotalCost = getTotalCost(splitItemAsset);

        splitItemAsset.setSplitQty(new KualiDecimal(1));
        purApLineService.processSplit(splitItemAsset, actionsTaken);

        // check new item created
        assertTrue(ERROR_PROCESS_SPLIT_SINGLE_ACCOUNT, ++oldItemSize == preqDocumentWithSingleItemSingleAccount.getPurchasingAccountsPayableItemAssets().size());
        // check split item new quantity
        assertTrue(ERROR_PROCESS_SPLIT_SINGLE_ACCOUNT, splitItemAsset.getAccountsPayableItemQuantity().equals(oldQuantity.subtract(new KualiDecimal(1))));
        // check the total quantity after split does not change
        KualiDecimal newQuantity = KualiDecimal.ZERO;
        for (PurchasingAccountsPayableItemAsset item : preqDocumentWithSingleItemSingleAccount.getPurchasingAccountsPayableItemAssets()) {
            newQuantity = newQuantity.add(item.getAccountsPayableItemQuantity());
        }
        assertEquals(oldQuantity, newQuantity);

        // check the total cost after split does not change
        KualiDecimal newTotalCost = KualiDecimal.ZERO;
        newTotalCost = newTotalCost.add(getTotalCost(preqDocumentWithSingleItemSingleAccount.getPurchasingAccountsPayableItemAssets().get(0)));
        newTotalCost = newTotalCost.add(getTotalCost(preqDocumentWithSingleItemSingleAccount.getPurchasingAccountsPayableItemAssets().get(1)));
        assertTrue(ERROR_PROCESS_SPLIT_SINGLE_ACCOUNT, oldTotalCost.equals(newTotalCost));

        // check the actionsTakenHistory has one entry for each account moved from source item to new
        assertTrue(ERROR_PROCESS_SPLIT_SINGLE_ACCOUNT, actionsTaken.size() == 1);
    }

    public void testProcessSplit_ItemHasTwoAccounts() {
        List<PurchasingAccountsPayableActionHistory> actionsTaken = new ArrayList<PurchasingAccountsPayableActionHistory>();

        PurchasingAccountsPayableItemAsset splitItemAsset = cmDocumentWithSingleItemTwoAccounts.getPurchasingAccountsPayableItemAssets().get(0);
        int oldItemSize = cmDocumentWithSingleItemTwoAccounts.getPurchasingAccountsPayableItemAssets().size();
        KualiDecimal oldQuantity = splitItemAsset.getAccountsPayableItemQuantity();
        KualiDecimal oldTotalCost = getTotalCost(splitItemAsset);

        splitItemAsset.setSplitQty(new KualiDecimal(1));
        purApLineService.processSplit(splitItemAsset, actionsTaken);

        // check new item created
        assertTrue(ERROR_PROCESS_SPLIT_MULTIPLE_ACCOUNT, ++oldItemSize == cmDocumentWithSingleItemTwoAccounts.getPurchasingAccountsPayableItemAssets().size());
        // check split item new quantity
        assertTrue(ERROR_PROCESS_SPLIT_MULTIPLE_ACCOUNT, splitItemAsset.getAccountsPayableItemQuantity().equals(oldQuantity.subtract(new KualiDecimal(1))));
        // check the total quantity after split does not change
        KualiDecimal newQuantity = KualiDecimal.ZERO;
        for (PurchasingAccountsPayableItemAsset item : cmDocumentWithSingleItemTwoAccounts.getPurchasingAccountsPayableItemAssets()) {
            newQuantity = newQuantity.add(item.getAccountsPayableItemQuantity());
        }
        assertEquals(oldQuantity, newQuantity);

        // check the total cost after split does not change
        KualiDecimal newTotalCost = KualiDecimal.ZERO;
        newTotalCost = newTotalCost.add(getTotalCost(cmDocumentWithSingleItemTwoAccounts.getPurchasingAccountsPayableItemAssets().get(0)));
        newTotalCost = newTotalCost.add(getTotalCost(cmDocumentWithSingleItemTwoAccounts.getPurchasingAccountsPayableItemAssets().get(1)));
        assertTrue(ERROR_PROCESS_SPLIT_MULTIPLE_ACCOUNT, oldTotalCost.equals(newTotalCost));

        // check the actionsTakenHistory has two entries since the source split item has two accounts.
        assertTrue(ERROR_PROCESS_SPLIT_MULTIPLE_ACCOUNT, actionsTaken.size() == 2);
    }

    public void testProcessMerge_NotMergeAll() {
        List<PurchasingAccountsPayableActionHistory> actionsTakeHistory = new ArrayList<PurchasingAccountsPayableActionHistory>();
        List<PurchasingAccountsPayableItemAsset> mergeLines = new ArrayList<PurchasingAccountsPayableItemAsset>();
        mergeLines.addAll(preqDocumentWithSingleItemSingleAccount.getPurchasingAccountsPayableItemAssets());
        mergeLines.addAll(preqDocumentWithTwoItemsSingleAccount.getPurchasingAccountsPayableItemAssets());

        // calculate the total cost of all merge lines before action
        KualiDecimal oldTotalCost = KualiDecimal.ZERO;
        for (PurchasingAccountsPayableItemAsset item : mergeLines) {
            oldTotalCost = oldTotalCost.add(getTotalCost(item));
        }

        purApLineService.processMerge(mergeLines, actionsTakeHistory, false);

        // check all lines merge into the first item.
        assertTrue(ERROR_PROCESS_MERGE, preqDocumentWithSingleItemSingleAccount.getPurchasingAccountsPayableItemAssets().size() == 1);
        assertTrue(ERROR_PROCESS_MERGE, preqDocumentWithTwoItemsSingleAccount.getPurchasingAccountsPayableItemAssets().isEmpty());

        // check the total cost after merge does not change
        KualiDecimal newTotalCost = getTotalCost(preqDocumentWithSingleItemSingleAccount.getPurchasingAccountsPayableItemAssets().get(0));
        assertTrue(ERROR_PROCESS_MERGE, oldTotalCost.equals(newTotalCost));

        // check document is inactive since all its items are merged into another document item
        assertTrue(ERROR_PROCESS_MERGE, !preqDocumentWithTwoItemsSingleAccount.isActive());

        // check the actionsTakeHistory
        assertTrue(ERROR_PROCESS_MERGE, actionsTakeHistory.size() == 2);
    }

    public void testProcessAllocate_AllocateOneItemToAllTheOtherItems() {
        List<PurchasingAccountsPayableActionHistory> actionsTakeHistory = new ArrayList<PurchasingAccountsPayableActionHistory>();
        PurchasingAccountsPayableItemAsset sourceLineItem = preqDocumentWithSingleItemSingleAccount.getPurchasingAccountsPayableItemAssets().get(0);
        List<PurchasingAccountsPayableItemAsset> allocateTargetLines = new ArrayList<PurchasingAccountsPayableItemAsset>();
        // preserve the total cost before allocate.
        KualiDecimal oldTotalCost = getTotalCost(sourceLineItem);


        // build the allocate target item list
        for (PurchasingAccountsPayableItemAsset item : preqDocumentWithTwoItemsSingleAccount.getPurchasingAccountsPayableItemAssets()) {
            allocateTargetLines.add(item);
            oldTotalCost = oldTotalCost.add(getTotalCost(item));
        }

        for (PurchasingAccountsPayableItemAsset item : cmDocumentWithSingleItemTwoAccounts.getPurchasingAccountsPayableItemAssets()) {
            allocateTargetLines.add(item);
            oldTotalCost = oldTotalCost.add(getTotalCost(item));
        }

        purApLineService.processAllocate(sourceLineItem, allocateTargetLines, actionsTakeHistory, purApDocuments, false);

        // check the source item is removed from the document
        assertTrue("PurchasingAccountsPayableItemAssets was not empty", preqDocumentWithSingleItemSingleAccount.getPurchasingAccountsPayableItemAssets().isEmpty());
        // check the source is inactive
        assertFalse("preqDocumentWithSingleItemSingleAccount was active", preqDocumentWithSingleItemSingleAccount.isActive());

        // check total amount of all documents doesn't change after allocate
        KualiDecimal newTotalCost = KualiDecimal.ZERO;
        for (PurchasingAccountsPayableItemAsset item : allocateTargetLines) {
            newTotalCost = newTotalCost.add(getTotalCost(item));
        }
        assertTrue(ERROR_PROCESS_ALLOCATE, oldTotalCost.equals(newTotalCost));

        // check the actionTakenHistory, allocate based on target account amount. so the number of actions taken equals the target accounts.
        assertTrue(ERROR_PROCESS_ALLOCATE, actionsTakeHistory.size() == 4);
    }
}
