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

import org.apache.commons.lang.time.DateUtils;
import org.kuali.kfs.krad.service.BusinessObjectService;
import org.kuali.kfs.module.cam.CamsConstants;
import org.kuali.kfs.module.cam.CamsPropertyConstants;
import org.kuali.kfs.module.cam.businessobject.Asset;
import org.kuali.kfs.module.cam.businessobject.AssetLocation;
import org.kuali.kfs.module.cam.businessobject.AssetOrganization;
import org.kuali.kfs.module.cam.businessobject.AssetPayment;
import org.kuali.kfs.module.cam.document.AssetTransferDocument;
import org.kuali.kfs.module.cam.fixture.AssetTransferFixture;
import org.kuali.kfs.sys.ConfigureContext;
import org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntrySourceDetail;
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.UniversityDateService;
import org.kuali.rice.core.api.util.type.KualiDecimal;

import java.util.Collections;
import java.util.Comparator;
import java.util.List;

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

public class AssetTransferServiceTest extends KualiTestBase {

    private UniversityDateService universityDateService;
    private AssetTransferService assetTransferService;

    @Override
    @ConfigureContext(session = UserNameFixture.khuntley, shouldCommitTransactions = false)
    protected void setUp() throws Exception {
        super.setUp();
        universityDateService = SpringContext.getBean(UniversityDateService.class);
        assetTransferService = SpringContext.getBean(AssetTransferService.class);
    }


    /**
     * Test capital asset with active payments
     *
     * @throws Exception
     */
    @ConfigureContext(session = khuntley, shouldCommitTransactions = false)
    public void testCreateGLPostables_Success() throws Exception {
        // set up the data
        AssetTransferDocument document = buildTransferDocument(AssetTransferFixture.ACTIVE_CAPITAL_ASSET.newAsset(), true);
        this.assetTransferService.createGLPostables(document);
        // assert gl postables
        List<GeneralLedgerPendingEntrySourceDetail> generalLedgerPostables = document.getGeneralLedgerPendingEntrySourceDetails();
        assertFalse(generalLedgerPostables.isEmpty());
        assertEquals(12, generalLedgerPostables.size());
        Asset asset = document.getAsset();
        // assert source gl postable for first payment
        AssetPayment payment1 = asset.getAssetPayments().get(0);
        assertGLPostable(generalLedgerPostables.get(0), asset.getOrganizationOwnerChartOfAccountsCode(), payment1.getAccountChargeAmount(), "9520004", "Reverse asset cost", "8610");
        assertGLPostable(generalLedgerPostables.get(1), asset.getOrganizationOwnerChartOfAccountsCode(), payment1.getAccumulatedPrimaryDepreciationAmount(), "9520004", "Reverse accumulated depreciation", "8910");
        assertGLPostable(generalLedgerPostables.get(2), asset.getOrganizationOwnerChartOfAccountsCode(), payment1.getAccountChargeAmount().subtract(payment1.getAccumulatedPrimaryDepreciationAmount()), "9520004", "Reverse offset amount", "9899");

        // assert source gl postable for second payment
        AssetPayment payment2 = asset.getAssetPayments().get(1);
        assertGLPostable(generalLedgerPostables.get(3), asset.getOrganizationOwnerChartOfAccountsCode(), payment2.getAccountChargeAmount(), "9520004", "Reverse asset cost", "8610");
        assertGLPostable(generalLedgerPostables.get(4), asset.getOrganizationOwnerChartOfAccountsCode(), payment2.getAccumulatedPrimaryDepreciationAmount(), "9520004", "Reverse accumulated depreciation", "8910");
        assertGLPostable(generalLedgerPostables.get(5), asset.getOrganizationOwnerChartOfAccountsCode(), payment2.getAccountChargeAmount().subtract(payment2.getAccumulatedPrimaryDepreciationAmount()), "9520004", "Reverse offset amount", "9899");

        // assert target gl postable for first payment
        assertGLPostable(generalLedgerPostables.get(6), document.getOrganizationOwnerChartOfAccountsCode(), payment1.getAccountChargeAmount(), "9567077", "Transfer asset cost", "8610");
        assertGLPostable(generalLedgerPostables.get(7), document.getOrganizationOwnerChartOfAccountsCode(), payment1.getAccumulatedPrimaryDepreciationAmount(), "9567077", "Transfer accumulated depreciation", "8910");
        assertGLPostable(generalLedgerPostables.get(8), document.getOrganizationOwnerChartOfAccountsCode(), payment1.getAccountChargeAmount().subtract(payment1.getAccumulatedPrimaryDepreciationAmount()), "9567077", "Transfer offset amount", "9899");

        // assert target gl postable for second payment
        assertGLPostable(generalLedgerPostables.get(9), document.getOrganizationOwnerChartOfAccountsCode(), payment2.getAccountChargeAmount(), "9567077", "Transfer asset cost", "8610");
        assertGLPostable(generalLedgerPostables.get(10), document.getOrganizationOwnerChartOfAccountsCode(), payment2.getAccumulatedPrimaryDepreciationAmount(), "9567077", "Transfer accumulated depreciation", "8910");
        assertGLPostable(generalLedgerPostables.get(11), document.getOrganizationOwnerChartOfAccountsCode(), payment2.getAccountChargeAmount().subtract(payment2.getAccumulatedPrimaryDepreciationAmount()), "9567077", "Transfer offset amount", "9899");


    }

