from __future__ import annotations

from typing import Literal
from uuid import uuid4

import pytest
from fastapi import HTTPException
from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine
from sqlmodel import SQLModel
from sqlmodel.ext.asyncio.session import AsyncSession

from app.api import tasks as tasks_api
from app.api.deps import ActorContext
from app.models.agents import Agent
from app.models.approval_task_links import ApprovalTaskLink
from app.models.approvals import Approval
from app.models.boards import Board
from app.models.gateways import Gateway
from app.models.organizations import Organization
from app.models.tasks import Task
from app.schemas.tasks import TaskRead, TaskUpdate


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


async def _make_session(engine: AsyncEngine) -> AsyncSession:
    return AsyncSession(engine, expire_on_commit=False)


async def _seed_board_task_and_agent(
    session: AsyncSession,
    *,
    task_status: str = "review",
    require_approval_for_done: bool = True,
    require_review_before_done: bool = False,
    block_status_changes_with_pending_approval: bool = False,
    only_lead_can_change_status: bool = False,
    agent_is_board_lead: bool = False,
) -> tuple[Board, Task, Agent]:
    organization_id = uuid4()
    gateway = Gateway(
        id=uuid4(),
        organization_id=organization_id,
        name="gateway",
        url="https://gateway.local",
        workspace_root="/tmp/workspace",
    )
    board = Board(
        id=uuid4(),
        organization_id=organization_id,
        gateway_id=gateway.id,
        name="board",
        slug=f"board-{uuid4()}",
        require_approval_for_done=require_approval_for_done,
        require_review_before_done=require_review_before_done,
        block_status_changes_with_pending_approval=block_status_changes_with_pending_approval,
        only_lead_can_change_status=only_lead_can_change_status,
    )
    agent = Agent(
        id=uuid4(),
        board_id=board.id,
        gateway_id=gateway.id,
        name="agent",
        status="online",
        is_board_lead=agent_is_board_lead,
    )
    task = Task(
        id=uuid4(),
        board_id=board.id,
        title="Task",
        status=task_status,
        assigned_agent_id=agent.id,
    )

    session.add(Organization(id=organization_id, name=f"org-{organization_id}"))
    session.add(gateway)
    session.add(board)
    session.add(task)
    session.add(agent)
    await session.commit()
    return board, task, agent


async def _update_task_to_done(
    session: AsyncSession,
    *,
    task: Task,
    agent: Agent,
) -> None:
    await _update_task_status(
        session,
        task=task,
        agent=agent,
        status="done",
    )


async def _update_task_status(
    session: AsyncSession,
    *,
    task: Task,
    agent: Agent,
    status: Literal["inbox", "in_progress", "review", "done"],
) -> TaskRead:
    return await tasks_api.update_task(
        payload=TaskUpdate(status=status),
        task=task,
        session=session,
        actor=ActorContext(actor_type="agent", agent=agent),
    )


@pytest.mark.asyncio
async def test_update_task_rejects_done_without_approved_linked_approval() -> None:
    engine = await _make_engine()
    try:
        async with await _make_session(engine) as session:
            board, task, agent = await _seed_board_task_and_agent(session)
            session.add(
                Approval(
                    id=uuid4(),
                    board_id=board.id,
                    task_id=task.id,
                    action_type="task.review",
                    confidence=65,
                    status="pending",
                ),
            )
            await session.commit()

            with pytest.raises(HTTPException) as exc:
                await _update_task_to_done(session, task=task, agent=agent)

            assert exc.value.status_code == 409
            detail = exc.value.detail
            assert isinstance(detail, dict)
            assert detail["message"] == (
                "Task can only be marked done when a linked approval has been approved."
            )
    finally:
        await engine.dispose()


@pytest.mark.asyncio
async def test_update_task_allows_done_with_approved_primary_task_approval() -> None:
    engine = await _make_engine()
    try:
        async with await _make_session(engine) as session:
            board, task, agent = await _seed_board_task_and_agent(session)
            session.add(
                Approval(
                    id=uuid4(),
                    board_id=board.id,
                    task_id=task.id,
                    action_type="task.review",
                    confidence=92,
                    status="approved",
                ),
            )
            await session.commit()

            updated = await tasks_api.update_task(
                payload=TaskUpdate(status="done"),
                task=task,
                session=session,
                actor=ActorContext(actor_type="agent", agent=agent),
            )

            assert updated.status == "done"
            assert updated.assigned_agent_id == agent.id
    finally:
        await engine.dispose()


