# imports

# local, internal
from src.topupopt.data.finance.invest import Investment, discount_factor, npv
from src.topupopt.data.finance.invest import salvage_value_linear_depreciation
from src.topupopt.data.finance.invest import present_salvage_value_annuity
from src.topupopt.data.finance.invest import salvage_value_annuity
from src.topupopt.data.finance.utils import ArcInvestments

# local, external
import math

# ******************************************************************************
# ******************************************************************************


class TestArcInvestments:
    def test_object_creation(self):
        discount_rate = 0.035
        analysis_period = 20
        discount_rates = tuple(discount_rate for i in range(analysis_period))
        cash_flows_npv = [3.673079208612225, 1.5610827143687658, 17, 117, 1842]
        abs_cash_flows_npv = [1e-1, 1e-1, 1e-1, 0.5, 1]
        investments = []

        # limited longevity operation, does not go beyond planning horizon
        cash_flow = 1
        cash_flow_start = 1
        myinv = Investment(discount_rates=discount_rates)
        myinv.add_operational_cash_flows(
            cash_flow=cash_flow, start_period=cash_flow_start, longevity=4
        )
        investments.append(myinv)

        # limited longevity operation, goes beyond planning horizon
        cash_flow = 1
        cash_flow_start = 18
        myinv = Investment(discount_rates=discount_rates)
        myinv.add_operational_cash_flows(
            cash_flow=cash_flow, start_period=cash_flow_start, longevity=4
        )
        investments.append(myinv)

        # D&V omkostninger kundeanlæg: Sengeløse Skole
        cash_flow = 1.2
        cash_flow_start = 1
        myinv = Investment(discount_rates=discount_rates)
        myinv.add_operational_cash_flows(
            cash_flow=cash_flow, start_period=cash_flow_start, longevity=None
        )
        investments.append(myinv)

        # D&V omkostninger kundeanlæg: Større forbrugere
        cash_flow = 6 / 10
        myinv = Investment(discount_rates=discount_rates)
        myinv.add_operational_cash_flows(
            cash_flow=10 * cash_flow, start_period=1, longevity=None
        )
        myinv.add_operational_cash_flows(
            cash_flow=3 * cash_flow, start_period=2, longevity=None
        )
        myinv.add_operational_cash_flows(
            cash_flow=1 * cash_flow, start_period=3, longevity=None
        )
        investments.append(myinv)

        # D&V omkostninger kundeanlæg: Mindre forbrugere
        cash_flow = 0.320
        myinv = Investment(discount_rates=discount_rates)
        myinv.add_operational_cash_flows(
            cash_flow=197 * cash_flow, start_period=1, longevity=None
        )
        myinv.add_operational_cash_flows(
            cash_flow=(233 - 197) * cash_flow, start_period=2, longevity=None
        )
        myinv.add_operational_cash_flows(
            cash_flow=(269 - 233) * cash_flow, start_period=3, longevity=None
        )
        myinv.add_operational_cash_flows(
            cash_flow=(305 - 269) * cash_flow, start_period=4, longevity=None
        )
        myinv.add_operational_cash_flows(
            cash_flow=(341 - 305) * cash_flow, start_period=5, longevity=None
        )
        myinv.add_operational_cash_flows(
            cash_flow=(377 - 341) * cash_flow, start_period=6, longevity=None
        )
        myinv.add_operational_cash_flows(
            cash_flow=(413 - 377) * cash_flow, start_period=7, longevity=None
        )
        myinv.add_operational_cash_flows(
            cash_flow=(449 - 413) * cash_flow, start_period=8, longevity=None
        )
        myinv.add_operational_cash_flows(
            cash_flow=(488 - 449) * cash_flow, start_period=9, longevity=None
        )
        investments.append(myinv)
        # check
        for inv, true_npv, _abs in zip(investments, cash_flows_npv, abs_cash_flows_npv):
            assert math.isclose(inv.net_present_value(), true_npv, abs_tol=_abs)
        # create object
        number_options = 5
        static_loss = {
            (h, q, k): 1 + q * 0.25 + h * 0.15 + k * 0.05
            for h in range(number_options)
            for q in range(2)
            for k in range(3)
        }
        arc_invs = ArcInvestments(
            investments=investments,
            name="any",
            efficiency=None,
            efficiency_reverse=None,
            capacity=tuple(1 + o for o in range(number_options)),
            # minimum_cost=tuple(1+o for o in range(number_options)),
            specific_capacity_cost=1,
            capacity_is_instantaneous=False,
            static_loss=static_loss,
            validate=True,
        )
        # check the costs
        for _npv, true_npv, _abs in zip(
            arc_invs.minimum_cost, cash_flows_npv, abs_cash_flows_npv
        ):
            assert math.isclose(_npv, true_npv, abs_tol=_abs)
        # change something in the investments
        for inv in arc_invs.investments:
            inv.add_investment(
                investment=1, investment_period=0, investment_longevity=10
            )
        # update the minimum costs
        arc_invs.update_minimum_cost()
        # make sure the comparison fails
        for _npv, true_npv, _abs in zip(
            arc_invs.minimum_cost, cash_flows_npv, abs_cash_flows_npv
        ):
            error_raised = False
            try:
                assert math.isclose(_npv, true_npv, abs_tol=_abs)
            except AssertionError:
                error_raised = True
            assert error_raised
        # new true values
        new_true_npv = [
            4.673079208612225,
            2.561082714368766,
            18.054883962342764,
            117.50524073646935,
            1843.1804103901486,
        ]
        # print([inv.net_present_value() for inv in arc_invs])
        for _npv, true_npv, _abs in zip(
            arc_invs.minimum_cost, new_true_npv, abs_cash_flows_npv
        ):
            assert math.isclose(_npv, true_npv, abs_tol=_abs)


