"""6O CRM App Block / Feature Pack composition."""
from __future__ import annotations

from src.domain.app_blocks.compiler import compile_install_plan
from src.domain.app_blocks.crm_composition import (
    ARTIFACTS,
    CONTACT_FORM_UPSERT_CAPABILITY,
    CRM_EXTERNAL_SYNC_KIND,
    CRM_PIPELINE_ALIAS_DECISION,
    CRM_PIPELINE_ALIAS_TARGET,
    CRM_RUNTIME_REASON_UX,
    FORBIDDEN_PROVIDER_FORM_FIELDS,
    INBOUND_CRM_EVENT_COMPOSITION,
    OBJECT_REF_PERSISTENCE,
    PROVIDER_COUPLING,
    SYNC_STATUS_UX,
    capability_readiness,
    catalog_binding_capability,
    classify_artifact,
    contact_form_crm_capability,
    expand_external_sync,
    generic_block_must_not_branch_on_provider,
    local_stage_to_crm_deal_update,
    map_form_fields_to_crm_contact,
    select_contact_form_composition,
    select_local_lead_composition,
)
from src.domain.app_blocks.registry import load_default_registry, load_manifests
from src.domain.codegen.crm_codegen import select_crm_generation
from src.domain.codegen.prompt_wrapper import _APP_BLOCK_CONTRACT
from src.domain.connectors.invoke_capabilities import (
    CAPABILITY_CONVERSION_PATTERN,
    CRM_COMPOSITION_PHASE_6O,
    CRM_FAMILY_PLAN,
    CRM_PUBLIC_INVOKE_VERBS,
    INVOKE_CAPABILITIES,
)
from src.domain.connectors.public_gateway import PHASE_6_STATUS
from src.domain.projects.discovery_schemas import (
    Actor,
    AuthenticationSpec,
    DiscoverySpec,
    Entity,
    ProductDefinition,
    TenancySpec,
    Workflow,
)


def _spec(**overrides) -> DiscoverySpec:
    data = {
        "product": ProductDefinition(
            name="Agency Site",
            name_status="working",
            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),
    }
    data.update(overrides)
    return DiscoverySpec(**data)


def test_6o_closes_crm_composition() -> None:
    assert PHASE_6_STATUS == "6p_complete"
    assert CAPABILITY_CONVERSION_PATTERN["phase_6_status"] == "6p_complete"
    assert CAPABILITY_CONVERSION_PATTERN["next_program_slice"] == (
        "9_app_block_modernization"
    )
    assert CRM_FAMILY_PLAN["status"] == "6p_complete"
    assert CRM_FAMILY_PLAN["next_slice"] == "forms_first_class_product_domain"
    assert CRM_COMPOSITION_PHASE_6O["status"] == "complete"
    assert CRM_COMPOSITION_PHASE_6O["crm_lead"] == "local_p2hs_product"
    assert CRM_COMPOSITION_PHASE_6O["inbound_events"] == "DEFER"
    assert CRM_COMPOSITION_PHASE_6O["object_ref_persistence"] == "GAP_DOCUMENTED"
    assert CRM_COMPOSITION_PHASE_6O["no_parallel_readiness_flags"] is True
    assert CONTACT_FORM_UPSERT_CAPABILITY in CRM_PUBLIC_INVOKE_VERBS
    assert CONTACT_FORM_UPSERT_CAPABILITY in INVOKE_CAPABILITIES
    assert "crm.external_sync" not in INVOKE_CAPABILITIES


def test_artifact_classifications() -> None:
    assert classify_artifact("crm.lead")["classification"] == "P"
    assert classify_artifact("lead-lifecycle")["classification"] == "P"
    assert classify_artifact("forms.contact")["classification"] == "B"
    assert classify_artifact("contact-capture")["classification"] == "B"
    assert classify_artifact("crm.pipeline")["classification"] == "P"
    assert classify_artifact("crm-pipeline")["classification"] == "LEGACY"
    assert classify_artifact("salesforce-order-sync")["decision"] == "VALID_PROVIDER_SPECIFIC"
    assert classify_artifact("crm.external_sync")["decision"] == "FORMALIZE"
    assert set(ARTIFACTS) >= {
        "crm.lead",
        "forms.contact",
        "lead-lifecycle",
        "crm.pipeline",
        "crm.external_sync",
    }


