"""Tests for the COL module — baseline lookup + ratio + simulator wiring.""" from __future__ import annotations from decimal import Decimal import pytest from fire_planner.col import ( JURISDICTION_REPRESENTATIVE_CITY, compute_col_ratio, lookup_city, representative_city_for, ) from fire_planner.col.baseline import BASELINES from fire_planner.col.models import CityCostIndex class TestBaselineCoverage: """Every jurisdiction with a representative city must have a baseline.""" def test_all_representative_cities_have_baselines(self) -> None: missing = [ city for city in JURISDICTION_REPRESENTATIVE_CITY.values() if city not in BASELINES ] assert missing == [], ( f"jurisdiction map points at city(s) without baselines: {missing}" ) def test_baselines_have_positive_totals(self) -> None: for slug, idx in BASELINES.items(): assert idx.total_single_no_rent_gbp > 0, f"{slug} no_rent must be positive" assert idx.total_single_with_rent_gbp > idx.total_single_no_rent_gbp, ( f"{slug} with_rent must exceed no_rent — rent should be a positive add" ) def test_baseline_source_provenance_present(self) -> None: for slug, idx in BASELINES.items(): assert idx.source.name in {"numbeo", "expatistan", "baseline", "manual"} assert idx.source.url is not None, f"{slug} baseline missing source URL" assert idx.source.url.startswith("https://"), f"{slug} URL must be https" class TestLookup: def test_lookup_known_city(self) -> None: london = lookup_city("london") assert isinstance(london, CityCostIndex) assert london.city == "London" assert london.country == "United Kingdom" def test_lookup_normalises_input(self) -> None: # mixed case, spaces → slug assert lookup_city("Kuala Lumpur").city == "Kuala Lumpur" assert lookup_city(" Bangkok ").city == "Bangkok" def test_lookup_unknown_raises(self) -> None: with pytest.raises(KeyError, match="No COL baseline"): lookup_city("atlantis") class TestColRatio: def test_identity_returns_one(self) -> None: assert compute_col_ratio("london", "london") == Decimal("1") def test_sofia_cheaper_than_london(self) -> None: ratio = compute_col_ratio("london", "sofia") assert ratio < Decimal("1"), "Sofia must be cheaper than London" assert ratio > Decimal("0.2"), "Sofia ratio looks implausibly low" # Real Numbeo number is ~0.41 assert Decimal("0.35") < ratio < Decimal("0.50") def test_dubai_cheaper_than_london(self) -> None: # Dubai is *cheaper* than London on Numbeo's headline because # London rent dominates. This was a surprise — flag it in the # baseline note for future-us. ratio = compute_col_ratio("london", "dubai") assert ratio < Decimal("1") assert Decimal("0.70") < ratio < Decimal("0.95") def test_bangkok_far_cheaper_than_london(self) -> None: ratio = compute_col_ratio("london", "bangkok") assert ratio < Decimal("0.40") def test_inverse_consistency(self) -> None: # If london→sofia is X, sofia→london should be ~1/X within rounding. l2s = compute_col_ratio("london", "sofia") s2l = compute_col_ratio("sofia", "london") assert abs(l2s * s2l - Decimal("1")) < Decimal("0.001") class TestRepresentativeCity: def test_known_jurisdictions(self) -> None: assert representative_city_for("uk") == "london" assert representative_city_for("cyprus") == "limassol" assert representative_city_for("bulgaria") == "sofia" assert representative_city_for("uae") == "dubai" assert representative_city_for("malaysia") == "kuala-lumpur" assert representative_city_for("thailand") == "bangkok" def test_nomad_returns_none(self) -> None: # Nomad mode is COL-invariant by design — auto-adjust skipped. assert representative_city_for("nomad") is None def test_unknown_returns_none(self) -> None: assert representative_city_for("vulcan") is None