    /**
     * Test capital asset with active payments
     *
     * @throws Exception
     */
    @ConfigureContext(session = khuntley, shouldCommitTransactions = false)
    public void testCreateGLPostables_No_Offset() throws Exception {
        // set up the data
        AssetTransferDocument document = buildTransferDocumentWithoutOffset(AssetTransferFixture.ACTIVE_CAPITAL_ASSET.newAsset(), true);
        this.assetTransferService.createGLPostables(document);
        // assert gl postables
        List<GeneralLedgerPendingEntrySourceDetail> generalLedgerPostables = document.getGeneralLedgerPendingEntrySourceDetails();
        assertFalse(generalLedgerPostables.isEmpty());
        assertEquals(8, generalLedgerPostables.size());
        Asset asset = document.getAsset();
        // assert source gl postable for first payment
        AssetPayment payment1 = asset.getAssetPayments().get(0);
        assertGLPostable(generalLedgerPostables.get(0), asset.getOrganizationOwnerChartOfAccountsCode(), payment1.getAccountChargeAmount(), "9520004", "Reverse asset cost", "8610");
        assertGLPostable(generalLedgerPostables.get(1), asset.getOrganizationOwnerChartOfAccountsCode(), payment1.getAccumulatedPrimaryDepreciationAmount(), "9520004", "Reverse accumulated depreciation", "8910");

        // assert source gl postable for second payment
        AssetPayment payment2 = asset.getAssetPayments().get(1);
        assertGLPostable(generalLedgerPostables.get(2), asset.getOrganizationOwnerChartOfAccountsCode(), payment2.getAccountChargeAmount(), "9520004", "Reverse asset cost", "8610");
        assertGLPostable(generalLedgerPostables.get(3), asset.getOrganizationOwnerChartOfAccountsCode(), payment2.getAccumulatedPrimaryDepreciationAmount(), "9520004", "Reverse accumulated depreciation", "8910");

        // assert target gl postable for first payment
        assertGLPostable(generalLedgerPostables.get(4), document.getOrganizationOwnerChartOfAccountsCode(), payment1.getAccountChargeAmount(), "9567077", "Transfer asset cost", "8610");
        assertGLPostable(generalLedgerPostables.get(5), document.getOrganizationOwnerChartOfAccountsCode(), payment1.getAccumulatedPrimaryDepreciationAmount(), "9567077", "Transfer accumulated depreciation", "8910");

        // assert target gl postable for second payment
        assertGLPostable(generalLedgerPostables.get(6), document.getOrganizationOwnerChartOfAccountsCode(), payment2.getAccountChargeAmount(), "9567077", "Transfer asset cost", "8610");
        assertGLPostable(generalLedgerPostables.get(7), document.getOrganizationOwnerChartOfAccountsCode(), payment2.getAccumulatedPrimaryDepreciationAmount(), "9567077", "Transfer accumulated depreciation", "8910");


    }

    /**
     * test non-capital assets
     *
     * @throws Exception
     */
    @ConfigureContext(session = khuntley, shouldCommitTransactions = false)
    public void testCreateGLPostables_Non_CapitalAsset() throws Exception {
        // set up the data
        AssetTransferDocument document = buildTransferDocument(AssetTransferFixture.ACTIVE_NON_CAPITAL_ASSET.newAsset(), true);
        this.assetTransferService.createGLPostables(document);
        // assert gl postables
        List<GeneralLedgerPendingEntrySourceDetail> generalLedgerPostables = document.getGeneralLedgerPendingEntrySourceDetails();
        assertTrue(generalLedgerPostables.isEmpty());
    }

