"""Reproduce the panel's confirm-batch payload shape.

The panel's ``rebuildTheWork`` writes each task's project into
``the_work[].project_id`` and sets the top-level ``project_id`` to ``null``
(multi-cell model), whereas the existing MegaTask tests use the legacy
top-level ``project_id``. This drives ``confirm_live_batch`` with the real
panel shape to surface the live 500.
"""

from __future__ import annotations

from typing import Any, Literal, cast
from unittest.mock import patch
from uuid import uuid4

import pytest
from roboco.services.prompter import get_prompter_service
from tests.unit.services.test_prompter import (
    _FakeRedis,
    _seed_project_and_ceo,
    _seed_second_project,
)


@pytest.mark.asyncio
async def test_confirm_live_batch_panel_the_work_shape(db_session: Any) -> None:
    project1, ceo_id = await _seed_project_and_ceo(db_session)
    project2 = await _seed_second_project(db_session, ceo_id)
    service = get_prompter_service(db=db_session)

    # Panel shape: project lives in the_work[].project_id, top-level is null.
    drafts: list[dict[str, Any]] = [
        {
            "title": "A: backend work",
            "acceptance_criteria": ["a"],
            "project_id": None,
            "the_work": [
                {
                    "team": "backend",
                    "summary": "do the thing",
                    "items": ["unit 1"],
                    "project_id": str(project1),
                }
            ],
            "intends_to_touch": ["roboco/services/foo.py"],
        },
        {
            "title": "B: frontend work",
            "acceptance_criteria": ["b"],
            "project_id": None,
            "the_work": [
                {
                    "team": "frontend",
                    "summary": "ui",
                    "items": ["unit 1"],
                    "project_id": str(project2),
                }
            ],
            "intends_to_touch": ["panel/src/widget.tsx"],
        },
    ]
    with patch("roboco.services.prompter.redis.from_url", return_value=_FakeRedis()):
        result = await service.confirm_live_batch(
            "Panel shape batch",
            drafts,
            ceo_id,
            project_ids=[project1, project2],
            route="main_pm",
            session_id=f"sess-panel-{uuid4().hex[:8]}",
        )

    ids = result["root_subtask_ids"]
    assert len(ids) == len(drafts)


@pytest.mark.asyncio
async def test_confirm_live_batch_ignores_vestigial_top_level_slug(
    db_session: Any,
) -> None:
    """A batch draft whose the_work cell-map carries the real project UUID but
    that also still has the intake agent's top-level project_id SLUG (the panel
    only nulls it on a manual picker toggle) must NOT 400 the whole batch on the
    UUID parse — the cell map is authoritative. Repro for the live 400
    "Invalid project_id UUID: roboco-api"."""
    project1, ceo_id = await _seed_project_and_ceo(db_session)
    project2 = await _seed_second_project(db_session, ceo_id)
    service = get_prompter_service(db=db_session)

    drafts: list[dict[str, Any]] = [
        {
            "title": "A: backend work",
            "acceptance_criteria": ["a"],
            "project_id": "roboco-api",  # agent's slug, panel left it in place
            "the_work": [
                {
                    "team": "backend",
                    "summary": "s",
                    "items": ["u"],
                    "project_id": str(project1),
                },
            ],
        },
        {
            "title": "B: frontend work",
            "acceptance_criteria": ["b"],
            "project_id": "roboco-panel",  # slug on the other draft too
            "the_work": [
                {
                    "team": "frontend",
                    "summary": "s",
                    "items": ["u"],
                    "project_id": str(project2),
                },
            ],
        },
    ]
    with patch("roboco.services.prompter.redis.from_url", return_value=_FakeRedis()):
        result = await service.confirm_live_batch(
            "Slug batch",
            drafts,
            ceo_id,
            project_ids=[project1, project2],
            route="main_pm",
            session_id=f"sess-slug-{uuid4().hex[:8]}",
        )
    assert len(result["root_subtask_ids"]) == len(drafts)


@pytest.mark.asyncio
async def test_confirm_live_batch_panel_multicell_root_subtask(db_session: Any) -> None:
    """A batch draft that targets TWO cells (backend+frontend, one repo each) →
    the cell_projects persistence path on a batch root-subtask."""
    project1, ceo_id = await _seed_project_and_ceo(db_session)
    project2 = await _seed_second_project(db_session, ceo_id)
    service = get_prompter_service(db=db_session)

    drafts: list[dict[str, Any]] = [
        {
            "title": "A: cross-cell task",
            "acceptance_criteria": ["a"],
            "project_id": None,
            "the_work": [
                {
                    "team": "backend",
                    "summary": "api",
                    "items": ["u1"],
                    "project_id": str(project1),
                },
                {
                    "team": "frontend",
                    "summary": "ui",
                    "items": ["u2"],
                    "project_id": str(project2),
                },
            ],
        },
        {
            "title": "B: backend only",
            "acceptance_criteria": ["b"],
            "project_id": None,
            "the_work": [
                {
                    "team": "backend",
                    "summary": "more api",
                    "items": ["u1"],
                    "project_id": str(project1),
                },
            ],
        },
    ]
    with patch("roboco.services.prompter.redis.from_url", return_value=_FakeRedis()):
        result = await service.confirm_live_batch(
            "Cross-cell batch",
            drafts,
            ceo_id,
            project_ids=[project1, project2],
            route="main_pm",
            session_id=f"sess-mc-{uuid4().hex[:8]}",
        )
    assert len(result["root_subtask_ids"]) == len(drafts)


