"""Unit tests: task_id is Optional in git request schemas and service methods.

These tests verify:
- The four git schemas accept None task_id (no 422 on absent field).
- Existing callers that pass task_id still work (regression).
- The HTTP endpoints return 200 when task_id is omitted.

All tests run without a real database or git process.
"""

from __future__ import annotations

from typing import TYPE_CHECKING
from unittest.mock import AsyncMock, patch
from uuid import uuid4

import pytest
import pytest_asyncio
from fastapi import FastAPI
from httpx import ASGITransport, AsyncClient
from roboco.api.deps import get_agent_context, get_db
from roboco.api.routes.git import router as git_router
from roboco.api.schemas.git import (
    GitCommitRequest,
    GitCreatePRRequest,
    GitMergePRRequest,
    GitPushRequest,
)
from roboco.models.base import AgentRole, Team
from roboco.models.permissions import AgentContext

if TYPE_CHECKING:
    from collections.abc import AsyncGenerator, AsyncIterator

_HTTP_UNPROCESSABLE = 422
_HTTP_OK = 200

_AGENT_ID = uuid4()
_TASK_ID = uuid4()


# ---------------------------------------------------------------------------
# Fixtures — no real DB required
# ---------------------------------------------------------------------------


@pytest_asyncio.fixture
async def client() -> AsyncIterator[AsyncClient]:
    """FastAPI test client with mocked auth + DB; no Postgres needed."""
    app = FastAPI()
    app.include_router(git_router, prefix="/api/git")

    async def _mock_db() -> AsyncGenerator:
        yield AsyncMock()  # DB session is never touched in these tests

    async def _mock_agent() -> AgentContext:
        return AgentContext(
            agent_id=_AGENT_ID,
            role=AgentRole.DEVELOPER,
            team=Team.BACKEND,
        )

    app.dependency_overrides[get_db] = _mock_db
    app.dependency_overrides[get_agent_context] = _mock_agent

    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url="http://test") as ac:
        yield ac
    app.dependency_overrides.clear()


_HDR = {"X-Agent-ID": str(_AGENT_ID), "X-Agent-Role": "developer"}

# ---------------------------------------------------------------------------
# Schema unit tests — direct Pydantic validation, no HTTP needed
# ---------------------------------------------------------------------------


def test_commit_request_task_id_optional() -> None:
    """GitCommitRequest accepts a missing task_id (defaults to None)."""
    req = GitCommitRequest(
        project_slug="roboco",
        message="add something new here",
        commit_type="feat",
    )
    assert req.task_id is None


def test_commit_request_task_id_present() -> None:
    """GitCommitRequest still accepts an explicit task_id (regression)."""
    req = GitCommitRequest(
        project_slug="roboco",
        task_id=_TASK_ID,
        message="add something new here",
        commit_type="feat",
    )
    assert req.task_id == _TASK_ID


def test_push_request_task_id_optional() -> None:
    """GitPushRequest accepts a missing task_id (defaults to None)."""
    req = GitPushRequest(project_slug="roboco")
    assert req.task_id is None


def test_push_request_task_id_present() -> None:
    """GitPushRequest still accepts an explicit task_id (regression)."""
    req = GitPushRequest(project_slug="roboco", task_id=_TASK_ID)
    assert req.task_id == _TASK_ID


def test_create_pr_request_task_id_optional() -> None:
    """GitCreatePRRequest accepts a missing task_id (defaults to None)."""
    req = GitCreatePRRequest(project_slug="roboco")
    assert req.task_id is None


def test_create_pr_request_task_id_present() -> None:
    """GitCreatePRRequest still accepts an explicit task_id (regression)."""
    req = GitCreatePRRequest(project_slug="roboco", task_id=_TASK_ID)
    assert req.task_id == _TASK_ID


def test_merge_pr_request_task_id_optional() -> None:
    """GitMergePRRequest accepts a missing task_id (defaults to None)."""
    req = GitMergePRRequest(project_slug="roboco", pr_number=42)
    assert req.task_id is None


def test_merge_pr_request_task_id_present() -> None:
    """GitMergePRRequest still accepts an explicit task_id (regression)."""
    req = GitMergePRRequest(project_slug="roboco", pr_number=42, task_id=_TASK_ID)
    assert req.task_id == _TASK_ID


# ---------------------------------------------------------------------------
# HTTP endpoint tests — verify no 422 when task_id is absent
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_commit_without_task_id_returns_200_not_422(
    client: AsyncClient,
) -> None:
    """POST /commit without task_id must not return 422 (schema error)."""
    with patch("roboco.api.routes.git.get_git_service") as mock_svc:
        svc = AsyncMock()
        svc.commit_for_task = AsyncMock(
            return_value=("deadbeef", "feat: add something", 1, 5, 2)
        )
        mock_svc.return_value = svc
        response = await client.post(
            "/api/git/commit",
            json={
                "project_slug": "roboco",
                "agent_id": str(_AGENT_ID),
                "message": "add something new",
                "commit_type": "feat",
                # task_id intentionally omitted
            },
            headers=_HDR,
        )
    assert response.status_code != _HTTP_UNPROCESSABLE, (
        f"Expected no 422, got {response.status_code}: {response.text}"
    )
    assert response.status_code == _HTTP_OK


