"""Natural-language prompts become installed composition without dropping it later."""

from uuid import uuid4

from src.domain.codegen.composition_reconcile import (
    compile_reconciled_plan,
    union_generation_plans,
)
from src.domain.codegen.crm_codegen import select_crm_generation
from src.domain.codegen.generation_plan import GenerationPlanV1, compile_generation_plan
from src.domain.forms.canonical import FormDefinition
from src.domain.forms.validation import validate_form_values
from src.domain.projects.discovery_schemas import (
    Actor,
    DiscoveryResult,
    DiscoverySpec,
    Entity,
    ProductDefinition,
    TenancySpec,
    Workflow,
)
from src.domain.projects.form_field_enumeration import enumerated_field_delta
from src.domain.projects.form_requirements import apply_form_requirement_delta
from src.domain.app_blocks.compiler import compile_install_plan

CONTACT_PROMPT = (
    "Create a contact form with name, email, phone, subject, and message fields. "
    "Validate the fields and show the form on the contact page."
)


def _discovery(**spec_kwargs) -> DiscoveryResult:
    spec = DiscoverySpec(
        product=ProductDefinition(
            name="Studio",
            purpose="A small studio site",
            first_build_goal="Publish the first pages",
        ),
        actors=[Actor(id="visitor", name="Website visitor")],
        workflows=[Workflow(id="browse", name="Browse", description="Read the site")],
        entities=[Entity(id="page", name="Page", description="A page")],
        tenancy=TenancySpec(model="single_workspace"),
        **spec_kwargs,
    )
    return DiscoveryResult(
        decision="ready",
        message="Ready to build",
        spec=spec,
        project_type="html",
        product_name="Studio",
        summary="A small studio site",
        specification="Publish the first pages",
    )


def test_enumerated_contact_fields_include_subject():
    delta = enumerated_field_delta(CONTACT_PROMPT)
    assert delta is not None
    assert delta.form_key == "contact"
    assert delta.replace_fields is True
    requirements = apply_form_requirement_delta((), delta)
    fields = {field.key: field for field in requirements[0].fields}
    assert set(fields) == {"name", "email", "phone", "subject", "message"}
    assert fields["subject"].required is False
    assert fields["name"].required is True
    assert fields["email"].required is True
    assert fields["message"].required is True
    assert fields["phone"].required is False


def test_explicit_required_and_optional_groups():
    delta = enumerated_field_delta(
        "Install a contact form with required name, email, and message fields, "
        "plus optional phone and subject fields."
    )
    assert delta is not None
    fields = {field.key: field.required for field in delta.add_fields}
    assert fields == {
        "name": True,
        "email": True,
        "message": True,
        "phone": False,
        "subject": False,
    }


def test_contact_prompt_reconciles_block_pack_and_subject():
    delta = enumerated_field_delta(CONTACT_PROMPT)
    assert delta is not None
    requirements = apply_form_requirement_delta((), delta)
    discovery = _discovery()
    discovery.spec.form_requirements = list(requirements)
    plan = compile_reconciled_plan(
        discovery,
        source_prompt=CONTACT_PROMPT,
        baseline=None,
        project_id=uuid4(),
        source_discovery_revision=1,
    )
    composition = plan["composition"]
    assert "forms.contact" in composition["app_block_ids"]
    assert "contact-capture" in composition["feature_pack_ids"]
    definition = FormDefinition.model_validate(composition["form_definitions"]["contact"])
    accepted = validate_form_values(
        definition,
        {
            "name": "Ada",
            "email": "ada@example.com",
            "phone": "555-0100",
            "subject": "Hello",
            "message": "I would like a quote.",
        },
    )
    assert accepted.ok
    assert accepted.values["subject"] == "Hello"
    rejected = validate_form_values(definition, {"name": "Ada", "email": "ada@example.com", "message": "Hi", "__undeclared_golden_field": "x"})
    assert not rejected.ok
    assert any(error.code == "unknown_field" for error in rejected.errors)


def test_copy_only_prompt_keeps_installed_contact_block():
    installed = compile_reconciled_plan(
        _discovery(),
        source_prompt=CONTACT_PROMPT,
        baseline=None,
        project_id=uuid4(),
        source_discovery_revision=1,
    )
    # The installed plan may not yet carry subject; pin the block the union must keep.
    installed["composition"]["app_block_ids"] = sorted(
        set(installed["composition"]["app_block_ids"]) | {"forms.contact"}
    )
    installed["composition"]["feature_pack_ids"] = sorted(
        set(installed["composition"]["feature_pack_ids"]) | {"contact-capture"}
    )
    fresh = compile_generation_plan(
        _discovery(),
        compile_install_plan(_discovery().spec, source_prompt="Make the hero banner blue"),
        project_id=uuid4(),
    )
    merged = union_generation_plans(fresh, installed)
    assert "forms.contact" in merged.composition.app_block_ids
    assert "contact-capture" in merged.composition.feature_pack_ids


