# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Email: Georgescottfoley@proton.me
# Licensed under the MIT License - see LICENSE file for details

"""Tests for Sprint 3 skills: Spoonacular, Open Food Facts, Mealie."""

from unittest.mock import MagicMock, patch

HTTPX_GET = "familiar.skills._api_utils.httpx.get"
HTTPX_POST = "familiar.skills._api_utils.httpx.post"
CACHE_GET = "familiar.core.api_cache.get_cached"
CACHE_SET = "familiar.core.api_cache.set_cached"


def _mock_response(data, status=200):
    resp = MagicMock()
    resp.status_code = status
    resp.json.return_value = data
    resp.content = b'{"ok": true}'
    return resp


# ── Spoonacular ──────────────────────────────────────────────────────────


class TestRecipeSearch:
    def test_no_criteria(self):
        from familiar.skills.spoonacular.skill import recipe_search

        with patch("familiar.skills.spoonacular.skill.get_env", return_value="key"):
            result = recipe_search({})
        assert "provide a search query" in result.lower()

    def test_no_api_key(self):
        from familiar.skills.spoonacular.skill import recipe_search

        with patch("familiar.skills.spoonacular.skill.get_env", return_value=""):
            result = recipe_search({"query": "pasta"})
        assert "not configured" in result.lower()

    def test_success(self):
        from familiar.skills.spoonacular.skill import recipe_search

        resp = _mock_response(
            {
                "totalResults": 100,
                "results": [
                    {
                        "id": 12345,
                        "title": "Pasta Carbonara",
                        "readyInMinutes": 30,
                        "servings": 4,
                        "image": "https://example.com/carbonara.jpg",
                    },
                    {
                        "id": 67890,
                        "title": "Pasta Bolognese",
                        "readyInMinutes": 45,
                        "servings": 6,
                    },
                ],
            }
        )

        with (
            patch("familiar.skills.spoonacular.skill.get_env", return_value="test_key"),
            patch(HTTPX_GET, return_value=resp),
            patch(CACHE_GET, return_value=None),
            patch(CACHE_SET),
        ):
            result = recipe_search({"query": "pasta"})
        assert "Pasta Carbonara" in result
        assert "12345" in result
        assert "30 min" in result


class TestParseRecipe:
    def test_no_url(self):
        from familiar.skills.spoonacular.skill import parse_recipe

        with patch("familiar.skills.spoonacular.skill.get_env", return_value="key"):
            result = parse_recipe({"url": ""})
        assert "provide a recipe url" in result.lower()

    def test_success(self):
        from familiar.skills.spoonacular.skill import parse_recipe

        resp = _mock_response(
            {
                "title": "Classic Pancakes",
                "servings": 4,
                "readyInMinutes": 20,
                "extendedIngredients": [
                    {"original": "2 cups flour"},
                    {"original": "1 cup milk"},
                ],
                "instructions": "Mix ingredients. Cook on griddle.",
                "nutrition": {"nutrients": []},
            }
        )

        with (
            patch("familiar.skills.spoonacular.skill.get_env", return_value="test_key"),
            patch(HTTPX_GET, return_value=resp),
            patch(CACHE_GET, return_value=None),
            patch(CACHE_SET),
        ):
            result = parse_recipe({"url": "https://example.com/pancakes"})
        assert "Classic Pancakes" in result
        assert "2 cups flour" in result
        assert "Mix ingredients" in result


class TestRecipeNutrition:
    def test_no_id(self):
        from familiar.skills.spoonacular.skill import recipe_nutrition

        with patch("familiar.skills.spoonacular.skill.get_env", return_value="key"):
            result = recipe_nutrition({"recipe_id": ""})
        assert "provide a recipe id" in result.lower()

    def test_success(self):
        from familiar.skills.spoonacular.skill import recipe_nutrition

        resp = _mock_response(
            {
                "nutrients": [
                    {
                        "name": "Calories",
                        "amount": 450,
                        "unit": "kcal",
                        "percentOfDailyNeeds": 22.5,
                    },
                    {"name": "Protein", "amount": 20, "unit": "g", "percentOfDailyNeeds": 40},
                ],
                "ingredients": [
                    {
                        "name": "pasta",
                        "amount": "200",
                        "unit": "g",
                        "nutrients": [{"name": "Calories", "amount": 300}],
                    }
                ],
            }
        )

        with (
            patch("familiar.skills.spoonacular.skill.get_env", return_value="test_key"),
            patch(HTTPX_GET, return_value=resp),
            patch(CACHE_GET, return_value=None),
            patch(CACHE_SET),
        ):
            result = recipe_nutrition({"recipe_id": "12345"})
        assert "Calories" in result
        assert "450" in result
        assert "Protein" in result


