from datetime import date from csv_parser.executor import guess_nutrition_item_fields from data_layer.food_mapping import ( fetch_unmapped_rows, filter_unmapped_since, list_quantity_units, list_unmapped_payload, merge_unmapped_rows, names_match_list, normalize_food_name, parse_quantity, parse_quantity_g, primary_search_query, resolve_quantity, ) from data_layer.nutrition_items import macros_differ def test_primary_query_keeps_decimal_comma(): assert primary_search_query("Joghurt 9,5%") == "Joghurt 9,5%" assert primary_search_query("Joghurt, aus Kuhmilch") == "Joghurt" def test_normalize_strips_leading_quantity(): assert normalize_food_name("50 g Hähnchen") == "hähnchen" assert normalize_food_name(" Vollmilch 3,5% ") == "vollmilch 3.5%" assert normalize_food_name("1 ml Olivenöl") == normalize_food_name("2 ml Olivenöl") == "olivenöl" def test_merge_unmapped_collapses_quantity_variants(): rows = merge_unmapped_rows([ {"source_name_raw": "1 ml Olivenöl", "source_name_normalized": "1 ml olivenöl", "count": 3, "kind": "diary"}, {"source_name_raw": "2 ml Olivenöl", "source_name_normalized": "2 ml olivenöl", "count": 1, "kind": "diary"}, ]) assert len(rows) == 1 assert rows[0]["source_name_normalized"] == "olivenöl" assert rows[0]["count"] == 4 assert rows[0]["source_name_raw"].lower() == "olivenöl" def test_parse_quantity_g(): assert parse_quantity_g("150 g") == 150.0 assert parse_quantity_g("150") == 150.0 assert parse_quantity_g("1 kg") == 1000.0 assert parse_quantity_g("5 ml") == 5.0 assert parse_quantity_g("1 Stück") is None assert parse_quantity_g("1 Stück", 60) == 60.0 assert parse_quantity_g("2 EL") == 30.0 assert parse_quantity("1 Scheibe")["needs_unit_map"] is True assert parse_quantity("2 EL")["unit"] == "el" def test_resolve_quantity_household_units(): two_tl = resolve_quantity(quantity_amount=2, source_unit="tl") assert two_tl["quantity_g"] == 10.0 assert two_tl["quantity_raw"] == "2 TL" pinch = resolve_quantity(quantity_amount=1, source_unit="prise") assert pinch["quantity_g"] == 0.3 slice_ = resolve_quantity(quantity_amount=1, source_unit="stück") assert slice_["quantity_g"] is None assert slice_["needs_unit_map"] is True assert resolve_quantity(quantity_amount=1, source_unit="stück", grams_per_unit=60)["quantity_g"] == 60.0 ids = [u["id"] for u in list_quantity_units()] assert "tl" in ids and "prise" in ids def test_guess_fddb_bezeichnung_without_template_mapping(): name, qty = guess_nutrition_item_fields({ "bezeichnung": "50 g Hähnchen", "menge": "50 g", "kj": "800", }) assert name == "50 g Hähnchen" assert qty == "50 g" def test_recent_window_drops_old_and_dateless_foods(): today = date(2026, 9, 12) rows = [ {"source_name_raw": "Haferflocken", "last_date": "2026-09-10", "count": 2}, {"source_name_raw": "Weißbrot", "last_date": "2026-01-02", "count": 40}, {"source_name_raw": "Altes Rezept-Salz", "last_date": None, "count": 1}, ] recent = filter_unmapped_since(rows, 28, today=today) assert [r["source_name_raw"] for r in recent] == ["Haferflocken"] assert len(filter_unmapped_since(rows, 0, today=today)) == 3 class _FakeCur: def __init__(self, batches): self.batches = list(batches) self._i = 0 self._rows = [] def execute(self, sql, params=None): self._rows = self.batches[self._i] self._i += 1 def fetchall(self): return self._rows def test_fetch_unmapped_keeps_ingredient_dates_and_skips_mapped(): cur = _FakeCur([ [{"source_name_normalized": "haferflocken"}], [], [], [ { "source_name_raw": "Salz", "source_name_normalized": "salz", "count": 2, "first_date": date(2026, 9, 10), "last_date": date(2026, 9, 11), "sample_quantity_raw": "1 g", }, { "source_name_raw": "Haferflocken", "source_name_normalized": "haferflocken", "count": 1, "first_date": date(2026, 9, 1), "last_date": date(2026, 9, 8), "sample_quantity_raw": "40 g", }, ], ]) rows = fetch_unmapped_rows(cur, "p") assert [r["source_name_normalized"] for r in rows] == ["salz"] assert rows[0]["last_date"] == "2026-09-11" assert rows[0]["kind"] == "recipe_ingredient" def test_list_name_matches_despite_quantity_prefix(): assert names_match_list("50 g Müsli Mix", "50 g müsli mix", "müsli mix") assert names_match_list("Müsli Mix", "müsli mix", "müsli mix") assert not names_match_list("Haferflocken", "haferflocken", "müsli mix") def test_unmapped_meta_includes_recent_ingredients(): batches = [ [], [], [], [{ "source_name_raw": "Salz", "source_name_normalized": "salz", "count": 1, "first_date": date(2026, 9, 10), "last_date": date(2026, 9, 11), "sample_quantity_raw": "1 g", }], ] payload = list_unmapped_payload(_FakeCur(batches), "p", since_days=0, meta=True) assert payload["total"] == 1 assert payload["count"] == 1 assert payload["items"][0]["source_name_normalized"] == "salz" def test_macros_differ_rounds(): assert not macros_differ({"kcal": 1.04, "protein_g": 0, "fat_g": 0, "carbs_g": 0}, {"kcal": 1.0, "protein_g": 0, "fat_g": 0, "carbs_g": 0}) assert macros_differ({"kcal": 10, "protein_g": 0, "fat_g": 0, "carbs_g": 0}, {"kcal": 11, "protein_g": 0, "fat_g": 0, "carbs_g": 0})