"""external.entity_ingest: contract validation + the content-ingest Feature
Pack's Core-side service (verified webhook -> bounded mapping -> upsert).
"""
from __future__ import annotations

from unittest.mock import AsyncMock
from uuid import uuid4

import pytest

from src.domain.entity_ingest.service import EntityIngestError, EntityIngestService
from src.domain.feature_packs.catalog_v2 import CATALOG_OVERLAYS
from src.domain.feature_packs.contract import (
    FeaturePackContract,
    PackEntityContract,
    PackEntityIngestBinding,
    PackWebhookBinding,
)
from src.domain.feature_packs.normalize import load_pack_contract, stamp_composition

_CONTENT_ITEM_FIELDS = [
    {"key": "title", "name": "Title", "type": "text", "required": True},
    {"key": "slug", "name": "Slug", "type": "text", "indexed": True},
    {"key": "body", "name": "Body", "type": "text"},
    {"key": "excerpt", "name": "Excerpt", "type": "text"},
    {"key": "cover_image_file_id", "name": "Cover image", "type": "text"},
    {"key": "published_at", "name": "Published at", "type": "date"},
    {"key": "external_id", "name": "External ID", "type": "text", "required": True, "indexed": True},
    {"key": "source", "name": "Source", "type": "text", "required": True, "indexed": True},
    {"key": "status", "name": "Status", "type": "select", "options": ["draft", "published"]},
    {"key": "ingest_error", "name": "Ingest error", "type": "text"},
]


def _raw_composition() -> dict:
    return {
        "function_slugs": [], "connector_actions": [], "requires": [],
        "entities": [{
            "entity_key": "content_item", "name": "Content Item",
            "schema": {"fields": [dict(f) for f in _CONTENT_ITEM_FIELDS]},
        }],
    }


# ---------------------------------------------------------------------------
# Contract-level
# ---------------------------------------------------------------------------

def test_catalog_entry_stamps_provides_security_and_field_flags():
    composition = stamp_composition("content-ingest", _raw_composition())
    entity = next(e for e in composition["entities"] if e["entity_key"] == "content_item")
    fields = {f["key"]: f for f in entity["schema"]["fields"]}
    assert fields["title"]["public_create"] is True
    assert fields["status"]["public_create"] is False
    assert fields["status"]["system_managed"] is True
    assert fields["external_id"]["public_create"] is False
    assert fields["source"]["public_create"] is False
    assert composition["webhooks"][0]["provider"] == "content_ingest"
    assert composition["webhooks"][0]["forward_path"] == "/api/webhooks/content-ingest"
    assert composition["entity_ingest_bindings"][0]["entity_key"] == "content_item"
    assert composition["entity_ingest_bindings"][0]["identity_fields"] == ["source", "external_id"]
    assert composition["security"]["ownership_mode"] == "public_create"


def test_content_ingest_provides_and_planner_not_default():
    contract = load_pack_contract("content-ingest", _raw_composition())
    assert {c.id for c in contract.provides} == {"content.ingest_webhook", "content.item"}
    assert contract.planner.selectable is True
    assert contract.planner.preferred_for_new_projects is False


def test_entity_ingest_binding_rejects_undeclared_identity_field():
    # Construction-time (FeaturePackContract's own model_validator) only checks
    # entity_key/webhook_provider existence -- entities there are policy-only
    # (PackEntityContract has no field list), so this does NOT raise here...
    contract = FeaturePackContract(
        slug="x",
        entities=[PackEntityContract(entity_key="thing")],
        webhook_bindings=[
            PackWebhookBinding(provider="p", target="t", forward_path="/x")
        ],
        entity_ingest_bindings=[
            PackEntityIngestBinding(
                entity_key="thing", webhook_provider="p", identity_fields=["nope"],
            )
        ],
    )
    assert contract.entity_ingest_bindings[0].identity_fields == ["nope"]
    # ...identity-field-vs-schema is checked by stamp_composition instead, where
    # the entity's real field list lives:
    with pytest.raises(ValueError, match="identity_fields not declared"):
        stamp_composition("content-ingest", {
            "entities": [{
                "entity_key": "content_item",
                "schema": {"fields": [{"key": "title", "type": "text"}]},
            }],
        })