@pytest.mark.asyncio
async def test_update_task_allows_done_with_approved_multi_task_link() -> None:
    engine = await _make_engine()
    try:
        async with await _make_session(engine) as session:
            board, task, agent = await _seed_board_task_and_agent(session)
            primary_task_id = uuid4()
            session.add(Task(id=primary_task_id, board_id=board.id, title="Primary"))

            approval_id = uuid4()
            session.add(
                Approval(
                    id=approval_id,
                    board_id=board.id,
                    task_id=primary_task_id,
                    action_type="task.batch_review",
                    confidence=88,
                    status="approved",
                ),
            )
            await session.commit()

            session.add(ApprovalTaskLink(approval_id=approval_id, task_id=task.id))
            await session.commit()

            updated = await tasks_api.update_task(
                payload=TaskUpdate(status="done"),
                task=task,
                session=session,
                actor=ActorContext(actor_type="agent", agent=agent),
            )

            assert updated.status == "done"
    finally:
        await engine.dispose()


@pytest.mark.asyncio
async def test_update_task_allows_done_without_approval_when_board_toggle_disabled() -> None:
    engine = await _make_engine()
    try:
        async with await _make_session(engine) as session:
            _board, task, agent = await _seed_board_task_and_agent(
                session,
                require_approval_for_done=False,
            )

            updated = await tasks_api.update_task(
                payload=TaskUpdate(status="done"),
                task=task,
                session=session,
                actor=ActorContext(actor_type="agent", agent=agent),
            )

            assert updated.status == "done"
    finally:
        await engine.dispose()


@pytest.mark.asyncio
async def test_update_task_rejects_done_from_in_progress_when_review_toggle_enabled() -> None:
    engine = await _make_engine()
    try:
        async with await _make_session(engine) as session:
            _board, task, agent = await _seed_board_task_and_agent(
                session,
                task_status="in_progress",
                require_approval_for_done=False,
                require_review_before_done=True,
            )

            with pytest.raises(HTTPException) as exc:
                await _update_task_to_done(session, task=task, agent=agent)

            assert exc.value.status_code == 409
            detail = exc.value.detail
            assert isinstance(detail, dict)
            assert detail["message"] == (
                "Task can only be marked done from review when the board rule is enabled."
            )
    finally:
        await engine.dispose()


@pytest.mark.asyncio
async def test_update_task_allows_done_from_review_when_review_toggle_enabled() -> None:
    engine = await _make_engine()
    try:
        async with await _make_session(engine) as session:
            _board, task, agent = await _seed_board_task_and_agent(
                session,
                task_status="review",
                require_approval_for_done=False,
                require_review_before_done=True,
            )

            updated = await tasks_api.update_task(
                payload=TaskUpdate(status="done"),
                task=task,
                session=session,
                actor=ActorContext(actor_type="agent", agent=agent),
            )

            assert updated.status == "done"
    finally:
        await engine.dispose()


@pytest.mark.asyncio
async def test_update_task_rejects_status_change_with_pending_approval_when_toggle_enabled() -> (
    None
):
    engine = await _make_engine()
    try:
        async with await _make_session(engine) as session:
            board, task, agent = await _seed_board_task_and_agent(
                session,
                task_status="inbox",
                require_approval_for_done=False,
                block_status_changes_with_pending_approval=True,
            )
            session.add(
                Approval(
                    id=uuid4(),
                    board_id=board.id,
                    task_id=task.id,
                    action_type="task.execute",
                    confidence=70,
                    status="pending",
                ),
            )
            await session.commit()

            with pytest.raises(HTTPException) as exc:
                await _update_task_status(
                    session,
                    task=task,
                    agent=agent,
                    status="in_progress",
                )

            assert exc.value.status_code == 409
            detail = exc.value.detail
            assert isinstance(detail, dict)
            assert detail["message"] == (
                "Task status cannot be changed while a linked approval is pending."
            )
    finally:
        await engine.dispose()


@pytest.mark.asyncio
async def test_update_task_allows_status_change_with_pending_approval_when_toggle_disabled() -> (
    None
):
    engine = await _make_engine()
    try:
        async with await _make_session(engine) as session:
            board, task, agent = await _seed_board_task_and_agent(
                session,
                task_status="inbox",
                require_approval_for_done=False,
                block_status_changes_with_pending_approval=False,
            )
            session.add(
                Approval(
                    id=uuid4(),
                    board_id=board.id,
                    task_id=task.id,
                    action_type="task.execute",
                    confidence=70,
                    status="pending",
                ),
            )
            await session.commit()

            updated = await tasks_api.update_task(
                payload=TaskUpdate(status="in_progress"),
                task=task,
                session=session,
                actor=ActorContext(actor_type="agent", agent=agent),
            )

            assert updated.status == "in_progress"
    finally:
        await engine.dispose()