def test_crm_follows_reconciled_composition_without_a_fixture_sentence():
    decision = select_crm_generation(
        "Keep track of the customers we talk to on the site",
        composition_selected=True,
    )
    assert decision.surface == "canonical"
    assert "follow_reconciled_composition" in decision.notes
    untouched = select_crm_generation("Keep track of the customers we talk to on the site")
    assert untouched.surface == "gap"


def test_pinned_empty_recipe_does_not_import_yaml_recipe_blocks():
    from src.domain.app_blocks.compiler import PINNED_EMPTY_RECIPE

    prompt = "mortgage calculator product with payment summary chart"
    discovery = _discovery()
    open_plan = compile_install_plan(discovery.spec, source_prompt=prompt)
    pinned = compile_install_plan(
        discovery.spec,
        source_prompt=prompt,
        composition_recipe=PINNED_EMPTY_RECIPE,
    )
    assert open_plan.matched_recipe_id == "mortgage_payment_calculator"
    assert "reporting.dashboard" in open_plan.unsupported_assumptions
    assert pinned.matched_recipe_id is None
    assert pinned.product_archetype is None
    assert "reporting.dashboard" not in pinned.unsupported_assumptions


def test_copy_prompt_does_not_admit_appointment_booking():
    plan = compile_reconciled_plan(
        _discovery(),
        source_prompt=CONTACT_PROMPT,
        explicit_prompt="Make the hero banner blue",
        baseline=None,
        project_id=uuid4(),
        source_discovery_revision=1,
    )
    assert "booking.appointment" not in plan["composition"]["app_block_ids"]
    assert plan["_explicit_blocks_admitted"] == []
    assert "forms.contact" in plan["composition"]["app_block_ids"]


def test_explicit_booking_sentence_adds_only_the_new_block():
    plan = compile_reconciled_plan(
        _discovery(),
        source_prompt=CONTACT_PROMPT,
        explicit_prompt="Add appointment booking for customers",
        baseline=None,
        project_id=uuid4(),
        source_discovery_revision=1,
    )
    assert "forms.contact" in plan["composition"]["app_block_ids"]
    assert "booking.appointment" in plan["composition"]["app_block_ids"]
    assert plan["_explicit_blocks_admitted"] == ["booking.appointment"]


def test_passed_recipe_stays_when_the_later_sentence_differs():
    from src.domain.composition_recipes.schemas import CompositionRecipe

    recipe = CompositionRecipe(
        slug="shop",
        name="Shop",
        starter_id="internal_operations",
        required_app_blocks=["forms.contact"],
    )
    plan = compile_reconciled_plan(
        _discovery(),
        source_prompt="Publish a short studio homepage",
        explicit_prompt="Make the hero banner blue",
        baseline=None,
        project_id=uuid4(),
        source_discovery_revision=1,
        composition_recipe=recipe,
    )
    assert "forms.contact" in plan["composition"]["app_block_ids"]
    assert "shop" in plan["composition"]["recipe_ids"]
    assert plan["_explicit_blocks_admitted"] == []
    assert plan["_explicit_blocks_removed"] == []


def test_explicit_removal_drops_booking_and_keeps_contact():
    installed = compile_reconciled_plan(
        _discovery(),
        source_prompt=CONTACT_PROMPT,
        explicit_prompt="Add appointment booking for customers",
        baseline=None,
        project_id=uuid4(),
        source_discovery_revision=1,
    )
    installed.pop("_explicit_blocks_admitted", None)
    installed.pop("_explicit_blocks_removed", None)
    plan = compile_reconciled_plan(
        _discovery(),
        source_prompt=CONTACT_PROMPT,
        explicit_prompt="Do not include appointment booking.",
        baseline=installed,
        project_id=uuid4(),
        source_discovery_revision=1,
    )
    assert "forms.contact" in plan["composition"]["app_block_ids"]
    assert "booking.appointment" not in plan["composition"]["app_block_ids"]
    assert plan["_explicit_blocks_removed"] == ["booking.appointment"]


def test_privileged_checkout_stays_out_until_approved():
    from src.domain.codegen.composition_reconcile import apply_privileged_hold

    plan = compile_reconciled_plan(
        _discovery(),
        source_prompt=CONTACT_PROMPT,
        explicit_prompt="Add a checkout session so customers can pay",
        baseline=None,
        project_id=uuid4(),
        source_discovery_revision=1,
    )
    assert "payments.checkout" in plan["_explicit_blocks_admitted"]
    held = apply_privileged_hold(plan, plan["_explicit_blocks_admitted"], [])
    assert "payments.checkout" not in held["composition"]["app_block_ids"]
    assert held["_privileged_blocks_pending"] == ["payments.checkout"]
    assert "forms.contact" in held["composition"]["app_block_ids"]
    released = apply_privileged_hold(plan, plan["_explicit_blocks_admitted"], ["payments.checkout"])
    assert "payments.checkout" in released["composition"]["app_block_ids"]
    assert released["_privileged_blocks_pending"] == []
