from datetime import date, timedelta
from decimal import Decimal
from uuid import uuid4

import pytest

from app.services.planner.config import PlannerConfig
from app.services.planner.filter import filter_recipes
from app.services.planner.types import RecipeCost


_CFG = PlannerConfig()


def _recipe(
    recipe_id=None,
    name="r",
    prep=10,
    cook=20,
    calories=2000,
    protein="chicken",
    cuisine_tags=("american",),
):
    return {
        "id": recipe_id or uuid4(),
        "name": name,
        "prep_time_minutes": prep,
        "cook_time_minutes": cook,
        "calories_per_serving": calories,
        "protein_type": protein,
        "cuisine_tags": list(cuisine_tags),
    }


def _cost(recipe_id, total_cost):
    return RecipeCost(
        recipe_id=recipe_id,
        total_cost=Decimal(str(total_cost)),
        total_savings=Decimal("0"),
        sale_ingredient_count=0,
        matched_ingredient_count=1,
        total_ingredient_count=1,
        pantry_hit_count=0,
        line_items=[],
    )


def test_filter_blocks_by_never_suggest_ingredient():
    r1 = _recipe(name="has-mushrooms")
    r2 = _recipe(name="clean")
    blocked_ingredient = uuid4()
    recipe_ingredient_ids = {r1["id"]: {blocked_ingredient}, r2["id"]: {uuid4()}}

    result = filter_recipes(
        recipes=[r1, r2],
        recipe_ingredient_ids=recipe_ingredient_ids,
        recipe_costs={r1["id"]: _cost(r1["id"], 10), r2["id"]: _cost(r2["id"], 10)},
        blocked_ingredient_ids={blocked_ingredient},
        blocked_recipe_ids=set(),
        last_cooked_at={},
        family_calorie_target=2000,
        config=_CFG,
        today=date(2026, 5, 5),
    )
    assert r2["id"] in result.feasible_recipe_ids
    assert r1["id"] not in result.feasible_recipe_ids
    assert "blocked_ingredient" in result.rejected[r1["id"]]


def test_filter_blocks_by_never_suggest_recipe():
    r1 = _recipe()
    result = filter_recipes(
        recipes=[r1],
        recipe_ingredient_ids={r1["id"]: set()},
        recipe_costs={r1["id"]: _cost(r1["id"], 10)},
        blocked_ingredient_ids=set(),
        blocked_recipe_ids={r1["id"]},
        last_cooked_at={},
        family_calorie_target=2000,
        config=_CFG,
        today=date(2026, 5, 5),
    )
    assert r1["id"] not in result.feasible_recipe_ids
    assert "blocked_recipe" in result.rejected[r1["id"]]


def test_filter_blocks_by_recency():
    r1 = _recipe()
    last_week = date(2026, 5, 5) - timedelta(days=7)
    result = filter_recipes(
        recipes=[r1],
        recipe_ingredient_ids={r1["id"]: set()},
        recipe_costs={r1["id"]: _cost(r1["id"], 10)},
        blocked_ingredient_ids=set(),
        blocked_recipe_ids=set(),
        last_cooked_at={r1["id"]: last_week},
        family_calorie_target=2000,
        config=_CFG,
        today=date(2026, 5, 5),
    )
    assert "recency" in result.rejected[r1["id"]]


def test_filter_blocks_by_calories():
    over = _recipe(calories=3500)  # 75% over a 2000 target
    under = _recipe(calories=2200)
    result = filter_recipes(
        recipes=[over, under],
        recipe_ingredient_ids={over["id"]: set(), under["id"]: set()},
        recipe_costs={over["id"]: _cost(over["id"], 10), under["id"]: _cost(under["id"], 10)},
        blocked_ingredient_ids=set(),
        blocked_recipe_ids=set(),
        last_cooked_at={},
        family_calorie_target=2000,
        config=_CFG,
        today=date(2026, 5, 5),
    )
    assert under["id"] in result.feasible_recipe_ids
    assert "calories" in result.rejected[over["id"]]


def test_filter_blocks_by_time():
    slow = _recipe(prep=30, cook=20)  # 50 min total
    fast = _recipe(prep=10, cook=20)
    result = filter_recipes(
        recipes=[slow, fast],
        recipe_ingredient_ids={slow["id"]: set(), fast["id"]: set()},
        recipe_costs={slow["id"]: _cost(slow["id"], 10), fast["id"]: _cost(fast["id"], 10)},
        blocked_ingredient_ids=set(),
        blocked_recipe_ids=set(),
        last_cooked_at={},
        family_calorie_target=2000,
        config=_CFG,
        today=date(2026, 5, 5),
    )
    assert "time" in result.rejected[slow["id"]]
    assert fast["id"] in result.feasible_recipe_ids


def test_filter_blocks_by_cost():
    pricey = _recipe()
    cheap = _recipe()
    result = filter_recipes(
        recipes=[pricey, cheap],
        recipe_ingredient_ids={pricey["id"]: set(), cheap["id"]: set()},
        recipe_costs={pricey["id"]: _cost(pricey["id"], 50), cheap["id"]: _cost(cheap["id"], 25)},
        blocked_ingredient_ids=set(),
        blocked_recipe_ids=set(),
        last_cooked_at={},
        family_calorie_target=2000,
        config=_CFG,
        today=date(2026, 5, 5),
    )
    assert "cost" in result.rejected[pricey["id"]]
    assert cheap["id"] in result.feasible_recipe_ids


def test_filter_passes_when_calorie_target_is_none():
    r = _recipe(calories=9999)  # absurd, but no target → can't enforce
    result = filter_recipes(
        recipes=[r],
        recipe_ingredient_ids={r["id"]: set()},
        recipe_costs={r["id"]: _cost(r["id"], 10)},
        blocked_ingredient_ids=set(),
        blocked_recipe_ids=set(),
        last_cooked_at={},
        family_calorie_target=None,
        config=_CFG,
        today=date(2026, 5, 5),
    )
    assert r["id"] in result.feasible_recipe_ids


def test_filter_blocks_recipe_with_null_calories_when_target_set():
    r = _recipe(calories=None)
    result = filter_recipes(
        recipes=[r],
        recipe_ingredient_ids={r["id"]: set()},
        recipe_costs={r["id"]: _cost(r["id"], 10)},
        blocked_ingredient_ids=set(),
        blocked_recipe_ids=set(),
        last_cooked_at={},
        family_calorie_target=2000,
        config=_CFG,
        today=date(2026, 5, 5),
    )
    assert "calories" in result.rejected[r["id"]]