@pytest.mark.asyncio
async def test_update_task_rejects_non_lead_status_change_when_only_lead_rule_enabled() -> None:
    engine = await _make_engine()
    try:
        async with await _make_session(engine) as session:
            _board, task, agent = await _seed_board_task_and_agent(
                session,
                task_status="inbox",
                require_approval_for_done=False,
                only_lead_can_change_status=True,
            )

            with pytest.raises(HTTPException) as exc:
                await _update_task_status(
                    session,
                    task=task,
                    agent=agent,
                    status="in_progress",
                )

            assert exc.value.status_code == 403
    finally:
        await engine.dispose()


@pytest.mark.asyncio
async def test_update_task_allows_non_lead_status_change_when_only_lead_rule_disabled() -> None:
    engine = await _make_engine()
    try:
        async with await _make_session(engine) as session:
            _board, task, agent = await _seed_board_task_and_agent(
                session,
                task_status="inbox",
                require_approval_for_done=False,
                only_lead_can_change_status=False,
            )

            updated = await _update_task_status(
                session,
                task=task,
                agent=agent,
                status="in_progress",
            )

            assert updated.status == "in_progress"
    finally:
        await engine.dispose()


@pytest.mark.asyncio
async def test_update_task_lead_can_still_change_status_when_only_lead_rule_enabled() -> None:
    engine = await _make_engine()
    try:
        async with await _make_session(engine) as session:
            _board, task, lead_agent = await _seed_board_task_and_agent(
                session,
                task_status="review",
                require_approval_for_done=False,
                require_review_before_done=False,
                only_lead_can_change_status=True,
                agent_is_board_lead=True,
            )

            updated = await tasks_api.update_task(
                payload=TaskUpdate(status="inbox"),
                task=task,
                session=session,
                actor=ActorContext(actor_type="agent", agent=lead_agent),
            )

            assert updated.status == "inbox"
    finally:
        await engine.dispose()


@pytest.mark.asyncio
async def test_update_task_allows_dependency_change_with_pending_approval() -> None:
    engine = await _make_engine()
    try:
        async with await _make_session(engine) as session:
            board, task, _agent = await _seed_board_task_and_agent(
                session,
                task_status="review",
                require_approval_for_done=False,
                block_status_changes_with_pending_approval=True,
            )
            dependency = Task(
                id=uuid4(),
                board_id=board.id,
                title="Dependency",
                status="inbox",
            )
            session.add(dependency)
            session.add(
                Approval(
                    id=uuid4(),
                    board_id=board.id,
                    task_id=task.id,
                    action_type="task.execute",
                    confidence=70,
                    status="pending",
                ),
            )
            await session.commit()

            updated = await tasks_api.update_task(
                payload=TaskUpdate(
                    status="review",
                    depends_on_task_ids=[dependency.id],
                ),
                task=task,
                session=session,
                actor=ActorContext(actor_type="user"),
            )

            assert updated.depends_on_task_ids == [dependency.id]
            assert updated.status == "inbox"
            assert updated.blocked_by_task_ids == [dependency.id]
    finally:
        await engine.dispose()


@pytest.mark.asyncio
async def test_update_task_rejects_status_change_for_pending_multi_task_link_when_toggle_enabled() -> (
    None
):
    engine = await _make_engine()
    try:
        async with await _make_session(engine) as session:
            board, task, agent = await _seed_board_task_and_agent(
                session,
                task_status="inbox",
                require_approval_for_done=False,
                block_status_changes_with_pending_approval=True,
            )
            primary_task_id = uuid4()
            session.add(Task(id=primary_task_id, board_id=board.id, title="Primary"))

            approval_id = uuid4()
            session.add(
                Approval(
                    id=approval_id,
                    board_id=board.id,
                    task_id=primary_task_id,
                    action_type="task.batch_execute",
                    confidence=73,
                    status="pending",
                ),
            )
            await session.commit()

            session.add(ApprovalTaskLink(approval_id=approval_id, task_id=task.id))
            await session.commit()

            with pytest.raises(HTTPException) as exc:
                await _update_task_status(
                    session,
                    task=task,
                    agent=agent,
                    status="in_progress",
                )

            assert exc.value.status_code == 409
    finally:
        await engine.dispose()
