import json
from unittest.mock import AsyncMock

from src.domain.app_blocks.compiler import compile_install_plan
from src.domain.composition_recipes.schemas import CompositionRecipe
from src.domain.composition_recipes.service import CompositionRecipeService
from src.domain.composition_recipes.validation import readiness_for_recipe, validate_recipe


def _recipe(**overrides):
    data = {
        "slug": "shop.ecommerce",
        "name": "Ecommerce shop",
        "starter_id": "catalog_orders",
        "required_feature_packs": ["product-catalog", "order-management"],
        "required_app_blocks": ["catalog.product", "orders.order"],
        "required_capabilities": ["auth", "records"],
        "entity_roles": [
            {"role": "product", "required": True},
            {"role": "order", "required": True},
        ],
    }
    data.update(overrides)
    return CompositionRecipe.model_validate(data)


def test_recipe_references_admitted_catalogs():
    recipe = _recipe()
    assert validate_recipe(recipe) == []
    assert readiness_for_recipe(recipe).status == "ready"


def test_recipe_rejects_unknown_references_and_schema_leakage():
    recipe = _recipe(
        required_app_blocks=["not.admitted"],
        required_feature_packs=["not-admitted"],
        required_capabilities=["not.admitted"],
    )
    kinds = {gap.kind for gap in validate_recipe(recipe)}
    assert kinds == {"missing_app_block", "missing_feature_pack", "missing_capability"}


def test_recipe_baseline_feeds_existing_app_block_planner():
    plan = compile_install_plan(None, composition_recipe=_recipe())
    assert plan.matched_recipe_id == "shop.ecommerce"
    assert plan.composition_recipe_version == 1
    assert plan.starter_id == "catalog_orders"
    assert {block.id for block in plan.blocks} >= {"catalog.product", "orders.order"}
    assert {"product-catalog", "order-management"} <= set(plan.required_feature_packs)


def test_recipe_allows_multiple_detection_records_by_design():
    assert "prompt_suggestion_id" not in CompositionRecipe.model_fields


def test_write_brief_files_uses_passed_recipe(tmp_path):
    from src.workers.discovery_apply import write_brief_files

    recipe = _recipe()
    write_brief_files(
        tmp_path,
        {"specification": "Shop", "spec": {"product": {"purpose": "Sell products online"}}},
        composition_recipe=recipe,
    )
    payload = json.loads((tmp_path / "p2hs" / "app-blocks-plan.json").read_text(encoding="utf-8"))
    assert payload["matched_recipe_id"] == recipe.slug


async def test_pinned_slug_is_loaded_and_not_reresolved():
    service = CompositionRecipeService(AsyncMock())
    recipe = _recipe()
    service.get = AsyncMock(return_value=recipe)
    service.resolve_for_text = AsyncMock()
    found = await service.recipe_for_discovery_state(
        {"composition_recipe_pinned": True, "composition_recipe_slug": recipe.slug},
        source_prompt="online shop",
        project_type="nuxt_app",
    )
    assert found is recipe
    service.get.assert_awaited_once_with(recipe.slug)
    service.resolve_for_text.assert_not_awaited()


async def test_null_pin_does_not_resolve_or_load():
    service = CompositionRecipeService(AsyncMock())
    service.get = AsyncMock()
    service.resolve_for_text = AsyncMock()
    found = await service.recipe_for_discovery_state(
        {"composition_recipe_pinned": True, "composition_recipe_slug": None},
        source_prompt="online shop",
        project_type="nuxt_app",
    )
    assert found is None
    service.get.assert_not_awaited()
    service.resolve_for_text.assert_not_awaited()
