from datetime import date, timedelta from decimal import Decimal from uuid import uuid4 from app.services.planner.config import PlannerConfig from app.services.planner.score import ( score_recipes, time_bonus, recency_bonus, normalize_savings, ) from app.services.planner.types import RecipeCost _CFG = PlannerConfig() def test_time_bonus_capped_at_ideal(): assert time_bonus(20, _CFG) == 1.0 assert time_bonus(25, _CFG) == 1.0 def test_time_bonus_decays_to_zero_at_full(): assert time_bonus(45, _CFG) == 0.0 def test_time_bonus_linear_midpoint(): # 35 min is halfway between 25 and 45 assert abs(time_bonus(35, _CFG) - 0.5) < 1e-6 def test_recency_bonus_full_when_long_ago(): today = date(2026, 5, 5) long_ago = today - timedelta(weeks=20) assert recency_bonus(long_ago, today, _CFG) == 1.0 def test_recency_bonus_zero_when_just_eligible(): # right at recency_weeks boundary → 0 today = date(2026, 5, 5) cutoff = today - timedelta(weeks=_CFG.recency_weeks) assert recency_bonus(cutoff, today, _CFG) == 0.0 def test_recency_bonus_full_when_never_cooked(): assert recency_bonus(None, date(2026, 5, 5), _CFG) == 1.0 def test_normalize_savings_minmax(): out = normalize_savings([Decimal("0"), Decimal("5"), Decimal("10")]) assert out == [0.0, 0.5, 1.0] def test_normalize_savings_uniform_returns_zeros(): out = normalize_savings([Decimal("3"), Decimal("3"), Decimal("3")]) assert out == [0.0, 0.0, 0.0] def test_score_recipes_orders_by_combined_score(): recipes = [ { "id": uuid4(), "name": "low_savings", "prep_time_minutes": 10, "cook_time_minutes": 30, "protein_type": "chicken", "cuisine_tags": ["american"], }, { "id": uuid4(), "name": "high_savings", "prep_time_minutes": 10, "cook_time_minutes": 15, # also lower time "protein_type": "beef", "cuisine_tags": ["mexican"], }, ] costs = { recipes[0]["id"]: RecipeCost( recipe_id=recipes[0]["id"], total_cost=Decimal("10"), total_savings=Decimal("1"), sale_ingredient_count=1, matched_ingredient_count=4, total_ingredient_count=4, pantry_hit_count=0, line_items=[], ), recipes[1]["id"]: RecipeCost( recipe_id=recipes[1]["id"], total_cost=Decimal("12"), total_savings=Decimal("8"), sale_ingredient_count=3, matched_ingredient_count=4, total_ingredient_count=4, pantry_hit_count=2, line_items=[], ), } scored = score_recipes( recipes=recipes, recipe_costs=costs, last_cooked_at={}, config=_CFG, today=date(2026, 5, 5), ) assert scored[0].recipe_id == recipes[1]["id"] # high_savings first