    /**
     * Test active asset with no payments
     *
     * @throws Exception
     */
    @ConfigureContext(session = khuntley, shouldCommitTransactions = false)
    public void testCreateGLPostables_No_Payments() throws Exception {
        // set up the data
        AssetTransferDocument document = buildTransferDocument(AssetTransferFixture.ACTIVE_CAPITAL_ASSET.newAsset(), false);
        this.assetTransferService.createGLPostables(document);
        // assert gl postables
        List<GeneralLedgerPendingEntrySourceDetail> generalLedgerPostables = document.getGeneralLedgerPendingEntrySourceDetails();
        assertTrue(generalLedgerPostables.isEmpty());
    }

    /**
     * Assert GL postable entry
     *
     * @param glPostable
     * @param chartOfAccountsCode
     * @param amount
     * @param plantAccount
     * @param financialLineDesc
     * @param financialObjectCode
     */
    private void assertGLPostable(GeneralLedgerPendingEntrySourceDetail glPostable, String chartOfAccountsCode, KualiDecimal amount, String plantAccount, String financialLineDesc, String financialObjectCode) {
        assertEquals(plantAccount, glPostable.getAccountNumber());
        assertEquals(amount, glPostable.getAmount());
        assertEquals(CamsConstants.Postable.GL_BALANCE_TYPE_CODE_AC, glPostable.getBalanceTypeCode());
        assertEquals(chartOfAccountsCode, glPostable.getChartOfAccountsCode());
        assertEquals(financialLineDesc, glPostable.getFinancialDocumentLineDescription());
        assertEquals(financialObjectCode, glPostable.getFinancialObjectCode());
        assertEquals(this.universityDateService.getCurrentFiscalYear(), glPostable.getPostingYear());
        assertNull(glPostable.getOrganizationReferenceId());
        assertNull(glPostable.getProjectCode());
        assertNull(glPostable.getReferenceNumber());
        assertNull(glPostable.getReferenceOriginCode());
        assertNull(glPostable.getReferenceTypeCode());
    }

    private AssetTransferDocument buildTransferDocument(Asset asset, boolean addPayments) {
        BusinessObjectService boService = SpringContext.getBean(BusinessObjectService.class);
        AssetTransferDocument document = AssetTransferFixture.ASSET_TRANSFER.newAssetTransferDocument();
        asset.setCapitalAssetNumber(null);
        asset.setCapitalAssetTypeCode("665");
        boService.save(asset);
        if (addPayments) {
            AssetPayment payment1 = AssetTransferFixture.PAYMENT1_WITH_OFFSET.newAssetPayment();
            AssetPayment payment2 = AssetTransferFixture.PAYMENT2_WITH_OFFSET.newAssetPayment();
            payment1.setCapitalAssetNumber(asset.getCapitalAssetNumber());
            payment1.setPaymentSequenceNumber(1);
            payment2.setCapitalAssetNumber(asset.getCapitalAssetNumber());
            payment2.setPaymentSequenceNumber(2);
            boService.save(payment1);
            boService.save(payment2);
            payment1.refresh();
            payment2.refresh();
            asset.getAssetPayments().add(payment1);
            asset.getAssetPayments().add(payment2);
        }
        document.setAsset(asset);
        document.setCapitalAssetNumber(asset.getCapitalAssetNumber());
        return document;
    }

    private AssetTransferDocument buildTransferDocumentWithoutOffset(Asset asset, boolean addPayments) {
        BusinessObjectService boService = SpringContext.getBean(BusinessObjectService.class);
        AssetTransferDocument document = AssetTransferFixture.ASSET_TRANSFER.newAssetTransferDocument();
        asset.setCapitalAssetNumber(null);
        asset.setCapitalAssetTypeCode("665");
        boService.save(asset);
        if (addPayments) {
            AssetPayment payment1 = AssetTransferFixture.PAYMENT3_WITHOUT_OFFSET.newAssetPayment();
            AssetPayment payment2 = AssetTransferFixture.PAYMENT4_WITHOUT_OFFSET.newAssetPayment();
            payment1.setCapitalAssetNumber(asset.getCapitalAssetNumber());
            payment1.setPaymentSequenceNumber(1);
            payment2.setCapitalAssetNumber(asset.getCapitalAssetNumber());
            payment2.setPaymentSequenceNumber(2);
            boService.save(payment1);
            boService.save(payment2);
            payment1.refresh();
            payment2.refresh();
            asset.getAssetPayments().add(payment1);
            asset.getAssetPayments().add(payment2);
        }
        document.setAsset(asset);
        document.setCapitalAssetNumber(asset.getCapitalAssetNumber());
        return document;
    }

