from __future__ import annotations

import pytest

from src.domain.connectors.crm_events import (
    DEFERRED_CRM_EVENT_TYPES,
    FROZEN_CRM_EVENT_TYPES,
    REJECTED_CRM_EVENT_TYPES,
)
from src.domain.feature_packs.catalog_v2 import CATALOG_OVERLAYS
from src.domain.feature_packs.contract import (
    FeaturePackContract,
    PackEntityContract,
    PackEvent,
    PackEventAlias,
    PackEventProducer,
    PackWorkflowContract,
)
from src.domain.feature_packs.events import (
    COMPOSITION_ONLY_EVENT_IDS,
    GENERIC_RECORD_EVENT_IDS,
    INBOUND_CRM_EVENT_IDENTITIES,
    catalog_event_index,
    resolve_canonical_event_id,
    validate_catalog_event_registry,
    validate_pack_event_mappings,
)
from src.domain.feature_packs.normalize import load_pack_contract


def _catalog_contracts() -> list[FeaturePackContract]:
    return [load_pack_contract(slug, {}) for slug in CATALOG_OVERLAYS]


def _lead_workflow() -> PackWorkflowContract:
    return PackWorkflowContract(
        entity_key="lead",
        field="status",
        transitions={
            "new": ["contacted", "lost"],
            "contacted": ["qualified", "lost"],
            "qualified": ["won", "lost"],
            "won": [],
            "lost": [],
        },
    )


def test_catalog_event_registry_is_valid():
    validate_catalog_event_registry(_catalog_contracts())


def test_canonical_event_with_valid_producer_is_emittable():
    event = PackEvent(
        id="lead.won",
        producer=PackEventProducer(
            kind="workflow.transition",
            entity="lead",
            field="status",
            to_state="won",
        ),
    )
    contract = FeaturePackContract(
        slug="lead-lifecycle",
        events=[event],
        entities=[PackEntityContract(entity_key="lead")],
        workflows=[_lead_workflow()],
    )
    validate_pack_event_mappings(contract)
    assert event.emittable is True


def test_event_without_producer_is_declared_but_not_emittable():
    contract = load_pack_contract("contact-capture", {})
    by_id = {item.id: item for item in contract.events}
    assert by_id["contact.submitted"].emittable is True
    assert by_id["contact.notification_sent"].producer is None
    assert by_id["contact.notification_sent"].emittable is False


def test_unknown_event_alias_target_resolves_to_none():
    index = catalog_event_index(_catalog_contracts())
    assert resolve_canonical_event_id("not.a.declared.event", index) is None


def test_duplicate_alias_collision_rejected():
    event_a = PackEvent(
        id="lead.created",
        producer=PackEventProducer(kind="record.created", entity="lead"),
        aliases=[PackEventAlias(id="crm.lead.created", kind="composition")],
    )
    event_b = PackEvent(
        id="lead.status_changed",
        producer=PackEventProducer(
            kind="workflow.transition", entity="lead", field="status"
        ),
        aliases=[PackEventAlias(id="crm.lead.created", kind="composition")],
    )
    contract = FeaturePackContract(
        slug="lead-lifecycle",
        events=[event_a, event_b],
        entities=[PackEntityContract(entity_key="lead")],
        workflows=[_lead_workflow()],
    )
    with pytest.raises(ValueError, match="resolves to both"):
        validate_pack_event_mappings(contract)


def test_workflow_producer_rejects_unknown_entity():
    contract = FeaturePackContract(
        slug="lead-lifecycle",
        events=[
            PackEvent(
                id="lead.won",
                producer=PackEventProducer(
                    kind="workflow.transition",
                    entity="missing",
                    field="status",
                    to_state="won",
                ),
            )
        ],
        entities=[PackEntityContract(entity_key="lead")],
        workflows=[_lead_workflow()],
    )
    with pytest.raises(ValueError, match="is not declared"):
        validate_pack_event_mappings(contract)


