"""Amortization engine tests — Story 6.2."""
from decimal import Decimal
import datetime

import pytest

from app.services.amortization import (
    calculate_avalanche,
    calculate_snowball,
    calculate_highest_balance,
    calculate_proportional,
    calculate_custom,
    AmortizationResult,
)

# ── test fixtures ─────────────────────────────────────────────────────────────

CARD_A = {"id": 1, "name": "Visa",       "balance": "1000.00", "apr": "20.00", "min_payment": "25.00"}
CARD_B = {"id": 2, "name": "Mastercard", "balance": "2000.00", "apr": "15.00", "min_payment": "40.00"}
CARD_C = {"id": 3, "name": "Discover",   "balance": "500.00",  "apr": "25.00", "min_payment": "15.00"}
EXTRA = Decimal("200.00")


# ── AmortizationResult structure ──────────────────────────────────────────────

class TestAmortizationResultStructure:
    def test_returns_amortization_result(self):
        result = calculate_avalanche([CARD_A], Decimal("50"))
        assert isinstance(result, AmortizationResult)

    def test_strategy_name_is_string(self):
        result = calculate_avalanche([CARD_A], Decimal("50"))
        assert isinstance(result.strategy, str)
        assert result.strategy == "avalanche"

    def test_months_to_payoff_is_int(self):
        result = calculate_avalanche([CARD_A], Decimal("50"))
        assert isinstance(result.months_to_payoff, int)
        assert result.months_to_payoff > 0

    def test_total_interest_is_decimal(self):
        result = calculate_avalanche([CARD_A], Decimal("50"))
        assert isinstance(result.total_interest_paid, Decimal)

    def test_payoff_date_is_date(self):
        result = calculate_avalanche([CARD_A], Decimal("50"))
        assert isinstance(result.payoff_date, datetime.date)

    def test_per_card_schedule_is_list(self):
        result = calculate_avalanche([CARD_A], Decimal("50"))
        assert isinstance(result.per_card_schedule, list)
        assert len(result.per_card_schedule) == 1

    def test_per_card_schedule_has_required_keys(self):
        result = calculate_avalanche([CARD_A], Decimal("50"))
        card = result.per_card_schedule[0]
        assert "account_id" in card
        assert "account_name" in card
        assert "payoff_month" in card
        assert "payoff_date" in card
        assert "total_interest" in card
        assert "schedule" in card

    def test_schedule_rows_have_required_keys(self):
        result = calculate_avalanche([CARD_A], Decimal("50"))
        row = result.per_card_schedule[0]["schedule"][0]
        for key in ("month", "date", "payment", "principal", "interest", "balance"):
            assert key in row


# ── known-scenario correctness ────────────────────────────────────────────────

class TestKnownScenario:
    """Single card: $1000 balance, 20% APR, $25 min, $0 extra => ~48 months."""

    def test_single_card_eventually_pays_off(self):
        card = {"id": 1, "name": "A", "balance": "1000.00", "apr": "20.00", "min_payment": "25.00"}
        result = calculate_avalanche([card], Decimal("0"))
        assert result.months_to_payoff > 0
        last_row = result.per_card_schedule[0]["schedule"][-1]
        assert Decimal(last_row["balance"]) == Decimal("0.00")

    def test_extra_payment_reduces_payoff_time(self):
        card = {"id": 1, "name": "A", "balance": "1000.00", "apr": "20.00", "min_payment": "25.00"}
        r_no_extra = calculate_avalanche([card], Decimal("0"))
        r_with_extra = calculate_avalanche([card], Decimal("100"))
        assert r_with_extra.months_to_payoff < r_no_extra.months_to_payoff

    def test_extra_payment_reduces_interest(self):
        card = {"id": 1, "name": "A", "balance": "1000.00", "apr": "20.00", "min_payment": "25.00"}
        r_no_extra = calculate_avalanche([card], Decimal("0"))
        r_with_extra = calculate_avalanche([card], Decimal("100"))
        assert r_with_extra.total_interest_paid < r_no_extra.total_interest_paid


# ── zero extra ────────────────────────────────────────────────────────────────

class TestZeroExtra:
    def test_zero_extra_avalanche_pays_off(self):
        result = calculate_avalanche([CARD_A], Decimal("0"))
        assert result.months_to_payoff > 0
        assert Decimal(result.per_card_schedule[0]["schedule"][-1]["balance"]) == Decimal("0.00")

    def test_zero_extra_snowball_pays_off(self):
        result = calculate_snowball([CARD_A], Decimal("0"))
        assert result.months_to_payoff > 0


# ── strategy differences ──────────────────────────────────────────────────────

