import pytest
from src.domain.connectors.catalog import get_action
from src.domain.feature_packs.contract import (
    PackDependencyRef,
    PackProviderBinding,
    classify_legacy_token,
)
from src.domain.feature_packs.normalize import load_pack_contract, stamp_composition


def test_workflow_transition_is_a_primitive_not_a_provider():
    classified = classify_legacy_token("workflow.transition")
    assert isinstance(classified, PackDependencyRef)
    assert classified.kind == "primitive"
    assert classified.id == "workflow.transition"


def test_stripe_leaves_architectural_requires():
    classified = classify_legacy_token("stripe")
    assert isinstance(classified, PackProviderBinding)
    assert classified.capability == "payments.provider"
    assert classified.providers == ["stripe"]


def test_paypal_is_a_payment_provider_binding():
    classified = classify_legacy_token("paypal")
    assert isinstance(classified, PackProviderBinding)
    assert classified.capability == "payments.provider"
    assert classified.providers == ["paypal"]


def test_lead_lifecycle_legacy_blob_does_not_require_a_connector():
    contract = load_pack_contract(
        "lead-lifecycle",
        {"requires": ["workflow.transition"], "entities": []},
    )
    assert contract.required_primitive_ids() == ["workflow.transition"]
    assert contract.configuration_providers() == []
    assert "crm.lead" in contract.provide_ids()
    assert "lead.won" in [event.id for event in contract.events]
    assert contract.events[0].version == 1
    assert "record_id" in contract.events[0].payload


def test_file_attachments_pack_is_provider_neutral_and_entity_only():
    contract = load_pack_contract("file-attachments", {"requires": [], "entities": []})
    assert contract.required_primitive_ids() == []
    assert contract.configuration_providers() == []
    assert contract.provider_bindings == []
    assert "files.attachments" in contract.provide_ids()
    assert "file.attachments" in contract.provide_ids()
    assert contract.events == []
    assert [entity.entity_key for entity in contract.entities] == ["file_attachment"]


def test_assignment_tracking_legacy_blob_does_not_require_a_connector():
    """11B1: assignment-tracking mirrors lead-lifecycle's contract shape -- a
    canonical primitive requirement, no provider, and canonical domain events."""
    contract = load_pack_contract(
        "assignment-tracking",
        {"requires": ["workflow.transition"], "entities": []},
    )
    assert contract.required_primitive_ids() == ["workflow.transition"]
    assert contract.configuration_providers() == []
    assert "workflow.assignment" in contract.provide_ids()
    assert "assignment.tracking" in contract.provide_ids()
    ids = [event.id for event in contract.events]
    assert ids == [
        "assignment.created",
        "assignment.status_changed",
        "assignment.completed",
        "assignment.cancelled",
    ]
    assert contract.events[0].version == 1
    assert "record_id" in contract.events[0].payload


def test_stamp_adds_assignment_status_graph():
    stamped = stamp_composition(
        "assignment-tracking",
        {
            "requires": [],
            "entities": [
                {
                    "entity_key": "assignment",
                    "schema": {
                        "fields": [
                            {
                                "key": "status",
                                "type": "select",
                                "options": ["active", "completed", "cancelled"],
                            }
                        ]
                    },
                }
            ],
        },
    )
    workflow = stamped["entities"][0]["schema"]["workflow"]
    assert workflow["transitions"]["active"] == ["completed", "cancelled"]
    events = stamped["entities"][0]["schema"]["domain_events"]
    ids = [item["id"] for item in events]
    assert ids == [
        "assignment.created",
        "assignment.status_changed",
        "assignment.completed",
        "assignment.cancelled",
    ]
    completed = next(item for item in events if item["id"] == "assignment.completed")
    assert completed["producer"]["kind"] == "workflow.transition"
    assert completed["producer"]["to_state"] == "completed"


