"""CodegenService.retry_run rebuilds platform_backend fresh instead of replaying the
plan stored at the original queue time -- capabilities/routes can change between a
failed attempt and its retry."""
from __future__ import annotations

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

import pytest

from src.core.exceptions import NotFoundError, ValidationError
from src.domain.codegen.service import CodegenService


def _fake_run(*, status="failed", plan=None):
    return SimpleNamespace(
        id=uuid4(),
        project_id=None,  # set by caller
        status=status,
        plan=plan,
        selected_elements=None,
    )


def _service(run):
    codegen_repo = SimpleNamespace(
        get_by_id=AsyncMock(return_value=run),
        reset_run_from_failure=AsyncMock(return_value=run),
        requeue_with_plan=AsyncMock(
            side_effect=lambda run_id, plan: SimpleNamespace(id=run_id, plan=plan)
        ),
    )
    project_facade = SimpleNamespace(
        get_project_type=AsyncMock(return_value="html"),
        get_project_instruction_content=AsyncMock(return_value=""),
    )
    service = CodegenService(codegen_repo=codegen_repo, project_facade=project_facade)
    # Private helpers hit the DB/manifest/connector chain directly -- not this test's
    # concern. Stub them so we can assert *whether* retry_run calls them again, and
    # with what the rebuilt system_prompt actually reflects.
    service._platform_backend_for_prompt = AsyncMock(
        return_value={"runtime_contract": {"routes": {"admin_panel_path": "/be-admin"}}}
    )
    service._form_policy_for_prompt = AsyncMock(return_value={})
    return service, codegen_repo


@pytest.mark.asyncio
async def test_retry_run_rebuilds_platform_backend_instead_of_replaying_stale_plan():
    project_id = uuid4()
    stale_plan = {
        "system_prompt": "STALE PROMPT built before the admin path was ever named",
        "user_prompt": "wrapped prompt",
        "original_user_prompt": "build me a contact form",
        "context": {"run_mode": "agent", "project_type": "html"},
    }
    run = _fake_run(status="failed", plan=stale_plan)
    run.project_id = project_id
    service, codegen_repo = _service(run)

    await service.retry_run(run.id, project_id)

    service._platform_backend_for_prompt.assert_awaited_once_with(project_id)
    codegen_repo.reset_run_from_failure.assert_awaited_once_with(run.id)
    codegen_repo.requeue_with_plan.assert_awaited_once()
    _, kwargs_or_plan = codegen_repo.requeue_with_plan.call_args.args
    assert "STALE PROMPT" not in kwargs_or_plan["system_prompt"]


@pytest.mark.asyncio
async def test_retry_run_accepts_a_blocked_run():
    """A required-capability gap (resolveCapabilitiesBeforePush) never pushes a commit,
    so retry is the only way back -- and the platform_backend rebuild is exactly what
    re-resolves the gap once the owner configures the missing capability."""
    project_id = uuid4()
    run = _fake_run(status="blocked", plan={"system_prompt": "x", "context": {"run_mode": "agent"}})
    run.project_id = project_id
    service, codegen_repo = _service(run)

    await service.retry_run(run.id, project_id)

    codegen_repo.reset_run_from_failure.assert_awaited_once_with(run.id)
    codegen_repo.requeue_with_plan.assert_awaited_once()


@pytest.mark.asyncio
async def test_retry_run_rejects_a_run_that_is_still_in_progress():
    project_id = uuid4()
    run = _fake_run(status="running", plan={"system_prompt": "x", "context": {}})
    run.project_id = project_id
    service, _ = _service(run)

    with pytest.raises(ValidationError):
        await service.retry_run(run.id, project_id)


@pytest.mark.asyncio
async def test_retry_run_rejects_a_run_belonging_to_another_project():
    run = _fake_run(status="failed", plan={"system_prompt": "x", "context": {}})
    run.project_id = uuid4()
    service, _ = _service(run)

    with pytest.raises(NotFoundError):
        await service.retry_run(run.id, uuid4())


@pytest.mark.asyncio
async def test_retry_run_reads_the_run_under_a_row_lock_and_a_second_retry_is_refused():
    # Two simultaneous retries of one run must not both pass the status check and dispatch twice
    # onto the same work branch: the read is locked, so the second sees the run already queued.
    project_id = uuid4()
    run = _fake_run(status="failed", plan={"system_prompt": "s", "user_prompt": "u",
                                           "context": {"run_mode": "agent", "project_type": "html"}})
    run.project_id = project_id
    service, codegen_repo = _service(run)
    await service.retry_run(run.id, project_id)
    assert codegen_repo.get_by_id.await_args.kwargs.get("for_update") is True
    run.status = "queued"  # what the first retry left behind
    with pytest.raises(ValidationError):
        await service.retry_run(run.id, project_id)


def test_the_locking_read_really_is_select_for_update():
    from sqlalchemy.dialects import postgresql
    from src.domain.codegen.repository import CodegenRunRepository

    captured = []

    class Db:
        async def execute(self, statement):
            captured.append(str(statement.compile(dialect=postgresql.dialect())))
            return SimpleNamespace(scalar_one_or_none=lambda: None)

    import asyncio

    repo = CodegenRunRepository(Db())
    asyncio.run(repo.get_by_id(uuid4()))
    asyncio.run(repo.get_by_id(uuid4(), for_update=True))
    assert "FOR UPDATE" not in captured[0]
    assert "FOR UPDATE" in captured[1]