def test_crm_pipeline_alias_retired_block_stays_local() -> None:
    assert CRM_PIPELINE_ALIAS_DECISION == "RETIRED_FOR_APP_BLOCK"
    assert CRM_PIPELINE_ALIAS_TARGET == "lead-lifecycle"
    from src.domain.app_blocks.catalogs import FEATURE_PACK_ALIASES

    assert "crm.pipeline" not in FEATURE_PACK_ALIASES
    assert FEATURE_PACK_ALIASES["crm.lead"] == "lead-lifecycle"


def test_external_sync_is_marker_not_invoke() -> None:
    assert CRM_EXTERNAL_SYNC_KIND == "composition_marker"
    assert expand_external_sync("contact-capture") == (CONTACT_FORM_UPSERT_CAPABILITY,)
    assert expand_external_sync("lead-lifecycle") == ()
    assert catalog_binding_capability("contact-capture") == CONTACT_FORM_UPSERT_CAPABILITY
    assert catalog_binding_capability("lead-lifecycle") is None
    assert catalog_binding_capability("salesforce-order-sync") == "crm.external_sync"


def test_contact_form_without_crm_stays_local() -> None:
    plan = compile_install_plan(_spec())
    ids = [block.id for block in plan.blocks]
    assert "forms.contact" in ids
    assert "crm.lead" not in ids
    composition = select_contact_form_composition(sync_to_crm=False)
    assert composition["optional_capability"] is None
    assert composition["local_success_independent_of_crm"] is True


def test_contact_form_plus_crm_sync_uses_upsert() -> None:
    hubspot = select_contact_form_composition(sync_to_crm=True, hubspot_connected=True)
    salesforce = select_contact_form_composition(sync_to_crm=True, salesforce_connected=True)
    assert hubspot == salesforce
    assert hubspot["block"] == "forms.contact"
    assert hubspot["pack"] == "contact-capture"
    assert hubspot["optional_capability"] == "crm.contact.upsert"
    assert contact_form_crm_capability(enabled=True, has_email=True) == "crm.contact.upsert"
    assert contact_form_crm_capability(enabled=True, has_email=False) is None


def test_hubspot_and_salesforce_select_the_same_install_plan() -> None:
    spec = _spec(
        product=ProductDefinition(
            name="Lead site",
            name_status="working",
            purpose="Marketing site with a contact form that syncs submissions to CRM.",
            first_build_goal="Capture website enquiries.",
        )
    )
    plan = compile_install_plan(spec)
    ids = [block.id for block in plan.blocks]
    assert "forms.contact" in ids
    assert "crm.lead" not in ids
    assert "contact-capture" in plan.required_feature_packs
    crm_reqs = [
        item
        for item in plan.configuration_requirements
        if item.capability == "crm.contact.upsert"
    ]
    assert crm_reqs
    assert crm_reqs[0].behavior == "disabled"
    assert "hubspot" in crm_reqs[0].providers
    assert "salesforce" in crm_reqs[0].providers


def test_crm_lead_remains_local_and_is_not_rewritten() -> None:
    spec = _spec(
        product=ProductDefinition(
            name="Sales desk",
            name_status="working",
            purpose="Internal CRM for managing sales leads.",
            first_build_goal="Track leads through a pipeline.",
        ),
        archetypes=["operations"],
        actors=[Actor(id="agent", name="Sales agent")],
        workflows=[
            Workflow(
                id="pipeline",
                name="Lead pipeline",
                description="Move leads through stages",
            )
        ],
        entities=[Entity(id="lead", name="Lead", description="A sales lead")],
    )
    plan = compile_install_plan(spec)
    assert "crm.lead" in [block.id for block in plan.blocks]
    assert "lead-lifecycle" in plan.required_feature_packs
    lead = select_local_lead_composition()
    assert lead["rewritten_to_crm_contact"] is False
    assert lead["auto_external_stage_sync"] is False
    assert lead["local_entity"] == "lead"
    assert local_stage_to_crm_deal_update(configured=False, logical_alias="qualified") is None
    mapped = local_stage_to_crm_deal_update(configured=True, logical_alias="qualified")
    assert mapped == {"capability": "crm.deal.update", "stage": "qualified"}