def test_saas_membership_stripe_is_a_provider_binding():
    contract = load_pack_contract(
        "saas-membership",
        {"requires": ["stripe"], "entities": []},
    )
    assert all(dep.id != "stripe" for dep in contract.requires)
    assert contract.configuration_providers() == ["stripe"]
    assert any(
        item.capability == "payments.subscription" for item in contract.provider_bindings
    )


def test_subscription_operations_is_project_scoped_and_stripe_configured():
    contract = load_pack_contract("subscription-operations", {"requires": [], "entities": []})
    assert contract.required_pack_slugs() == []
    assert [entity.entity_key for entity in contract.entities] == ["subscription_record"]
    assert contract.configuration_providers() == ["stripe"]
    assert contract.security.rls_required is True
    assert contract.security.ownership_mode == "rls"
    assert "subscription.status_changed" in {event.id for event in contract.events}


def test_education_learning_pack_covers_progress_assessment_and_certificates():
    contract = load_pack_contract("education-learning", {"requires": [], "entities": []})
    assert contract.configuration_providers() == ["accredible"]
    assert {
        "course",
        "lesson",
        "enrollment",
        "lesson_progress",
        "assessment",
        "assessment_attempt",
        "certificate_record",
        "attendance_session",
        "attendance_record",
    } == {entity.entity_key for entity in contract.entities}
    assert {
        "education.enrollment_created",
        "education.progress_changed",
        "education.assessment_submitted",
        "education.certificate_issued",
    } <= {event.id for event in contract.events}


def test_billing_portal_pack_requires_stripe_configuration():
    contract = load_pack_contract("billing-portal", {"requires": [], "entities": []})
    assert contract.provide_ids() == ["billing.portal"]
    assert contract.configuration_providers() == ["stripe"]
    assert contract.security.ownership_mode == "owner_managed"


def test_tax_calculation_pack_requires_stripe_configuration():
    contract = load_pack_contract("tax-calculation", {"requires": [], "entities": []})
    assert contract.provide_ids() == ["billing.tax_calculation"]
    assert contract.configuration_providers() == ["stripe"]
    assert contract.security.ownership_mode == "staff_managed"
    assert get_action("stripe.create_tax_calculation") is not None
    assert get_action("stripe.create_tax_transaction_from_calculation") is not None


def test_empty_requires_connections_does_not_fall_back_to_requires():
    contract = load_pack_contract(
        "contact-capture",
        {
            "requires": [],
            "requires_connections": [],
            "entities": [],
        },
    )
    assert contract.configuration_providers() == []
    assert "form.public_submission" in contract.provide_ids()
    assert contract.required_primitive_ids() == []
    assert contract.optional_requires[0].id == "email.send"
    assert contract.optional_requires[0].when
    assert contract.optional_requires[1].id == "crm.contact.upsert"
    binding = contract.provider_bindings[0]
    assert binding.capability == "crm.contact.upsert"
    assert binding.configuration_required is False


def test_connections_pack_requires_directory_pack():
    contract = load_pack_contract(
        "connections-networking",
        {"requires": [], "requires_packs": ["business-directory"]},
    )
    assert contract.required_pack_slugs() == ["business-directory"]


def test_appointment_calendar_is_optional():
    contract = load_pack_contract(
        "appointment-booking",
        {"requires": [], "connector_actions": ["calendly.create_scheduling_link"]},
    )
    assert contract.configuration_providers() == []
    assert "booking.reserve" in contract.required_primitive_ids()
    assert "booking.availability" in contract.required_primitive_ids()
    calendar = next(
        item for item in contract.provider_bindings if item.capability == "calendar.sync"
    )
    assert calendar.configuration_required is False


