"""Plan monitor service tests — Story 7.1.

QA note: planned balances generated by calling amortization.py directly.
"""
import datetime
from decimal import Decimal

import pytest

from app.services.amortization import calculate_avalanche, AmortizationResult
from app.services.plan_monitor import get_monitor_statuses, CardMonitorStatus


# ── test fixture ──────────────────────────────────────────────────────────────

CARD = {"id": 1, "name": "Visa", "balance": "2000.00", "apr": "20.00", "min_payment": "50.00"}
EXTRA = Decimal("100.00")


def _make_result() -> AmortizationResult:
    return calculate_avalanche([CARD], EXTRA)


def _result_balance_at_month(result: AmortizationResult, month: int) -> Decimal:
    """Helper: get planned balance at a specific month number from schedule."""
    schedule = result.per_card_schedule[0]["schedule"]
    for row in schedule:
        if row["month"] == month:
            return Decimal(row["balance"])
    return Decimal("0")


def _reference_date_for_month(result: AmortizationResult, month: int) -> datetime.date:
    """Get the date of a specific schedule row."""
    schedule = result.per_card_schedule[0]["schedule"]
    for row in schedule:
        if row["month"] == month:
            return datetime.date.fromisoformat(row["date"])
    return datetime.date.today()


# ── basic structure ───────────────────────────────────────────────────────────

class TestMonitorBasicStructure:
    def test_returns_list_of_statuses(self):
        result = _make_result()
        statuses = get_monitor_statuses(result, [])
        assert isinstance(statuses, list)
        assert len(statuses) == 1

    def test_status_has_required_fields(self):
        result = _make_result()
        status = get_monitor_statuses(result, [])[0]
        assert isinstance(status, CardMonitorStatus)
        assert hasattr(status, "account_id")
        assert hasattr(status, "planned_balance")
        assert hasattr(status, "actual_balance")
        assert hasattr(status, "variance")
        assert hasattr(status, "is_behind")
        assert hasattr(status, "is_stale")

    def test_account_id_matches_card(self):
        result = _make_result()
        status = get_monitor_statuses(result, [])[0]
        assert status.account_id == 1

    def test_account_name_matches_card(self):
        result = _make_result()
        status = get_monitor_statuses(result, [])[0]
        assert status.account_name == "Visa"


# ── deviation detection ───────────────────────────────────────────────────────

class TestDeviationDetection:
    def test_exactly_at_5_percent_not_flagged(self):
        """Exactly 5% over is NOT behind — requires strictly > 5%."""
        result = _make_result()
        ref_date = _reference_date_for_month(result, 3)
        planned = _result_balance_at_month(result, 3)

        # Exactly 5% over: planned * 1.05 — use ROUND_DOWN to avoid floating up past threshold
        from decimal import ROUND_DOWN
        exactly_5pct = (planned * Decimal("1.05")).quantize(Decimal("0.01"), rounding=ROUND_DOWN)
        updates = [{"account_id": 1, "balance": exactly_5pct,
                    "updated_at": datetime.datetime.combine(ref_date, datetime.time())}]
        statuses = get_monitor_statuses(result, updates, reference_date=ref_date)
        assert statuses[0].is_behind is False

    def test_just_over_5_percent_is_flagged(self):
        """5.01% over IS behind."""
        result = _make_result()
        ref_date = _reference_date_for_month(result, 3)
        planned = _result_balance_at_month(result, 3)

        over_5pct = (planned * Decimal("1.0501")).quantize(Decimal("0.01"))
        updates = [{"account_id": 1, "balance": over_5pct,
                    "updated_at": datetime.datetime.combine(ref_date, datetime.time())}]
        statuses = get_monitor_statuses(result, updates, reference_date=ref_date)
        assert statuses[0].is_behind is True

    def test_below_plan_not_flagged(self):
        """Actual balance below planned is good — not flagged."""
        result = _make_result()
        ref_date = _reference_date_for_month(result, 3)
        planned = _result_balance_at_month(result, 3)

        below_plan = (planned * Decimal("0.90")).quantize(Decimal("0.01"))
        updates = [{"account_id": 1, "balance": below_plan,
                    "updated_at": datetime.datetime.combine(ref_date, datetime.time())}]
        statuses = get_monitor_statuses(result, updates, reference_date=ref_date)
        assert statuses[0].is_behind is False

    def test_no_updates_not_flagged(self):
        """No balance updates: no deviation flag (unknown, not behind)."""
        result = _make_result()
        statuses = get_monitor_statuses(result, [])
        assert statuses[0].is_behind is False


