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

import pytest
from src.core.exceptions import ValidationError
from src.domain.codegen.schemas import CodegenActionsCompleteBody
from src.domain.codegen.service import CodegenService


def setup_run(checkpoint="github_actions_dispatched", status="running", payload=None):
    run = SimpleNamespace(
        id=uuid4(),
        project_id=uuid4(),
        status=status,
        worker_checkpoint=checkpoint,
        checkpoint_payload=payload
        or {"dispatch_commit_sha": "dispatch", "continuation_attempt": 0},
    )
    repo = AsyncMock()
    repo.get_by_id.return_value = run
    return run, repo, CodegenService(repo, AsyncMock())


@pytest.mark.asyncio
async def test_codegen_finish_is_not_completion_until_deployed(monkeypatch):
    run, repo, service = setup_run()
    redis = AsyncMock()
    emit = AsyncMock()
    enqueue = AsyncMock()
    monkeypatch.setattr("src.core.pubsub.get_pubsub_redis", AsyncMock(return_value=redis))
    monkeypatch.setattr("src.core.run_events.publish_project_event_dual", emit)
    monkeypatch.setattr("src.workers.enqueue.enqueue_codegen_run", enqueue)
    result = await service.report_run_complete(
        run.id,
        CodegenActionsCompleteBody(
            status="deploying",
            commit_sha="saved",
            summary="Done",
            continuation_attempt=0,
        ),
    )
    assert result["status"] == "deploying"
    assert repo.update_status.await_args.args[1] == "preview_refreshing"
    assert repo.update_checkpoint.await_args.args[2]["dispatch_commit_sha"] == "dispatch"
    enqueue.assert_not_awaited()
    assert emit.await_args.args[2] == "codegen.status_changed"
    assert "deploying" in emit.await_args.args[3]["message"]


@pytest.mark.asyncio
async def test_deploy_success_resumes_finalization_with_resources(monkeypatch):
    run, repo, service = setup_run(
        "github_actions_deploying", "preview_refreshing", {"commit_sha": "saved"}
    )
    enqueue = AsyncMock()
    monkeypatch.setattr("src.workers.enqueue.enqueue_codegen_run", enqueue)
    result = await service.report_run_complete(
        run.id,
        CodegenActionsCompleteBody(
            status="completed",
            commit_sha="saved",
            pending_provision={"entities": []},
        ),
    )
    assert result["status"] == "accepted"
    assert repo.update_checkpoint.await_args.args[1] == "pushed"
    assert repo.update_checkpoint.await_args.args[2]["pending_provision"] == {"entities": []}
    enqueue.assert_awaited_once()


@pytest.mark.asyncio
async def test_optional_gap_outcomes_are_checkpointed_for_resume_finalize(monkeypatch):
    """An optional gap resolved by the runner's pre-push gate must survive into the
    checkpoint so run_codegen's resume_finalize can reconstruct it -- otherwise it's
    silently dropped once the runner merely reports "completed" (no required gap
    means no "blocked" status, but the optional one still needs to be seen)."""
    run, repo, service = setup_run(
        "github_actions_deploying", "preview_refreshing", {"commit_sha": "saved"}
    )
    monkeypatch.setattr("src.workers.enqueue.enqueue_codegen_run", AsyncMock())
    gap_outcome = [{"name": "expense_report", "status": "primitive_gap", "criticality": "optional"}]
    result = await service.report_run_complete(
        run.id,
        CodegenActionsCompleteBody(
            status="completed",
            commit_sha="saved",
            pending_pack_composition_outcomes=gap_outcome,
        ),
    )
    assert result["status"] == "accepted"
    checkpoint = repo.update_checkpoint.await_args.args[2]
    assert checkpoint["pending_pack_composition_outcomes"] == gap_outcome
    assert checkpoint["pending_pack_install_outcomes"] is None


@pytest.mark.asyncio
async def test_blocked_status_sets_run_blocked_not_failed_or_canceled(monkeypatch):
    """A required primitive_gap/blocked pack install (reported by the GitHub Actions
    runner's resolveCapabilitiesBeforePush before it ever pushed) is not an error --
    the generic "anything but completed" branch used to fold everything into
    failed/canceled; "blocked" needs its own status, not lumped into either."""
    run, repo, service = setup_run("github_actions_dispatched", "running")
    redis = AsyncMock()
    emit = AsyncMock()
    enqueue = AsyncMock()
    monkeypatch.setattr("src.core.pubsub.get_pubsub_redis", AsyncMock(return_value=redis))
    monkeypatch.setattr("src.core.run_events.publish_project_event_dual", emit)
    monkeypatch.setattr("src.workers.enqueue.enqueue_codegen_run", enqueue)

    result = await service.report_run_complete(
        run.id,
        CodegenActionsCompleteBody(
            status="blocked",
            summary="expense_report needs approval.quorum",
        ),
    )
    assert result == {"status": "blocked"}
    assert repo.update_status.await_args.args[1] == "blocked"
    assert repo.update_status.await_args.kwargs["error_message"] == "expense_report needs approval.quorum"
    enqueue.assert_not_awaited()  # nothing was pushed -- no re-run to enqueue
    assert emit.await_args.args[3]["status"] == "blocked"


@pytest.mark.asyncio
async def test_wrong_commit_cannot_complete_deployment():
    run, repo, service = setup_run(
        "github_actions_deploying", "preview_refreshing", {"commit_sha": "saved"}
    )
    with pytest.raises(ValidationError, match="pending commit"):
        await service.report_run_complete(
            run.id, CodegenActionsCompleteBody(status="completed", commit_sha="other")
        )
    repo.update_status.assert_not_awaited()


@pytest.mark.asyncio
async def test_failed_deploy_never_reports_completed(monkeypatch):
    emit = AsyncMock()
    monkeypatch.setattr("src.core.pubsub.get_pubsub_redis", AsyncMock(return_value=AsyncMock()))
    monkeypatch.setattr("src.core.run_events.publish_project_event_dual", emit)
    run, repo, service = setup_run(
        "github_actions_deploying", "preview_refreshing", {"commit_sha": "saved"}
    )
    result = await service.report_run_complete(
        run.id, CodegenActionsCompleteBody(status="failed", commit_sha="saved")
    )
    assert result["status"] == "failed"
    assert repo.update_status.await_args.args[1] == "failed"
    assert emit.await_args.args[2] == "codegen.failed"


@pytest.mark.asyncio
async def test_duplicate_continuation_does_not_dispatch_twice():
    run, repo, service = setup_run(payload={"continuation_attempt": 1})
    run.execution_target = "cloud"
    result = await service._continue_github_actions_run(
        run,
        CodegenActionsCompleteBody(
            status="needs_continuation",
            continuation_attempt=1,
            commit_sha="saved",
        ),
    )
    assert result["status"] == "continuing"
    repo.update_checkpoint.assert_not_awaited()