def test_stamp_adds_approval_workflow_graph():
    stamped = stamp_composition(
        "approval-workflow",
        {
            "requires": [],
            "entities": [
                {
                    "entity_key": "approval_request",
                    "schema": {
                        "fields": [
                            {
                                "key": "status",
                                "type": "select",
                                "options": [
                                    "pending",
                                    "approved",
                                    "rejected",
                                    "escalated",
                                ],
                            },
                            {"key": "approver_role", "type": "text"},
                            {"key": "requested_by", "type": "text"},
                        ]
                    },
                }
            ],
        },
    )
    assert stamped["schema_version"] == 2
    assert stamped["requires"][0]["id"] == "workflow.transition"
    assert "requires_packs" not in stamped
    workflow = stamped["entities"][0]["schema"]["workflow"]
    assert workflow["transitions"]["pending"] == [
        "approved",
        "rejected",
        "escalated",
    ]
    events = stamped["entities"][0]["schema"]["domain_events"]
    ids = [item["id"] for item in events]
    assert ids == [
        "approval.requested",
        "approval.approved",
        "approval.rejected",
        "approval.escalated",
    ]
    won = next(item for item in events if item["id"] == "approval.approved")
    assert won["producer"]["kind"] == "workflow.transition"
    assert won["producer"]["to_state"] == "approved"
    assert "from_state" not in won["producer"]
    assert stamped["entities"][0]["schema"]["fields"][0]["protected"] is True
    assert "fields" not in stamped["entity_contracts"][0]
    assert "protected_fields" not in stamped["entity_contracts"][0]


def _custom_workflow_composition(transitions: dict[str, list[str]]) -> dict:
    """A pack slug not present in catalog_v2's overlays -- overlay_for() then returns
    no fixed workflow, so the blob's own `workflows` entry (the same raw shape
    catalog_v2 authors, and load_pack_contract/_workflows() parses) is what actually
    reaches _apply_workflows_to_entities. Named catalog slugs like "approval-workflow"
    always have their transitions fixed by the overlay, so they can't be used to
    exercise arbitrary/invalid graphs here."""
    return {
        "requires": [],
        "entities": [
            {
                "entity_key": "approval_request",
                "schema": {
                    "fields": [
                        {
                            "key": "status",
                            "type": "select",
                            "options": ["pending", "approved", "rejected", "escalated"],
                        }
                    ]
                },
            }
        ],
        "workflows": [
            {"entity_key": "approval_request", "field": "status", "transitions": transitions}
        ],
    }


def test_stamp_rejects_unknown_transition_target_for_hand_authored_packs():
    """Hand-authored catalog_v2/migration-seeded packs get the same destination-state
    check as Level-1 synthesized packs (composer.py) -- previously this path had no
    Core-side validation at all and relied entirely on Cloud's schema-push-time
    re-validation as the only backstop."""
    with pytest.raises(ValueError, match="is not one of"):
        stamp_composition(
            "local-test-custom-workflow", _custom_workflow_composition({"pending": ["archived"]})
        )


def test_stamp_rejects_unknown_transition_source_for_hand_authored_packs():
    with pytest.raises(ValueError, match="is not one of"):
        stamp_composition(
            "local-test-custom-workflow", _custom_workflow_composition({"archived": ["approved"]})
        )


def test_stamp_rejects_workflow_field_missing_from_entity():
    composition = {
        "requires": [],
        "entities": [{"entity_key": "approval_request", "schema": {"fields": []}}],
        "workflows": [
            {"entity_key": "approval_request", "field": "status", "transitions": {"pending": ["approved"]}}
        ],
    }
    with pytest.raises(ValueError, match="not declared"):
        stamp_composition("local-test-custom-workflow", composition)


def test_stamp_dedupes_duplicate_transition_targets():
    stamped = stamp_composition(
        "local-test-custom-workflow",
        _custom_workflow_composition({"pending": ["approved", "approved", "rejected"]}),
    )
    workflow = stamped["entities"][0]["schema"]["workflow"]
    assert workflow["transitions"]["pending"] == ["approved", "rejected"]


