"""sync_entity_field_requirements_from_prompt + resolve_entity_fields.

Uses the same minimal fake-DB convention as test_form_requirement_sync.py and
monkeypatches the network extractor -- these exercise the real orchestration
functions, not a reimplementation of their logic.
"""
from types import SimpleNamespace
from unittest.mock import AsyncMock, patch
from uuid import uuid4

import pytest
from src.domain.codegen.provision_manifest import ProvisionField
from src.domain.projects.entity_requirement_sync import (
    resolve_entity_fields,
    sync_entity_field_requirements_from_prompt,
)
from src.domain.projects.entity_requirements import (
    EntityFieldRequirementDelta,
    EntityFieldRequirementExtractionResult,
)


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

    def scalar_one_or_none(self):
        return self._value


class _FakeDB:
    def __init__(self, project):
        self.project = project
        self.commits = 0

    async def execute(self, _query):
        return _FakeResult(self.project)

    async def commit(self):
        self.commits += 1


def _project(entity_requirements: list[dict] | None = None):
    return SimpleNamespace(
        id=uuid4(),
        discovery_json={"result": {"spec": {"entity_requirements": entity_requirements or []}}},
    )


def _requirements(project) -> list[dict]:
    return project.discovery_json["result"]["spec"]["entity_requirements"]


@pytest.mark.asyncio
async def test_non_entity_prompt_never_calls_extractor_or_writes():
    project = _project()
    db = _FakeDB(project)
    with patch(
        "src.integrations.project_intake_client.extract_entity_field_requirement_delta",
        new_callable=AsyncMock,
    ) as extractor:
        outcome = await sync_entity_field_requirements_from_prompt(
            db, project.id, "Make the hero banner blue", settings=object()
        )
    extractor.assert_not_called()
    assert db.commits == 0
    assert not outcome.changed


@pytest.mark.asyncio
async def test_confident_delta_merges_and_persists():
    project = _project()
    db = _FakeDB(project)
    delta = EntityFieldRequirementDelta(
        entity_key="lead",
        add_fields=(ProvisionField(key="status", label="Status", type="text"),),
    )
    with patch(
        "src.integrations.project_intake_client.extract_entity_field_requirement_delta",
        new_callable=AsyncMock,
        return_value=EntityFieldRequirementExtractionResult(
            applies=True, confident=True, delta=delta
        ),
    ):
        outcome = await sync_entity_field_requirements_from_prompt(
            db, project.id, "Add a status field to the lead entity", settings=object()
        )
    assert outcome.changed and not outcome.ambiguous
    requirements = _requirements(project)
    assert requirements[0]["entity_key"] == "lead"
    assert [f["key"] for f in requirements[0]["fields"]] == ["status"]
    assert "entity_requirement_sync" not in project.discovery_json
    assert db.commits == 1


@pytest.mark.asyncio
async def test_ambiguous_extraction_leaves_schema_unchanged_and_marks_project():
    project = _project([{"entity_key": "lead", "fields": []}])
    db = _FakeDB(project)
    with patch(
        "src.integrations.project_intake_client.extract_entity_field_requirement_delta",
        new_callable=AsyncMock,
        return_value=EntityFieldRequirementExtractionResult(applies=True, confident=False),
    ):
        outcome = await sync_entity_field_requirements_from_prompt(
            db, project.id, "Add some field to the lead entity", settings=object()
        )
    assert outcome.ambiguous and not outcome.changed
    assert _requirements(project) == [{"entity_key": "lead", "fields": []}]
    assert project.discovery_json["entity_requirement_sync"]["ambiguous"] is True


@pytest.mark.asyncio
async def test_provider_failure_is_treated_as_ambiguous_not_raised():
    project = _project()
    db = _FakeDB(project)
    with patch(
        "src.integrations.project_intake_client.extract_entity_field_requirement_delta",
        new_callable=AsyncMock,
        side_effect=RuntimeError("provider down"),
    ):
        outcome = await sync_entity_field_requirements_from_prompt(
            db, project.id, "Add a budget field to the order entity", settings=object()
        )
    assert outcome.ambiguous and not outcome.changed


@pytest.mark.asyncio
async def test_repeated_identical_extraction_is_idempotent():
    project = _project()
    db = _FakeDB(project)
    delta = EntityFieldRequirementDelta(
        entity_key="lead",
        add_fields=(ProvisionField(key="status", label="Status", type="text"),),
    )
    with patch(
        "src.integrations.project_intake_client.extract_entity_field_requirement_delta",
        new_callable=AsyncMock,
        return_value=EntityFieldRequirementExtractionResult(
            applies=True, confident=True, delta=delta
        ),
    ):
        await sync_entity_field_requirements_from_prompt(
            db, project.id, "Add a status field to the lead entity", settings=object()
        )
        after_first = _requirements(project)
        await sync_entity_field_requirements_from_prompt(
            db, project.id, "Add a status field to the lead entity", settings=object()
        )
        after_second = _requirements(project)
    assert after_first == after_second


@pytest.mark.asyncio
async def test_resolve_entity_fields_bootstraps_on_first_declaration():
    project = _project()
    db = _FakeDB(project)
    declared = [{"key": "name", "label": "Name", "type": "text", "required": True}]

    resolved = await resolve_entity_fields(db, project.id, "lead", declared)

    assert resolved == declared
    assert _requirements(project)[0]["entity_key"] == "lead"
    assert [f["key"] for f in _requirements(project)[0]["fields"]] == ["name"]
    assert db.commits == 1


@pytest.mark.asyncio
async def test_resolve_entity_fields_core_wins_on_conflicting_declaration():
    project = _project(
        [
            {
                "entity_key": "lead",
                "fields": [
                    {
                        "key": "status", "label": "Status", "type": "select",
                        "options": ["new", "won"],
                    }
                ],
            }
        ]
    )
    db = _FakeDB(project)
    # A later run declares a DIFFERENT (agent-invented) shape for the same field.
    declared = [{"key": "status", "label": "Status", "type": "text"}]

    resolved = await resolve_entity_fields(db, project.id, "lead", declared)

    assert resolved[0]["type"] == "select"
    assert resolved[0]["options"] == ["new", "won"]


@pytest.mark.asyncio
async def test_resolve_entity_fields_preserves_omitted_known_field():
    project = _project(
        [
            {
                "entity_key": "lead",
                "fields": [{"key": "status", "label": "Status", "type": "text"}],
            }
        ]
    )
    db = _FakeDB(project)
    # This run's declaration only mentions a brand-new field, not the known one.
    declared = [{"key": "name", "label": "Name", "type": "text", "required": True}]

    resolved = await resolve_entity_fields(db, project.id, "lead", declared)

    keys = {f["key"] for f in resolved}
    assert keys == {"name", "status"}


@pytest.mark.asyncio
async def test_resolve_entity_fields_is_idempotent_and_skips_write_when_unchanged():
    project = _project()
    db = _FakeDB(project)
    declared = [{"key": "name", "label": "Name", "type": "text", "required": True}]

    await resolve_entity_fields(db, project.id, "lead", declared)
    assert db.commits == 1

    await resolve_entity_fields(db, project.id, "lead", declared)
    assert db.commits == 1  # unchanged resolution -> no second write