# ── stale data ────────────────────────────────────────────────────────────────

class TestStaleData:
    def test_no_updates_is_stale(self):
        result = _make_result()
        statuses = get_monitor_statuses(result, [])
        assert statuses[0].is_stale is True

    def test_recent_update_is_not_stale(self):
        result = _make_result()
        today = datetime.date.today()
        updates = [{"account_id": 1, "balance": Decimal("1000.00"),
                    "updated_at": datetime.datetime.combine(today, datetime.time())}]
        statuses = get_monitor_statuses(result, updates)
        assert statuses[0].is_stale is False

    def test_update_over_30_days_is_stale(self):
        result = _make_result()
        old_date = datetime.date.today() - datetime.timedelta(days=31)
        updates = [{"account_id": 1, "balance": Decimal("1000.00"),
                    "updated_at": datetime.datetime.combine(old_date, datetime.time())}]
        statuses = get_monitor_statuses(result, updates)
        assert statuses[0].is_stale is True

    def test_update_exactly_30_days_not_stale(self):
        result = _make_result()
        thirty_days_ago = datetime.date.today() - datetime.timedelta(days=30)
        updates = [{"account_id": 1, "balance": Decimal("1000.00"),
                    "updated_at": datetime.datetime.combine(thirty_days_ago, datetime.time())}]
        statuses = get_monitor_statuses(result, updates)
        assert statuses[0].is_stale is False


# ── mid-month balance update ──────────────────────────────────────────────────

class TestMidMonthUpdate:
    def test_most_recent_update_used_when_multiple_exist(self):
        """When multiple updates exist, only the most recent one is used."""
        result = _make_result()
        today = datetime.date.today()
        yesterday = today - datetime.timedelta(days=1)
        updates = [
            {"account_id": 1, "balance": Decimal("1500.00"),
             "updated_at": datetime.datetime.combine(yesterday, datetime.time())},
            {"account_id": 1, "balance": Decimal("1200.00"),
             "updated_at": datetime.datetime.combine(today, datetime.time())},
        ]
        statuses = get_monitor_statuses(result, updates)
        assert statuses[0].actual_balance == Decimal("1200.00")


# ── variance calculation ──────────────────────────────────────────────────────

class TestVarianceCalculation:
    def test_variance_is_actual_minus_planned(self):
        result = _make_result()
        ref_date = _reference_date_for_month(result, 2)
        planned = _result_balance_at_month(result, 2)
        actual = planned + Decimal("100.00")
        updates = [{"account_id": 1, "balance": actual,
                    "updated_at": datetime.datetime.combine(ref_date, datetime.time())}]
        statuses = get_monitor_statuses(result, updates, reference_date=ref_date)
        assert statuses[0].variance == Decimal("100.00")

    def test_negative_variance_when_below_plan(self):
        result = _make_result()
        ref_date = _reference_date_for_month(result, 2)
        planned = _result_balance_at_month(result, 2)
        actual = planned - Decimal("100.00")
        updates = [{"account_id": 1, "balance": max(actual, Decimal("0")),
                    "updated_at": datetime.datetime.combine(ref_date, datetime.time())}]
        statuses = get_monitor_statuses(result, updates, reference_date=ref_date)
        assert statuses[0].variance < Decimal("0")