class TestFindSubstitutes:
    def test_no_ingredient(self):
        from familiar.skills.spoonacular.skill import find_substitutes

        with patch("familiar.skills.spoonacular.skill.get_env", return_value="key"):
            result = find_substitutes({"ingredient": ""})
        assert "provide an ingredient" in result.lower()

    def test_success(self):
        from familiar.skills.spoonacular.skill import find_substitutes

        resp = _mock_response(
            {
                "substitutes": [
                    "1 cup margarine",
                    "7/8 cup vegetable oil",
                    "1 cup coconut oil",
                ],
                "message": "Found 3 substitutes for butter.",
            }
        )

        with (
            patch("familiar.skills.spoonacular.skill.get_env", return_value="test_key"),
            patch(HTTPX_GET, return_value=resp),
            patch(CACHE_GET, return_value=None),
            patch(CACHE_SET),
        ):
            result = find_substitutes({"ingredient": "butter"})
        assert "margarine" in result
        assert "coconut oil" in result


class TestMealPlan:
    def test_invalid_timeframe(self):
        from familiar.skills.spoonacular.skill import meal_plan

        with patch("familiar.skills.spoonacular.skill.get_env", return_value="key"):
            result = meal_plan({"timeframe": "month"})
        assert "day" in result and "week" in result

    def test_day_plan(self):
        from familiar.skills.spoonacular.skill import meal_plan

        resp = _mock_response(
            {
                "meals": [
                    {"id": 1, "title": "Oatmeal Bowl", "readyInMinutes": 15, "servings": 1},
                    {"id": 2, "title": "Chicken Salad", "readyInMinutes": 20, "servings": 2},
                    {"id": 3, "title": "Grilled Salmon", "readyInMinutes": 30, "servings": 2},
                ],
                "nutrients": {"calories": 1950, "protein": 80, "fat": 65, "carbohydrates": 250},
            }
        )

        with (
            patch("familiar.skills.spoonacular.skill.get_env", return_value="test_key"),
            patch(HTTPX_GET, return_value=resp),
            patch(CACHE_GET, return_value=None),
            patch(CACHE_SET),
        ):
            result = meal_plan({"timeframe": "day", "target_calories": 2000})
        assert "Oatmeal Bowl" in result
        assert "Chicken Salad" in result
        assert "Breakfast" in result


class TestScaleRecipe:
    def test_no_id(self):
        from familiar.skills.spoonacular.skill import scale_recipe

        with patch("familiar.skills.spoonacular.skill.get_env", return_value="key"):
            result = scale_recipe({"recipe_id": ""})
        assert "provide a recipe id" in result.lower()

    def test_success(self):
        from familiar.skills.spoonacular.skill import scale_recipe

        resp = _mock_response(
            {
                "title": "Pasta Carbonara",
                "servings": 4,
                "extendedIngredients": [
                    {"amount": 400, "unit": "g", "name": "spaghetti"},
                    {"amount": 200, "unit": "g", "name": "pancetta"},
                    {"amount": 4, "unit": "", "name": "eggs"},
                ],
            }
        )

        with (
            patch("familiar.skills.spoonacular.skill.get_env", return_value="test_key"),
            patch(HTTPX_GET, return_value=resp),
            patch(CACHE_GET, return_value=None),
            patch(CACHE_SET),
        ):
            result = scale_recipe({"recipe_id": "12345", "new_servings": 8})
        assert "scaled to 8 servings" in result
        assert "800" in result  # 400g * 2
        assert "spaghetti" in result


class TestSpoonacularTools:
    def test_tools_registered(self):
        from familiar.skills.spoonacular.skill import TOOLS

        names = [t["name"] for t in TOOLS]
        assert "recipe_search" in names
        assert "parse_recipe" in names
        assert "recipe_nutrition" in names
        assert "find_substitutes" in names
        assert "meal_plan" in names
        assert "scale_recipe" in names

    def test_all_have_handlers(self):
        from familiar.skills.spoonacular.skill import TOOLS

        for tool in TOOLS:
            assert callable(tool["handler"])


# ── Open Food Facts ──────────────────────────────────────────────────────


