"""Phase 11B3: activity.timeline end-to-end proof.

The governing rule for this slice (stricter than 11B1/11B2): activity.timeline
is a projection/read-model over canonical domain events (10D-10D4). It must not
become an event producer, event transport, or second audit system.

Inspect-phase finding (see WORKFLOW_ACTIVITY_TIMELINE_11B3.md): Cloud's
cloud_event_outbox (P2HSCloud packages/shared/src/integration-events.ts
PlatformEventEnvelope + apps/pc-gateway/src/db/event-outbox.ts) already carries
a subject reference (`resource: {type, id}`), actor, timestamp, and canonical
type -- but it is a transient webhook-dispatch queue (status: pending/
dispatched/failed), not indexed or retained for "show this record's history"
queries. A projection entity is therefore reasonable, populated from canonical
event consumption -- which is Cloud-side runtime work explicitly NOT built in
this slice (see the audit's "deferred" section). This file proves the Core-side
contract only, at the same contract layer as 11B1/11B2.
"""
from __future__ import annotations

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, select_capabilities
from src.domain.app_blocks.public import _BLOCK_COPY
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_planner_matches_tight_activity_phrases_only():
    hit = select_capabilities(ProductIntent(purpose="We need an activity timeline for staff"))
    assert any(block_id == "activity.timeline" for _cap, block_id, _reason in hit)

    # Bare "activity"/"history" is far too common to match as a bag of words.
    miss = select_capabilities(ProductIntent(purpose="Track business activity for our clients"))
    assert not any(block_id == "activity.timeline" for _cap, block_id, _reason in miss)

    # "audit trail" names a separate, stronger capability (compliance/retention)
    # -- this matcher must not blur into admin.audit territory.
    audit = select_capabilities(ProductIntent(purpose="We need a compliance audit trail"))
    assert not any(block_id == "activity.timeline" for _cap, block_id, _reason in audit)


def test_host_catalog_lists_activity_slots():
    payload = host_map_payload()
    keys = {(item["block"], item["slot"]) for item in payload["hosts"]}
    assert ("activity.timeline", "pipeline") in keys
    assert ("activity.timeline", "detail") in keys
    assert ("activity.timeline", "empty") in keys


def test_public_label_exists_and_does_not_leak_the_raw_id():
    label = _BLOCK_COPY["activity.timeline"]
    assert label == "Activity timeline"
    assert label != "activity.timeline"


def test_install_plan_resolves_activity_timeline_pack_and_stays_satisfiable(monkeypatch):
    registry = load_default_registry()
    monkeypatch.setattr(
        compiler,
        "select_capabilities",
        lambda _intent: [("activity_timeline", "activity.timeline", "Selected.")],
    )
    plan = compiler.compile_install_plan(None, registry=registry)
    assert not plan.unsatisfiable
    assert plan.required_feature_packs == ["activity-timeline"]
    # The first Phase 11 capability that needs no workflow.transition at all --
    # it is an append-only log, not a lifecycle entity.
    assert "workflow.transition" not in plan.required_primitives
    block_ids = {b.id for b in plan.blocks}
    expected = {"activity.timeline", "admin.shell", "admin.record_table", "admin.record_detail"}
    assert expected <= block_ids


def test_missing_activity_timeline_pack_is_an_honest_gap_not_a_fallback(monkeypatch):
    from src.domain.app_blocks.catalogs import default_known_packs

    registry = load_default_registry()
    monkeypatch.setattr(
        compiler,
        "select_capabilities",
        lambda _intent: [("activity_timeline", "activity.timeline", "Selected.")],
    )
    plan = compiler.compile_install_plan(
        None, registry=registry, known_packs=default_known_packs() - {"activity-timeline"}
    )
    assert plan.unsatisfiable
    assert any(
        g.type == "feature_pack_missing"
        and g.id == "activity-timeline"
        and g.severity == "build_blocking"
        for g in plan.gaps
    )


def test_activity_timeline_defines_and_emits_no_new_domain_event():
    """Required failure-mode test #1: activity.timeline cannot define or emit a
    new domain event identity -- it is a pure consumer, never a producer."""
    contract = load_pack_contract(
        "activity-timeline",
        {"requires": [], "entities": []},
    )
    assert contract.events == []
    assert contract.required_primitive_ids() == []


