# ruff: noqa: INP001
"""Integration tests for tasking Forgejo issues to Pipeline agents."""

from __future__ import annotations

from datetime import datetime
from types import SimpleNamespace
from uuid import UUID, uuid4

import pytest
from fastapi import APIRouter, FastAPI
from httpx import ASGITransport, AsyncClient
from sqlalchemy.ext.asyncio import AsyncEngine, async_sessionmaker, create_async_engine
from sqlmodel import SQLModel, select
from sqlmodel.ext.asyncio.session import AsyncSession

from app import models as _models  # noqa: F401
from app.api.deps import require_org_member
from app.api.forgejo_issues import router as forgejo_issues_router
from app.core.auth import AuthContext, get_auth_context
from app.db.session import get_session
from app.models.activity_events import ActivityEvent
from app.models.agents import Agent
from app.models.board_repository_links import BoardRepositoryLink
from app.models.boards import Board
from app.models.forgejo_connections import ForgejoConnection
from app.models.forgejo_issue_task_links import ForgejoIssueTaskLink
from app.models.forgejo_issues import ForgejoIssue
from app.models.forgejo_repositories import ForgejoRepository
from app.models.gateways import Gateway
from app.models.organization_board_access import OrganizationBoardAccess
from app.models.organization_members import OrganizationMember
from app.models.organizations import Organization
from app.models.tasks import Task
from app.models.users import User
from app.services.organizations import OrganizationContext


async def _make_engine() -> AsyncEngine:
    engine = create_async_engine("sqlite+aiosqlite:///:memory:")
    async with engine.begin() as conn:
        await conn.run_sync(SQLModel.metadata.create_all)
    return engine


def _build_test_app(
    session_maker: async_sessionmaker[AsyncSession],
    *,
    org_ctx: OrganizationContext,
    auth_ctx: AuthContext,
) -> FastAPI:
    app = FastAPI()
    api_v1 = APIRouter(prefix="/api/v1")
    api_v1.include_router(forgejo_issues_router)
    app.include_router(api_v1)

    async def _override_get_session() -> AsyncSession:
        async with session_maker() as session:
            yield session

    async def _override_require_org_member() -> OrganizationContext:
        return org_ctx

    async def _override_get_auth_context() -> AuthContext:
        return auth_ctx

    app.dependency_overrides[get_session] = _override_get_session
    app.dependency_overrides[require_org_member] = _override_require_org_member
    app.dependency_overrides[get_auth_context] = _override_get_auth_context
    return app


