"""sync_form_requirements_from_prompt: detect/extract/merge for one build prompt.

Uses a minimal fake DB (mirrors the project's existing fake-DB test convention)
and monkeypatches the network extractor -- these tests exercise the real
orchestration function, not a reimplementation of its logic.
"""
from types import SimpleNamespace
from unittest.mock import AsyncMock, patch
from uuid import uuid4

import pytest
from src.domain.forms.canonical import FormField
from src.domain.projects.form_requirement_sync import sync_form_requirements_from_prompt
from src.domain.projects.form_requirements import (
    FormRequirementDelta,
    FormRequirementExtractionResult,
)

_CANONICAL_QUOTE_REQUEST_KEYS = [
    "name", "email", "project_type", "budget", "message", "attachment",
]


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(form_requirements: list[dict] | None = None):
    return SimpleNamespace(
        id=uuid4(),
        discovery_json={"result": {"spec": {"form_requirements": form_requirements or []}}},
    )


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


@pytest.mark.asyncio
async def test_non_form_prompt_never_calls_extractor_or_writes():
    project = _project()
    db = _FakeDB(project)
    with patch(
        "src.integrations.project_intake_client.extract_form_requirement_delta",
        new_callable=AsyncMock,
    ) as extractor:
        outcome = await sync_form_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 = FormRequirementDelta(
        form_key="quote_request", add_fields=(FormField(key="phone", type="phone"),)
    )
    with patch(
        "src.integrations.project_intake_client.extract_form_requirement_delta",
        new_callable=AsyncMock,
        return_value=FormRequirementExtractionResult(applies=True, confident=True, delta=delta),
    ):
        outcome = await sync_form_requirements_from_prompt(
            db, project.id, "Add a phone field to the quote form", settings=object()
        )
    assert outcome.changed and not outcome.ambiguous
    requirements = _requirements(project)
    assert requirements[0]["form_key"] == "quote_request"
    # Seeded from canonical, not empty (see test_form_requirements.py's canonical-seed test).
    assert [f["key"] for f in requirements[0]["fields"]] == [
        *_CANONICAL_QUOTE_REQUEST_KEYS, "phone"
    ]
    assert "form_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([{"form_key": "quote_request", "fields": [], "access": "public_anonymous"}])
    db = _FakeDB(project)
    with patch(
        "src.integrations.project_intake_client.extract_form_requirement_delta",
        new_callable=AsyncMock,
        return_value=FormRequirementExtractionResult(applies=True, confident=False),
    ):
        outcome = await sync_form_requirements_from_prompt(
            db, project.id, "Add some field to the quote form", settings=object()
        )
    assert outcome.ambiguous and not outcome.changed
    # Schema untouched.
    assert _requirements(project) == [
        {"form_key": "quote_request", "fields": [], "access": "public_anonymous"}
    ]
    # Project marked for dashboard visibility.
    assert project.discovery_json["form_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_form_requirement_delta",
        new_callable=AsyncMock,
        side_effect=RuntimeError("provider down"),
    ):
        outcome = await sync_form_requirements_from_prompt(
            db,
            project.id,
            "Make the quote form feel more personal",
            settings=object(),
        )
    assert outcome.ambiguous and not outcome.changed
    assert project.discovery_json["form_requirement_sync"]["ambiguous"] is True


@pytest.mark.asyncio
async def test_repeated_identical_extraction_is_idempotent():
    project = _project()
    db = _FakeDB(project)
    delta = FormRequirementDelta(
        form_key="quote_request", add_fields=(FormField(key="phone", type="phone"),)
    )
    with patch(
        "src.integrations.project_intake_client.extract_form_requirement_delta",
        new_callable=AsyncMock,
        return_value=FormRequirementExtractionResult(applies=True, confident=True, delta=delta),
    ):
        await sync_form_requirements_from_prompt(
            db, project.id, "Add a phone field to the quote form", settings=object()
        )
        after_first = _requirements(project)
        await sync_form_requirements_from_prompt(
            db, project.id, "Add a phone field to the quote form", settings=object()
        )
        after_second = _requirements(project)
    assert after_first == after_second
    assert [f["key"] for f in after_second[0]["fields"]] == [
        *_CANONICAL_QUOTE_REQUEST_KEYS, "phone"
    ]


@pytest.mark.asyncio
async def test_followup_add_then_update_then_remove_field():
    project = _project()
    db = _FakeDB(project)

    async def run(delta):
        with patch(
            "src.integrations.project_intake_client.extract_form_requirement_delta",
            new_callable=AsyncMock,
            return_value=FormRequirementExtractionResult(applies=True, confident=True, delta=delta),
        ):
            await sync_form_requirements_from_prompt(
                db, project.id, "Change the quote form fields", settings=object()
            )

    def _fields_by_key(project):
        return {f["key"]: f for f in _requirements(project)[0]["fields"]}

    await run(
        FormRequirementDelta(
            form_key="quote_request", add_fields=(FormField(key="phone", type="phone"),)
        )
    )
    # Seeded from canonical (see test_first_delta_for_a_form_seeds_from_canonical_not_empty),
    # plus the newly added field.
    assert set(_fields_by_key(project)) == {*_CANONICAL_QUOTE_REQUEST_KEYS, "phone"}

    await run(
        FormRequirementDelta(
            form_key="quote_request",
            update_fields=(FormField(key="phone", type="phone", required=True),),
        )
    )
    assert _fields_by_key(project)["phone"]["required"] is True

    await run(FormRequirementDelta(form_key="quote_request", remove_fields=("phone",)))
    assert set(_fields_by_key(project)) == set(_CANONICAL_QUOTE_REQUEST_KEYS)
