from uuid import uuid4

import pytest
from src.domain.app_blocks.schemas import (
    AppBlockGap,
    AppBlockPlan,
    ConfigurationRequirement,
    PlannedBlock,
)
from src.domain.codegen.generation_context import (
    build_codegen_input,
    build_codegen_request,
    resolve_generation_context,
)
from src.domain.codegen.generation_plan import (
    GenerationPlanCatalogError,
    compile_generation_plan,
)
from src.domain.projects.discovery_schemas import (
    Actor,
    DiscoveryResult,
    DiscoverySpec,
    Entity,
    IntegrationSpec,
    ProductDefinition,
    TenancySpec,
    Workflow,
)


def discovery() -> DiscoveryResult:
    return DiscoveryResult(
        decision="ready",
        message="Ready to build",
        spec=DiscoverySpec(
            product=ProductDefinition(
                name="Portfolio",
                purpose="A personal portfolio",
                first_build_goal="Show projects",
            ),
            actors=[Actor(id="owner", name="Owner", access_scope="own content")],
            workflows=[Workflow(id="browse", name="Browse projects", description="View work")],
            entities=[Entity(id="project", name="Project", description="Portfolio project")],
            tenancy=TenancySpec(model="individual_account"),
            integrations=[
                IntegrationSpec(
                    id="calendar",
                    provider="calendly",
                    purpose="Book a call",
                    required_for_first_build=False,
                )
            ],
            acceptance_outcomes=["Projects are visible"],
        ),
        project_type="html",
        product_name="Portfolio",
        summary="A personal portfolio",
        specification="Show projects",
    )


def app_plan() -> AppBlockPlan:
    return AppBlockPlan(
        blocks=[PlannedBlock(id="content.collection", version="1", reason="Projects")],
        required_feature_packs=["contact-capture"],
        required_primitives=["data.entity"],
        configuration_requirements=[
            ConfigurationRequirement(
                capability="email.send",
                providers=["resend"],
                behavior="degraded",
            )
        ],
        gaps=[
            AppBlockGap(
                type="unsupported",
                id="signature.authority",
                severity="optional",
                affects=["delivery"],
                message="No admitted signature authority exists.",
            )
        ],
        runtime_dependencies=["email.send"],
        starter_contracts=["collection.list"],
        matched_recipe_id="mkt_client_portal",
        starter_id="client_portal",
        feature_pack_ids=["contact-capture"],
        forbidden_capabilities=["payments.checkout"],
        product_archetype="client_portal",
    )


def test_compile_generation_plan_is_typed_and_deterministic():
    project_id = uuid4()
    first = compile_generation_plan(
        discovery(),
        app_plan(),
        project_id=project_id,
        source_discovery_revision=4,
        catalog_digests={"app_blocks": "abc"},
    )
    second = compile_generation_plan(
        discovery().model_dump(),
        app_plan().model_dump(),
        project_id=project_id,
        source_discovery_revision=4,
        catalog_digests={"app_blocks": "abc"},
    )

    assert first.metadata.schema_version == "1"
    assert first.metadata.project_id == project_id
    assert first.composition.feature_pack_ids == ["contact-capture"]
    assert first.composition.app_block_ids == ["content.collection"]
    assert first.constraints.forbidden_capabilities == ["payments.checkout"]
    assert first.runtime.starter_contract_ids == ["collection.list"]
    assert first.model_dump(exclude={"metadata"}) == second.model_dump(
        exclude={"metadata"}
    )


def test_compile_generation_plan_classifies_external_requirements_and_gaps():
    plan = compile_generation_plan(discovery(), app_plan())

    requirements = {(item.id, item.kind): item for item in plan.provider_requirements}
    assert requirements[("email.send", "credential_configuration")].providers == ["resend"]
    assert requirements[("calendar", "external_provider_required")].degraded_path == (
        "configuration_required"
    )
    gap = plan.gaps.unsupported_semantics[0]
    assert gap.kind == "semantic_gap"
    assert "invent_cloud_capability" in gap.prohibited_codegen_actions