# *****************************************************************************
# *****************************************************************************


class TestDataFinance:
    # TODO: make sure that all methods work with variable discount rates

    # assert that the discount factors match

    def test_operational_cash_flows(self):
        discount_rate = 0.035

        analysis_period = 20

        discount_rates = tuple(discount_rate for i in range(analysis_period))

        # limited longevity operation, does not go beyond planning horizon

        cash_flow = 1
        cash_flow_start = 1
        cash_flows_npv = 3.673079208612225
        myinv = Investment(discount_rates=discount_rates)
        myinv.add_operational_cash_flows(
            cash_flow=cash_flow, start_period=cash_flow_start, longevity=4
        )
        mynpv = myinv.net_present_value()
        assert math.isclose(mynpv, cash_flows_npv, abs_tol=1e-1)

        # limited longevity operation, goes beyond planning horizon

        cash_flow = 1
        cash_flow_start = 18
        cash_flows_npv = 1.5610827143687658
        myinv = Investment(discount_rates=discount_rates)
        myinv.add_operational_cash_flows(
            cash_flow=cash_flow, start_period=cash_flow_start, longevity=4
        )
        mynpv = myinv.net_present_value()
        assert math.isclose(mynpv, cash_flows_npv, abs_tol=1e-1)

        # D&V omkostninger kundeanlæg: Sengeløse Skole

        cash_flow = 1.2

        cash_flow_start = 1

        cash_flows_npv = 17

        myinv = Investment(discount_rates=discount_rates)
        myinv.add_operational_cash_flows(
            cash_flow=cash_flow, start_period=cash_flow_start, longevity=None
        )
        mynpv = myinv.net_present_value()

        assert math.isclose(mynpv, cash_flows_npv, abs_tol=1e-1)

        # D&V omkostninger kundeanlæg: Større forbrugere

        cash_flows_npv = 117
        cash_flow = 6 / 10

        myinv = Investment(discount_rates=discount_rates)
        myinv.add_operational_cash_flows(
            cash_flow=10 * cash_flow, start_period=1, longevity=None
        )
        myinv.add_operational_cash_flows(
            cash_flow=3 * cash_flow, start_period=2, longevity=None
        )
        myinv.add_operational_cash_flows(
            cash_flow=1 * cash_flow, start_period=3, longevity=None
        )
        mynpv = myinv.net_present_value()

        assert math.isclose(mynpv, cash_flows_npv, abs_tol=0.5)

        # D&V omkostninger kundeanlæg: Mindre forbrugere

        cash_flows_npv = 1842
        cash_flow = 0.320

        myinv = Investment(discount_rates=discount_rates)
        myinv.add_operational_cash_flows(
            cash_flow=197 * cash_flow, start_period=1, longevity=None
        )
        myinv.add_operational_cash_flows(
            cash_flow=(233 - 197) * cash_flow, start_period=2, longevity=None
        )
        myinv.add_operational_cash_flows(
            cash_flow=(269 - 233) * cash_flow, start_period=3, longevity=None
        )
        myinv.add_operational_cash_flows(
            cash_flow=(305 - 269) * cash_flow, start_period=4, longevity=None
        )
        myinv.add_operational_cash_flows(
            cash_flow=(341 - 305) * cash_flow, start_period=5, longevity=None
        )
        myinv.add_operational_cash_flows(
            cash_flow=(377 - 341) * cash_flow, start_period=6, longevity=None
        )
        myinv.add_operational_cash_flows(
            cash_flow=(413 - 377) * cash_flow, start_period=7, longevity=None
        )
        myinv.add_operational_cash_flows(
            cash_flow=(449 - 413) * cash_flow, start_period=8, longevity=None
        )
        myinv.add_operational_cash_flows(
            cash_flow=(488 - 449) * cash_flow, start_period=9, longevity=None
        )
        mynpv = myinv.net_present_value()

        assert math.isclose(mynpv, cash_flows_npv, abs_tol=1)

    # *************************************************************************
    # *************************************************************************

    # assert that the discount factors match

    def test_discount_factors(self):
        years = [
            2021,
            2022,
            2023,
            2024,
            2025,
            2026,
            2027,
            2028,
            2029,
            2030,
            2031,
            2036,
            2037,
            2038,
            2039,
            2040,
            2041,
        ]

        factors = [
            1.000,
            0.966,
            0.934,
            0.902,
            0.871,
            0.842,
            0.814,
            0.786,
            0.759,
            0.734,
            0.709,
            0.597,
            0.577,
            0.557,
            0.538,
            0.520,
            0.503,
        ]

        test_factors = [
            discount_factor([0.035 for i in range(year - years[0])]) for year in years
        ]

        for i, factor in enumerate(factors):
            assert math.isclose(factor, test_factors[i], abs_tol=0.001)

        # *********************************************************************

        # variable discount factors

        discount_rates = [0.035, 0.05, 0.075, 0.06]
        discount_factors = [1.00000, 0.96618, 0.92017, 0.85598, 0.80753]

        test_discount_factors = [
            discount_factor(discount_rates[0:t]) for t in range(len(discount_rates) + 1)
        ]

        assert len(discount_factors) == len(test_discount_factors)

        for df_true, df in zip(discount_factors, test_discount_factors):
            assert math.isclose(df_true, df, abs_tol=0.001)

    # *************************************************************************
    # *************************************************************************

    def test_salvage_value_sengelose_linear_depreciation(self):
        # *********************************************************************
        # *********************************************************************

        # example # 1: Investering kundeanlæg, Sengeløse Skole

        commissioning_delay_after_investment = 0

        investment_period = 1

        investment = 180  # 1E3 DKK

        investment_longevity = (
            25  # if simultaneous_commissioning_investment else 25 # years
        )

        analysis_period_span = 20  # years

        discount_rate = 0.035

        # discount rates' tuple: size does not change with first_period_is_present_time

        discount_rates = tuple(discount_rate for i in range(analysis_period_span))

        # *********************************************************************

        residual_value = salvage_value_linear_depreciation(
            investment=investment,
            investment_period=investment_period,
            investment_longevity=investment_longevity,
            analysis_period_span=analysis_period_span,
            commissioning_delay_after_investment=commissioning_delay_after_investment,
        )

        assert math.isclose(residual_value, 36, abs_tol=1)

        net_cash_flows = list(0 for i in range(analysis_period_span + 1))
        net_cash_flows[investment_period] = investment
        net_cash_flows[analysis_period_span] = -residual_value

        npv_inv_horizon = npv(
            discount_rates=discount_rates, net_cash_flows=net_cash_flows
        )
        assert math.isclose(npv_inv_horizon, 155.82067163872074, abs_tol=1e-3)

        myinv = Investment(discount_rates=discount_rates)
        myinv.add_investment(
            investment=investment,
            investment_period=investment_period,
            investment_longevity=investment_longevity,
            commissioning_delay_after_investment=commissioning_delay_after_investment,
            salvage_value_method="asd",
        )
        mynpv = myinv.net_present_value()
        assert math.isclose(mynpv, 155.82067163872074, abs_tol=1e-3)

        # *********************************************************************
        # *********************************************************************

        # example # 2: Forsyningsledning

        investment_period = 1

        investment = 13000  # 1E3 DKK

        investment_longevity = 60  # years

        analysis_period_span = 20  # years

        discount_rate = 0.035

        discount_rates = tuple(discount_rate for i in range(analysis_period_span))

        # *********************************************************************

        # using mean annual asset devaluation method

        residual_value = salvage_value_linear_depreciation(
            investment=investment,
            investment_period=investment_period,
            investment_longevity=investment_longevity,
            analysis_period_span=analysis_period_span,
            commissioning_delay_after_investment=commissioning_delay_after_investment,
        )

        assert math.isclose(residual_value, 8667, abs_tol=1)

        net_cash_flows = list(0 for i in range(analysis_period_span + 1))
        net_cash_flows[investment_period] = investment
        net_cash_flows[analysis_period_span] = -residual_value

        npv_inv_horizon = npv(
            discount_rates=discount_rates, net_cash_flows=net_cash_flows
        )
        assert math.isclose(npv_inv_horizon, 8204.815475022142, abs_tol=1e-3)

        myinv = Investment(discount_rates=discount_rates)
        myinv.add_investment(
            investment=investment,
            investment_period=investment_period,
            investment_longevity=investment_longevity,
            commissioning_delay_after_investment=commissioning_delay_after_investment,
            salvage_value_method="asd",
        )
        mynpv = myinv.net_present_value()
        assert math.isclose(mynpv, 8204.815475022142, abs_tol=1e-3)

        # *********************************************************************
        # *********************************************************************

        # example 3: Boosterpumpeanlæg

        investment_period = 7

        investment = 1500  # 1E3 DKK

        investment_longevity = 25  # years

        analysis_period_span = 20  # years

        discount_rate = 0.035

        discount_rates = tuple(discount_rate for i in range(analysis_period_span))

        # *********************************************************************

        residual_value = salvage_value_linear_depreciation(
            investment=investment,
            investment_period=investment_period,
            investment_longevity=investment_longevity,
            analysis_period_span=analysis_period_span,
            commissioning_delay_after_investment=commissioning_delay_after_investment,
        )

        assert math.isclose(residual_value, 660, abs_tol=1e-3)

        net_cash_flows = list(0 for i in range(analysis_period_span + 1))
        net_cash_flows[investment_period] = investment
        net_cash_flows[analysis_period_span] = -residual_value

        npv_inv_horizon = npv(
            discount_rates=discount_rates, net_cash_flows=net_cash_flows
        )
        assert math.isclose(npv_inv_horizon, 847, abs_tol=0.3)

        myinv = Investment(discount_rates=discount_rates)
        myinv.add_investment(
            investment=investment,
            investment_period=investment_period,
            investment_longevity=investment_longevity,
            commissioning_delay_after_investment=commissioning_delay_after_investment,
            salvage_value_method="asd",
        )
        mynpv = myinv.net_present_value()
        assert math.isclose(mynpv, 847, abs_tol=0.3)

        # *********************************************************************
        # *********************************************************************

        # example 4: Investering, Sengeløse Skole, (varmepumper)

        investment_period = 1

        investment = 1925  # 1E3 DKK

        investment_longevity = 20  # years

        analysis_period_span = 20  # years

        discount_rate = 0.035

        discount_rates = tuple(discount_rate for i in range(analysis_period_span))

        # *********************************************************************

        residual_value = salvage_value_linear_depreciation(
            investment=investment,
            investment_period=investment_period,
            investment_longevity=investment_longevity,
            analysis_period_span=analysis_period_span,
            commissioning_delay_after_investment=commissioning_delay_after_investment,
        )

        assert math.isclose(residual_value, 0, abs_tol=1e-3)

        net_cash_flows = list(0 for i in range(analysis_period_span + 1))
        net_cash_flows[investment_period] = investment
        net_cash_flows[analysis_period_span] = -residual_value

        npv_inv_horizon = npv(
            discount_rates=discount_rates, net_cash_flows=net_cash_flows
        )

        assert math.isclose(npv_inv_horizon, 1860, abs_tol=0.3)

        myinv = Investment(discount_rates=discount_rates)
        myinv.add_investment(
            investment=investment,
            investment_period=investment_period,
            investment_longevity=investment_longevity,
            commissioning_delay_after_investment=commissioning_delay_after_investment,
            salvage_value_method="asd",
        )
        mynpv = myinv.net_present_value()
        assert math.isclose(mynpv, 1860, abs_tol=0.3)

        # *********************************************************************
        # *********************************************************************

    # *************************************************************************
    # *************************************************************************

    def test_scrap_value_annuity(self):
        # Source:
        # Vejledning i samfundsøkonomiske analyser på energiområdet, juli 2021
        # Energistyrelsen, page 19

        investment_period = 0

        investment = 1e6  # 1E3 DKK

        investment_longevity = 30  # years

        analysis_period_span = 20  # years

        discount_rate = 0.035

        discount_rates = tuple(
            discount_rate for i in range(investment_longevity + investment_period)
        )

        # *********************************************************************
        # *********************************************************************

        # calculate the net present value with the salvage value deducted

        # annuity method

        annuity = (
            investment
            * discount_rate
            / (1 - (1 + discount_rate) ** (-investment_longevity))
        )

        net_cash_flows = list(
            annuity for i in range(investment_longevity + investment_period + 1)
        )

        for year_index in range(investment_period + 1):
            net_cash_flows[year_index] = 0

        npv_inv_horizon = npv(
            discount_rates=discount_rates[0:analysis_period_span],
            net_cash_flows=net_cash_flows[0 : analysis_period_span + 1],
        )

        assert math.isclose(npv_inv_horizon, 772747.2928688908, abs_tol=1e-3)

        # *********************************************************************

        # net present value for the whole investment

        npv_asset_long = npv(
            discount_rates=discount_rates, net_cash_flows=net_cash_flows
        )

        assert math.isclose(npv_asset_long, 1e6, abs_tol=1e-3)

        # calculate discounted salvage value directly

        npv_salvage = present_salvage_value_annuity(
            investment=investment,
            investment_longevity=investment_longevity,
            investment_period=investment_period,
            discount_rate=discount_rate,
            analysis_period_span=analysis_period_span,
        )

        assert math.isclose(npv_salvage, npv_asset_long - npv_inv_horizon, abs_tol=1e-3)

        # salvage value, as seen from the last period

        und_salvage_value = salvage_value_annuity(
            investment=investment,
            discount_rate=discount_rate,
            investment_longevity=investment_longevity,
            investment_period=investment_period,
            analysis_period_span=analysis_period_span,
        )

        assert math.isclose(und_salvage_value, 452184.9058419504, abs_tol=1e-3)

        # *********************************************************************

        # use only the part of discount_rates that overlaps with the planni. period

        myinv = Investment(discount_rates=discount_rates[0:analysis_period_span])

        myinv.add_investment(
            investment=investment,
            investment_period=investment_period,
            investment_longevity=investment_longevity,
        )
        mynpv = myinv.net_present_value()

        assert math.isclose(mynpv, npv_inv_horizon, abs_tol=1e-3)

        # *********************************************************************
        # *********************************************************************

        # trigger ValueError

        error_raised = False
        investment_period = analysis_period_span + 1
        try:
            npv_salvage = present_salvage_value_annuity(
                investment=investment,
                investment_longevity=investment_longevity,
                investment_period=investment_period,
                discount_rate=discount_rate,
                analysis_period_span=analysis_period_span,
            )
        except ValueError:
            error_raised = True
        assert error_raised

        # *********************************************************************

        investment = 1

        investment_period = 0

        investment_longevity = 4

        analysis_period_span = 3

        discount_rate = 0.035

        # *********************************************************************

        # analysis period equals longevity: no salvage value

        npv_salvage, annuity = present_salvage_value_annuity(
            investment=investment,
            investment_longevity=investment_longevity,
            investment_period=investment_period,
            discount_rate=discount_rate,
            analysis_period_span=analysis_period_span + 1,
            return_annuity=True,
        )

        assert npv_salvage == 0.0
        assert annuity > 0

        # *********************************************************************

        # increased longevity

        npv_salvage, annuity2 = present_salvage_value_annuity(
            investment=investment,
            investment_longevity=investment_longevity + 1,
            investment_period=investment_period,
            discount_rate=discount_rate,
            analysis_period_span=analysis_period_span,
            return_annuity=True,
        )

        assert math.isclose(npv_salvage, 0.37948959437673335, abs_tol=1e-3)
        assert annuity > annuity2

        # *********************************************************************
        # *********************************************************************

    # *************************************************************************
    # *************************************************************************

    def test_scrap_value_annuity_longer_longevity(self):
        # Source:
        # Vejledning i samfundsøkonomiske analyser på energiområdet, juli 2021
        # Energistyrelsen, page 19

        investment_period = 0

        investment = 1e6  # 1E3 DKK

        investment_longevity = 35  # years

        analysis_period_span = 20  # years

        discount_rate = 0.035

        discount_rates = tuple(
            discount_rate for i in range(investment_longevity + investment_period)
        )

        # *********************************************************************
        # *********************************************************************

        # calculate the net present value with the salvage value deducted

        # annuity method

        annuity = (
            investment
            * discount_rate
            / (1 - (1 + discount_rate) ** (-investment_longevity))
        )

        net_cash_flows = list(
            annuity for i in range(investment_longevity + investment_period + 1)
        )

        for year_index in range(investment_period + 1):
            net_cash_flows[year_index] = 0

        npv_inv_horizon = npv(
            discount_rates=discount_rates[0:analysis_period_span],
            net_cash_flows=net_cash_flows[0 : analysis_period_span + 1],
        )

        assert math.isclose(npv_inv_horizon, 710596.68, abs_tol=1e-2)

        # *********************************************************************

        # net present value for the whole investment

        npv_asset_long = npv(
            discount_rates=discount_rates, net_cash_flows=net_cash_flows
        )

        assert math.isclose(npv_asset_long, 1e6, abs_tol=1e-3)

        # calculate discounted salvage value directly

        npv_salvage = present_salvage_value_annuity(
            investment=investment,
            investment_longevity=investment_longevity,
            investment_period=investment_period,
            discount_rate=discount_rate,
            analysis_period_span=analysis_period_span,
        )

        assert math.isclose(npv_salvage, npv_asset_long - npv_inv_horizon, abs_tol=1e-3)

        # salvage value, as seen from the last period

        und_salvage_value = salvage_value_annuity(
            investment=investment,
            discount_rate=discount_rate,
            investment_longevity=investment_longevity,
            investment_period=investment_period,
            analysis_period_span=analysis_period_span,
        )

        assert math.isclose(und_salvage_value, 575851.51, abs_tol=1e-3)

    # *************************************************************************
    # *************************************************************************

    def test_scrap_value_annuity_starting_later(self):
        # Source:
        # Vejledning i samfundsøkonomiske analyser på energiområdet, juli 2021
        # Energistyrelsen, page 19

        investment_period = 10

        investment = 1e6  # 1E3 DKK

        investment_longevity = 30  # years

        analysis_period_span = 20  # years

        discount_rate = 0.035

        discount_rates = tuple(
            discount_rate for i in range(investment_longevity + investment_period)
        )

        # *********************************************************************
        # *********************************************************************

        # calculate the net present value with the salvage value deducted

        # annuity method

        annuity = (
            investment
            * discount_rate
            / (1 - (1 + discount_rate) ** (-investment_longevity))
        )

        net_cash_flows = list(
            annuity for i in range(investment_longevity + investment_period + 1)
        )

        for year_index in range(investment_period + 1):
            net_cash_flows[year_index] = 0

        npv_inv_horizon = npv(
            discount_rates=discount_rates[0:analysis_period_span],
            net_cash_flows=net_cash_flows[0 : analysis_period_span + 1],
        )

        assert math.isclose(npv_inv_horizon, 320562.3870269, abs_tol=1e-2)

        # *********************************************************************

        # net present value for the whole investment

        npv_asset_long = npv(
            discount_rates=discount_rates, net_cash_flows=net_cash_flows
        )

        assert math.isclose(npv_asset_long, 708918.8137098, abs_tol=1e-3)

        # calculate discounted salvage value directly

        npv_salvage = present_salvage_value_annuity(
            investment=investment,
            investment_longevity=investment_longevity,
            investment_period=investment_period,
            discount_rate=discount_rate,
            analysis_period_span=analysis_period_span,
        )

        assert math.isclose(npv_salvage, npv_asset_long - npv_inv_horizon, abs_tol=1e-3)
        assert math.isclose(npv_salvage, 388356.4266828, abs_tol=1e-3)

        # salvage value, as seen from the last period

        und_salvage_value = salvage_value_annuity(
            investment=investment,
            discount_rate=discount_rate,
            investment_longevity=investment_longevity,
            investment_period=investment_period,
            analysis_period_span=analysis_period_span,
        )

        assert math.isclose(und_salvage_value, 772747.2928689, abs_tol=1e-3)

    # *************************************************************************
    # *************************************************************************

    def test_scrap_value_annuity_longer_planning_period(self):
        # Source:
        # Vejledning i samfundsøkonomiske analyser på energiområdet, juli 2021
        # Energistyrelsen, page 19

        investment_period = 0

        investment = 1e6  # 1E3 DKK

        investment_longevity = 30  # years

        analysis_period_span = 23  # years

        discount_rate = 0.035

        discount_rates = tuple(
            discount_rate for i in range(investment_longevity + investment_period)
        )

        # *********************************************************************

        # calculate the net present value with the salvage value deducted

        # annuity method

        annuity = (
            investment
            * discount_rate
            / (1 - (1 + discount_rate) ** (-investment_longevity))
        )

        net_cash_flows = list(
            annuity for i in range(investment_longevity + investment_period + 1)
        )

        for year_index in range(investment_period + 1):
            net_cash_flows[year_index] = 0

        npv_inv_horizon = npv(
            discount_rates=discount_rates[0:analysis_period_span],
            net_cash_flows=net_cash_flows[0 : analysis_period_span + 1],
        )

        assert math.isclose(npv_inv_horizon, 849302.517460684, abs_tol=1e-3)

        # *********************************************************************

        # net present value for the whole investment

        npv_asset_long = npv(
            discount_rates=discount_rates, net_cash_flows=net_cash_flows
        )

        assert math.isclose(npv_asset_long, 1e6, abs_tol=1e-3)

        # calculate discounted salvage value directly

        npv_salvage = present_salvage_value_annuity(
            investment=investment,
            investment_longevity=investment_longevity,
            investment_period=investment_period,
            discount_rate=discount_rate,
            analysis_period_span=analysis_period_span,
        )

        assert math.isclose(npv_salvage, npv_asset_long - npv_inv_horizon, abs_tol=1e-3)

        # salvage value, as seen from the last period

        und_salvage_value = salvage_value_annuity(
            investment=investment,
            discount_rate=discount_rate,
            investment_longevity=investment_longevity,
            investment_period=investment_period,
            analysis_period_span=analysis_period_span,
        )

        assert math.isclose(und_salvage_value, 332455.89838989300, abs_tol=1e-3)

    # *************************************************************************
    # *************************************************************************

    def test_scrap_value_annuity_matching_periods(self):
        # Source:
        # Vejledning i samfundsøkonomiske analyser på energiområdet, juli 2021
        # Energistyrelsen, page 19

        investment_period = 0

        investment = 1e6  # 1E3 DKK

        investment_longevity = 20  # years

        analysis_period_span = 20  # years

        discount_rate = 0.035

        discount_rates = tuple(
            discount_rate for i in range(investment_longevity + investment_period)
        )

        # *********************************************************************

        # calculate the net present value with the salvage value deducted

        # annuity method

        annuity = (
            investment
            * discount_rate
            / (1 - (1 + discount_rate) ** (-investment_longevity))
        )

        net_cash_flows = list(
            annuity for i in range(investment_longevity + investment_period + 1)
        )

        for year_index in range(investment_period + 1):
            net_cash_flows[year_index] = 0

        npv_inv_horizon = npv(
            discount_rates=discount_rates[0:analysis_period_span],
            net_cash_flows=net_cash_flows[0 : analysis_period_span + 1],
        )

        assert math.isclose(npv_inv_horizon, 1e6, abs_tol=1e-3)

        # *********************************************************************

        # net present value for the whole investment

        npv_asset_long = npv(
            discount_rates=discount_rates, net_cash_flows=net_cash_flows
        )

        assert math.isclose(npv_asset_long, 1e6, abs_tol=1e-3)

        # calculate discounted salvage value directly

        npv_salvage = present_salvage_value_annuity(
            investment=investment,
            investment_longevity=investment_longevity,
            investment_period=investment_period,
            discount_rate=discount_rate,
            analysis_period_span=analysis_period_span,
        )

        assert math.isclose(npv_salvage, npv_asset_long - npv_inv_horizon, abs_tol=1e-3)

        # salvage value, as seen from the last period

        und_salvage_value = salvage_value_annuity(
            investment=investment,
            discount_rate=discount_rate,
            investment_longevity=investment_longevity,
            investment_period=investment_period,
            analysis_period_span=analysis_period_span,
        )

        assert math.isclose(und_salvage_value, 0, abs_tol=1e-3)

    # *************************************************************************
    # *************************************************************************

    def test_scrap_value_annuity_ending_before_horizon(self):
        # Source:
        # Vejledning i samfundsøkonomiske analyser på energiområdet, juli 2021
        # Energistyrelsen, page 19

        investment_period = 0

        investment = 1e6  # 1E3 DKK

        investment_longevity = 15  # years

        analysis_period_span = 20  # years

        discount_rate = 0.035

        discount_rates = tuple(
            discount_rate for i in range(investment_longevity + investment_period)
        )

        # *********************************************************************

        # calculate the net present value with the salvage value deducted

        # annuity method

        annuity = (
            investment
            * discount_rate
            / (1 - (1 + discount_rate) ** (-investment_longevity))
        )

        net_cash_flows = list(
            annuity for i in range(investment_longevity + investment_period + 1)
        )

        for year_index in range(investment_period + 1):
            net_cash_flows[year_index] = 0

        npv_inv_horizon = npv(
            discount_rates=discount_rates[0:analysis_period_span],
            net_cash_flows=net_cash_flows[0 : analysis_period_span + 1],
        )

        assert math.isclose(npv_inv_horizon, 1e6, abs_tol=1e-3)

        # *********************************************************************

        # net present value for the whole investment

        npv_asset_long = npv(
            discount_rates=discount_rates, net_cash_flows=net_cash_flows
        )

        assert math.isclose(npv_asset_long, 1e6, abs_tol=1e-3)

        # calculate discounted salvage value directly

        npv_salvage = present_salvage_value_annuity(
            investment=investment,
            investment_longevity=investment_longevity,
            investment_period=investment_period,
            discount_rate=discount_rate,
            analysis_period_span=analysis_period_span,
        )

        assert math.isclose(npv_salvage, npv_asset_long - npv_inv_horizon, abs_tol=1e-3)

        # salvage value, as seen from the last period

        und_salvage_value = salvage_value_annuity(
            investment=investment,
            discount_rate=discount_rate,
            investment_longevity=investment_longevity,
            investment_period=investment_period,
            analysis_period_span=analysis_period_span,
        )

        assert math.isclose(und_salvage_value, 0, abs_tol=1e-3)

    # *************************************************************************
    # *************************************************************************

    def test_scrap_value_commissioning_delay_linear_depreciation(self):
        # **************************************************************************

        investment = 10

        investment_period = 0

        investment_longevity = 4

        analysis_period_span = 3

        # *********************************************************************

        # the investment still produces benefits after the evaluation phase

        residual_value = salvage_value_linear_depreciation(
            investment=investment,
            investment_period=investment_period,
            investment_longevity=investment_longevity,
            analysis_period_span=analysis_period_span,
        )

        assert residual_value == 2.5

        # the investment is delayed

        investment_period = 1

        residual_value = salvage_value_linear_depreciation(
            investment=investment,
            investment_period=investment_period,
            investment_longevity=investment_longevity,
            analysis_period_span=analysis_period_span,
        )

        assert residual_value == 5.0

        # the investment is delayed even more

        investment_period = 2

        residual_value = salvage_value_linear_depreciation(
            investment=investment,
            investment_period=investment_period,
            investment_longevity=investment_longevity,
            analysis_period_span=analysis_period_span,
        )

        assert residual_value == 7.5

        # the evaluation phase is longer

        investment_period = 0

        analysis_period_span = 4

        residual_value = salvage_value_linear_depreciation(
            investment=investment,
            investment_period=investment_period,
            investment_longevity=investment_longevity,
            analysis_period_span=analysis_period_span,
        )

        assert residual_value == 0.0

        # trigger ValueError: the investment takes place after the eval. phase

        investment_period = analysis_period_span + 1

        error_raised = False
        try:
            residual_value = salvage_value_linear_depreciation(
                investment=investment,
                investment_period=investment_period,
                investment_longevity=investment_longevity,
                analysis_period_span=analysis_period_span,
            )
        except ValueError:
            error_raised = True
        assert error_raised

    # *************************************************************************
    # *************************************************************************

    def test_npv(self):
        # *********************************************************************

        # data

        R_t = [
            0,
            21579,
            4002,
            3302,
            2952,
            2952,
            2952,
            2952,
            2952,
            3198,
            0,
            0,
            0,
            16154,
            2952,
            6452,
            -21930,
        ]

        n_periods = len(R_t) - 1

        R_t2 = [ncf_t * 1.5 for ncf_t in R_t]

        i_t = tuple([0.035 for k in range(n_periods)])

        i_t2 = tuple([ii_t * 1.5 for ii_t in i_t])

        # *********************************************************************

        # compute the NPV via the object

        my_inv = Investment(i_t, R_t)

        assert math.isclose(
            my_inv.net_present_value(), 45287.96018387402, abs_tol=0.001
        )

        # compute the NPV via the object using i_t2 and R_t

        my_inv.discount_rates = i_t2
        my_npv = my_inv.net_present_value()

        assert math.isclose(my_npv, 42923.405014, abs_tol=0.001)

        # compute the NPV via the object using i_t2 and R_t2

        my_inv.net_cash_flows = R_t2
        my_npv = my_inv.net_present_value()

        assert math.isclose(my_npv, 64385.107522, abs_tol=0.001)

        # compute the NPV via the _npv method using i_t and R_t2

        my_inv.discount_rates = i_t
        my_inv.net_cash_flows = R_t2
        my_npv = my_inv.net_present_value()

        assert math.isclose(my_npv, 67931.940276, abs_tol=0.001)

        # compute the NPV via the npv method using i_t and R_t

        my_npv, my_df = npv(
            discount_rates=i_t, net_cash_flows=R_t, return_discount_factors=True
        )

        assert math.isclose(my_npv, 45287.960184, abs_tol=0.001)

        # create new object without specifying the net cash flows

        my_inv = Investment(discount_rates=i_t)

        for ncf_t in my_inv.net_cash_flows:
            assert ncf_t >= 0

        my_inv.net_cash_flows = R_t
        my_npv = my_inv.net_present_value()

        assert math.isclose(my_npv, 45287.960184, abs_tol=0.001)

        # create new object by specifying the discount rate and the analysis period

        my_inv = Investment(
            None,
            net_cash_flows=R_t,
            discount_rate=i_t[0],
            analysis_period_span=len(i_t),
        )

        my_npv = my_inv.net_present_value()

        assert math.isclose(my_npv, 45287.960184, abs_tol=0.001)

        # *********************************************************************

        # force errors

        # *********************************************************************

        # TypeError('The discount rates must be provided as a tuple.')

        error_raised = False
        try:
            my_inv = Investment(list(i_t), R_t)
        except TypeError:
            error_raised = True
        assert error_raised

        # *********************************************************************

        # ValueError('The duration of the period under analysis must be positive.')

        error_raised = False
        try:
            my_inv = Investment(tuple())
        except ValueError:
            error_raised = True
        assert error_raised

        # *********************************************************************

        # TypeError('The discount rate must be provided as a float.')

        error_raised = False
        try:
            my_inv = Investment(None, None, 5, 10)
        except TypeError:
            error_raised = True
        assert error_raised

        # *********************************************************************

        # ValueError('The discount rate must be in the open interval between 0 and 1.)

        error_raised = False
        try:
            my_inv = Investment(None, None, 1.35, 10)
        except ValueError:
            error_raised = True
        assert error_raised

        # *********************************************************************

        # TypeError('The duration of the period under consideration must be provided as an integer.')

        error_raised = False
        try:
            my_inv = Investment(None, None, 0.35, 10.0)
        except TypeError:
            error_raised = True
        assert error_raised

        # *********************************************************************

        # ValueError('The duration of the period under analysis must be positive.)

        error_raised = False
        try:
            my_inv = Investment(None, None, 0.35, 0)
        except ValueError:
            error_raised = True
        assert error_raised

        # *********************************************************************

        # TypeError('The net cash flows must be provided as a list.')

        error_raised = False
        try:
            my_inv = Investment(i_t, tuple(R_t))
        except TypeError:
            error_raised = True
        assert error_raised

        # *********************************************************************

        # trigger the error for differently-sized inputs using npv() and not _npv()

        number_errors = 0

        try:
            my_npv = npv(i_t[0:-1], R_t)

        except ValueError:
            number_errors += 1

        assert number_errors == 1

        # *********************************************************************

        # trigger the error for differently-sized inputs using the __init__ method

        number_errors = 0

        try:
            my_inv = Investment(i_t[0:-1], R_t)

        except ValueError:
            number_errors += 1

        assert number_errors == 1

        # *********************************************************************


# ******************************************************************************
# ******************************************************************************