class TestBarcodeLookup:
    def test_no_barcode(self):
        from familiar.skills.openfoodfacts.skill import barcode_lookup

        result = barcode_lookup({"barcode": ""})
        assert "provide a barcode" in result.lower()

    def test_invalid_barcode(self):
        from familiar.skills.openfoodfacts.skill import barcode_lookup

        result = barcode_lookup({"barcode": "abc"})
        assert "invalid barcode" in result.lower()

    def test_success(self):
        from familiar.skills.openfoodfacts.skill import barcode_lookup

        resp = _mock_response(
            {
                "status": 1,
                "product": {
                    "product_name": "Nutella",
                    "brands": "Ferrero",
                    "nutriments": {
                        "energy-kcal_100g": 539,
                        "fat_100g": 30.9,
                        "proteins_100g": 6.3,
                    },
                    "allergens_tags": ["en:milk", "en:nuts"],
                    "ingredients_text": "Sugar, palm oil, hazelnuts, cocoa, milk",
                },
            }
        )

        with (
            patch(HTTPX_GET, return_value=resp),
            patch(CACHE_GET, return_value=None),
            patch(CACHE_SET),
        ):
            result = barcode_lookup({"barcode": "3017624010701"})
        assert "Nutella" in result
        assert "Ferrero" in result
        assert "539" in result

    def test_not_found(self):
        from familiar.skills.openfoodfacts.skill import barcode_lookup

        resp = _mock_response({"status": 0, "product": {}})

        with (
            patch(HTTPX_GET, return_value=resp),
            patch(CACHE_GET, return_value=None),
            patch(CACHE_SET),
        ):
            result = barcode_lookup({"barcode": "0000000000000"})
        assert "not found" in result.lower()


class TestProductSearch:
    def test_no_query(self):
        from familiar.skills.openfoodfacts.skill import product_search

        result = product_search({"query": ""})
        assert "provide a product name" in result.lower()

    def test_success(self):
        from familiar.skills.openfoodfacts.skill import product_search

        resp = _mock_response(
            {
                "count": 50,
                "products": [
                    {
                        "product_name": "Oatly Oat Milk",
                        "brands": "Oatly",
                        "code": "7394376616013",
                        "nutrition_grades": "a",
                        "categories": "Plant-based milk, Beverages",
                    }
                ],
            }
        )

        with (
            patch(HTTPX_GET, return_value=resp),
            patch(CACHE_GET, return_value=None),
            patch(CACHE_SET),
        ):
            result = product_search({"query": "oat milk"})
        assert "Oatly" in result
        assert "7394376616013" in result


class TestAllergenCheck:
    def test_no_barcode(self):
        from familiar.skills.openfoodfacts.skill import allergen_check

        result = allergen_check({"barcode": ""})
        assert "provide a barcode" in result.lower()

    def test_success_with_allergens(self):
        from familiar.skills.openfoodfacts.skill import allergen_check

        resp = _mock_response(
            {
                "status": 1,
                "product": {
                    "product_name": "Cookies",
                    "brands": "TestBrand",
                    "allergens_tags": ["en:gluten", "en:milk", "en:eggs"],
                    "traces_tags": ["en:nuts"],
                    "ingredients_text": "Wheat flour, sugar, butter, eggs",
                },
            }
        )

        with (
            patch(HTTPX_GET, return_value=resp),
            patch(CACHE_GET, return_value=None),
            patch(CACHE_SET),
        ):
            result = allergen_check({"barcode": "12345678"})
        assert "Gluten" in result
        assert "Milk" in result
        assert "Eggs" in result
        assert "Nuts" in result
        assert "traces" in result.lower() or "May contain" in result


class TestOpenFoodFactsTools:
    def test_tools_registered(self):
        from familiar.skills.openfoodfacts.skill import TOOLS

        names = [t["name"] for t in TOOLS]
        assert "barcode_lookup" in names
        assert "product_search" in names
        assert "allergen_check" in names


# ── Mealie ───────────────────────────────────────────────────────────────


ENV_MEALIE = {"MEALIE_URL": "http://mealie:9000", "MEALIE_TOKEN": "test_token"}


def _mealie_env(key):
    return ENV_MEALIE.get(key, "")


class TestMealieRecipes:
    def test_not_configured(self):
        from familiar.skills.mealie.skill import mealie_recipes

        with patch("familiar.skills.mealie.skill.get_env", return_value=""):
            result = mealie_recipes({})
        assert "not configured" in result.lower()

    def test_success(self):
        from familiar.skills.mealie.skill import mealie_recipes

        resp = _mock_response(
            {
                "total": 3,
                "items": [
                    {
                        "id": "abc-123",
                        "name": "Chicken Tikka Masala",
                        "slug": "chicken-tikka-masala",
                        "totalTime": "45 minutes",
                        "rating": 5,
                        "tags": [{"name": "Indian"}, {"name": "Dinner"}],
                    },
                    {
                        "id": "def-456",
                        "name": "Caesar Salad",
                        "slug": "caesar-salad",
                        "tags": [],
                    },
                ],
            }
        )

        with (
            patch("familiar.skills.mealie.skill.get_env", side_effect=_mealie_env),
            patch(HTTPX_GET, return_value=resp),
            patch(CACHE_GET, return_value=None),
            patch(CACHE_SET),
        ):
            result = mealie_recipes({})
        assert "Chicken Tikka Masala" in result
        assert "Caesar Salad" in result
        assert "Indian" in result

    def test_search(self):
        from familiar.skills.mealie.skill import mealie_recipes

        resp = _mock_response({"total": 1, "items": [{"id": "x", "name": "Ramen", "tags": []}]})

        with (
            patch("familiar.skills.mealie.skill.get_env", side_effect=_mealie_env),
            patch(HTTPX_GET, return_value=resp),
            patch(CACHE_GET, return_value=None),
            patch(CACHE_SET),
        ):
            result = mealie_recipes({"query": "ramen"})
        assert "Ramen" in result


