from types import SimpleNamespace
from unittest.mock import AsyncMock
from uuid import uuid4

import pytest
from pydantic import ValidationError as SchemaError
from src.core.exceptions import ValidationError
from src.domain.projects import intake
from src.domain.projects.intake_schemas import ProjectUnderstanding


def result(decision="allow", **kwargs):
    return ProjectUnderstanding(
        decision=decision,
        category="website",
        summary="A business website",
        requested_features=[],
        template_ref=None,
        confidence=kwargs.get("confidence", 0.9),
    )


@pytest.mark.parametrize("decision", ["clarify", "out_of_scope", "blocked"])
def test_distinct_admission_errors(decision):
    with pytest.raises(ValidationError) as exc:
        intake.require_allowed(result(decision))
    assert exc.value.public_error == f"project_{decision}"
    assert "illegal" not in exc.value.message


@pytest.mark.parametrize("confidence", [-1, 1.1, float("nan"), float("inf")])
def test_confidence_is_bounded(confidence):
    with pytest.raises(SchemaError):
        result(confidence=confidence)


@pytest.mark.asyncio
async def test_unconfigured_enforcement_does_not_allow_a_build(monkeypatch):
    monkeypatch.setattr(
        intake,
        "get_settings",
        lambda: SimpleNamespace(
            project_intake_enabled=True, project_intake_max_chars=20000, openai_api_key=""
        ),
    )
    with pytest.raises(intake.IntakeUnavailableError):
        await intake.assess_project_intent("Build a portfolio")


@pytest.mark.asyncio
async def test_empty_brief_clarifies_without_provider(monkeypatch):
    monkeypatch.setattr(
        intake, "get_settings", lambda: SimpleNamespace(project_intake_enabled=True)
    )
    assert (await intake.assess_project_intent("   ")).decision == "clarify"


@pytest.mark.asyncio
async def test_guest_capture_defers_intake(monkeypatch):
    from src.domain.guest_drafts.router import GuestDraftCreate, create_guest_draft

    monkeypatch.setattr(
        intake, "assess_project_intent", AsyncMock(return_value=result("out_of_scope"))
    )
    db = AsyncMock()
    repo = SimpleNamespace(create=AsyncMock(return_value=SimpleNamespace(expires_at=None)))
    monkeypatch.setattr("src.domain.guest_drafts.router.GuestDraftRepository", lambda db: repo)
    await create_guest_draft(GuestDraftCreate(prompt="Tell my fortune"), db)
    intake.assess_project_intent.assert_not_awaited()
    db.commit.assert_awaited_once()


@pytest.mark.asyncio
async def test_rejected_codegen_does_not_cancel_existing_run(monkeypatch):
    from src.domain.codegen.service import CodegenService

    monkeypatch.setattr(intake, "assess_project_intent", AsyncMock(return_value=result("blocked")))
    svc = object.__new__(CodegenService)
    svc._project_facade = AsyncMock()
    svc._project_facade.get_project_type.return_value = "nuxt_app"
    svc._codegen_repo = AsyncMock()
    monkeypatch.setattr("src.domain.projects.discovery_repository.DiscoveryRepository", lambda db: SimpleNamespace(
        get=AsyncMock(return_value=SimpleNamespace(discovery_json={"status": "pending"}))
    ))
    with pytest.raises(ValidationError):
        await svc.create_run(project_id=uuid4(), prompt_id=uuid4(), user_prompt="unsafe request")
    svc._codegen_repo.lock_project_for_run_creation.assert_awaited_once()
    svc._codegen_repo.create.assert_not_called()


@pytest.mark.asyncio
async def test_rejected_builder_chat_does_not_supersede_or_write(monkeypatch):
    from src.domain.builder import chat_handler

    project_repo = AsyncMock()
    project_repo.get_by_id.return_value = SimpleNamespace(id=uuid4())
    codegen_repo = AsyncMock()
    monkeypatch.setattr(chat_handler, "ProjectRepository", lambda db: project_repo)
    monkeypatch.setattr(chat_handler, "CodegenRunRepository", lambda db: codegen_repo)
    monkeypatch.setattr(intake, "assess_project_intent", AsyncMock(return_value=result("blocked")))
    monkeypatch.setattr("src.domain.projects.discovery_repository.DiscoveryRepository", lambda db: SimpleNamespace(
        get=AsyncMock(return_value=SimpleNamespace(discovery_json={"status": "pending"}))
    ))
    db, svc = AsyncMock(), AsyncMock()
    with pytest.raises(ValidationError):
        await chat_handler.handle_project_chat(
            db,
            uuid4(),
            SimpleNamespace(id=uuid4()),
            chat_handler.ChatRequest(message="unsafe request"),
            codegen_service=svc,
        )
    svc.supersede_active_run_for_new_chat.assert_not_called()
    codegen_repo.create.assert_not_called()
    db.commit.assert_not_called()


@pytest.mark.asyncio
async def test_cache_budget_and_catalog_validation(monkeypatch):
    from redis.asyncio import Redis
    from src.domain.projects.intake_schemas import IntakeCandidate
    from src.integrations import project_intake_client

    settings = SimpleNamespace(
        project_intake_enabled=True,
        project_intake_max_chars=20000,
        openai_api_key="test",
        project_intake_model="test",
        redis_url="redis://unused",
        project_intake_calls_per_minute=5,
        project_intake_timeout_seconds=2,
    )
    monkeypatch.setattr(intake, "get_settings", lambda: settings)
    redis = AsyncMock()
    redis.__aenter__.return_value = redis
    redis.get.return_value = None
    redis.eval.return_value = 1
    monkeypatch.setattr(Redis, "from_url", lambda *a, **kw: redis)
    invented = result()
    invented.template_ref = "not-in-catalog"
    provider = AsyncMock(return_value=invented)
    monkeypatch.setattr(project_intake_client, "understand_project", provider)
    answer = await intake.assess_project_intent(
        "A portfolio", candidates=[IntakeCandidate(ref="portfolio", name="Portfolio")]
    )
    assert answer.template_ref is None
    redis.set.assert_awaited_once()
    redis.eval.return_value = 6
    with pytest.raises(intake.IntakeUnavailableError):
        await intake.assess_project_intent("Another brief")
    assert provider.await_count == 1
