"""Phase 11D5E: generated-app Documents UX via generic record.create / record.edit."""

from __future__ import annotations

import json
from pathlib import Path

from src.domain.app_blocks import compiler
from src.domain.app_blocks.hosts import host_map_payload
from src.domain.app_blocks.planner import ProductIntent, default_slots, select_capabilities
from src.domain.app_blocks.public import _BLOCK_COPY, public_composition
from src.domain.app_blocks.registry import load_default_registry
from src.domain.projects.discovery_schemas import (
    Actor,
    AuthenticationSpec,
    DiscoverySpec,
    Entity,
    ProductDefinition,
    TenancySpec,
    Workflow,
)
from src.domain.starter_library.catalog import known_contract_ids
from src.domain.starter_library.record_form import (
    FORBIDDEN_RAW_HTML_MARKERS,
    form_standard_can_serve_authenticated_crud,
    html_escape,
    initial_form_values,
    merge_submit_values,
    plan_record_form_fields,
    utf8_byte_length,
)

REPO = Path(__file__).resolve().parents[1]
STARTERS = {
    "nuxt": REPO / "boilerplates" / "nuxt_app",
    "html": REPO / "boilerplates" / "html",
    "react": REPO / "boilerplates" / "react_app",
}
GENERIC_HOSTS = [
    REPO / "boilerplates/nuxt_app/components/starter/RecordCreate.vue",
    REPO / "boilerplates/nuxt_app/components/starter/RecordEdit.vue",
    REPO / "boilerplates/react_app/src/components/starter/RecordCreate.jsx",
    REPO / "boilerplates/react_app/src/components/starter/RecordEdit.jsx",
    REPO / "src/domain/codegen/runtime/record-form.mjs",
    REPO / "boilerplates/nuxt_app/lib/record-form.mjs",
]
DOCUMENT_UX = [
    *GENERIC_HOSTS,
    REPO / "boilerplates/nuxt_app/components/starter/blocks/DocumentsCreate.vue",
    REPO / "boilerplates/nuxt_app/components/starter/blocks/DocumentsEdit.vue",
    REPO / "boilerplates/nuxt_app/components/starter/blocks/DocumentsDetail.vue",
    REPO / "boilerplates/nuxt_app/pages/documents/index.vue",
    REPO / "boilerplates/nuxt_app/pages/documents/create.vue",
    REPO / "boilerplates/nuxt_app/pages/documents/[id].vue",
    REPO / "boilerplates/nuxt_app/pages/documents/[id]/edit.vue",
    REPO / "boilerplates/html/js/starter-blocks.js",
    REPO / "boilerplates/html/documents.html",
]
_PROVIDER_LEAKS = (
    "cloudflare",
    "amazon s3",
    "aws",
    "object_key",
    "storage_key",
    "bucket_name",
    "secret_access_key",
)

DOCUMENT_SCHEMA = json.loads(
    (REPO / "boilerplates/nuxt_app/lib/entity-schemas/document.json").read_text(encoding="utf-8")
)
NOTE_SCHEMA = {
    "fields": [
        {"key": "heading", "name": "Heading", "type": "text", "required": True},
        {"key": "body", "name": "Body", "type": "text", "required": True, "max_bytes": 4096},
        {"key": "kind", "name": "Kind", "type": "select", "required": True, "options": ["memo"]},
        {"key": "state", "name": "State", "type": "select", "required": True, "options": ["open", "closed"]},
        {"key": "owner_user_id", "name": "Owner", "type": "text"},
    ]
}


def test_form_standard_is_reused_not_forked_for_authenticated_crud():
    inspection = form_standard_can_serve_authenticated_crud()
    assert inspection["reuse"] is True
    assert inspection["owns_submit_transport"] is False
    assert inspection["fork_product_form"] is False
    assert "record.create" in known_contract_ids()
    assert "record.edit" in known_contract_ids()


def test_single_option_select_is_hidden_and_system_keys_are_not_editable():
    plan = plan_record_form_fields(DOCUMENT_SCHEMA)
    visible_keys = [field["key"] for field in plan["visible"]]
    hidden = {field["key"]: field["fixed_value"] for field in plan["hidden"]}
    assert visible_keys == ["title", "content", "status"]
    assert hidden == {"content_format": "plain_text"}
    assert all(field["type"] != "hidden" for field in plan["visible"])
    content = next(field for field in plan["visible"] if field["key"] == "content")
    assert content["type"] == "textarea"
    assert content["max_bytes"] == 262144
    status = next(field for field in plan["visible"] if field["key"] == "status")
    assert status["options"] == ["draft", "active", "archived"]

    payload = merge_submit_values(
        DOCUMENT_SCHEMA,
        {
            "title": "Memo",
            "content": "Hello",
            "status": "draft",
            "owner_user_id": "attacker",
            "project_id": "other",
            "created_at": "nope",
        },
    )
    assert payload == {
        "content_format": "plain_text",
        "title": "Memo",
        "content": "Hello",
        "status": "draft",
    }
    assert "owner_user_id" not in payload


def test_same_planner_renders_a_non_document_entity():
    plan = plan_record_form_fields(NOTE_SCHEMA)
    visible_keys = [field["key"] for field in plan["visible"]]
    hidden = {field["key"]: field["fixed_value"] for field in plan["hidden"]}
    assert visible_keys == ["heading", "body", "state"]
    assert hidden == {"kind": "memo"}
    assert "owner_user_id" not in visible_keys
    assert merge_submit_values(NOTE_SCHEMA, {"heading": "A", "body": "B", "state": "open"}) == {
        "kind": "memo",
        "heading": "A",
        "body": "B",
        "state": "open",
    }


