from src.domain.app_blocks.canonical import (
    APP_BLOCK_CANONICAL_MODEL,
    CANONICAL_BLOCK_IDS,
    CANONICAL_EMAIL_CAPABILITY,
    FORM_RUNTIME_BY_BLOCK,
    LEGACY_CAPABILITY_POLICY,
    REQUIREMENTS_SEMANTICS,
    canonicalize_capability_id,
    canonicalize_manifest,
    compile_canonical_catalog,
    feature_pack_only_aliases,
)
from src.domain.app_blocks.compiler import compile_install_plan
from src.domain.app_blocks.errors import InvalidAppBlockManifestError, UnknownAppBlockError
from src.domain.app_blocks.registry import load_default_registry, load_manifests
from src.domain.app_blocks.validation import validate_manifest
from src.domain.projects.discovery_schemas import (
    Actor,
    AuthenticationSpec,
    DiscoverySpec,
    Entity,
    ProductDefinition,
    TenancySpec,
    Workflow,
)


def _planned(**overrides) -> dict:
    data = {
        "id": "auth.profile",
        "version": "1",
        "family": "auth",
        "title": "Profile",
        "description": "Test block",
        "status": "planned",
        "provides": [{"id": "user_profile"}],
    }
    data.update(overrides)
    return data


def test_canonical_model_freeze_flags():
    assert APP_BLOCK_CANONICAL_MODEL["status"] == "complete"
    assert APP_BLOCK_CANONICAL_MODEL["catalog_authority"] == "core_yaml_only"
    assert APP_BLOCK_CANONICAL_MODEL["durable_install_authority"] == "feature_packs"
    assert APP_BLOCK_CANONICAL_MODEL["forms_runtime_identity"] == "form_key"
    assert APP_BLOCK_CANONICAL_MODEL["database_schema_change"] is False
    assert APP_BLOCK_CANONICAL_MODEL["runtime_change"] is False
    assert APP_BLOCK_CANONICAL_MODEL["codegen_behavior_change"] is False
    assert APP_BLOCK_CANONICAL_MODEL["legacy_contact_removal"] is False
    assert LEGACY_CAPABILITY_POLICY == "legacy_alias_deprecated"
    assert REQUIREMENTS_SEMANTICS["requires"]
    assert "later slice" in REQUIREMENTS_SEMANTICS["optional_requires"]


def test_default_catalog_loads_exactly_the_canonical_block_ids():
    registry = load_default_registry()
    assert registry.canonical.ids == CANONICAL_BLOCK_IDS
    assert len(load_manifests()) == len(CANONICAL_BLOCK_IDS)


def test_crm_pipeline_resolves_as_canonical_block_after_alias_retirement():
    registry = load_default_registry()
    assert "crm.pipeline" not in feature_pack_only_aliases()
    assert registry.require("crm.pipeline").id == "crm.pipeline"
    assert registry.canonical.get("crm.pipeline").installation.feature_packs == ("lead-lifecycle",)


def test_crm_lead_is_a_block_not_inferred_from_pack_alias():
    registry = load_default_registry()
    assert registry.require("crm.lead").id == "crm.lead"
    lead = registry.canonical.require("crm.lead")
    assert lead.installation.feature_packs == ("lead-lifecycle",)
    assert lead.id != "lead-lifecycle"


def test_workflow_assignment_resolves_to_its_own_pack_not_a_collapsed_identity():
    """11B1: App Block id, capability id, and Feature Pack slug must stay distinct
    (frozen 9B rule) -- the block requires 'assignment.tracking' (the pack's
    lifecycle-specific provide), never its own id or the pack slug directly."""
    registry = load_default_registry()
    assert registry.require("workflow.assignment").id == "workflow.assignment"
    assignment = registry.canonical.require("workflow.assignment")
    assert assignment.installation.feature_packs == ("assignment-tracking",)
    assert assignment.id != "assignment-tracking"


def test_task_management_resolves_to_its_own_pack_not_a_collapsed_identity():
    """11B2: same 9B identity-distinctness rule as workflow.assignment."""
    registry = load_default_registry()
    assert registry.require("task.management").id == "task.management"
    task = registry.canonical.require("task.management")
    assert task.installation.feature_packs == ("task-management",)
    assert task.id != "task-management"