def test_compile_generation_plan_carries_resolved_form_requirements():
    from src.domain.forms.canonical import FormField
    from src.domain.projects.form_requirements import FormRequirement

    result = discovery()
    result.spec.form_requirements = [
        FormRequirement(
            form_key="quote_request",
            fields=(
                FormField(key="name", type="text", required=True),
                FormField(key="phone", type="phone", required=False),
            ),
        )
    ]
    compiled = compile_generation_plan(result, app_plan())
    definition = compiled.composition.form_definitions["quote_request"]
    assert [field.key for field in definition.fields] == ["name", "phone"]
    assert definition.fields[1].type == "phone"


def test_resolved_contact_form_selects_canonical_block_and_pack():
    """A form-field delta must not create an unbound generated form route."""
    from src.domain.forms.canonical import FormField
    from src.domain.projects.form_requirements import FormRequirement

    result = discovery()
    result.spec.form_requirements = [
        FormRequirement(
            form_key="contact",
            fields=(
                FormField(key="name", type="text", required=True),
                FormField(key="email", type="email", required=True),
                FormField(key="phone", type="phone"),
                FormField(key="subject", type="text"),
                FormField(key="message", type="textarea", required=True),
            ),
        )
    ]

    compiled = compile_generation_plan(result, app_plan())

    assert "forms.contact" in compiled.composition.app_block_ids
    assert "contact-capture" in compiled.composition.feature_pack_ids
    assert compiled.composition.form_definitions["contact"].fields[-1].key == "message"


def test_compile_generation_plan_omits_form_with_no_fields_yet():
    from src.domain.projects.form_requirements import FormRequirement

    result = discovery()
    result.spec.form_requirements = [FormRequirement(form_key="contact")]
    compiled = compile_generation_plan(result, app_plan())
    assert "contact" not in compiled.composition.form_definitions


def test_compile_generation_plan_rejects_unknown_catalog_references():
    invalid = app_plan().model_copy(update={"required_primitives": ["made.up.primitive"]})

    try:
        compile_generation_plan(discovery(), invalid)
    except GenerationPlanCatalogError as exc:
        assert "unknown_primitive:made.up.primitive" in str(exc)
    else:
        raise AssertionError("unknown primitive should fail GenerationPlan admission")


def test_resolved_generation_context_contains_only_admitted_selected_assets():
    plan = compile_generation_plan(discovery(), app_plan())
    context = resolve_generation_context(plan)

    assert context.plan_hash == plan.provenance.plan_hash
    assert [item.id for item in context.app_blocks] == ["content.collection"]
    assert [item.id for item in context.feature_packs] == ["contact-capture"]
    assert all(item.status == "admitted" for item in context.starter_contracts)
    assert "invent_private_backend_for_unimplemented_capability" in (
        context.forbidden_constraints
    )


def test_plan_first_input_ignores_conflicting_legacy_architecture_prose():
    generation_plan = compile_generation_plan(discovery(), app_plan())
    context = resolve_generation_context(generation_plan)
    request = build_codegen_request(
        project_id=uuid4(),
        plan=generation_plan,
        resolved_context=context,
        legacy_prompt="create /api/private/tasks and implement payments natively",
        execution_mode="plan_first",
    )
    request.compatibility["legacy_prompt"] = (
        "create /api/private/tasks and implement payments natively"
    )

    system_prompt, user_prompt, telemetry = build_codegen_input(
        request,
        legacy_system_prompt="Use a private backend and native payments.",
        legacy_user_prompt="Use a private backend and native payments.",
    )

    assert "Use a private backend" not in system_prompt
    assert "Use a private backend" not in user_prompt
    assert "create /api/private/tasks" not in system_prompt
    assert telemetry["execution_mode"] == "plan_first"
    assert telemetry["resolved_context_hash"]


def test_codegen_request_rejects_tampered_plan_or_context_hash():
    generation_plan = compile_generation_plan(discovery(), app_plan())
    context = resolve_generation_context(generation_plan)
    request = build_codegen_request(
        project_id=uuid4(),
        plan=generation_plan,
        resolved_context=context,
        legacy_prompt="",
        execution_mode="plan_first",
    )

    tampered_plan = request.model_dump(mode="json")
    tampered_plan["generation_plan"]["provenance"]["plan_hash"] = "tampered"
    with pytest.raises(ValueError, match="generation_plan_hash_mismatch"):
        type(request).model_validate(tampered_plan)

    tampered_context = request.model_dump(mode="json")
    tampered_context["resolved_context"]["required_constraints"] = []
    with pytest.raises(ValueError, match="resolved_context_hash_mismatch"):
        type(request).model_validate(tampered_context)