def test_generic_hosts_do_not_branch_on_document_fields():
    banned = ("document", "content_format", "plain_text")
    for path in GENERIC_HOSTS:
        text = path.read_text(encoding="utf-8").lower()
        for token in banned:
            assert token not in text, f"{path} leaked {token}"


def test_plain_text_rendering_escapes_and_forbids_raw_html():
    payload = "<script>alert(1)</script><b>test</b><img src=x onerror=alert(1)>"
    escaped = html_escape(payload)
    assert "<script>" not in escaped
    assert "&lt;script&gt;" in escaped
    assert "<b>" not in escaped
    detail_vue = (REPO / "boilerplates/nuxt_app/components/starter/RecordDetail.vue").read_text(
        encoding="utf-8"
    )
    assert "{{ valueFor(field) }}" in detail_vue
    html_detail = (REPO / "boilerplates/html/js/starter-library.js").read_text(encoding="utf-8")
    assert "node.textContent = String(text)" in html_detail
    for path in DOCUMENT_UX:
        text = path.read_text(encoding="utf-8")
        for marker in FORBIDDEN_RAW_HTML_MARKERS:
            assert marker not in text, f"{path} used {marker}"


def test_content_max_bytes_uses_utf8_semantics():
    assert utf8_byte_length("é") == 2
    assert utf8_byte_length("a" * 10) == 10
    values = initial_form_values(DOCUMENT_SCHEMA, {"data": {"title": "T", "content": "C", "status": "draft"}})
    assert values["content_format"] == "plain_text"
    assert values["title"] == "T"


def test_documents_slots_compose_generic_record_contracts():
    manifest = load_default_registry().require("documents")
    contracts = {slot.id: slot.component_contract for slot in manifest.slots if slot.component_contract}
    assert contracts["list"] == "record.table"
    assert contracts["detail"] == "record.detail"
    assert contracts["create"] == "record.create"
    assert contracts["edit"] == "record.edit"
    catalog_keys = {(item["block"], item["slot"]) for item in host_map_payload()["hosts"]}
    assert ("documents", "create") in catalog_keys
    assert ("documents", "edit") in catalog_keys


def test_starter_runtimes_host_documents_create_and_edit():
    for folder in STARTERS.values():
        hosts = json.loads((folder / "p2hs" / "app-block-hosts.json").read_text(encoding="utf-8"))
        keys = {(item["block"], item["slot"]): item for item in hosts["hosts"]}
        assert keys[("documents", "create")]["component_contract"] == "record.create"
        assert keys[("documents", "edit")]["component_contract"] == "record.edit"
        assert keys[("documents", "list")]["implementation"]
        contracts = {
            item["id"]: item["implementation"]
            for item in json.loads((folder / "p2hs" / "starter-contracts.json").read_text(encoding="utf-8"))[
                "contracts"
            ]
        }
        assert contracts["record.create"]
        assert contracts["record.edit"]


def test_planner_matches_documents_tightly():
    hit = select_capabilities(ProductIntent(purpose="Internal document management for members"))
    assert any(block_id == "documents" for _cap, block_id, _reason in hit)
    miss = select_capabilities(ProductIntent(purpose="Publish documentation and Google Docs later"))
    assert not any(block_id == "documents" for _cap, block_id, _reason in miss)
    manifest = load_default_registry().require("documents")
    assert default_slots(manifest, ProductIntent())["heading"] == "Documents"


def test_public_copy_and_install_plan(monkeypatch):
    assert _BLOCK_COPY["documents"] == "Documents"
    registry = load_default_registry()
    monkeypatch.setattr(
        compiler,
        "select_capabilities",
        lambda _intent: [("documents", "documents", "Selected.")],
    )
    plan = compiler.compile_install_plan(None, registry=registry)
    assert not plan.unsatisfiable
    assert "documents-lifecycle" in plan.required_feature_packs
    assert {block.id for block in plan.blocks} >= {
        "documents",
        "admin.shell",
        "admin.record_table",
        "admin.record_detail",
    }
    spec = DiscoverySpec(
        product=ProductDefinition(
            name="Docs",
            name_status="working",
            purpose="Internal document management for members",
            first_build_goal="Let members create documents.",
        ),
        archetypes=["internal_tool"],
        actors=[Actor(id="member", name="Member")],
        workflows=[Workflow(id="write", name="Write documents", description="Create documents")],
        entities=[Entity(id="document", name="Document", description="A document")],
        tenancy=TenancySpec(model="single_workspace"),
        authentication=AuthenticationSpec(required=True),
    )
    composition = public_composition(spec.model_dump())
    assert "Documents" in composition["building"]


def test_documents_ux_is_provider_neutral():
    blob = "\n".join(path.read_text(encoding="utf-8") for path in DOCUMENT_UX).lower()
    for leak in _PROVIDER_LEAKS:
        assert leak not in blob, leak


def test_runtime_and_files_layers_were_not_reopened():
    assert "kernel_change=no" in (
        REPO / "docs/audits/DOCUMENTS_RUNTIME_STABILIZATION_11D5C.md"
    ).read_text(encoding="utf-8")
    files_audit = (REPO / "docs/audits/DOCUMENTS_FILES_INTEGRATION_11D5D.md").read_text(encoding="utf-8")
    assert "new_attachment_code=none" in files_audit
    create_vue = (REPO / "boilerplates/nuxt_app/components/starter/RecordCreate.vue").read_text(
        encoding="utf-8"
    )
    edit_vue = (REPO / "boilerplates/nuxt_app/components/starter/RecordEdit.vue").read_text(encoding="utf-8")
    assert "createRecord" in create_vue
    assert "patchRecord" in edit_vue
    assert "/documents" not in create_vue
    assert "/api/documents" not in create_vue + edit_vue