async def _seed(session: AsyncSession) -> SimpleNamespace:
    org = Organization(id=uuid4(), name="Pipeline Org")
    user = User(
        id=uuid4(),
        clerk_user_id="user_123",
        email="user@example.com",
        name="User",
        active_organization_id=org.id,
    )
    member = OrganizationMember(
        id=uuid4(),
        organization_id=org.id,
        user_id=user.id,
        role="member",
        all_boards_read=False,
        all_boards_write=False,
    )
    connection = ForgejoConnection(
        id=uuid4(),
        organization_id=org.id,
        name="Dream Forgejo",
        base_url="https://forgejo.example.local",
        token="temp-token",
        token_last_eight="p-token",
    )
    repository = ForgejoRepository(
        id=uuid4(),
        organization_id=org.id,
        connection_id=connection.id,
        owner="openclaw",
        repo="pipeline",
        display_name="Pipeline",
    )
    board = Board(
        id=uuid4(),
        organization_id=org.id,
        name="Pipeline Board",
        slug="pipeline-board",
    )
    board_access = OrganizationBoardAccess(
        id=uuid4(),
        organization_member_id=member.id,
        board_id=board.id,
        can_read=True,
        can_write=True,
    )
    link = BoardRepositoryLink(
        id=uuid4(),
        organization_id=org.id,
        board_id=board.id,
        repository_id=repository.id,
    )
    issue = ForgejoIssue(
        id=uuid4(),
        organization_id=org.id,
        repository_id=repository.id,
        forgejo_issue_number=42,
        title="Implement agent tasking",
        body="Wire the backend so agents can work this issue.",
        body_preview="Wire the backend",
        state="open",
        is_pull_request=False,
        labels=[{"name": "backend"}, {"name": "agents"}],
        assignees=[],
        forgejo_payload={"number": 42, "state": "open"},
        author="kaspa",
        html_url="https://forgejo.example.local/openclaw/pipeline/issues/42",
        forgejo_created_at=datetime(2026, 5, 19, 12, 0, 0),
        forgejo_updated_at=datetime(2026, 5, 19, 12, 5, 0),
    )
    gateway = Gateway(
        id=uuid4(),
        organization_id=org.id,
        name="Gateway",
        url="https://gateway.example.local",
        workspace_root="/workspace",
    )
    worker_agent = Agent(
        id=uuid4(),
        board_id=board.id,
        gateway_id=gateway.id,
        name="Worker Agent",
        is_board_lead=False,
    )

    session.add(org)
    session.add(user)
    session.add(member)
    session.add(connection)
    session.add(repository)
    session.add(board)
    session.add(board_access)
    session.add(link)
    session.add(issue)
    session.add(gateway)
    session.add(worker_agent)
    await session.commit()
    return SimpleNamespace(
        org=org,
        user=user,
        member=member,
        repository=repository,
        board=board,
        issue=issue,
        gateway=gateway,
        worker_agent=worker_agent,
    )


def _client_for(
    session_maker: async_sessionmaker[AsyncSession],
    seeded: SimpleNamespace,
) -> AsyncClient:
    org_ctx = OrganizationContext(organization=seeded.org, member=seeded.member)
    auth_ctx = AuthContext(actor_type="user", user=seeded.user)
    app = _build_test_app(session_maker, org_ctx=org_ctx, auth_ctx=auth_ctx)
    return AsyncClient(transport=ASGITransport(app=app), base_url="http://testserver")


@pytest.mark.asyncio
async def test_human_writer_can_create_task_from_issue_and_assign_agent() -> None:
    engine = await _make_engine()
    session_maker = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)

    try:
        async with session_maker() as session:
            seeded = await _seed(session)

        async with _client_for(session_maker, seeded) as client:
            response = await client.post(
                f"/api/v1/forgejo/issues/{seeded.issue.id}/task",
                json={
                    "board_id": str(seeded.board.id),
                    "assigned_agent_id": str(seeded.worker_agent.id),
                    "instructions": "Start by reproducing the issue locally.",
                },
            )

        assert response.status_code == 200
        payload = response.json()
        assert payload["success"] is True
        assert payload["created"] is True
        assert payload["issue_id"] == str(seeded.issue.id)
        assert payload["board_id"] == str(seeded.board.id)
        assert payload["assigned_agent_id"] == str(seeded.worker_agent.id)
        assert payload["status"] == "in_progress"
        assert payload["title"] == "Git issue #42: Implement agent tasking"

        async with session_maker() as session:
            task = await session.get(Task, UUID(payload["task_id"]))
            assert task is not None
            assert task.board_id == seeded.board.id
            assert task.assigned_agent_id == seeded.worker_agent.id
            assert task.auto_created is True
            assert task.auto_reason == f"forgejo_issue:{seeded.issue.id}"
            assert task.in_progress_at is not None
            assert task.description is not None
            assert "Repository: Pipeline" in task.description
            assert "Labels: backend, agents" in task.description
            assert "Start by reproducing the issue locally." in task.description

            links = (await session.exec(select(ForgejoIssueTaskLink))).all()
            assert len(links) == 1
            assert links[0].issue_id == seeded.issue.id
            assert links[0].task_id == task.id

            events = (await session.exec(select(ActivityEvent))).all()
            assert len(events) == 1
            assert events[0].event_type == "forgejo.issue.task.created"
            assert events[0].board_id == seeded.board.id
            assert events[0].task_id == task.id
    finally:
        await engine.dispose()