def test_activity_timeline_provides_no_generated_app_write_path():
    """Required failure-mode test #2: activity.timeline cannot accept an
    arbitrary generated-code write path. Provable at the Core contract layer:
    no form.public_create requirement (no public write path exists at all),
    no actor is granted a public/write action, no function_slugs (no
    Codegen-facing helper to call), and the entity is schema_mode=protected
    (the same vocabulary price/order/app_subscription already use for
    "Codegen must not casually alter this state")."""
    contract = load_pack_contract(
        "activity-timeline",
        {"requires": [], "entities": []},
    )
    assert "form.public_create" not in contract.required_primitive_ids()
    assert contract.security.public_actions == []

    raw = CATALOG_OVERLAYS["activity-timeline"]
    assert raw.get("function_slugs") in (None, [])
    entity_meta = next(e for e in raw["entities"] if e.entity_key == "activity_entry")
    assert entity_meta.schema_mode == "protected"


def test_stamp_produces_no_workflow_key_for_an_append_only_projection():
    stamped = stamp_composition(
        "activity-timeline",
        {
            "requires": [],
            "entities": [
                {
                    "entity_key": "activity_entry",
                    "schema": {
                        "fields": [
                            {"key": "canonical_event_id", "type": "text"},
                            {"key": "subject_entity_key", "type": "text"},
                            {"key": "subject_record_id", "type": "text"},
                        ]
                    },
                }
            ],
        },
    )
    schema = stamped["entities"][0]["schema"]
    assert "workflow" not in schema
    assert schema["schema_mode"] == "protected"


def _project_envelope_onto_activity_entry(canonical_event_id: str, producer, entity: str) -> dict:
    """Proof-harness only, not shipped: models the mapping a future Cloud
    consumer would perform from a PlatformEventEnvelope (type, resource.{type,
    id}, actor, timestamp, data) onto the activity_entry field shape. One
    generic mapping, no per-event special-casing -- proves the stable/bounded
    split holds across heterogeneous producers."""
    category = {
        "record.created": "created",
        "workflow.transition": "status_changed",
        "field.changed": "other",
    }[producer.kind]
    if producer.kind == "workflow.transition" and producer.to_state:
        category = "completed"
    return {
        "canonical_event_id": canonical_event_id,
        "subject_entity_key": entity,
        "subject_record_id": "<opaque-record-id>",
        "actor_type": "service",
        "actor_id": "<opaque-actor-id>",
        "occurred_at": "<iso-timestamp>",
        "category": category,
        "producer_capability": "<pack-slug>",
        "payload": {},
    }


def test_heterogeneous_canonical_events_project_through_one_contract_without_aliases():
    """multi_producer_events_proven=yes: pulls REAL canonical events from
    multiple existing Phase 6-11 packs (lead, case, task, assignment, contact)
    and proves every one maps onto the same activity_entry shape through one
    generic function -- no per-event UI naming, no alias resolution needed
    (canonical PackEvent.id is used directly, never an alias)."""
    cases = [
        ("lead-lifecycle", "lead.status_changed", "lead"),
        ("case-lifecycle", "case.resolved", "case_record"),
        ("task-management", "task.completed", "platform_task"),
        ("assignment-tracking", "assignment.status_changed", "assignment"),
        ("contact-capture", "contact.submitted", "contact_submission"),
    ]
    projected = []
    for slug, event_id, expected_entity in cases:
        contract = load_pack_contract(slug, {})
        event = next(e for e in contract.events if e.id == event_id)
        assert event.producer is not None, f"{event_id} must be emittable to appear on a timeline"
        assert event.producer.entity == expected_entity
        row = _project_envelope_onto_activity_entry(event.id, event.producer, event.producer.entity)
        # Canonical id used directly -- never an alias substituted for it.
        assert row["canonical_event_id"] == event_id
        assert set(row) == {
            "canonical_event_id", "subject_entity_key", "subject_record_id",
            "actor_type", "actor_id", "occurred_at", "category",
            "producer_capability", "payload",
        }
        projected.append(row)

    assert len(projected) == len(cases)
    categories = {row["category"] for row in projected}
    assert categories == {"status_changed", "completed", "created"}