class TestAvalancheVsSnowball:
    def test_avalanche_directs_extra_to_highest_apr(self):
        """With extra money, avalanche should target CARD_C (25% APR) first."""
        result = calculate_avalanche([CARD_A, CARD_B, CARD_C], EXTRA)
        # CARD_C (25% APR, $500) should pay off before CARD_A (20%, $1000)
        card_c = next(c for c in result.per_card_schedule if c["account_id"] == 3)
        card_a = next(c for c in result.per_card_schedule if c["account_id"] == 1)
        assert card_c["payoff_month"] <= card_a["payoff_month"]

    def test_snowball_directs_extra_to_lowest_balance(self):
        """With extra money, snowball should target CARD_C ($500) first."""
        result = calculate_snowball([CARD_A, CARD_B, CARD_C], EXTRA)
        card_c = next(c for c in result.per_card_schedule if c["account_id"] == 3)
        card_b = next(c for c in result.per_card_schedule if c["account_id"] == 2)
        assert card_c["payoff_month"] <= card_b["payoff_month"]

    def test_highest_balance_directs_extra_to_largest_balance(self):
        """Highest balance strategy should target CARD_B ($2000) first."""
        result = calculate_highest_balance([CARD_A, CARD_B, CARD_C], EXTRA)
        card_b = next(c for c in result.per_card_schedule if c["account_id"] == 2)
        card_c = next(c for c in result.per_card_schedule if c["account_id"] == 3)
        assert card_b["payoff_month"] <= card_c["payoff_month"]

    def test_avalanche_minimizes_total_interest(self):
        """Avalanche should produce <= interest compared to snowball on high-APR-variance cards."""
        r_av = calculate_avalanche([CARD_A, CARD_B, CARD_C], EXTRA)
        r_sb = calculate_snowball([CARD_A, CARD_B, CARD_C], EXTRA)
        assert r_av.total_interest_paid <= r_sb.total_interest_paid


# ── single card edge cases ────────────────────────────────────────────────────

class TestSingleCard:
    def test_all_strategies_handle_single_card(self):
        card = {"id": 1, "name": "Solo", "balance": "500.00", "apr": "18.00", "min_payment": "15.00"}
        extra = Decimal("50.00")
        for fn in [calculate_avalanche, calculate_snowball, calculate_highest_balance, calculate_proportional]:
            result = fn([card], extra)
            assert result.months_to_payoff > 0


# ── zero-balance card skipped ─────────────────────────────────────────────────

class TestZeroBalanceSkipped:
    def test_zero_balance_card_does_not_affect_payoff(self):
        card_live = {"id": 1, "name": "Live", "balance": "1000.00", "apr": "20.00", "min_payment": "25.00"}
        card_zero = {"id": 2, "name": "Zero", "balance": "0.00",    "apr": "15.00", "min_payment": "10.00"}
        result = calculate_avalanche([card_live, card_zero], Decimal("50"))
        zero_card = next(c for c in result.per_card_schedule if c["account_id"] == 2)
        assert zero_card["total_interest"] == "0.00"

    def test_zero_balance_card_included_in_per_card(self):
        card_live = {"id": 1, "name": "Live", "balance": "1000.00", "apr": "20.00", "min_payment": "25.00"}
        card_zero = {"id": 2, "name": "Zero", "balance": "0.00",    "apr": "15.00", "min_payment": "10.00"}
        result = calculate_avalanche([card_live, card_zero], Decimal("0"))
        ids = [c["account_id"] for c in result.per_card_schedule]
        assert 2 in ids


# ── proportional ──────────────────────────────────────────────────────────────

class TestProportional:
    def test_proportional_pays_off_all_cards(self):
        result = calculate_proportional([CARD_A, CARD_B, CARD_C], EXTRA)
        for card in result.per_card_schedule:
            assert Decimal(card["schedule"][-1]["balance"]) == Decimal("0.00")

    def test_proportional_strategy_name(self):
        result = calculate_proportional([CARD_A], Decimal("50"))
        assert result.strategy == "proportional"


# ── custom ────────────────────────────────────────────────────────────────────

class TestCustom:
    def test_custom_allocations_respected(self):
        allocations = {1: Decimal("200.00"), 2: Decimal("300.00")}
        result = calculate_custom([CARD_A, CARD_B], allocations)
        assert isinstance(result, AmortizationResult)
        assert result.strategy == "custom"

    def test_custom_below_minimum_raised_to_minimum(self):
        """If custom allocation is below minimum, minimum is used."""
        allocations = {1: Decimal("5.00")}  # min is $25
        card = {"id": 1, "name": "A", "balance": "500.00", "apr": "20.00", "min_payment": "25.00"}
        result = calculate_custom([card], allocations)
        assert result.months_to_payoff > 0
        # Should still pay off (because minimum is enforced)
        assert Decimal(result.per_card_schedule[0]["schedule"][-1]["balance"]) == Decimal("0.00")

    def test_custom_pays_off_all(self):
        allocations = {1: Decimal("100.00"), 2: Decimal("200.00"), 3: Decimal("80.00")}
        result = calculate_custom([CARD_A, CARD_B, CARD_C], allocations)
        for card in result.per_card_schedule:
            assert Decimal(card["schedule"][-1]["balance"]) == Decimal("0.00")


# ── decimal type enforcement ──────────────────────────────────────────────────

class TestDecimalTypes:
    def test_total_interest_is_decimal_type(self):
        result = calculate_avalanche([CARD_A], Decimal("50"))
        assert type(result.total_interest_paid) is Decimal

    def test_schedule_amounts_are_string_formatted_decimals(self):
        result = calculate_avalanche([CARD_A], Decimal("50"))
        row = result.per_card_schedule[0]["schedule"][0]
        for key in ("payment", "principal", "interest", "balance"):
            val = Decimal(row[key])  # must be parseable as Decimal
            assert isinstance(val, Decimal)
