"""Core's side of the work branch: what report_run_complete records from the runner, and how
retry_run resumes from the run's own branch instead of regenerating the frontend."""

from __future__ import annotations

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

import pytest
from src.config import get_settings
from src.domain.codegen.schemas import CodegenActionsCompleteBody
from src.domain.codegen.service import CodegenService
from src.domain.codegen.work_branch import apply_report, new_state

BASE = "a" * 40
CKPT = "b" * 40
CKPT2 = "c" * 40


def settings(enabled=True, corrections=2):
    return get_settings().model_copy(
        update={"codegen_work_branch_enabled": enabled,
                "codegen_manifest_correction_attempts": corrections}
    )


def make_service(run):
    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)),
        update_status=AsyncMock(),
        update_checkpoint=AsyncMock(),
        commit=AsyncMock(),
        refresh_started_at=AsyncMock(),
    )
    facade = SimpleNamespace(
        get_project_type=AsyncMock(return_value="html"),
        get_project_instruction_content=AsyncMock(return_value=""),
    )
    service = CodegenService(codegen_repo=repo, project_facade=facade)
    service._platform_backend_for_prompt = AsyncMock(return_value={})
    service._form_policy_for_prompt = AsyncMock(return_value={})
    return service, repo


def failed_run(state, status="failed", **ctx):
    run_id = uuid4()
    state = {**state, "branch": f"p2hs/codegen/{run_id}"} if state else None
    context = {"run_mode": "agent", "project_type": "html", **ctx}
    if state:
        context["work_branch"] = state
    run = SimpleNamespace(
        id=run_id, project_id=uuid4(), status=status, selected_elements=None,
        plan={"system_prompt": "x", "user_prompt": "u", "context": context},
        worker_checkpoint=None, checkpoint_payload=None, execution_target="cloud",
    )
    return run


def state_after_failure(failure_class="internal_error", checkpoint="agent_output", **extra):
    return apply_report(
        new_state(uuid4(), "preview", BASE), status="failed", checkpoint=checkpoint,
        checkpoint_sha=CKPT, failure_class=failure_class, retryable=True, **extra,
    )


async def retry(run, **overrides):
    service, repo = make_service(run)
    with patch("src.config.get_settings", return_value=settings(**overrides)):
        await service.retry_run(run.id, run.project_id)
    return repo.requeue_with_plan.await_args.args[1]["context"]


# ---- retry resumes from the branch ----------------------------------------------------------


@pytest.mark.asyncio
async def test_a_retry_after_a_platform_failure_skips_the_agent_and_keeps_the_branch():
    run = failed_run(state_after_failure("internal_error"))
    branch = run.plan["context"]["work_branch"]["branch"]
    ctx = await retry(run)
    assert ctx["resume"] == {"skip_agent": True, "last_checkpoint": "agent_output"}
    state = ctx["work_branch"]
    assert state["branch"] == branch and state["base_sha"] == BASE  # never recreated
    assert state["last_checkpoint_sha"] == CKPT  # the checkpoint survives the retry
    assert state["status"] == "active" and state["failure_class"] is None
    assert "correction" not in ctx


@pytest.mark.asyncio
async def test_a_retry_after_an_invalid_manifest_sends_the_agent_back_with_the_errors():
    correction = {"entries": [{"name": "contact_submission", "errors": [
        {"path": "fields.0.public", "code": "unknown_key", "message": "not recognized"}]}]}
    run = failed_run(state_after_failure("invalid_requirement", correction=correction))
    ctx = await retry(run)
    assert ctx["resume"]["skip_agent"] is False
    assert ctx["correction"]["attempt"] == 1
    assert ctx["correction"]["entries"][0]["name"] == "contact_submission"
    assert ctx["work_branch"]["correction_attempts"] == 1


@pytest.mark.asyncio
async def test_corrections_stop_at_the_configured_limit():
    correction = {"entries": [{"name": "x", "errors": [{"path": "p", "code": "c", "message": "m"}]}]}
    state = {**state_after_failure("invalid_requirement", correction=correction), "correction_attempts": 2}
    ctx = await retry(failed_run(state))
    assert "correction" not in ctx and "resume" not in ctx  # a plain run on the branch


@pytest.mark.asyncio
async def test_stale_resume_and_correction_from_an_earlier_attempt_are_cleared():
    run = failed_run(None, resume={"skip_agent": True}, correction={"attempt": 1, "entries": []})
    ctx = await retry(run)
    assert "resume" not in ctx and "correction" not in ctx