def test_entity_ingest_binding_rejects_unknown_entity_or_provider():
    with pytest.raises(ValueError, match="undeclared entity"):
        FeaturePackContract(
            slug="x",
            webhook_bindings=[PackWebhookBinding(provider="p", target="t", forward_path="/x")],
            entity_ingest_bindings=[
                PackEntityIngestBinding(entity_key="ghost", webhook_provider="p")
            ],
        )
    with pytest.raises(ValueError, match="no matching webhook_bindings"):
        FeaturePackContract(
            slug="x",
            entities=[PackEntityContract(entity_key="thing")],
            entity_ingest_bindings=[
                PackEntityIngestBinding(entity_key="thing", webhook_provider="missing")
            ],
        )


def test_default_status_field_and_value_must_pair():
    with pytest.raises(ValueError, match="must be set together"):
        PackEntityIngestBinding(
            entity_key="thing", webhook_provider="p", default_status_field="status"
        )


def test_external_entity_ingest_is_catalog_only_primitive():
    from src.domain.feature_packs.contract import CATALOG_PRIMITIVES, SUPPORTED_PRIMITIVES

    assert "external.entity_ingest" in CATALOG_PRIMITIVES
    assert "external.entity_ingest" not in SUPPORTED_PRIMITIVES


def test_content_ingest_registered_in_default_known_packs():
    from src.domain.app_blocks.catalogs import DEFAULT_KNOWN_PACKS, canonical_feature_pack

    assert "content-ingest" in DEFAULT_KNOWN_PACKS
    assert canonical_feature_pack("content.item") == "content-ingest"


def test_content_ingest_overlay_present():
    assert "content-ingest" in CATALOG_OVERLAYS


# ---------------------------------------------------------------------------
# Service-level: bounded mapping, identity, create/update upsert
# ---------------------------------------------------------------------------

class _Row:
    def __init__(self, **kw):
        self.__dict__.update(kw)


class _Result:
    def __init__(self, value):
        self._value = value

    def scalar_one_or_none(self):
        return self._value


class _FakeDB:
    def __init__(self, values: list):
        self._values = list(values)

    async def execute(self, _stmt):
        return _Result(self._values.pop(0))


def _service(install_status="installed", cloud=None):
    db = _FakeDB([
        _Row(status=install_status),
        _Row(composition=_raw_composition()),
    ])
    cloud = cloud or AsyncMock()
    return EntityIngestService(db, cloud), cloud


@pytest.mark.asyncio
async def test_pack_not_installed_rejected():
    service, _ = _service(install_status=None)
    service._db._values = [None, _Row(composition=_raw_composition())]
    with pytest.raises(EntityIngestError) as exc:
        await service.ingest(
            project_id=uuid4(), pack_slug="content-ingest", provider="content_ingest",
            envelope={"provider": "content_ingest", "payload": {}},
        )
    assert exc.value.code == "pack_not_installed"


@pytest.mark.asyncio
async def test_unknown_provider_rejected():
    service, _ = _service()
    with pytest.raises(EntityIngestError) as exc:
        await service.ingest(
            project_id=uuid4(), pack_slug="content-ingest", provider="not_a_real_provider",
            envelope={"provider": "content_ingest", "payload": {"external_id": "1"}},
        )
    assert exc.value.code == "binding_not_found"


@pytest.mark.asyncio
async def test_missing_external_id_rejected():
    service, _ = _service()
    with pytest.raises(EntityIngestError) as exc:
        await service.ingest(
            project_id=uuid4(), pack_slug="content-ingest", provider="content_ingest",
            envelope={"provider": "content_ingest", "payload": {"title": "Hello"}},
        )
    assert exc.value.code == "missing_identity"