class TestMealieRecipeDetail:
    def test_no_id(self):
        from familiar.skills.mealie.skill import mealie_recipe_detail

        with patch("familiar.skills.mealie.skill.get_env", side_effect=_mealie_env):
            result = mealie_recipe_detail({"recipe_id": ""})
        assert "provide a recipe id" in result.lower()

    def test_success(self):
        from familiar.skills.mealie.skill import mealie_recipe_detail

        resp = _mock_response(
            {
                "name": "Pasta Aglio e Olio",
                "description": "Simple Italian garlic pasta",
                "prepTime": "10 min",
                "performTime": "15 min",
                "totalTime": "25 min",
                "recipeYield": "4 servings",
                "recipeIngredient": [
                    {"note": "400g spaghetti"},
                    {"note": "6 cloves garlic, sliced"},
                    {"note": "1/2 cup olive oil"},
                ],
                "recipeInstructions": [
                    {"text": "Cook pasta al dente."},
                    {"text": "Sauté garlic in olive oil."},
                    {"text": "Toss with pasta."},
                ],
            }
        )

        with (
            patch("familiar.skills.mealie.skill.get_env", side_effect=_mealie_env),
            patch(HTTPX_GET, return_value=resp),
            patch(CACHE_GET, return_value=None),
            patch(CACHE_SET),
        ):
            result = mealie_recipe_detail({"recipe_id": "pasta-aglio-e-olio"})
        assert "Pasta Aglio e Olio" in result
        assert "400g spaghetti" in result
        assert "Cook pasta" in result


class TestMealieAddRecipe:
    def test_no_url(self):
        from familiar.skills.mealie.skill import mealie_add_recipe

        with patch("familiar.skills.mealie.skill.get_env", side_effect=_mealie_env):
            result = mealie_add_recipe({"url": ""})
        assert "provide a recipe url" in result.lower()

    def test_success(self):
        from familiar.skills.mealie.skill import mealie_add_recipe

        resp = _mock_response("pasta-carbonara")
        resp.json.return_value = "pasta-carbonara"

        with (
            patch("familiar.skills.mealie.skill.get_env", side_effect=_mealie_env),
            patch(HTTPX_POST, return_value=resp),
        ):
            result = mealie_add_recipe({"url": "https://example.com/recipe"})
        assert "imported" in result.lower()


class TestMealieMealPlan:
    def test_success(self):
        from familiar.skills.mealie.skill import mealie_meal_plan

        resp = _mock_response(
            {
                "items": [
                    {
                        "date": "2026-03-06",
                        "entryType": "dinner",
                        "title": "Grilled Chicken",
                        "recipe": {"name": "Grilled Chicken"},
                    },
                    {
                        "date": "2026-03-07",
                        "entryType": "lunch",
                        "title": "Caesar Salad",
                        "recipe": {"name": "Caesar Salad"},
                    },
                ]
            }
        )

        with (
            patch("familiar.skills.mealie.skill.get_env", side_effect=_mealie_env),
            patch(HTTPX_GET, return_value=resp),
            patch(CACHE_GET, return_value=None),
            patch(CACHE_SET),
        ):
            result = mealie_meal_plan({})
        assert "Grilled Chicken" in result
        assert "2026-03-06" in result


class TestMealieShoppingList:
    def test_list_all(self):
        from familiar.skills.mealie.skill import mealie_shopping_list

        resp = _mock_response(
            {
                "items": [
                    {"id": "list-1", "name": "Weekly Groceries", "listItems": [{}, {}, {}]},
                ]
            }
        )

        with (
            patch("familiar.skills.mealie.skill.get_env", side_effect=_mealie_env),
            patch(HTTPX_GET, return_value=resp),
            patch(CACHE_GET, return_value=None),
            patch(CACHE_SET),
        ):
            result = mealie_shopping_list({})
        assert "Weekly Groceries" in result
        assert "3 items" in result


class TestMealieTools:
    def test_tools_registered(self):
        from familiar.skills.mealie.skill import TOOLS

        names = [t["name"] for t in TOOLS]
        assert "mealie_recipes" in names
        assert "mealie_recipe_detail" in names
        assert "mealie_add_recipe" in names
        assert "mealie_meal_plan" in names
        assert "mealie_shopping_list" in names

    def test_all_have_handlers(self):
        from familiar.skills.mealie.skill import TOOLS

        for tool in TOOLS:
            assert callable(tool["handler"])