@pytest.mark.asyncio
async def test_with_the_setting_off_the_work_branch_state_is_left_alone():
    run = failed_run(state_after_failure("internal_error"))
    before = dict(run.plan["context"]["work_branch"])
    ctx = await retry(run, enabled=False)
    assert "resume" not in ctx
    assert ctx["work_branch"] == before


@pytest.mark.asyncio
async def test_a_state_that_belongs_to_another_run_is_ignored():
    run = failed_run(state_after_failure("internal_error"))
    run.plan["context"]["work_branch"]["branch"] = f"p2hs/codegen/{uuid4()}"
    ctx = await retry(run)
    assert "resume" not in ctx


# ---- reports are recorded ---------------------------------------------------------------------


async def report(run, monkeypatch, **fields):
    service, repo = make_service(run)
    service._continue_github_actions_run = AsyncMock(return_value={"status": "continuing"})
    monkeypatch.setattr("src.core.pubsub.get_pubsub_redis", AsyncMock(return_value=AsyncMock()))
    monkeypatch.setattr("src.core.run_events.publish_project_event_dual", AsyncMock())
    body = CodegenActionsCompleteBody(**fields)
    await service.report_run_complete(run.id, body)
    return run.plan["context"]["work_branch"], repo


def active_run(**overrides):
    run = failed_run({**new_state(uuid4(), "preview", BASE)}, status="running")
    run.plan["context"]["work_branch"].update(overrides)
    return run


@pytest.mark.asyncio
async def test_a_failure_after_the_checkpoint_is_recorded_with_its_class_and_retryability(monkeypatch):
    run = active_run()
    state, _ = await report(
        run, monkeypatch, status="failed", error_message="P2HS hit an internal error (AttributeError)",
        checkpoint="agent_output", checkpoint_sha=CKPT, failure_class="internal_error", retryable=True,
        work_branch=run.plan["context"]["work_branch"]["branch"],
    )
    assert state["status"] == "failed" and state["retryable"] is True
    assert state["failure_class"] == "internal_error"
    assert state["last_checkpoint"] == "agent_output" and state["last_checkpoint_sha"] == CKPT
    assert "AttributeError" in state["failure_message"]


@pytest.mark.asyncio
async def test_a_report_naming_another_branch_is_ignored(monkeypatch):
    run = active_run()
    state, _ = await report(
        run, monkeypatch, status="failed", checkpoint="agent_output", checkpoint_sha=CKPT,
        work_branch=f"p2hs/codegen/{uuid4()}",
    )
    assert state["last_checkpoint"] is None and state["status"] == "active"


@pytest.mark.asyncio
async def test_a_late_or_repeated_report_cannot_rewind_the_checkpoint(monkeypatch):
    run = active_run(last_checkpoint="capabilities_provisioned", last_checkpoint_sha=CKPT2)
    state, _ = await report(
        run, monkeypatch, status="failed", checkpoint="agent_output", checkpoint_sha=CKPT,
        failure_class="internal_error", retryable=True,
    )
    assert state["last_checkpoint"] == "capabilities_provisioned"
    assert state["last_checkpoint_sha"] == CKPT2


@pytest.mark.asyncio
async def test_a_gap_that_blocks_is_recorded_as_retryable_after_configuration(monkeypatch):
    run = active_run()
    state, _ = await report(
        run, monkeypatch, status="blocked", summary="needs a connector",
        checkpoint="agent_output", checkpoint_sha=CKPT, failure_class="capability_gap", retryable=True,
    )
    assert state["status"] == "blocked" and state["failure_class"] == "capability_gap"
    assert state["retryable"] is True


@pytest.mark.asyncio
async def test_promotion_marks_the_branch_promoted_and_clears_failure(monkeypatch):
    run = active_run(status="failed", failure_class="internal_error", retryable=True,
                     failure_message="old")
    state, _ = await report(
        run, monkeypatch, status="deploying", commit_sha=CKPT2, checkpoint="promoted",
        checkpoint_sha=CKPT2,
    )
    assert state["last_checkpoint"] == "promoted"
    assert state["failure_class"] is None and state["retryable"] is False
    assert state["failure_message"] is None


@pytest.mark.asyncio
async def test_a_time_budget_restart_keeps_the_run_active_at_its_checkpoint(monkeypatch):
    run = active_run()
    state, _ = await report(
        run, monkeypatch, status="needs_continuation", commit_sha=CKPT, checkpoint="agent_output",
        checkpoint_sha=CKPT, continuation_attempt=1,
    )
    assert state["status"] == "active" and state["last_checkpoint_sha"] == CKPT


def test_an_unknown_checkpoint_name_is_rejected_by_the_schema():
    with pytest.raises(Exception):
        CodegenActionsCompleteBody(status="failed", checkpoint="made_up")