def test_workflow_producer_rejects_wrong_field():
    contract = FeaturePackContract(
        slug="lead-lifecycle",
        events=[
            PackEvent(
                id="lead.status_changed",
                producer=PackEventProducer(
                    kind="workflow.transition", entity="lead", field="stage"
                ),
            )
        ],
        entities=[PackEntityContract(entity_key="lead")],
        workflows=[_lead_workflow()],
    )
    with pytest.raises(ValueError, match="does not match"):
        validate_pack_event_mappings(contract)


def test_workflow_producer_rejects_unknown_to_state():
    contract = FeaturePackContract(
        slug="lead-lifecycle",
        events=[
            PackEvent(
                id="lead.won",
                producer=PackEventProducer(
                    kind="workflow.transition",
                    entity="lead",
                    field="status",
                    to_state="archived",
                ),
            )
        ],
        entities=[PackEntityContract(entity_key="lead")],
        workflows=[_lead_workflow()],
    )
    with pytest.raises(ValueError, match="to_state"):
        validate_pack_event_mappings(contract)


def test_workflow_producer_rejects_unknown_from_state():
    contract = FeaturePackContract(
        slug="lead-lifecycle",
        events=[
            PackEvent(
                id="lead.reopened",
                producer=PackEventProducer(
                    kind="workflow.transition",
                    entity="lead",
                    field="status",
                    from_state="archived",
                    to_state="new",
                ),
            )
        ],
        entities=[PackEntityContract(entity_key="lead")],
        workflows=[_lead_workflow()],
    )
    with pytest.raises(ValueError, match="from_state"):
        validate_pack_event_mappings(contract)


def test_record_created_producer_rejects_unknown_entity():
    contract = FeaturePackContract(
        slug="lead-lifecycle",
        events=[
            PackEvent(
                id="lead.created",
                producer=PackEventProducer(kind="record.created", entity="deal"),
            )
        ],
        entities=[PackEntityContract(entity_key="lead")],
    )
    with pytest.raises(ValueError, match="is not declared"):
        validate_pack_event_mappings(contract)


def test_field_change_producer_rejects_unknown_field():
    contract = FeaturePackContract(
        slug="support-inbox",
        events=[
            PackEvent(
                id="ticket.assigned",
                producer=PackEventProducer(
                    kind="field.changed",
                    entity="support_ticket",
                    field="assignee_id",
                ),
            )
        ],
        entities=[PackEntityContract(entity_key="support_ticket")],
    )
    with pytest.raises(ValueError, match="field.changed"):
        validate_pack_event_mappings(
            contract, entity_fields={"support_ticket": {"subject", "status", "assigned_to"}}
        )


def test_lead_status_changed_and_won_mappings_are_valid():
    contract = load_pack_contract("lead-lifecycle", {})
    by_id = {item.id: item for item in contract.events}
    changed = by_id["lead.status_changed"].producer
    won = by_id["lead.won"].producer
    assert changed is not None
    assert changed.kind == "workflow.transition"
    assert changed.entity == "lead"
    assert changed.field == "status"
    assert changed.to_state is None
    assert won is not None
    assert won.to_state == "won"
    validate_pack_event_mappings(contract)


def test_case_resolved_and_approval_approved_mappings_are_valid():
    case = load_pack_contract("case-lifecycle", {})
    approval = load_pack_contract("approval-workflow", {})
    resolved = next(item for item in case.events if item.id == "case.resolved")
    approved = next(item for item in approval.events if item.id == "approval.approved")
    assert resolved.producer is not None
    assert resolved.producer.entity == "case_record"
    assert resolved.producer.to_state == "resolved"
    assert approved.producer is not None
    assert approved.producer.entity == "approval_request"
    assert approved.producer.to_state == "approved"
    validate_pack_event_mappings(case)
    validate_pack_event_mappings(approval)


def test_contact_submitted_aliases_resolve():
    index = catalog_event_index(_catalog_contracts())
    assert resolve_canonical_event_id("contact.submitted", index) == "contact.submitted"
    assert (
        resolve_canonical_event_id("forms.contact.submitted", index)
        == "contact.submitted"
    )
    assert resolve_canonical_event_id("form.submitted", index) == "contact.submitted"
    contract = load_pack_contract("contact-capture", {})
    aliases = next(item for item in contract.events if item.id == "contact.submitted").aliases
    assert {item.id: item.kind for item in aliases} == {
        "forms.contact.submitted": "composition",
        "form.submitted": "transport",
    }


