"""Phase 10E — App Block install execution contract (forms.contact).

Codegen may fill editable slots. It may not replace protected bindings, invent a
private backend, or emit canonical domain events. Email configuration is optional.
"""

from __future__ import annotations

import json
import tempfile
from pathlib import Path

import pytest

from src.domain.app_blocks.catalogs import default_primitives
from src.domain.app_blocks.compiler import compile_install_plan
from src.domain.app_blocks.execution import assert_install_plan_installable
from src.domain.app_blocks.registry import load_default_registry
from src.domain.codegen.backend_guard import validate_backend_repo
from src.domain.codegen.prompt_wrapper import _APP_BLOCK_CONTRACT
from src.domain.feature_packs.events import catalog_event_index, resolve_canonical_event_id
from src.domain.feature_packs.normalize import load_pack_contract
from src.domain.projects.discovery_schemas import (
    Actor,
    AuthenticationSpec,
    DiscoverySpec,
    Entity,
    ProductDefinition,
    TenancySpec,
    Workflow,
)


def _contact_spec() -> DiscoverySpec:
    return DiscoverySpec(
        product=ProductDefinition(
            purpose="Allow prospective customers to contact the agency.",
            first_build_goal="Capture website enquiries and let staff review them.",
        ),
        archetypes=["marketing_site"],
        actors=[
            Actor(id="visitor", name="Website visitor"),
            Actor(id="staff", name="Staff"),
        ],
        workflows=[
            Workflow(id="submit_enquiry", name="Submit enquiry", description="Visitor sends a message"),
        ],
        entities=[Entity(id="enquiry", name="Enquiry", description="A website enquiry")],
        tenancy=TenancySpec(model="single_workspace"),
        authentication=AuthenticationSpec(required=False),
    )


def _contact_plan(**compile_kw):
    return compile_install_plan(_contact_spec(), **compile_kw)


def _contact_block(plan):
    return next(block for block in plan.blocks if block.id == "forms.contact")


def _authoritative_contact_repo(root: Path, page_source: str, extra: dict[str, str] | None = None) -> None:
    (root / "p2hs").mkdir()
    (root / "pages").mkdir()
    (root / "p2hs" / "app-blocks-plan.json").write_text(
        json.dumps(
            {
                "adoption": "authoritative",
                "blocks": [{"id": "forms.contact", "version": "1.0.0", "reason": "x"}],
            }
        ),
        encoding="utf-8",
    )
    (root / "pages" / "contact.vue").write_text(page_source, encoding="utf-8")
    for rel, source in (extra or {}).items():
        path = root / rel
        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_text(source, encoding="utf-8")


def test_10e_forms_contact_plan_is_installable_without_email_config():
    plan = _contact_plan()
    assert_install_plan_installable(plan)
    assert plan.unsatisfiable is False
    block = _contact_block(plan)
    assert "contact-capture" in plan.required_feature_packs
    assert "persistence" in block.protected_bindings
    assert "submitted_event" in block.protected_bindings
    assert "allowed_fields" in block.protected_bindings
    assert "abuse_control" in block.protected_bindings
    email_gaps = [gap for gap in plan.gaps if gap.id == "email.send"]
    assert email_gaps
    assert all(gap.severity != "build_blocking" for gap in email_gaps)
    assert "email.send" in plan.runtime_dependencies
    assert any(item.capability == "email.send" for item in plan.configuration_requirements)


def test_10e_editable_slots_are_customizable_protected_ui_slots_are_not():
    registry = load_default_registry()
    manifest = registry.require("forms.contact")
    editable = {slot.id for slot in manifest.slots if slot.constraints.editable}
    protected_ui = {slot.id for slot in manifest.slots if not slot.constraints.editable}
    assert {"heading", "description", "fields", "route", "success_message", "submit_label"} <= editable
    assert {"public_form", "success", "admin_list", "admin_detail"} <= protected_ui
    plan = _contact_plan()
    block = _contact_block(plan)
    assert block.slots["heading"] == "Contact us"
    assert block.slots["route"] == "/contact"
    assert "public_form" not in block.slots
    persistence = next(item for item in manifest.bindings if item.id == "persistence")
    assert persistence.target == "pack:forms.public_submission"
    assert persistence.protected is True
    assert manifest.protected.persistence is True
    assert manifest.protected.events is True
    assert manifest.protected.bff is True


def test_10e_event_authority_stays_on_cloud_mapping_not_codegen():
    registry = load_default_registry()
    manifest = registry.require("forms.contact")
    assert any(item.id == "forms.contact.submitted" for item in manifest.events)
    catalog = catalog_event_index([load_pack_contract("contact-capture", {})])
    assert resolve_canonical_event_id("forms.contact.submitted", catalog) == "contact.submitted"
    assert "Do not POST /events" in _APP_BLOCK_CONTRACT
    assert "Cloud emits canonical domain events" in _APP_BLOCK_CONTRACT
    assert "You MUST NOT replace protected" in _APP_BLOCK_CONTRACT


def test_10e_missing_email_primitive_does_not_block_install():
    primitives = frozenset(p for p in default_primitives() if p != "email.send")
    plan = _contact_plan(primitives=primitives)
    assert_install_plan_installable(plan)
    block = _contact_block(plan)
    email_optional = next(
        item for item in block.optional_requirements if item.id == "email.send"
    )
    assert email_optional.status == "unavailable"
    assert plan.unsatisfiable is False


def test_10e_generated_contact_page_may_customize_copy_through_platform_submit():
    with tempfile.TemporaryDirectory() as raw:
        _authoritative_contact_repo(
            Path(raw),
            "<script>const heading='Get in touch';"
            "submitGeneratedForm({ formKey: 'contact', values: { email: 'a@b.c' } })</script>",
        )
        validate_backend_repo(raw)


@pytest.mark.parametrize(
    "rel,source,match",
    [
        ("server/api/db.post.ts", "import Database from 'better-sqlite3';", "local SQLite"),
        (
            "server/utils/store.ts",
            "import { PrismaClient } from '@prisma/client';",
            "ORM/database client",
        ),
        (
            "server/api/events.post.ts",
            "export default defineEventHandler(() => emitDomainEvent('contact.submitted'))",
            "project-local domain event",
        ),
        (
            "server/api/notify.ts",
            "import { Resend } from 'resend';",
            "vendor email client",
        ),
        (
            "server/api/inbox.get.ts",
            "const contacts = []; contacts.push(body);",
            "mutable local backend collection",
        ),
    ],
)
def test_10e_rejects_private_backend_substitutes(rel, source, match):
    with tempfile.TemporaryDirectory() as raw:
        _authoritative_contact_repo(
            Path(raw),
            "<script>submitGeneratedForm({ formKey: 'contact', values: {} })</script>",
            extra={rel: source},
        )
        with pytest.raises(ValueError, match="Cloud persistence contract failed") as error:
            validate_backend_repo(raw)
    assert match in str(error.value)
    assert "a@b.c" not in str(error.value)