def test_payment_checkout_stamp_drops_requires_connections():
    stamped = stamp_composition(
        "payment-checkout",
        {
            "requires": [],
            "requires_connections": ["stripe"],
            "entities": [],
        },
    )
    assert "requires_connections" not in stamped
    assert stamped["provider_bindings"][0]["providers"] == ["stripe", "paypal"]
    assert stamped["webhooks"][0]["target"] == "pack:payment-checkout.reconcile"
    assert stamped["webhooks"][0]["forward_path"] == "/api/webhooks/checkout-completed"
    assert "forward_url_config_key" not in stamped["webhooks"][0]
    assert stamped["connector_actions_role"] == "resolved_implementation"


def test_legacy_crm_pipeline_events_are_not_transported():
    stamped = stamp_composition(
        "crm-pipeline",
        {
            "requires": [],
            "entities": [
                {"entity_key": "crm_contact", "schema": {"fields": []}},
                {"entity_key": "crm_deal", "schema": {"fields": []}},
            ],
        },
    )
    for entity in stamped["entities"]:
        assert "domain_events" not in entity["schema"]
    assert "crm.contact_created" in [item["id"] for item in stamped["events"]]
    assert all(item.get("producer") is None for item in stamped["events"])
    contract = load_pack_contract("crm-pipeline", {"requires": []})
    assert contract.compatibility.legacy_aggregate is True
    assert "crm.deal" in contract.provide_ids()
    assert contract.planner.preferred_for_new_projects is False
    assert contract.planner.selectable is False


def test_contact_stamp_keeps_field_flags_on_schema_only():
    stamped = stamp_composition(
        "contact-capture",
        {
            "requires": [],
            "entities": [
                {
                    "entity_key": "contact_submission",
                    "schema": {
                        "fields": [
                            {"key": "name", "type": "text"},
                            {"key": "notify_attempts", "type": "number"},
                        ]
                    },
                }
            ],
        },
    )
    fields = {
        item["key"]: item for item in stamped["entities"][0]["schema"]["fields"]
    }
    assert fields["name"]["public_create"] is True
    assert fields["notify_attempts"]["system_managed"] is True
    domain_ids = [item["id"] for item in stamped["entities"][0]["schema"]["domain_events"]]
    assert domain_ids == ["contact.submitted"]
    aliases = stamped["entities"][0]["schema"]["domain_events"][0]["aliases"]
    assert {item["id"]: item["kind"] for item in aliases} == {
        "forms.contact.submitted": "composition",
        "form.submitted": "transport",
    }


def test_quote_request_entity_is_not_stamped_with_inferred_domain_events():
    stamped = stamp_composition(
        "contact-capture",
        {
            "requires": [],
            "entities": [
                {
                    "entity_key": "quote_request",
                    "schema": {"fields": [{"key": "email", "type": "text"}]},
                }
            ],
        },
    )
    assert "domain_events" not in stamped["entities"][0]["schema"]
    contract = stamped["entity_contracts"][0]
    assert set(contract) == {
        "entity_key",
        "schema_mode",
        "workflow_configurable",
    }


def test_required_capabilities_reads_kind_capability_refs_not_primitives_or_packs():
    from src.domain.capabilities.dependencies import CapabilityDependency

    contract = load_pack_contract(
        "demo-pack",
        {
            "schema_version": 2,
            "requires": [
                {"kind": "capability", "id": "auth", "min_version": "2"},
                {"kind": "primitive", "id": "form.public_create"},
            ],
            "optional_requires": [
                {"kind": "capability", "id": "notifications"},
            ],
        },
    )

    capabilities = contract.required_capabilities()
    assert CapabilityDependency(key="auth", required=True, minimum_version=2) in capabilities
    assert CapabilityDependency(key="notifications", required=False) in capabilities
    assert len(capabilities) == 2
    assert contract.required_primitive_ids() == ["form.public_create"]


def test_required_capabilities_is_empty_when_none_declared():
    contract = load_pack_contract(
        "demo-pack",
        {"schema_version": 2, "requires": [{"kind": "primitive", "id": "auth.roles"}]},
    )

    assert contract.required_capabilities() == ()
