"""Phase 11C2: notifications.email end-to-end proof.

11C2 does NOT add a new Feature Pack or App Block. It extends 11C1's existing
notifications-in-app pack/App Block with an optional email delivery channel
on the SAME row -- the notification intent (title/message/category/recipient)
is shared; only the delivery outcome differs per channel. This mirrors the
user's explicit "three separate concepts" model:

    business event        (assignment.created, task.completed, ...) -- unchanged
    notification intent   (a platform_notification row)              -- 11C1
    delivery              (read_at for in-app; email_status/email_error
                            for email)                                -- 11C1 + 11C2

Inspect-phase finding that shaped this design: P2HS provides a managed
default email delivery path (never disclosed to Codegen or the generated app
as a specific provider) unless a project configures its own. This means
email.send has no per-project "missing connector connection" gate for the
baseline case, unlike crm.contact.upsert (hubspot/salesforce), which has no
default and genuinely needs one. Declaring a provider_bindings entry anyway
would invent a connector gate that email.send does not have. Checkout lists
Stripe and PayPal on one configuration_required binding; `_missing_prerequisites`
ORs those providers so one connected adapter satisfies payment-checkout.
See catalog_v2.py's comment on the "notifications-in-app" entry.
"""
from __future__ import annotations

from src.domain.app_blocks import compiler
from src.domain.app_blocks.registry import load_default_registry
from src.domain.feature_packs.catalog_v2 import CATALOG_OVERLAYS
from src.domain.feature_packs.normalize import load_pack_contract, stamp_composition


def test_email_channel_is_optional_never_required_for_install():
    """Provider absent -> configuration_required/degraded, never a build
    failure: email.send is declared via optional_requires only. Selecting
    notifications.in_app never requires email.send, so an install plan
    compiles and is satisfiable with zero connector connections configured
    anywhere -- there is nothing Core-side that could block the build on a
    missing email provider, by construction."""
    contract = load_pack_contract("notifications-in-app", {"entities": []})
    assert contract.required_primitive_ids() == []
    optional_ids = {dep.id for dep in contract.optional_requires}
    assert "email.send" in optional_ids
    email_dep = next(dep for dep in contract.optional_requires if dep.id == "email.send")
    assert email_dep.when == "email_notifications_enabled"
    assert contract.provider_bindings == [], (
        "no provider binding: the platform-managed default means there is no "
        "per-project connector-connection gate for the baseline case"
    )


def test_install_plan_for_notifications_never_depends_on_any_connector_connection(
    monkeypatch,
):
    """Concretely proves the previous test's claim at the install-plan layer:
    compiling a plan for notifications.in_app succeeds and is unaffected by
    known_packs/primitives content related to any email provider connector."""
    registry = load_default_registry()
    monkeypatch.setattr(
        compiler,
        "select_capabilities",
        lambda _intent: [("notifications_in_app", "notifications.in_app", "Selected.")],
    )
    plan = compiler.compile_install_plan(None, registry=registry)
    assert not plan.unsatisfiable
    assert plan.required_feature_packs == ["notifications-in-app"]
    assert plan.gaps == []


def test_originating_capabilities_events_are_completely_unaffected():
    """Originating business operations remain successful (and their canonical
    event identity remains unchanged) whether or not the email channel exists,
    fires, or fails -- proven by showing assignment-tracking and
    task-management's own event declarations are untouched by this slice."""
    assignment = load_pack_contract("assignment-tracking", {})
    assert [e.id for e in assignment.events] == [
        "assignment.created",
        "assignment.status_changed",
        "assignment.completed",
        "assignment.cancelled",
    ]
    task = load_pack_contract("task-management", {})
    assert [e.id for e in task.events] == [
        "task.created",
        "task.status_changed",
        "task.completed",
    ]


def test_email_delivery_outcome_is_a_distinct_event_from_notification_intent():
    """Delivery failure is never confused with business-event (or notification-
    intent) failure: the email delivery event is scoped specifically to the
    email_status field, entirely separate from whatever created the
    notification row in the first place, and cannot collide with a business
    event's namespace."""
    contract = load_pack_contract("notifications-in-app", {"entities": []})
    event = next(e for e in contract.events if e.id == "notification.email_delivery_attempted")
    assert event.producer.kind == "field.changed"
    assert event.producer.field == "email_status"
    # A single field.changed producer cannot carry a to_state (only
    # workflow.transition can -- PackEventProducer.shape_matches_kind), so
    # sent/failed/configuration_required must live in the event's bounded
    # payload at emission time, not as three separate producer identities
    # that would collide on (kind, entity, field) uniqueness.
    assert event.producer.to_state is None
    assert event.producer.from_state is None


def test_recipient_email_resolution_reuses_11c1_identity_not_a_new_mechanism():
    """Recipient email resolution has an explicit, single authority: the same
    recipient_user_id established in 11C1 (== the app_user JWT `sub` /
    Cloud's CallerAuth.userId), resolved via Cloud's cloud_app_users.email --
    never the live caller's own JWT (the caller creating/consuming the event
    is not the recipient). No second identity field, no email address stored
    redundantly on the notification row itself."""
    raw = CATALOG_OVERLAYS["notifications-in-app"]
    field_policy = raw["field_policy"]["platform_notification"]
    assert "recipient_user_id" in field_policy
    assert "recipient_email" not in field_policy, (
        "email must be resolved from recipient_user_id via cloud_app_users at "
        "delivery time, never duplicated as a second stored identity field"
    )


def test_stamp_adds_email_status_field_and_delivery_event():
    stamped = stamp_composition(
        "notifications-in-app",
        {
            "requires": [],
            "entities": [
                {
                    "entity_key": "platform_notification",
                    "schema": {
                        "fields": [
                            {
                                "key": "email_status",
                                "type": "select",
                                "options": [
                                    "not_requested", "sent", "failed", "configuration_required",
                                ],
                            },
                        ]
                    },
                }
            ],
        },
    )
    schema = stamped["entities"][0]["schema"]
    assert "workflow" not in schema, "delivery status is application state, not a lifecycle"
    events = schema["domain_events"]
    assert [e["id"] for e in events] == ["notification.email_delivery_attempted"]
    assert events[0]["producer"] == {
        "kind": "field.changed",
        "entity": "platform_notification",
        "field": "email_status",
    }