@pytest.mark.asyncio
async def test_push_without_task_id_returns_200_not_422(
    client: AsyncClient,
) -> None:
    """POST /push without task_id must not return 422 (schema error)."""
    with patch("roboco.api.routes.git.get_git_service") as mock_svc:
        svc = AsyncMock()
        svc.push_for_task = AsyncMock(return_value=("feature/x", 3))
        mock_svc.return_value = svc
        response = await client.post(
            "/api/git/push",
            json={
                "project_slug": "roboco",
                # task_id intentionally omitted
            },
            headers=_HDR,
        )
    assert response.status_code != _HTTP_UNPROCESSABLE, (
        f"Expected no 422, got {response.status_code}: {response.text}"
    )
    assert response.status_code == _HTTP_OK


@pytest.mark.asyncio
async def test_create_pr_without_task_id_returns_200_not_422(
    client: AsyncClient,
) -> None:
    """POST /pr/create without task_id must not return 422 (schema error)."""
    with patch("roboco.api.routes.git.get_git_service") as mock_svc:
        svc = AsyncMock()
        svc.create_pr_for_task = AsyncMock(
            return_value=(
                7,
                "https://github.com/x/y/pull/7",
                "feat: add thing",
                "feature/x",
                "main",
            )
        )
        mock_svc.return_value = svc
        response = await client.post(
            "/api/git/pr/create",
            json={
                "project_slug": "roboco",
                # task_id intentionally omitted
            },
            headers=_HDR,
        )
    assert response.status_code != _HTTP_UNPROCESSABLE, (
        f"Expected no 422, got {response.status_code}: {response.text}"
    )
    assert response.status_code == _HTTP_OK


@pytest.mark.asyncio
async def test_merge_pr_without_task_id_returns_200_not_422(
    client: AsyncClient,
) -> None:
    """POST /pr/merge without task_id must not return 422 (schema error)."""
    with patch("roboco.api.routes.git.get_git_service") as mock_svc:
        svc = AsyncMock()
        svc.merge_pr_for_task = AsyncMock(return_value=("main", "deadbeef"))
        mock_svc.return_value = svc
        response = await client.post(
            "/api/git/pr/merge",
            json={
                "project_slug": "roboco",
                "pr_number": 99,
                # task_id intentionally omitted
            },
            headers=_HDR,
        )
    assert response.status_code != _HTTP_UNPROCESSABLE, (
        f"Expected no 422, got {response.status_code}: {response.text}"
    )
    assert response.status_code == _HTTP_OK


# ---------------------------------------------------------------------------
# Regression: existing callers that pass task_id still get 200
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_commit_with_task_id_still_works(client: AsyncClient) -> None:
    """POST /commit with task_id (regression) must still return 200."""
    with patch("roboco.api.routes.git.get_git_service") as mock_svc:
        svc = AsyncMock()
        svc.commit_for_task = AsyncMock(
            return_value=("abc123", "fix(auth): correct thing", 2, 10, 3)
        )
        mock_svc.return_value = svc
        response = await client.post(
            "/api/git/commit",
            json={
                "project_slug": "roboco",
                "task_id": str(_TASK_ID),
                "agent_id": str(_AGENT_ID),
                "message": "correct the auth flow",
                "commit_type": "fix",
            },
            headers=_HDR,
        )
    assert response.status_code == _HTTP_OK


@pytest.mark.asyncio
async def test_push_with_task_id_still_works(client: AsyncClient) -> None:
    """POST /push with task_id (regression) must still return 200."""
    with patch("roboco.api.routes.git.get_git_service") as mock_svc:
        svc = AsyncMock()
        svc.push_for_task = AsyncMock(return_value=("feature/x", 2))
        mock_svc.return_value = svc
        response = await client.post(
            "/api/git/push",
            json={
                "project_slug": "roboco",
                "task_id": str(_TASK_ID),
            },
            headers=_HDR,
        )
    assert response.status_code == _HTTP_OK


@pytest.mark.asyncio
async def test_create_pr_with_task_id_still_works(client: AsyncClient) -> None:
    """POST /pr/create with task_id (regression) must still return 200."""
    with patch("roboco.api.routes.git.get_git_service") as mock_svc:
        svc = AsyncMock()
        svc.create_pr_for_task = AsyncMock(
            return_value=(5, "https://github.com/x/y/pull/5", "T", "feat", "main")
        )
        mock_svc.return_value = svc
        response = await client.post(
            "/api/git/pr/create",
            json={
                "project_slug": "roboco",
                "task_id": str(_TASK_ID),
            },
            headers=_HDR,
        )
    assert response.status_code == _HTTP_OK


@pytest.mark.asyncio
async def test_merge_pr_with_task_id_still_works(client: AsyncClient) -> None:
    """POST /pr/merge with task_id (regression) must still return 200."""
    with patch("roboco.api.routes.git.get_git_service") as mock_svc:
        svc = AsyncMock()
        svc.merge_pr_for_task = AsyncMock(return_value=("main", "cafebabe"))
        mock_svc.return_value = svc
        response = await client.post(
            "/api/git/pr/merge",
            json={
                "project_slug": "roboco",
                "pr_number": 5,
                "task_id": str(_TASK_ID),
            },
            headers=_HDR,
        )
    assert response.status_code == _HTTP_OK
