"""Tests for ContentActions.pr_update — gateway verb behavior matrix.

Covers: missing pr_number, all-None fields, non-assignee non-PM rejection,
per-field forwarding, and GitError surfacing.
"""

from __future__ import annotations

from unittest.mock import AsyncMock, MagicMock
from uuid import UUID, uuid4

import pytest
from roboco.exceptions import GitError
from roboco.services.gateway.content_actions import ContentActions, ContentActionsDeps


def _make_deps(**overrides: AsyncMock) -> ContentActionsDeps:
    """Wire ContentActionsDeps with mocks; honour caller overrides."""
    task = overrides.get("task", AsyncMock())
    git = overrides.get("git", AsyncMock())
    return ContentActionsDeps(
        task=task,
        git=git,
        messaging=overrides.get("messaging", AsyncMock()),
        a2a=overrides.get("a2a", AsyncMock()),
        journal=overrides.get("journal", AsyncMock()),
        workspace=overrides.get("workspace", AsyncMock()),
        notifications=overrides.get("notifications", AsyncMock()),
        notification_delivery=overrides.get("notification_delivery", AsyncMock()),
    )


def _task(
    *,
    pr_number: int | None = 7,
    assigned_to: UUID,
    team: str = "backend",
) -> MagicMock:
    return MagicMock(
        id=uuid4(),
        status="in_progress",
        pr_number=pr_number,
        pr_url=(
            f"https://github.com/acme/repo/pull/{pr_number}"
            if pr_number is not None
            else None
        ),
        assigned_to=assigned_to,
        team=team,
    )


def _agent(role: str, team: str | None = None) -> MagicMock:
    return MagicMock(id=uuid4(), role=role, team=team)


@pytest.mark.asyncio
async def test_pr_update_missing_pr_number_returns_invalid_state() -> None:
    """task.pr_number is None → invalid_state with remediate 'call open_pr'."""
    agent_id = uuid4()
    task = _task(pr_number=None, assigned_to=agent_id)
    task_svc = AsyncMock()
    task_svc.get.return_value = task
    task_svc.agent_for.return_value = _agent("developer", "backend")

    deps = _make_deps(task=task_svc)
    ca = ContentActions(deps)

    env = await ca.pr_update(
        agent_id=agent_id, task_id=task.id, title="new", body=None, reviewers=None
    )
    body = env.as_dict()

    assert body["error"] == "invalid_state"
    assert "open_pr" in body["remediate"]
    deps.git.update_pr_for_task.assert_not_awaited()


@pytest.mark.asyncio
async def test_pr_update_all_fields_none_returns_invalid_state() -> None:
    """All of title/body/reviewers None → invalid_state."""
    agent_id = uuid4()
    task = _task(assigned_to=agent_id)
    task_svc = AsyncMock()
    task_svc.get.return_value = task
    task_svc.agent_for.return_value = _agent("developer", "backend")

    deps = _make_deps(task=task_svc)
    ca = ContentActions(deps)

    env = await ca.pr_update(
        agent_id=agent_id, task_id=task.id, title=None, body=None, reviewers=None
    )
    body = env.as_dict()

    assert body["error"] == "invalid_state"
    assert "at least one" in body["remediate"]
    deps.git.update_pr_for_task.assert_not_awaited()


@pytest.mark.asyncio
async def test_pr_update_non_assignee_non_pm_returns_not_authorized() -> None:
    """A developer who is neither the assignee nor a PM → not_authorized."""
    assignee_id = uuid4()
    caller_id = uuid4()
    task = _task(assigned_to=assignee_id)
    task_svc = AsyncMock()
    task_svc.get.return_value = task
    task_svc.agent_for.return_value = _agent("developer", "backend")

    deps = _make_deps(task=task_svc)
    ca = ContentActions(deps)

    env = await ca.pr_update(
        agent_id=caller_id,
        task_id=task.id,
        title="new title",
        body=None,
        reviewers=None,
    )
    body = env.as_dict()

    assert body["error"] == "not_authorized"
    deps.git.update_pr_for_task.assert_not_awaited()