@pytest.mark.asyncio
@pytest.mark.parametrize("route", ["main_pm", "board"])
async def test_confirm_live_batch_dense_same_repo_collisions(
    db_session: Any, route: str
) -> None:
    """The real panel-MegaTask shape: MANY tasks in the SAME repo with heavy
    file overlap → dense sequencing edges. Repro for the live confirm-batch 500."""
    project1, ceo_id = await _seed_project_and_ceo(db_session)
    project2 = await _seed_second_project(db_session, ceo_id)
    service = get_prompter_service(db=db_session)

    shared = "panel/src/app/layout.tsx"
    drafts: list[dict[str, Any]] = []
    # 6 backend/panel tasks in project1, overlapping on a shared file + migrations.
    for i in range(6):
        drafts.append(
            {
                "title": f"Panel task {i}",
                "acceptance_criteria": [f"c{i}"],
                "project_id": None,
                "the_work": [
                    {
                        "team": "frontend",
                        "summary": f"s{i}",
                        "items": ["u"],
                        "project_id": str(project1),
                    },
                ],
                "intends_to_touch": [shared, f"panel/src/f{i}.tsx"],
                "adds_migration": i % 2 == 0,
                "touches_shared": True,
            }
        )
    # 1 task in project2 so the batch spans ≥2 repos.
    drafts.append(
        {
            "title": "Other repo",
            "acceptance_criteria": ["c"],
            "project_id": None,
            "the_work": [
                {
                    "team": "frontend",
                    "summary": "s",
                    "items": ["u"],
                    "project_id": str(project2),
                },
            ],
            "intends_to_touch": ["src/other.ts"],
        }
    )
    with patch("roboco.services.prompter.redis.from_url", return_value=_FakeRedis()):
        result = await service.confirm_live_batch(
            "Panel MegaTask",
            drafts,
            ceo_id,
            project_ids=[project1, project2],
            route=cast("Literal['board', 'main_pm']", route),
            session_id=f"sess-dense-{route}-{uuid4().hex[:8]}",
        )
    assert len(result["root_subtask_ids"]) == len(drafts)


class _DeleteFakeRedis(_FakeRedis):
    """_FakeRedis + delete tracking, for the guard-release-on-failure test."""

    def __init__(self) -> None:
        super().__init__()
        self.deleted: list[str] = []

    async def delete(self, name: str) -> int:
        self.deleted.append(name)
        return int(self._store.pop(name, None) is not None)


@pytest.mark.asyncio
async def test_confirm_live_batch_releases_guard_on_build_failure(
    db_session: Any,
) -> None:
    """A build failure must DELETE the idempotency guard so a retry re-attempts,
    instead of the 1h key wedging every retry into a 500 ('already in progress').
    """
    project1, ceo_id = await _seed_project_and_ceo(db_session)
    project2 = await _seed_second_project(db_session, ceo_id)
    service = get_prompter_service(db=db_session)
    drafts: list[dict[str, Any]] = [
        {
            "title": "A",
            "acceptance_criteria": ["a"],
            "team": "backend",
            "project_id": str(project1),
        },
        {
            "title": "B",
            "acceptance_criteria": ["b"],
            "team": "frontend",
            "project_id": str(project2),
        },
    ]
    fake = _DeleteFakeRedis()
    session_id = f"sess-fail-{uuid4().hex[:8]}"

    async def _boom(*_a: Any, **_kw: Any) -> Any:
        raise RuntimeError("build blew up")

    with patch("roboco.services.prompter.redis.from_url", return_value=fake):
        with (
            patch.object(service, "_build_confirm_batch", _boom),
            pytest.raises(RuntimeError, match="build blew up"),
        ):
            await service.confirm_live_batch(
                "Batch",
                drafts,
                ceo_id,
                project_ids=[project1, project2],
                route="main_pm",
                session_id=session_id,
            )
        # The guard was released, so the key no longer holds the session.
        assert f"roboco:megatask_confirm:{session_id}" in fake.deleted
        # A retry can now re-acquire the guard (not a permanent 'in progress').
        assert await service._acquire_confirm_guard(session_id) is None
