"""Scenario: the video-generation pipeline, render loop through CEO approval.

Regression coverage for the video engine's cross-layer wiring (Phase H): a
completed ``source=video`` authoring task is rendered by the orchestrator's
render loop into a held ``source=video_post`` draft, which the CEO approves
through ``VideoPostService``. Exercises the REAL dispatcher skip-predicates
(``video_post`` must never reach a dev/PM dispatcher — the authoring source
itself is the contrast case, since it dispatches normally), the REAL
``propose_video`` do-tool via the REAL do_server registry (mirrors
``test_feature_spotlight.py``'s guard against a verb wired at
role_config/content_actions but dropped from ``do_server._TOOLS``), the REAL
render -> materialize chain (only the video-renderer sidecar client + the
workspace read-clone are mocked — the external-I/O boundary), and the REAL
``VideoPostService.approve`` (only the X-v2 + TikTok posters are mocked)
including its already-posted idempotency.
"""

from __future__ import annotations

from types import SimpleNamespace
from typing import TYPE_CHECKING, Any
from unittest.mock import AsyncMock, patch

from roboco.runtime.orchestrator import _is_held_ceo_source, _is_non_dev_dispatch_source
from roboco.services.heartbeat_mutex import HeartbeatMutex
from roboco.services.video_post_service import (
    TikTokPoster,
    TikTokUploadResult,
    XVideoPoster,
    XVideoPostResult,
)
from tests.e2e_smoke.arcs import seed_company, seed_project, seed_task
from tests.e2e_smoke.harness import ScriptedAgent, expect_error

if TYPE_CHECKING:
    from uuid import UUID

    from sqlalchemy.ext.asyncio import AsyncSession
    from tests.e2e_smoke.arcs import Company
    from tests.e2e_smoke.harness import E2EStack


def _seed_video_agents(stack: E2EStack) -> None:
    """Seed ``system`` / ``secretary-1`` / ``ux-dev-1`` / ``ux-dev-2`` at their
    FIXED foundation UUIDs — the video engine writes ``created_by`` /
    ``assigned_to`` straight from the static identity registry (not a
    role-keyed DB lookup), so those exact ids must exist as real agent rows
    for the FK to resolve. Idempotent (safe if ever called more than once
    against the same stack), mirroring
    ``test_feature_spotlight._seed_system_and_secretary``.
    """
    from roboco.db.tables import AgentTable
    from roboco.foundation import identity as _foundation
    from roboco.models import AgentRole, AgentStatus, Team

    async def _run(session: AsyncSession) -> None:
        for agent_uuid, slug, role, team in (
            (_foundation.AGENTS["system"].uuid, "system", AgentRole.SYSTEM, None),
            (
                _foundation.AGENTS["secretary-1"].uuid,
                "secretary-1",
                AgentRole.SECRETARY,
                None,
            ),
            (
                _foundation.AGENTS["ux-dev-1"].uuid,
                "ux-dev-1",
                AgentRole.DEVELOPER,
                Team.UX_UI,
            ),
            (
                _foundation.AGENTS["ux-dev-2"].uuid,
                "ux-dev-2",
                AgentRole.DEVELOPER,
                Team.UX_UI,
            ),
        ):
            if await session.get(AgentTable, agent_uuid) is not None:
                continue
            session.add(
                AgentTable(
                    id=agent_uuid,
                    name=slug,
                    slug=slug,
                    role=role,
                    team=team,
                    status=AgentStatus.ACTIVE,
                    model_config={},
                    system_prompt=slug,
                    capabilities=[],
                    permissions={},
                    metrics={},
                )
            )

    stack.run_db(_run)