    @ConfigureContext(session = khuntley, shouldCommitTransactions = false)
    public void testSaveApprovedChanges() throws Exception {
        AssetTransferDocument document = buildTransferDocument(AssetTransferFixture.ACTIVE_CAPITAL_ASSET.newAsset(), true);
        this.assetTransferService.saveApprovedChanges(document);
        BusinessObjectService boService = SpringContext.getBean(BusinessObjectService.class);
        Asset asset = document.getAsset();
        asset = (Asset) boService.retrieve(asset);
        asset.refreshReferenceObject(CamsPropertyConstants.Asset.ASSET_PAYMENTS);
        assertNotNull(asset);
        assertEquals(document.getOrganizationOwnerChartOfAccountsCode(), asset.getOrganizationOwnerChartOfAccountsCode());
        assertEquals(document.getOrganizationOwnerAccountNumber(), asset.getOrganizationOwnerAccountNumber());
        assertEquals(document.getCampusCode(), asset.getCampusCode());
        assertEquals(document.getBuildingCode(), asset.getBuildingCode());
        assertEquals(document.getBuildingRoomNumber(), asset.getBuildingRoomNumber());
        assertEquals(document.getBuildingSubRoomNumber(), asset.getBuildingSubRoomNumber());
        assertTrue(DateUtils.isSameDay(new java.util.Date(), asset.getLastInventoryDate()));
        List<AssetPayment> assetPayments = asset.getAssetPayments();
        // sort the payment
        Collections.sort(assetPayments, new Comparator<AssetPayment>() {
            @Override
            public int compare(AssetPayment o1, AssetPayment o2) {
                return o1.getPaymentSequenceNumber().compareTo(o2.getPaymentSequenceNumber());
            }
        });
        assertEquals(6, assetPayments.size());
        // Asset original payment 1
        AssetPayment testDataPayment = AssetTransferFixture.PAYMENT1_WITH_OFFSET.newAssetPayment();
        AssetPayment assetPayment1 = assetPayments.get(0);
        assertOriginalPayment(testDataPayment, assetPayment1);

        // Assert original payment 2
        testDataPayment = AssetTransferFixture.PAYMENT2_WITH_OFFSET.newAssetPayment();
        AssetPayment assetPayment2 = assetPayments.get(1);
        assertOriginalPayment(testDataPayment, assetPayment2);

        // Assert offset payment 1
        testDataPayment = AssetTransferFixture.PAYMENT1_WITH_OFFSET.newAssetPayment();
        AssetPayment offsetPayment1 = assetPayments.get(2);
        assertOffsetPayment(testDataPayment, offsetPayment1);

        // Assert offset payment 2
        testDataPayment = AssetTransferFixture.PAYMENT2_WITH_OFFSET.newAssetPayment();
        AssetPayment offsetPayment2 = assetPayments.get(3);
        assertOffsetPayment(testDataPayment, offsetPayment2);

        // Assert new payment 1
        testDataPayment = AssetTransferFixture.PAYMENT1_WITH_OFFSET.newAssetPayment();
        AssetPayment newPayment1 = assetPayments.get(4);
        assertNewPayment(document, testDataPayment, newPayment1);

        // Assert new payment 2
        testDataPayment = AssetTransferFixture.PAYMENT2_WITH_OFFSET.newAssetPayment();
        AssetPayment newPayment2 = assetPayments.get(5);
        assertNewPayment(document, testDataPayment, newPayment2);
    }