def test_crm_lead_created_resolves_to_lead_created():
    index = catalog_event_index(_catalog_contracts())
    assert resolve_canonical_event_id("crm.lead.created", index) == "lead.created"
    assert (
        resolve_canonical_event_id("crm.lead.status_changed", index)
        == "lead.status_changed"
    )


def test_crm_dotted_inbound_types_remain_separate():
    index = catalog_event_index(_catalog_contracts())
    inbound = set(FROZEN_CRM_EVENT_TYPES) | set(DEFERRED_CRM_EVENT_TYPES) | set(
        REJECTED_CRM_EVENT_TYPES
    )
    assert inbound == INBOUND_CRM_EVENT_IDENTITIES
    assert resolve_canonical_event_id("crm.contact.created", index) is None
    assert resolve_canonical_event_id("crm.deal.created", index) is None
    pipeline = load_pack_contract("crm-pipeline", {})
    pack_ids = {item.id for item in pipeline.events}
    assert "crm.contact_created" in pack_ids
    assert "crm.contact.created" not in pack_ids
    assert all(item.emittable is False for item in pipeline.events)


def test_message_sent_is_shared_across_packs():
    support = load_pack_contract("support-inbox", {})
    messaging = load_pack_contract("messaging", {})
    support_msg = next(item for item in support.events if item.id == "message.sent")
    messaging_msg = next(item for item in messaging.events if item.id == "message.sent")
    assert support_msg.id == messaging_msg.id == "message.sent"
    assert support_msg.producer is not None
    assert messaging_msg.producer is not None
    assert support_msg.producer.entity == "support_message"
    assert messaging_msg.producer.entity == "message"
    validate_catalog_event_registry(_catalog_contracts())


def test_generic_record_resource_events_are_not_aliases():
    index = catalog_event_index(_catalog_contracts())
    for name in GENERIC_RECORD_EVENT_IDS:
        assert name not in index
        assert resolve_canonical_event_id(name, index) is None
    contract = FeaturePackContract(
        slug="contact-capture",
        events=[
            PackEvent(
                id="contact.submitted",
                producer=PackEventProducer(
                    kind="record.created", entity="contact_submission"
                ),
                aliases=[PackEventAlias(id="record.created", kind="transport")],
            )
        ],
        entities=[PackEntityContract(entity_key="contact_submission")],
    )
    with pytest.raises(ValueError, match="generic record/resource"):
        validate_pack_event_mappings(contract)


def test_quote_request_submitted_is_composition_only():
    index = catalog_event_index(_catalog_contracts())
    assert "forms.quote_request.submitted" in COMPOSITION_ONLY_EVENT_IDS
    assert resolve_canonical_event_id("forms.quote_request.submitted", index) is None
    assert "quote_request.submitted" not in index


def test_alias_cannot_shadow_a_canonical_pack_event_id():
    contract = FeaturePackContract(
        slug="lead-lifecycle",
        events=[
            PackEvent(
                id="lead.created",
                producer=PackEventProducer(kind="record.created", entity="lead"),
                aliases=[PackEventAlias(id="lead.won", kind="composition")],
            ),
            PackEvent(
                id="lead.won",
                producer=PackEventProducer(
                    kind="workflow.transition",
                    entity="lead",
                    field="status",
                    to_state="won",
                ),
            ),
        ],
        entities=[PackEntityContract(entity_key="lead")],
        workflows=[_lead_workflow()],
    )
    with pytest.raises(ValueError, match="shadows canonical"):
        validate_pack_event_mappings(contract)


def test_inbound_crm_lead_created_is_rejected_type_not_merged():
    assert "crm.lead.created" in REJECTED_CRM_EVENT_TYPES
    index = catalog_event_index(_catalog_contracts())
    assert resolve_canonical_event_id("crm.lead.created", index) == "lead.created"
