"""Spend model: per-Case real-GBP spend, COL-scaled by country. London is the identity baseline (ratios = 1.0); cheaper countries scale the COL-driven buckets (rent, non-rent essentials, kids) down while Holidays stay fixed. The ×1.5 safety multiplier applies to the whole spend. """ from __future__ import annotations import pytest from fire_planner.spend_model import ( ANCA, LONDON_RATIOS, VIKTOR, Case, ColRatios, case_base_spend, col_ratios_from_snapshot, kids_annual_spend, scaled_person_spend, ) def test_london_identity_is_raw_sum() -> None: # Viktor's measured buckets sum to his nominal trailing-12mo spend. assert scaled_person_spend(VIKTOR, LONDON_RATIOS) == pytest.approx(40_492.0, abs=1.0) def test_holidays_are_fixed_across_countries() -> None: cheap = ColRatios(rent_ratio=0.5, non_rent_ratio=0.5) scaled = scaled_person_spend(VIKTOR, cheap) # rent + non-rent halve; holidays unchanged. expected = VIKTOR.rent * 0.5 + VIKTOR.non_rent_usual * 0.5 + VIKTOR.holidays assert scaled == pytest.approx(expected) # Holidays floor: spend can never drop below the fixed holiday spend. assert scaled > VIKTOR.holidays def test_safety_multiplier_applies_to_case() -> None: solo = case_base_spend(Case.SOLO, LONDON_RATIOS) assert solo == pytest.approx(scaled_person_spend(VIKTOR, LONDON_RATIOS) * 1.5) def test_household_adds_anca() -> None: hh = case_base_spend(Case.HOUSEHOLD, LONDON_RATIOS) expected = (scaled_person_spend(VIKTOR, LONDON_RATIOS) + scaled_person_spend(ANCA, LONDON_RATIOS)) * 1.5 assert hh == pytest.approx(expected) # Household ~£82k * 1.5 ≈ £121.6k at London prices. assert hh == pytest.approx(121_638.0, abs=50.0) def test_family_base_equals_household_kids_are_separate() -> None: # Kids are modelled as a cashflow, not folded into the GK spend target. assert case_base_spend(Case.FAMILY, LONDON_RATIOS) == pytest.approx( case_base_spend(Case.HOUSEHOLD, LONDON_RATIOS)) def test_kids_are_col_driven_and_safety_scaled() -> None: assert kids_annual_spend(LONDON_RATIOS) == pytest.approx(15_000 * 1.5) cheap = ColRatios(rent_ratio=0.3, non_rent_ratio=0.5) # Kids scale by the non-rent (services) ratio. assert kids_annual_spend(cheap) == pytest.approx(15_000 * 0.5 * 1.5) def test_col_ratios_from_snapshot_sofia() -> None: # Sofia vs London (Numbeo, May 2026): rent 679/2317, no-rent 713/1092. r = col_ratios_from_snapshot( city_no_rent=713.0, city_rent_1bed=679.0, london_no_rent=1092.0, london_rent_1bed=2317.0, ) assert r.rent_ratio == pytest.approx(679.0 / 2317.0) assert r.non_rent_ratio == pytest.approx(713.0 / 1092.0) def test_cheaper_country_lowers_case_spend() -> None: sofia = col_ratios_from_snapshot(713.0, 679.0, 1092.0, 2317.0) assert case_base_spend(Case.SOLO, sofia) < case_base_spend(Case.SOLO, LONDON_RATIOS)