@pytest.mark.asyncio
async def test_pr_update_assignee_title_only_forwarded() -> None:
    """Assignee + title only forwards (title=X, body=None, reviewers=None)."""
    agent_id = uuid4()
    task = _task(assigned_to=agent_id)
    task_svc = AsyncMock()
    task_svc.get.return_value = task
    task_svc.agent_for.return_value = _agent("developer", "backend")

    git_svc = AsyncMock()
    git_svc.update_pr_for_task.return_value = {
        "pr_number": 7,
        "pr_url": task.pr_url,
        "updated_fields": ["title"],
    }
    deps = _make_deps(task=task_svc, git=git_svc)
    ca = ContentActions(deps)

    env = await ca.pr_update(
        agent_id=agent_id,
        task_id=task.id,
        title="new title",
        body=None,
        reviewers=None,
    )
    body = env.as_dict()

    assert body["error"] is None
    assert body["evidence"]["updated_fields"] == ["title"]
    git_svc.update_pr_for_task.assert_awaited_once()
    call_kwargs = git_svc.update_pr_for_task.call_args.kwargs
    assert call_kwargs["title"] == "new title"
    assert call_kwargs["body"] is None
    assert call_kwargs["reviewers"] is None


@pytest.mark.asyncio
async def test_pr_update_reviewers_only_forwarded() -> None:
    """Assignee + reviewers only → forwarded as the only non-None arg."""
    agent_id = uuid4()
    task = _task(assigned_to=agent_id)
    task_svc = AsyncMock()
    task_svc.get.return_value = task
    task_svc.agent_for.return_value = _agent("developer", "backend")

    git_svc = AsyncMock()
    git_svc.update_pr_for_task.return_value = {
        "pr_number": 7,
        "pr_url": task.pr_url,
        "updated_fields": ["reviewers"],
    }
    deps = _make_deps(task=task_svc, git=git_svc)
    ca = ContentActions(deps)

    env = await ca.pr_update(
        agent_id=agent_id,
        task_id=task.id,
        title=None,
        body=None,
        reviewers=["be-dev-2"],
    )
    body = env.as_dict()

    assert body["error"] is None
    git_svc.update_pr_for_task.assert_awaited_once()
    call_kwargs = git_svc.update_pr_for_task.call_args.kwargs
    assert call_kwargs["title"] is None
    assert call_kwargs["body"] is None
    assert call_kwargs["reviewers"] == ["be-dev-2"]


@pytest.mark.asyncio
async def test_pr_update_all_three_forwarded() -> None:
    """All three fields → all forwarded; updated_fields reflects all three."""
    agent_id = uuid4()
    task = _task(assigned_to=agent_id)
    task_svc = AsyncMock()
    task_svc.get.return_value = task
    task_svc.agent_for.return_value = _agent("developer", "backend")

    git_svc = AsyncMock()
    git_svc.update_pr_for_task.return_value = {
        "pr_number": 7,
        "pr_url": task.pr_url,
        "updated_fields": ["title", "body", "reviewers"],
    }
    deps = _make_deps(task=task_svc, git=git_svc)
    ca = ContentActions(deps)

    env = await ca.pr_update(
        agent_id=agent_id,
        task_id=task.id,
        title="t",
        body="b",
        reviewers=["be-dev-2"],
    )
    body = env.as_dict()

    assert body["error"] is None
    call_kwargs = git_svc.update_pr_for_task.call_args.kwargs
    assert call_kwargs["title"] == "t"
    assert call_kwargs["body"] == "b"
    assert call_kwargs["reviewers"] == ["be-dev-2"]
    assert set(body["evidence"]["updated_fields"]) == {"title", "body", "reviewers"}