def test_local_stage_rejects_provider_wire_values() -> None:
    try:
        local_stage_to_crm_deal_update(configured=True, logical_alias="dealstage")
    except ValueError as exc:
        assert "provider stage" in str(exc)
    else:
        raise AssertionError("expected provider stage rejection")


def test_form_mapping_is_canonical_only() -> None:
    contact = map_form_fields_to_crm_contact(
        {
            "email": "a@b.com",
            "first_name": "Ada",
            "last_name": "Lovelace",
            "phone": "1",
            "message": "hi",
        }
    )
    assert contact == {
        "email": "a@b.com",
        "first_name": "Ada",
        "last_name": "Lovelace",
        "phone": "1",
    }
    try:
        map_form_fields_to_crm_contact({"email": "a@b.com", "firstname": "Ada"})
    except ValueError as exc:
        assert "firstname" in str(exc)
    else:
        raise AssertionError("expected provider field rejection")
    assert "AccountId" in FORBIDDEN_PROVIDER_FORM_FIELDS


def test_readiness_derives_from_catalog_and_bindings() -> None:
    assert capability_readiness("crm.contact.upsert", bound=True) == "ready"
    assert capability_readiness("crm.contact.upsert", bound=False) == "available_unconfigured"
    assert (
        capability_readiness("crm.contact.upsert", bound=False, ambiguous=True)
        == "binding_ambiguous"
    )
    assert (
        capability_readiness("crm.contact.upsert", bound=True, reauthorization_required=True)
        == "reauthorization_required"
    )
    assert capability_readiness("crm.external_sync", bound=True) == "unavailable"
    assert "CRM_BLOCK_READY" not in CRM_RUNTIME_REASON_UX
    assert CRM_RUNTIME_REASON_UX["crm_unbound"] == "Connect/configure a CRM"
    assert "Needs CRM configuration" in SYNC_STATUS_UX


def test_inbound_events_are_deferred() -> None:
    assert INBOUND_CRM_EVENT_COMPOSITION == "DEFER"
    assert OBJECT_REF_PERSISTENCE["decision"] == "GAP_DOCUMENTED"
    assert OBJECT_REF_PERSISTENCE["local_id_is_not_external_ref"] is True


def test_forms_contact_manifest_declares_optional_upsert() -> None:
    registry = load_default_registry()
    block = registry.require("forms.contact")
    assert any(dep.id == "crm.contact.upsert" for dep in block.optional_requires)
    binding = next(
        item for item in block.provider_bindings if item.capability == "crm.contact.upsert"
    )
    assert binding.configuration_required is False
    assert binding.missing_configuration_behavior == "disabled"
    lead = registry.require("crm.lead")
    assert not any(item.capability.startswith("crm.contact.") for item in lead.provider_bindings)
    assert not any(dep.id.startswith("crm.contact.") for dep in lead.requires)


def test_generic_blocks_have_no_provider_branching() -> None:
    for manifest in load_manifests():
        text = str(manifest.model_dump())
        assert generic_block_must_not_branch_on_provider(text)
        if manifest.id in {"forms.contact", "crm.lead"}:
            for field in FORBIDDEN_PROVIDER_FORM_FIELDS:
                assert field not in text
    classes = {row["class"] for row in PROVIDER_COUPLING}
    assert "MUST_NORMALIZE" in classes
    assert "VALID_PROVIDER_SPECIFIC" in classes


def test_codegen_handoff_matches_block_composition() -> None:
    composed = select_contact_form_composition(sync_to_crm=True)
    generated = select_crm_generation("Sync form submissions to CRM by email")
    assert generated.surface == "canonical"
    assert generated.capability == "crm.contact.upsert"
    assert composed["optional_capability"] == generated.capability
    hubspot = select_crm_generation(
        "Sync form submissions to CRM by email", connected_provider="hubspot"
    )
    salesforce = select_crm_generation(
        "Sync form submissions to CRM by email", connected_provider="salesforce"
    )
    assert hubspot.capability == salesforce.capability == "crm.contact.upsert"
    assert "Do not rewrite that App Block onto `crm.contact.create`" in _APP_BLOCK_CONTRACT
