from __future__ import annotations

from datetime import date

import pytest

from services.financial import (
    CashFlow,
    PaybackNotReachedError,
    internal_rate_of_return,
    net_present_value,
    normalize_cash_flows,
    payback_period,
)


def test_normalize_cash_flows_with_dates() -> None:
    base = date(2025, 1, 1)
    period_length = 365.0 / 4
    flows = [
        CashFlow(amount=-1_000_000, date=base),
        CashFlow(amount=350_000, date=date(2025, 4, 1)),
        CashFlow(amount=420_000, date=date(2025, 7, 1)),
    ]

    normalised = normalize_cash_flows(flows, compounds_per_year=4)

    assert normalised[0] == (-1_000_000.0, 0.0)
    expected_second = (date(2025, 4, 1) - base).days / period_length
    expected_third = (date(2025, 7, 1) - base).days / period_length

    assert normalised[1][1] == pytest.approx(expected_second, rel=1e-6)
    assert normalised[2][1] == pytest.approx(expected_third, rel=1e-6)


def test_net_present_value_with_period_indices() -> None:
    rate = 0.10
    flows = [
        CashFlow(amount=-1_000, period_index=0),
        CashFlow(amount=500, period_index=1),
        CashFlow(amount=500, period_index=2),
        CashFlow(amount=500, period_index=3),
    ]

    expected = -1_000 + sum(500 / (1 + rate) **
                            period for period in range(1, 4))

    result = net_present_value(rate, flows)

    assert result == pytest.approx(expected, rel=1e-9)


def test_net_present_value_with_residual_value() -> None:
    rate = 0.08
    flows = [
        CashFlow(amount=-100_000, period_index=0),
        CashFlow(amount=30_000, period_index=1),
        CashFlow(amount=35_000, period_index=2),
    ]

    expected = (
        -100_000
        + 30_000 / (1 + rate)
        + 35_000 / (1 + rate) ** 2
        + 25_000 / (1 + rate) ** 3
    )

    result = net_present_value(rate, flows, residual_value=25_000)

    assert result == pytest.approx(expected, rel=1e-9)


def test_internal_rate_of_return_simple_case() -> None:
    flows = [
        CashFlow(amount=-1_000, period_index=0),
        CashFlow(amount=1_210, period_index=1),
    ]

    irr = internal_rate_of_return(flows, guess=0.05)

    assert irr == pytest.approx(0.21, rel=1e-9)


def test_internal_rate_of_return_multiple_sign_changes() -> None:
    flows = [
        CashFlow(amount=-500_000, period_index=0),
        CashFlow(amount=250_000, period_index=1),
        CashFlow(amount=-100_000, period_index=2),
        CashFlow(amount=425_000, period_index=3),
    ]

    irr = internal_rate_of_return(flows, guess=0.2)

    npv = net_present_value(irr, flows)

    assert npv == pytest.approx(0.0, abs=1e-6)


def test_internal_rate_of_return_requires_mixed_signs() -> None:
    flows = [
        CashFlow(amount=100_000, period_index=0),
        CashFlow(amount=150_000, period_index=1),
    ]

    with pytest.raises(ValueError):
        internal_rate_of_return(flows)


def test_payback_period_exact_period() -> None:
    flows = [
        CashFlow(amount=-120_000, period_index=0),
        CashFlow(amount=40_000, period_index=1),
        CashFlow(amount=40_000, period_index=2),
        CashFlow(amount=40_000, period_index=3),
    ]

    period = payback_period(flows, allow_fractional=False)

    assert period == pytest.approx(3.0)


def test_payback_period_fractional_period() -> None:
    flows = [
        CashFlow(amount=-100_000, period_index=0),
        CashFlow(amount=80_000, period_index=1),
        CashFlow(amount=30_000, period_index=2),
    ]

    fractional = payback_period(flows)
    whole = payback_period(flows, allow_fractional=False)

    assert fractional == pytest.approx(1 + 20_000 / 30_000, rel=1e-9)
    assert whole == pytest.approx(2.0)


def test_payback_period_raises_when_never_recovered() -> None:
    flows = [
        CashFlow(amount=-250_000, period_index=0),
        CashFlow(amount=50_000, period_index=1),
        CashFlow(amount=60_000, period_index=2),
        CashFlow(amount=70_000, period_index=3),
    ]

    with pytest.raises(PaybackNotReachedError):
        payback_period(flows)


def test_payback_period_with_quarterly_compounding() -> None:
    base = date(2025, 1, 1)
    flows = [
        CashFlow(amount=-120_000, date=base),
        CashFlow(amount=35_000, date=date(2025, 4, 1)),
        CashFlow(amount=35_000, date=date(2025, 7, 1)),
        CashFlow(amount=50_000, date=date(2025, 10, 1)),
    ]

    period = payback_period(flows, compounds_per_year=4)

    period_length = 365.0 / 4
    expected_period = (date(2025, 10, 1) - base).days / period_length

    assert period == pytest.approx(expected_period, abs=1e-6)