@pytest.mark.asyncio
async def test_tasking_same_issue_reuses_existing_task() -> None:
    engine = await _make_engine()
    session_maker = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)

    try:
        async with session_maker() as session:
            seeded = await _seed(session)

        async with _client_for(session_maker, seeded) as client:
            first = await client.post(
                f"/api/v1/forgejo/issues/{seeded.issue.id}/task",
                json={"board_id": str(seeded.board.id)},
            )
            second = await client.post(
                f"/api/v1/forgejo/issues/{seeded.issue.id}/task",
                json={
                    "board_id": str(seeded.board.id),
                    "assigned_agent_id": str(seeded.worker_agent.id),
                    "priority": "high",
                },
            )

        assert first.status_code == 200
        assert second.status_code == 200
        first_payload = first.json()
        second_payload = second.json()
        assert first_payload["created"] is True
        assert second_payload["created"] is False
        assert second_payload["task_id"] == first_payload["task_id"]
        assert second_payload["assigned_agent_id"] == str(seeded.worker_agent.id)
        assert second_payload["status"] == "in_progress"

        async with session_maker() as session:
            tasks = (await session.exec(select(Task))).all()
            links = (await session.exec(select(ForgejoIssueTaskLink))).all()
            events = (await session.exec(select(ActivityEvent))).all()
            assert len(tasks) == 1
            assert len(links) == 1
            assert tasks[0].priority == "high"
            assert len(events) == 2
            assert [event.event_type for event in events] == [
                "forgejo.issue.task.created",
                "forgejo.issue.task.updated",
            ]
    finally:
        await engine.dispose()


@pytest.mark.asyncio
async def test_tasking_issue_rejects_agent_from_another_board() -> None:
    engine = await _make_engine()
    session_maker = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)

    try:
        async with session_maker() as session:
            seeded = await _seed(session)
            other_board = Board(
                id=uuid4(),
                organization_id=seeded.org.id,
                name="Other Board",
                slug="other-board",
            )
            other_agent = Agent(
                id=uuid4(),
                board_id=other_board.id,
                gateway_id=seeded.gateway.id,
                name="Other Agent",
            )
            session.add(other_board)
            session.add(other_agent)
            await session.commit()

        async with _client_for(session_maker, seeded) as client:
            response = await client.post(
                f"/api/v1/forgejo/issues/{seeded.issue.id}/task",
                json={
                    "board_id": str(seeded.board.id),
                    "assigned_agent_id": str(other_agent.id),
                },
            )

        assert response.status_code == 422
        assert response.json()["detail"] == "Agent must belong to the selected board"
    finally:
        await engine.dispose()


@pytest.mark.asyncio
async def test_tasking_issue_requires_board_when_multiple_linked_boards_are_writable() -> None:
    engine = await _make_engine()
    session_maker = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)

    try:
        async with session_maker() as session:
            seeded = await _seed(session)
            second_board = Board(
                id=uuid4(),
                organization_id=seeded.org.id,
                name="Second Board",
                slug="second-board",
            )
            session.add(second_board)
            session.add(
                OrganizationBoardAccess(
                    id=uuid4(),
                    organization_member_id=seeded.member.id,
                    board_id=second_board.id,
                    can_read=True,
                    can_write=True,
                )
            )
            session.add(
                BoardRepositoryLink(
                    id=uuid4(),
                    organization_id=seeded.org.id,
                    board_id=second_board.id,
                    repository_id=seeded.repository.id,
                )
            )
            await session.commit()

        async with _client_for(session_maker, seeded) as client:
            response = await client.post(f"/api/v1/forgejo/issues/{seeded.issue.id}/task")

        assert response.status_code == 422
        assert (
            response.json()["detail"]
            == "board_id is required because this issue is linked to multiple writable boards"
        )
    finally:
        await engine.dispose()
