"""Fast Lane composition for Visualization + Calculation.

Composes admitted App Blocks only. No Cloud calculation.evaluate, no Reporting
reopen, no chart_definition / calculator_definition entities.
"""

from src.domain.app_blocks.compiler import compile_install_plan
from src.domain.app_blocks.composition_contract import (
    COMPOSITION_INVARIANTS,
    DATASET_COMPOSITION_PATTERNS,
    VIS_CALC_BLOCKS,
)
from src.domain.app_blocks.execution import assert_install_plan_installable
from src.domain.app_blocks.planner import ProductIntent, default_slots, select_capabilities
from src.domain.app_blocks.public import public_composition
from src.domain.app_blocks.registry import load_default_registry
from src.domain.app_blocks.vertical_recipes import (
    load_recipe_catalogs,
    load_recipes,
    match_recipe_for_intent,
    match_vertical_recipe,
    validate_recipes,
)
from src.domain.app_blocks.visualization_calculation_catalog import (
    composition_catalog,
    composition_coverage_summary,
)
from src.domain.projects.discovery_schemas import (
    Actor,
    AuthenticationSpec,
    DiscoverySpec,
    Entity,
    ProductDefinition,
    TenancySpec,
    Workflow,
)

GATED = ("payments.checkout", "membership.portal", "reporting.dashboard")
COMPOSED = (
    "mortgage_payment_calculator",
    "project_margin_calculator",
    "roi_estimator",
    "dataset_bar_chart",
)


def _ids(intent: ProductIntent) -> set[str]:
    return {block_id for _cap, block_id, _reason in select_capabilities(intent)}


def _spec(prompt: str) -> DiscoverySpec:
    return DiscoverySpec(
        product=ProductDefinition(purpose=prompt, first_build_goal=prompt),
        original_intent=prompt,
        actors=[Actor(id="staff", name="Staff")],
        workflows=[Workflow(id="run", name="Run", description=prompt)],
        entities=[Entity(id="record", name="Record", description="A record")],
        tenancy=TenancySpec(model="single_workspace"),
        authentication=AuthenticationSpec(required=False),
    )


def test_visualization_calculation_batch_is_composition_ready():
    catalogs = {
        catalog.batch: catalog for catalog in load_recipe_catalogs()
    }
    batch = catalogs["visualization_calculation_10"]
    assert batch.ready_for_fast_lane_composition is True
    assert {recipe.id for recipe in batch.recipes} == set(COMPOSED)
    assert validate_recipes(batch.recipes) == []
    live_ids = {recipe.id for recipe in load_recipes(composition_ready_only=True)}
    assert set(COMPOSED) <= live_ids


def test_composition_catalog_is_live_and_validates():
    catalogs = {catalog.batch: catalog for catalog in load_recipe_catalogs()}
    catalog = catalogs["visualization_calculation_catalog"]
    assert catalog.ready_for_fast_lane_composition is True
    assert validate_recipes(catalog.recipes) == []
    summary = composition_coverage_summary()
    assert summary["by_kind"]["calculation"] >= 30
    assert summary["by_kind"]["visualization"] >= 20
    assert summary["by_kind"]["calculated_chart"] >= 3
    assert set(DATASET_COMPOSITION_PATTERNS) <= set(summary["dataset_patterns"])
    assert set(COMPOSITION_INVARIANTS) <= set(summary["invariants"])
    live_ids = {recipe.id for recipe in load_recipes(composition_ready_only=True)}
    assert {recipe.id for recipe in catalog.recipes} <= live_ids
    # Batch 10 owns the four seed recipes; catalog must not duplicate them.
    assert set(COMPOSED).isdisjoint({recipe.id for recipe in catalog.recipes})


def test_composition_catalog_only_uses_vis_calc_blocks():
    for recipe in composition_catalog().recipes:
        assert set(recipe.required_app_blocks) <= VIS_CALC_BLOCKS
        assert recipe.kind in {"calculation", "visualization", "calculated_chart"}
        assert "reporting.dashboard" not in recipe.required_app_blocks


def test_each_composed_recipe_installs_without_reporting_or_cloud_evaluate():
    live = {
        recipe.id: recipe
        for recipe in load_recipes(composition_ready_only=True)
        if recipe.id in COMPOSED
    }
    assert set(live) == set(COMPOSED)
    registry = load_default_registry()
    for recipe in live.values():
        prompt = recipe.intent_phrases[0]
        plan = compile_install_plan(_spec(prompt), source_prompt=prompt)
        assert_install_plan_installable(plan)
        selected = {block.id for block in plan.blocks}
        assert set(recipe.required_app_blocks) <= selected
        assert "reporting.dashboard" not in selected
        assert "reporting-dashboards" not in plan.required_feature_packs
        assert match_recipe_for_intent(
            ProductIntent(purpose=prompt, first_build_goal=prompt)
        ).id == recipe.id
        for block_id in recipe.required_app_blocks:
            slots = default_slots(registry.require(block_id), ProductIntent(purpose=prompt))
            overlay = recipe.terminology[block_id]
            if overlay.heading:
                assert slots.get("heading") == overlay.heading