def _seed_completed_authoring_task(stack: E2EStack, project_id: Any) -> UUID:
    """A completed ``source=video`` authoring task carrying a proposed
    composition — the render loop's scan basis. Mirrors the shape a real
    ``VideoEngine.open_video_task`` + ``propose_video`` call would leave
    behind, seeded directly (the harness's own convention for mid-flight
    setup — see ``arcs.seed_hierarchy``); the render/approve/gate wiring
    under test doesn't depend on how the authoring task got here.
    """
    from roboco.foundation import identity as _foundation
    from roboco.foundation.policy.content import markers as _markers
    from roboco.models import Team
    from roboco.models.base import Complexity, TaskNature, TaskStatus, TaskType
    from roboco.services.task import VIDEO_SOURCE

    draft: dict[str, Any] = {
        "occasion": "e2e pipeline test",
        "script": "Here's what shipped",
        "brief": "Announce the e2e video pipeline",
        "composition_id": "Intro",
        "input_props": {"title": "hello"},
        "x_caption": "Check out our new release!",
        "tiktok_caption": "New release, check it out",
        "platforms": ["x", "tiktok"],
    }
    task_id: UUID = seed_task(
        stack,
        title="Video: e2e pipeline test",
        description="Announce the e2e video pipeline",
        acceptance_criteria=["Both 9:16 and 1:1 cuts render"],
        task_type=TaskType.CODE,
        nature=TaskNature.TECHNICAL,
        estimated_complexity=Complexity.LOW,
        team=Team.UX_UI,
        project_id=project_id,
        created_by=_foundation.AGENTS["system"].uuid,
        assigned_to=_foundation.AGENTS["ux-dev-1"].uuid,
        status=TaskStatus.COMPLETED,
        source=VIDEO_SOURCE,
        confirmed_by_human=True,
        orchestration_markers={_markers.VIDEO_DRAFT: draft},
    )
    return task_id


class _FakeRenderer:
    """Stands in for the video-renderer sidecar: returns a deterministic
    path per orientation, no tar/HTTP anywhere."""

    async def render(
        self,
        *,
        source_dir: str,
        composition_id: str,
        input_props: dict[str, Any],
        orientation: str,
        render_key: str,
    ) -> str:
        _ = (source_dir, input_props)
        return f"/fake-out/{render_key}-{composition_id}-{orientation}.mp4"


def _render_completed_task(stack: E2EStack, task_id: UUID) -> None:
    """Drive the REAL orchestrator render step against the completed
    authoring task — only the sidecar client + workspace read-clone are
    mocked (the render step's external-I/O boundary)."""
    from pathlib import Path as _Path

    from roboco.db.tables import TaskTable
    from roboco.runtime.orchestrator import AgentOrchestrator
    from sqlalchemy import select

    workspace = SimpleNamespace(
        ensure_read_clone=AsyncMock(return_value=_Path("/fake-clone"))
    )
    orch = AgentOrchestrator.__new__(AgentOrchestrator)

    async def _run(session: AsyncSession) -> None:
        row = (
            await session.execute(select(TaskTable).where(TaskTable.id == task_id))
        ).scalar_one()
        with (
            patch(
                "roboco.services.video_renderer_client.get_video_renderer",
                _FakeRenderer,
            ),
            patch(
                "roboco.services.workspace.get_workspace_service",
                lambda _db: workspace,
            ),
        ):
            await orch._render_video_task(session, row)

    stack.run_db(_run)


def _task_dict(stack: E2EStack, task_id: UUID) -> dict[str, Any]:
    """The (source, confirmed_by_human) shape a dispatcher reads off a task
    — real committed values, not a hand-crafted stand-in."""
    from roboco.db.tables import TaskTable
    from sqlalchemy import select

    async def _run(session: AsyncSession) -> dict[str, Any]:
        row = (
            await session.execute(select(TaskTable).where(TaskTable.id == task_id))
        ).scalar_one()
        return {
            "source": row.source,
            "confirmed_by_human": row.confirmed_by_human,
            "status": str(row.status),
        }

    result: dict[str, Any] = stack.run_db(_run)
    return result


def _find_video_post_draft(stack: E2EStack, source_task_id: UUID) -> dict[str, Any]:
    """The held video_post draft the render step materialized for
    ``source_task_id`` — located via the marker's own back-reference
    (``_originate_video_post`` stamps ``source_task_id``), robust against any
    other video_post rows in this session-scoped shared test DB."""
    from roboco.foundation.policy.content import markers as _markers
    from roboco.services.task import get_task_service

    async def _run(session: AsyncSession) -> dict[str, Any]:
        drafts = await get_task_service(session).list_open_video_post_drafts()
        match = next(
            t
            for t in drafts
            if (_markers.get_video_draft(t) or {}).get("source_task_id")
            == str(source_task_id)
        )
        draft = _markers.get_video_draft(match) or {}
        return {
            "id": match.id,
            "source": match.source,
            "confirmed_by_human": match.confirmed_by_human,
            "status": str(match.status),
            "mp4_paths": dict(draft.get("mp4_paths") or {}),
        }

    result: dict[str, Any] = stack.run_db(_run)
    return result


class _FakeXPoster(XVideoPoster):
    @property
    def configured(self) -> bool:
        return True

    async def post_video(self, *, mp4_path: str, caption: str) -> XVideoPostResult:
        _ = (mp4_path, caption)
        return XVideoPostResult(posted=True, video_id="e2e-x-vid", detail="posted")