@pytest.mark.asyncio
async def test_pr_update_cell_pm_on_same_team_allowed() -> None:
    """Cell PM whose team == task.team can update the PR (PM authority)."""
    pm_id = uuid4()
    assignee_id = uuid4()
    task = _task(assigned_to=assignee_id, team="backend")
    task_svc = AsyncMock()
    task_svc.get.return_value = task
    task_svc.agent_for.return_value = _agent("cell_pm", "backend")

    git_svc = AsyncMock()
    git_svc.update_pr_for_task.return_value = {
        "pr_number": 7,
        "pr_url": task.pr_url,
        "updated_fields": ["title"],
    }
    deps = _make_deps(task=task_svc, git=git_svc)
    ca = ContentActions(deps)

    env = await ca.pr_update(
        agent_id=pm_id, task_id=task.id, title="t", body=None, reviewers=None
    )
    body = env.as_dict()

    assert body["error"] is None
    git_svc.update_pr_for_task.assert_awaited_once()


@pytest.mark.asyncio
async def test_pr_update_cell_pm_on_other_team_rejected() -> None:
    """Cell PM on a different team than the task → not_authorized."""
    pm_id = uuid4()
    assignee_id = uuid4()
    task = _task(assigned_to=assignee_id, team="backend")
    task_svc = AsyncMock()
    task_svc.get.return_value = task
    task_svc.agent_for.return_value = _agent("cell_pm", "frontend")

    deps = _make_deps(task=task_svc)
    ca = ContentActions(deps)

    env = await ca.pr_update(
        agent_id=pm_id, task_id=task.id, title="t", body=None, reviewers=None
    )

    assert env.as_dict()["error"] == "not_authorized"
    deps.git.update_pr_for_task.assert_not_awaited()


@pytest.mark.asyncio
async def test_pr_update_main_pm_any_team_allowed() -> None:
    """Main PM is cross-team and may update any task's PR."""
    pm_id = uuid4()
    assignee_id = uuid4()
    task = _task(assigned_to=assignee_id, team="backend")
    task_svc = AsyncMock()
    task_svc.get.return_value = task
    task_svc.agent_for.return_value = _agent("main_pm", team=None)

    git_svc = AsyncMock()
    git_svc.update_pr_for_task.return_value = {
        "pr_number": 7,
        "pr_url": task.pr_url,
        "updated_fields": ["title"],
    }
    deps = _make_deps(task=task_svc, git=git_svc)
    ca = ContentActions(deps)

    env = await ca.pr_update(
        agent_id=pm_id, task_id=task.id, title="t", body=None, reviewers=None
    )

    assert env.as_dict()["error"] is None
    git_svc.update_pr_for_task.assert_awaited_once()


@pytest.mark.asyncio
async def test_pr_update_task_not_found_returns_not_found() -> None:
    """Unknown task_id → not_found envelope; git layer never invoked."""
    agent_id = uuid4()
    task_svc = AsyncMock()
    task_svc.get.return_value = None
    deps = _make_deps(task=task_svc)
    ca = ContentActions(deps)

    env = await ca.pr_update(
        agent_id=agent_id, task_id=uuid4(), title="t", body=None, reviewers=None
    )
    body = env.as_dict()

    assert body["error"] == "not_found"
    deps.git.update_pr_for_task.assert_not_awaited()


@pytest.mark.asyncio
async def test_pr_update_git_error_returned_as_invalid_state() -> None:
    """GitService raises GitError → mapped to invalid_state envelope with detail."""
    agent_id = uuid4()
    task = _task(assigned_to=agent_id)
    task_svc = AsyncMock()
    task_svc.get.return_value = task
    task_svc.agent_for.return_value = _agent("developer", "backend")

    git_svc = AsyncMock()
    git_svc.update_pr_for_task.side_effect = GitError("PR not found: #7 on acme/repo")
    deps = _make_deps(task=task_svc, git=git_svc)
    ca = ContentActions(deps)

    env = await ca.pr_update(
        agent_id=agent_id, task_id=task.id, title="t", body=None, reviewers=None
    )
    body = env.as_dict()

    assert body["error"] == "invalid_state"
    assert "PR not found" in body["message"]