    @ConfigureContext(session = khuntley, shouldCommitTransactions = false)
    public void testSaveApprovedChanges_Offcampus() throws Exception {
        AssetTransferDocument document = buildTransferDocument(AssetTransferFixture.ACTIVE_CAPITAL_ASSET.newAsset(), true);
        document.setOffCampusAddress("4700 S HAGADORN");
        document.setOffCampusCityName("E LANSING");
        document.setOffCampusStateCode("MI");
        document.setOffCampusZipCode("48823");

        // clear fields that conflict with off-campus data
        document.setCampusCode(null);
        document.setBuildingCode(null);
        document.setBuildingRoomNumber(null);
        document.setBuildingSubRoomNumber(null);

        this.assetTransferService.saveApprovedChanges(document);
        BusinessObjectService boService = SpringContext.getBean(BusinessObjectService.class);
        Asset asset = document.getAsset();
        asset = (Asset) boService.retrieve(asset);
        asset.refreshReferenceObject(CamsPropertyConstants.Asset.ASSET_LOCATIONS);
        SpringContext.getBean(AssetLocationService.class).setOffCampusLocation(asset);
        assertNotNull(asset);
        assertEquals(document.getOrganizationOwnerChartOfAccountsCode(), asset.getOrganizationOwnerChartOfAccountsCode());
        assertEquals(document.getOrganizationOwnerAccountNumber(), asset.getOrganizationOwnerAccountNumber());
        assertEquals(document.getCampusCode(), asset.getCampusCode());
        assertTrue(DateUtils.isSameDay(new java.util.Date(), asset.getLastInventoryDate()));
        AssetLocation offCampusLocation = asset.getOffCampusLocation();
        assertNotNull(offCampusLocation);
        assertEquals("4700 S HAGADORN", offCampusLocation.getAssetLocationStreetAddress());
        assertEquals("E LANSING", offCampusLocation.getAssetLocationCityName());
        assertEquals("MI", offCampusLocation.getAssetLocationStateCode());
        assertEquals("48823", offCampusLocation.getAssetLocationZipCode());
    }

    @ConfigureContext(session = khuntley, shouldCommitTransactions = false)
    public void testSaveApprovedChanges_AssetOrgInfo() throws Exception {
        AssetTransferDocument document = buildTransferDocument(AssetTransferFixture.ACTIVE_CAPITAL_ASSET.newAsset(), true);
        document.setOrganizationTagNumber("TEL");
        document.setOrganizationText("TEXT");
        this.assetTransferService.saveApprovedChanges(document);
        BusinessObjectService boService = SpringContext.getBean(BusinessObjectService.class);
        Asset asset = document.getAsset();
        asset = (Asset) boService.retrieve(asset);
        asset.refreshReferenceObject(CamsPropertyConstants.AssetOrganization.ASSET_ORGANIZATION);
        AssetOrganization assetOrganization = asset.getAssetOrganization();
        assertNotNull(assetOrganization);
        assertEquals("TEL", assetOrganization.getOrganizationTagNumber());
        assertEquals("TEXT", assetOrganization.getOrganizationText());
    }

    private void assertNewPayment(AssetTransferDocument document, AssetPayment referencePayment, AssetPayment newPayment) {
        assertEquals(document.getOrganizationOwnerAccountNumber(), newPayment.getAccountNumber());
        assertEquals(referencePayment.getAccountChargeAmount(), newPayment.getAccountChargeAmount());
        assertEquals(referencePayment.getAccumulatedPrimaryDepreciationAmount(), newPayment.getAccumulatedPrimaryDepreciationAmount());
        assertEquals(null, newPayment.getPeriod1Depreciation1Amount());
        assertEquals(CamsConstants.AssetPayment.TRANSFER_PAYMENT_CODE_N, newPayment.getTransferPaymentCode());
    }

    private void assertOriginalPayment(AssetPayment referencePayment, AssetPayment assetOriginalPayment) {
        assertEquals(referencePayment.getAccountNumber(), assetOriginalPayment.getAccountNumber());
        assertEquals(referencePayment.getAccountChargeAmount(), assetOriginalPayment.getAccountChargeAmount());
        assertEquals(referencePayment.getAccumulatedPrimaryDepreciationAmount(), assetOriginalPayment.getAccumulatedPrimaryDepreciationAmount());
        assertEquals(referencePayment.getPeriod1Depreciation1Amount(), assetOriginalPayment.getPeriod1Depreciation1Amount());
        assertEquals(CamsConstants.AssetPayment.TRANSFER_PAYMENT_CODE_Y, assetOriginalPayment.getTransferPaymentCode());
    }

    private void assertOffsetPayment(AssetPayment referencePayment, AssetPayment assetOffsetPayment) {
        assertEquals(referencePayment.getAccountNumber(), assetOffsetPayment.getAccountNumber());
        KualiDecimal negative = new KualiDecimal(-1);
        assertEquals(referencePayment.getAccountChargeAmount().multiply(negative), assetOffsetPayment.getAccountChargeAmount());
        assertEquals(referencePayment.getAccumulatedPrimaryDepreciationAmount().multiply(negative), assetOffsetPayment.getAccumulatedPrimaryDepreciationAmount());
        assertEquals(null, assetOffsetPayment.getPeriod1Depreciation1Amount());
        assertEquals(CamsConstants.AssetPayment.TRANSFER_PAYMENT_CODE_Y, assetOffsetPayment.getTransferPaymentCode());
    }
}