class _FakeTikTokPoster(TikTokPoster):
    @property
    def configured(self) -> bool:
        return True

    async def upload_to_inbox(
        self, *, mp4_path: str, caption: str
    ) -> TikTokUploadResult:
        _ = (mp4_path, caption)
        return TikTokUploadResult(
            uploaded=True, publish_id="e2e-tt-pub", detail="uploaded"
        )


# No real Redis in this harness (and the root conftest's autouse fixture
# points settings.redis_url at an unreachable port for every test regardless)
# — mocked the same way tests/integration/test_video_routes.py does.
_LOCKED = (
    patch.object(HeartbeatMutex, "acquire", AsyncMock(return_value="e2e-lock-token")),
    patch.object(HeartbeatMutex, "release", AsyncMock(return_value=None)),
)


def _approve(stack: E2EStack, draft_id: UUID) -> dict[str, Any]:
    from roboco.services.video_post_service import get_video_post_service

    async def _run(session: AsyncSession) -> dict[str, Any]:
        svc = get_video_post_service(
            session, x_poster=_FakeXPoster(), tiktok_poster=_FakeTikTokPoster()
        )
        result = await svc.approve(draft_id)
        assert result is not None
        return {"status": result.status, "posted": dict(result.posted)}

    with _LOCKED[0], _LOCKED[1]:
        outcome: dict[str, Any] = stack.run_db(_run)
    return outcome


def test_video_pipeline_render_and_approve(e2e_stack: E2EStack) -> None:
    stack = e2e_stack
    company: Company = seed_company(stack)
    _seed_video_agents(stack)
    project_id, _project_slug = seed_project(stack, company)

    task_id = _seed_completed_authoring_task(stack, project_id)

    # The exact bug class test_feature_spotlight.py guards against: a verb
    # granted (role_config) + implemented (ContentActions) + routed
    # (api/v1/do) but missing from do_server's _TOOLS/_REGISTERED_TOOLS is
    # silently uncallable over MCP no matter what the gateway layers say.
    dev = ScriptedAgent(stack, company.dev_id, "be-dev-1", "developer")
    do_module = dev._module("roboco.mcp.do_server")
    assert "propose_video" in do_module._TOOLS, (
        "propose_video missing from do_server._TOOLS — no role could ever "
        "call it over MCP"
    )
    assert "propose_video" in do_module._REGISTERED_TOOLS, (
        "propose_video is granted to developer in role_config but absent "
        "from this agent's _register_tools() output"
    )

    # propose_video is granted to every developer role (be/fe/ux-dev share
    # Role.DEVELOPER) — the runtime TEAM gate is the real enforcement, so a
    # be-dev's call must be rejected even though the tool is on their manifest.
    env = dev.do(
        "propose_video",
        composition_id="Intro",
        x_caption="Check it out",
        tiktok_caption="Check it out on TikTok",
        platforms=["x"],
    )
    expect_error(env, "not_authorized", "be-dev propose_video team gate")

    # Contrast case: the authoring task's own source is normal delivery work
    # (confirmed_by_human=True) — neither dispatcher treats it as held.
    before = _task_dict(stack, task_id)
    assert before["status"] == "completed", before
    assert _is_non_dev_dispatch_source(before) is False, before
    assert _is_held_ceo_source(before) is False, before

    # The render loop: only the sidecar client + workspace read-clone mocked.
    _render_completed_task(stack, task_id)

    draft = _find_video_post_draft(stack, task_id)
    assert draft["source"] == "video_post", draft
    assert draft["confirmed_by_human"] is False, draft
    assert draft["status"] == "pending", draft  # held, awaiting the CEO
    assert set(draft["mp4_paths"]) == {"vertical", "square"}, draft

    # The key wiring: video_post is skipped by BOTH dispatchers.
    held_shape = {
        "source": draft["source"],
        "confirmed_by_human": draft["confirmed_by_human"],
    }
    assert _is_non_dev_dispatch_source(held_shape) is True, held_shape
    assert _is_held_ceo_source(held_shape) is True, held_shape

    # VideoPostService.approve posts via mocked X-v2 + TikTok posters, then is
    # idempotent on a second call (no re-post, same ids returned).
    first = _approve(stack, draft["id"])
    assert first["status"] == "posted", first
    assert first["posted"] == {"x": "e2e-x-vid", "tiktok": "e2e-tt-pub"}, first

    second = _approve(stack, draft["id"])
    assert second["status"] == "already_posted", second
    assert second["posted"] == first["posted"], second