def test_activity_timeline_resolves_to_its_own_pack_not_a_collapsed_identity():
    """11B3: same 9B identity-distinctness rule; also confirms activity.timeline
    declares no App Block-level events (it is a read model, not a producer)."""
    registry = load_default_registry()
    manifest = registry.require("activity.timeline")
    assert manifest.id == "activity.timeline"
    assert manifest.events == []
    activity = registry.canonical.require("activity.timeline")
    assert activity.installation.feature_packs == ("activity-timeline",)
    assert activity.id != "activity-timeline"


def test_files_attachments_resolves_to_its_own_pack_not_a_collapsed_identity():
    """11D3: same 9B identity-distinctness rule; no App Block events."""
    registry = load_default_registry()
    manifest = registry.require("files.attachments")
    assert manifest.id == "files.attachments"
    assert manifest.events == []
    attachments = registry.canonical.require("files.attachments")
    assert attachments.installation.feature_packs == ("file-attachments",)
    assert attachments.id != "file-attachments"


def test_booking_appointment_resolves_to_appointment_booking_pack():
    registry = load_default_registry()
    assert "booking.appointment" not in feature_pack_only_aliases()
    assert registry.require("booking.appointment").id == "booking.appointment"
    block = registry.canonical.require("booking.appointment")
    assert block.installation.feature_packs == ("appointment-booking",)
    assert block.id != "appointment-booking"


def test_catalog_product_resolves_to_product_catalog_pack():
    registry = load_default_registry()
    assert "catalog.product" not in feature_pack_only_aliases()
    assert registry.require("catalog.product").id == "catalog.product"
    block = registry.canonical.require("catalog.product")
    assert block.installation.feature_packs == ("product-catalog",)
    assert block.id != "product-catalog"
    assert "data.entity" in {dep.id for dep in block.requires if dep.kind == "primitive"}


def test_membership_member_resolves_to_membership_management_pack():
    registry = load_default_registry()
    assert "membership.member" not in feature_pack_only_aliases()
    assert registry.require("membership.member").id == "membership.member"
    block = registry.canonical.require("membership.member")
    assert block.installation.feature_packs == ("membership-management",)
    assert block.id != "membership-management"
    assert "data.entity" in {dep.id for dep in block.requires if dep.kind == "primitive"}
    assert "saas-membership" not in block.installation.feature_packs
    assert "payment-checkout" not in block.installation.feature_packs


def test_inventory_stock_resolves_to_inventory_management_pack():
    registry = load_default_registry()
    assert "inventory.stock" not in feature_pack_only_aliases()
    assert registry.require("inventory.stock").id == "inventory.stock"
    block = registry.canonical.require("inventory.stock")
    assert block.installation.feature_packs == ("inventory-management",)
    assert block.id != "inventory-management"
    assert "data.entity" in {dep.id for dep in block.requires if dep.kind == "primitive"}
    assert "payment-checkout" not in block.installation.feature_packs
    assert "product-catalog" not in block.installation.feature_packs


def test_billing_invoice_resolves_to_billing_management_pack():
    registry = load_default_registry()
    assert "billing.invoice" not in feature_pack_only_aliases()
    assert registry.require("billing.invoice").id == "billing.invoice"
    block = registry.canonical.require("billing.invoice")
    assert block.installation.feature_packs == ("billing-management",)
    assert block.id != "billing-management"
    assert "data.entity" in {dep.id for dep in block.requires if dep.kind == "primitive"}
    assert "payment-checkout" not in block.installation.feature_packs
    assert "saas-membership" not in block.installation.feature_packs


def test_orders_order_resolves_to_order_management_pack():
    registry = load_default_registry()
    assert "orders.order" not in feature_pack_only_aliases()
    assert registry.require("orders.order").id == "orders.order"
    block = registry.canonical.require("orders.order")
    assert block.installation.feature_packs == ("order-management",)
    assert block.id != "order-management"
    assert "data.entity" in {dep.id for dep in block.requires if dep.kind == "primitive"}
    assert "payment-checkout" not in block.installation.feature_packs


def test_notifications_in_app_resolves_to_its_own_pack_not_a_collapsed_identity():
    """11C1: same 9B identity-distinctness rule; also confirms
    notifications.in_app declares no App Block-level events (pure consumer)."""
    registry = load_default_registry()
    manifest = registry.require("notifications.in_app")
    assert manifest.id == "notifications.in_app"
    assert manifest.events == []
    notifications = registry.canonical.require("notifications.in_app")
    assert notifications.installation.feature_packs == ("notifications-in-app",)
    assert notifications.id != "notifications-in-app"