def test_batch_10_recipes_carry_composition_contract():
    recipes = {
        recipe.id: recipe
        for recipe in load_recipes(composition_ready_only=True)
        if recipe.id in COMPOSED
    }
    mortgage = recipes["mortgage_payment_calculator"]
    assert mortgage.kind == "calculation"
    assert mortgage.calculator is not None
    assert mortgage.calculator.author == "feature_pack"
    assert mortgage.calculator.pack_slug == "calculator-mortgage"
    assert mortgage.calculator.execution == "local"
    assert {field.id for field in mortgage.calculator.inputs} >= {
        "principal",
        "annual_rate",
        "term_years",
    }
    assert any(chart.chart_type == "area" for chart in mortgage.presentation.charts)
    assert all(metric.presentation == "metric.card" for metric in mortgage.presentation.metrics)

    margin = recipes["project_margin_calculator"]
    assert margin.calculator.author == "codegen"
    assert margin.calculator.pack_slug is None
    assert margin.calculator.dataset_input == "project_cost_snapshot"

    chart = recipes["dataset_bar_chart"]
    assert chart.kind == "visualization"
    assert chart.dataset is not None
    assert chart.dataset.pattern == "value_trend"
    assert chart.dataset.source_kind == "records.aggregate"
    assert chart.dataset.row_budget == 24


def test_catalog_recipe_installs_sample_coverage():
    samples = (
        "saas growth calculator composition recipe",
        "sales pipeline by stage chart composition recipe",
        "break even calculator with cost revenue chart composition recipe",
        "weighted pipeline calculator composition recipe",
    )
    for prompt in samples:
        recipe = match_vertical_recipe(prompt)
        assert recipe is not None, prompt
        plan = compile_install_plan(_spec(prompt), source_prompt=prompt)
        assert_install_plan_installable(plan)
        selected = {block.id for block in plan.blocks}
        assert set(recipe.required_app_blocks) <= selected
        assert "reporting.dashboard" not in selected
        assert "reporting-dashboards" not in plan.required_feature_packs


def test_mortgage_recipe_composes_calculator_chart_and_mortgage_pack():
    prompt = "mortgage calculator product with payment summary chart"
    intent = ProductIntent(purpose=prompt, first_build_goal=prompt, source_prompt=prompt)
    ids = _ids(intent)
    assert {"calculation.calculator", "visualization.chart"} <= ids
    assert not (ids & set(GATED))
    plan = compile_install_plan(_spec(prompt), source_prompt=prompt)
    assert "calculator-mortgage" in plan.required_feature_packs
    assert "reporting-dashboards" not in plan.required_feature_packs
    composition = public_composition(_spec(prompt).model_dump(), source_prompt=prompt)
    assert set(composition) == {"building", "later"}
    assert "Mortgage calculator" in composition["building"]
    assert "Payment summary chart" in composition["building"]
    assert plan.matched_recipe_id == "mortgage_payment_calculator"
    assert "reporting.dashboard" not in composition["building"]
    assert "reporting.dashboard" in plan.unsupported_assumptions


def test_margin_and_roi_recipes_are_codegen_calculators_without_mortgage_pack():
    for prompt in (
        "consulting project margin calculator product",
        "public roi estimator calculator",
    ):
        plan = compile_install_plan(_spec(prompt), source_prompt=prompt)
        assert "calculation.calculator" in {block.id for block in plan.blocks}
        assert "calculator-mortgage" not in plan.required_feature_packs
        assert "visualization.chart" not in {block.id for block in plan.blocks}
        assert "reporting.dashboard" not in {block.id for block in plan.blocks}


def test_dataset_chart_recipe_does_not_require_reporting():
    prompt = "dataset bar chart of monthly revenue"
    ids = _ids(ProductIntent(purpose=prompt, source_prompt=prompt))
    assert "visualization.chart" in ids
    assert "reporting.dashboard" not in ids
    assert "calculation.calculator" not in ids
    plan = compile_install_plan(_spec(prompt), source_prompt=prompt)
    assert plan.required_feature_packs == []
    assert match_vertical_recipe(prompt).id == "dataset_bar_chart"


def test_composition_does_not_reopen_reporting_or_cloud_evaluate():
    for recipe_id in COMPOSED:
        recipe = next(
            item
            for item in load_recipes(composition_ready_only=True)
            if item.id == recipe_id
        )
        assert "reporting.dashboard" not in recipe.required_app_blocks
        assert "chart_definition_entity" in recipe.unsupported_assumptions
        assert "reporting.dashboard" in recipe.unsupported_assumptions
        if "calculation.calculator" in recipe.required_app_blocks:
            assert "calculation.evaluate" in recipe.unsupported_assumptions
            assert "calculator_definition_entity" in recipe.unsupported_assumptions


def test_calculated_chart_keeps_calculator_as_computation_authority():
    recipe = match_vertical_recipe(
        "break even calculator with cost revenue chart composition recipe"
    )
    assert recipe is not None
    assert recipe.kind == "calculated_chart"
    assert recipe.calculator is not None
    assert recipe.dataset is not None
    assert recipe.dataset.source_kind == "calculator.emit"
    assert recipe.dataset.pattern == "calculation_output"
    assert {block for block in recipe.required_app_blocks} == {
        "calculation.calculator",
        "visualization.chart",
    }