@pytest.mark.asyncio
async def test_protected_fields_cannot_be_supplied_by_payload_on_create():
    cloud = AsyncMock()
    cloud.query_project_records.return_value = {"records": []}
    cloud.create_entity_records.return_value = [{"id": "rec-1"}]
    service, _ = _service(cloud=cloud)
    project_id = uuid4()

    result = await service.ingest(
        project_id=project_id, pack_slug="content-ingest", provider="content_ingest",
        envelope={
            "provider": "content_ingest",
            "payload": {
                "title": "Hello world",
                "external_id": "airtable-rec-42",
                "status": "published",       # attacker/producer-supplied -- must be ignored
                "source": "someone-else",     # producer cannot choose its own source either
                "ingest_error": "haha",       # internal field -- must be ignored
            },
        },
    )

    assert result == {"ok": True, "action": "created", "record_id": "rec-1"}
    created_data = cloud.create_entity_records.call_args.args[2][0]["data"]
    assert created_data["title"] == "Hello world"
    assert created_data["source"] == "content_ingest"   # from envelope provenance, not payload
    assert created_data["external_id"] == "airtable-rec-42"
    assert created_data["status"] == "draft"            # safest default, never "published"
    assert "ingest_error" not in created_data


@pytest.mark.asyncio
async def test_first_delivery_creates_one_record():
    cloud = AsyncMock()
    cloud.query_project_records.return_value = {"records": []}
    cloud.create_entity_records.return_value = [{"id": "rec-1"}]
    service, _ = _service(cloud=cloud)

    result = await service.ingest(
        project_id=uuid4(), pack_slug="content-ingest", provider="content_ingest",
        envelope={"provider": "content_ingest", "payload": {"title": "A", "external_id": "ext-1"}},
    )
    assert result["action"] == "created"
    cloud.create_entity_records.assert_awaited_once()
    cloud.update_entity_record_data.assert_not_called()


@pytest.mark.asyncio
async def test_redelivery_with_same_identity_updates_not_duplicates():
    cloud = AsyncMock()
    cloud.query_project_records.return_value = {"records": [{"id": "rec-1"}]}
    service, _ = _service(cloud=cloud)

    result = await service.ingest(
        project_id=uuid4(), pack_slug="content-ingest", provider="content_ingest",
        envelope={"provider": "content_ingest", "payload": {"title": "A changed", "external_id": "ext-1"}},
    )
    assert result == {"ok": True, "action": "updated", "record_id": "rec-1"}
    cloud.update_entity_record_data.assert_awaited_once()
    cloud.create_entity_records.assert_not_called()
    updated_data = cloud.update_entity_record_data.call_args.args[3]
    assert updated_data == {"title": "A changed"}   # bounded: no identity/status field re-sent


@pytest.mark.asyncio
async def test_update_does_not_reset_status_back_to_draft():
    """A staff member may have since published the record by hand; a follow-up
    content update from the same external source must not silently unpublish it."""
    cloud = AsyncMock()
    cloud.query_project_records.return_value = {"records": [{"id": "rec-1"}]}
    service, _ = _service(cloud=cloud)

    await service.ingest(
        project_id=uuid4(), pack_slug="content-ingest", provider="content_ingest",
        envelope={"provider": "content_ingest", "payload": {"title": "A changed", "external_id": "ext-1"}},
    )
    updated_data = cloud.update_entity_record_data.call_args.args[3]
    assert "status" not in updated_data


@pytest.mark.asyncio
async def test_different_sources_same_external_id_do_not_collide():
    cloud = AsyncMock()
    cloud.query_project_records.return_value = {"records": []}
    cloud.create_entity_records.return_value = [{"id": "rec-1"}]
    service, _ = _service(cloud=cloud)

    await service.ingest(
        project_id=uuid4(), pack_slug="content-ingest", provider="content_ingest",
        envelope={"provider": "content_ingest", "payload": {"title": "A", "external_id": "shared-id-7"}},
    )
    where = cloud.query_project_records.call_args.kwargs["where"]
    assert where == {"source": "content_ingest", "external_id": "shared-id-7"}
