from decimal import Decimal
from uuid import uuid4

from app.services.planner.config import PlannerConfig
from app.services.planner.select import select_set, set_diversity_penalty
from app.services.planner.types import RecipeCost, ScoredRecipe


_CFG = PlannerConfig()
_CFG_3 = PlannerConfig(w_savings=0.30, w_coverage=0.25, w_pantry=0.10, w_time=0.15, w_recency=0.20,
                       set_size=3, top_k=20, p_protein=0.15, p_cuisine=0.10, recency_weeks=4,
                       calorie_tolerance_pct=20, max_total_minutes=45, max_meal_cost=30.00,
                       time_ideal_minutes=25, time_full_minutes=45, recency_full_weeks=12)


def _mk(score, protein, cuisine):
    rid = uuid4()
    return ScoredRecipe(
        recipe_id=rid,
        score=score,
        components={},
        cost=RecipeCost(
            recipe_id=rid,
            total_cost=Decimal("10"),
            total_savings=Decimal("1"),
            sale_ingredient_count=1,
            matched_ingredient_count=1,
            total_ingredient_count=1,
            pantry_hit_count=0,
            line_items=[],
        ),
        protein=protein,
        cuisine=cuisine,
    )


def test_diversity_penalty_zero_when_all_unique():
    a = _mk(0.5, "chicken", "american")
    b = _mk(0.5, "beef", "mexican")
    c = _mk(0.5, "fish", "italian")
    assert set_diversity_penalty([a, b, c], _CFG_3) == 0.0


def test_diversity_penalty_three_chickens():
    a = _mk(0.5, "chicken", "american")
    b = _mk(0.5, "chicken", "italian")
    c = _mk(0.5, "chicken", "mexican")
    # 3 protein pairs * 0.15 = 0.45, no cuisine pairs
    p = set_diversity_penalty([a, b, c], _CFG_3)
    assert abs(p - 0.45) < 1e-6


def test_select_set_picks_diverse_over_homogeneous():
    """Three chicken/american (each 0.95) lose to fully-diverse mixed
    (each 0.90) once protein and cuisine penalties apply.

    Mix candidates each overlap high on exactly one attribute so that
    *any* "1 high + 2 mix" pairing also incurs penalty -- otherwise the
    optimum would mix a single high with two unrelated mixes."""
    high1 = _mk(0.95, "chicken", "american")
    high2 = _mk(0.95, "chicken", "american")
    high3 = _mk(0.95, "chicken", "american")
    mix1 = _mk(0.90, "pork", "thai")          # fully distinct from high
    mix2 = _mk(0.90, "chicken", "mexican")    # shares protein with high
    mix3 = _mk(0.90, "fish", "american")      # shares cuisine with high

    chosen, set_score = select_set([high1, high2, high3, mix1, mix2, mix3], _CFG_3)
    chosen_ids = {s.recipe_id for s in chosen}
    assert chosen_ids == {mix1.recipe_id, mix2.recipe_id, mix3.recipe_id}


def test_select_set_handles_too_few():
    a = _mk(0.5, "x", "y")
    b = _mk(0.5, "x", "y")
    chosen, _ = select_set([a, b], _CFG_3)
    assert len(chosen) == 2  # less than set_size returns what we have


def test_select_set_empty_input():
    chosen, score = select_set([], _CFG_3)
    assert chosen == []
    assert score == 0.0