def test_duplicate_block_ids_fail_catalog_compilation():
    first = validate_manifest(_planned(id="auth.profile"))
    second = validate_manifest(_planned(id="auth.profile", title="Copy"))
    try:
        compile_canonical_catalog([first, second])
        raise AssertionError("expected InvalidAppBlockManifestError")
    except InvalidAppBlockManifestError as exc:
        assert "duplicate" in str(exc)


def test_unknown_manifest_schema_version_fails():
    try:
        validate_manifest(_planned(schema_version=2))
        raise AssertionError("expected InvalidAppBlockManifestError")
    except InvalidAppBlockManifestError:
        pass


def test_malformed_and_unknown_capabilities_fail():
    try:
        validate_manifest(
            _planned(requires=[{"kind": "primitive", "id": "form.submit"}])
        )
        raise AssertionError("expected InvalidAppBlockManifestError")
    except InvalidAppBlockManifestError as exc:
        assert "form.submit" in str(exc)
    try:
        validate_manifest(
            _planned(
                provider_bindings=[{"capability": "crm.lead.write", "providers": []}]
            )
        )
        raise AssertionError("expected InvalidAppBlockManifestError")
    except InvalidAppBlockManifestError:
        pass


def test_canonical_capability_names_and_legacy_email_alias():
    canonical, alias = canonicalize_capability_id("email.transactional")
    assert canonical == CANONICAL_EMAIL_CAPABILITY
    assert alias == "email.transactional"
    assert canonicalize_capability_id("email.send") == ("email.send", None)
    registry = load_default_registry()
    contact = canonicalize_manifest(registry.require("forms.contact"))
    assert contact.deprecated_capability_aliases == ()
    assert any(dep.id == "email.send" for dep in contact.optional_requires)
    yaml_binding = next(
        item.capability for item in contact.manifest.provider_bindings if "email" in item.capability
    )
    assert yaml_binding == "email.send"
    aliased = canonicalize_manifest(
        registry.require("forms.contact").model_copy(
            update={
                "provider_bindings": [
                    item.model_copy(update={"capability": "email.transactional"})
                    if "email" in item.capability
                    else item
                    for item in registry.require("forms.contact").provider_bindings
                ]
            }
        )
    )
    assert "email.transactional" in aliased.deprecated_capability_aliases


def test_shared_feature_pack_is_legal_and_not_block_identity():
    catalog = load_default_registry().canonical
    owners = catalog.blocks_for_feature_pack("contact-capture")
    assert "forms.contact" in owners
    assert "forms.quote_request" in owners
    contact = catalog.require("forms.contact")
    quote = catalog.require("forms.quote_request")
    assert contact.installation.mechanism == "feature_pack"
    assert contact.installation.feature_packs == ("contact-capture",)
    assert quote.installation.feature_packs == ("contact-capture",)
    assert contact.id != "contact-capture"
    assert quote.id != "contact-capture"


def test_block_ids_are_not_runtime_form_keys():
    for block_id, form_key in FORM_RUNTIME_BY_BLOCK.items():
        assert block_id != form_key
        runtime = load_default_registry().canonical.require(block_id).runtime
        assert runtime.ownership == "external"
        assert runtime.identity_kind == "form_key"
        assert runtime.identity == form_key
    try:
        validate_manifest(_planned(id="contact"))
        raise AssertionError("expected InvalidAppBlockManifestError")
    except InvalidAppBlockManifestError:
        pass


def test_planner_output_originates_from_canonical_catalog():
    spec = 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),
    )
    registry = load_default_registry()
    plan = compile_install_plan(spec, registry=registry)
    assert {block.id for block in plan.blocks} <= registry.canonical.ids
    assert "forms.contact" in {block.id for block in plan.blocks}
    assert "crm.pipeline" not in {block.id for block in plan.blocks}


def test_feature_pack_alias_manifest_id_is_rejected():
    # "workflow.approval" then "payments.checkout" were this test's examples
    # until each was admitted as a dual-namespace App Block. payments.subscription
    # remains a genuine feature-pack-only alias.
    try:
        validate_manifest(
            {
                "id": "payments.subscription",
                "version": "1.0.0",
                "family": "crm",
                "title": "Pipeline",
                "description": "Must not be a block",
                "status": "planned",
                "provides": [{"id": "not_a_block"}],
            }
        )
        raise AssertionError("expected InvalidAppBlockManifestError")
    except InvalidAppBlockManifestError as exc:
        assert "Feature Pack alias" in str(exc)
