# ruff: noqa: INP001
"""Integration tests for Forgejo metrics scoping."""

from __future__ import annotations

from datetime import datetime, timedelta
from uuid import 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
from sqlmodel.ext.asyncio.session import AsyncSession

from app import models as _models
from app.api.deps import require_org_member
from app.api.forgejo_metrics import router as forgejo_metrics_router
from app.db.session import get_session
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_issues import ForgejoIssue
from app.models.forgejo_repositories import ForgejoRepository
from app.models.organization_members import OrganizationMember
from app.models.organizations import Organization
from app.core.time import utcnow
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],
    ctx: OrganizationContext,
) -> FastAPI:
    app = FastAPI()
    api_v1 = APIRouter(prefix="/api/v1")
    api_v1.include_router(forgejo_metrics_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 ctx

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


def _issue(
    *,
    organization_id,
    repository_id,
    issue_number: int,
) -> ForgejoIssue:
    return ForgejoIssue(
        id=uuid4(),
        organization_id=organization_id,
        repository_id=repository_id,
        forgejo_issue_number=issue_number,
        title=f"Issue {issue_number}",
        body_preview="Cached issue body",
        state="open",
        is_pull_request=False,
        labels=[],
        assignees=[],
        author="kaspa",
        html_url=f"https://forgejo.example.local/openclaw/pipeline/issues/{issue_number}",
        forgejo_created_at=datetime(2026, 5, 19, 12, 0, 0),
        forgejo_updated_at=datetime(2026, 5, 19, 12, 5, 0),
    )


@pytest.mark.asyncio
async def test_forgejo_metrics_are_scoped_to_active_organization() -> None:
    engine = await _make_engine()
    session_maker = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
    org_a = Organization(id=uuid4(), name="Org A")
    org_b = Organization(id=uuid4(), name="Org B")
    member = OrganizationMember(
        id=uuid4(),
        organization_id=org_a.id,
        user_id=uuid4(),
        role="owner",
    )
    app = _build_test_app(
        session_maker,
        OrganizationContext(organization=org_a, member=member),
    )

    conn_a = ForgejoConnection(
        id=uuid4(),
        organization_id=org_a.id,
        name="A Forgejo",
        base_url="https://forgejo-a.example.local",
    )
    conn_b = ForgejoConnection(
        id=uuid4(),
        organization_id=org_b.id,
        name="B Forgejo",
        base_url="https://forgejo-b.example.local",
    )
    repo_a = ForgejoRepository(
        id=uuid4(),
        organization_id=org_a.id,
        connection_id=conn_a.id,
        owner="openclaw",
        repo="pipeline",
        display_name="Pipeline",
    )
    repo_b = ForgejoRepository(
        id=uuid4(),
        organization_id=org_b.id,
        connection_id=conn_b.id,
        owner="other",
        repo="private",
        display_name="Private",
    )
    board_b = Board(
        id=uuid4(),
        organization_id=org_b.id,
        name="Other Board",
        slug="other-board",
    )
    link_b = BoardRepositoryLink(
        id=uuid4(),
        organization_id=org_b.id,
        board_id=board_b.id,
        repository_id=repo_b.id,
    )

    try:
        async with session_maker() as session:
            session.add(org_a)
            session.add(org_b)
            session.add(conn_a)
            session.add(conn_b)
            session.add(repo_a)
            session.add(repo_b)
            session.add(board_b)
            session.add(link_b)
            session.add(
                _issue(
                    organization_id=org_a.id,
                    repository_id=repo_a.id,
                    issue_number=1,
                )
            )
            session.add(
                _issue(
                    organization_id=org_b.id,
                    repository_id=repo_b.id,
                    issue_number=2,
                )
            )
            await session.commit()

        async with AsyncClient(
            transport=ASGITransport(app=app),
            base_url="http://testserver",
        ) as client:
            own_org = await client.get(
                f"/api/v1/forgejo/metrics?organization_id={org_a.id}"
            )
            other_org = await client.get(
                f"/api/v1/forgejo/metrics?organization_id={org_b.id}"
            )
            other_repo = await client.get(
                f"/api/v1/forgejo/metrics?repository_id={repo_b.id}"
            )
            other_board = await client.get(
                f"/api/v1/forgejo/metrics?board_id={board_b.id}"
            )

        assert own_org.status_code == 200
        assert own_org.json()["open_issues"] == 1
        assert other_org.status_code == 403
        assert other_repo.status_code == 200
        assert other_repo.json()["open_issues"] == 0
        assert other_board.status_code == 200
        assert other_board.json()["open_issues"] == 0
    finally:
        await engine.dispose()


@pytest.mark.asyncio
async def test_forgejo_metrics_use_remote_issue_timestamps() -> None:
    engine = await _make_engine()
    session_maker = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
    organization = Organization(id=uuid4(), name="Pipeline")
    member = OrganizationMember(
        id=uuid4(),
        organization_id=organization.id,
        user_id=uuid4(),
        role="owner",
    )
    app = _build_test_app(
        session_maker,
        OrganizationContext(organization=organization, member=member),
    )
    connection = ForgejoConnection(
        id=uuid4(),
        organization_id=organization.id,
        name="Dream Forgejo",
        base_url="https://forgejo.example.local",
    )
    repository = ForgejoRepository(
        id=uuid4(),
        organization_id=organization.id,
        connection_id=connection.id,
        owner="openclaw",
        repo="pipeline",
        display_name="Pipeline",
    )
    now = utcnow()
    stale_open = _issue(
        organization_id=organization.id,
        repository_id=repository.id,
        issue_number=1,
    )
    stale_open.forgejo_updated_at = now - timedelta(days=20)
    stale_open.updated_at = now

    recently_closed = _issue(
        organization_id=organization.id,
        repository_id=repository.id,
        issue_number=2,
    )
    recently_closed.state = "closed"
    recently_closed.forgejo_closed_at = now - timedelta(days=2)
    recently_closed.updated_at = now - timedelta(days=90)

    old_closed = _issue(
        organization_id=organization.id,
        repository_id=repository.id,
        issue_number=3,
    )
    old_closed.state = "closed"
    old_closed.forgejo_closed_at = now - timedelta(days=45)
    old_closed.updated_at = now

    try:
        async with session_maker() as session:
            session.add(organization)
            session.add(connection)
            session.add(repository)
            session.add(stale_open)
            session.add(recently_closed)
            session.add(old_closed)
            await session.commit()

        async with AsyncClient(
            transport=ASGITransport(app=app),
            base_url="http://testserver",
        ) as client:
            response = await client.get(
                f"/api/v1/forgejo/metrics?organization_id={organization.id}"
            )

        assert response.status_code == 200
        data = response.json()
        assert data["open_issues"] == 1
        assert data["closed_issues"] == 2
        assert data["closed_last_7_days"] == 1
        assert data["closed_last_30_days"] == 1
        assert data["stale_open_issues"] == 1
    finally:
        await engine.dispose()


@pytest.mark.asyncio
async def test_forgejo_metrics_empty_scope_returns_zeroes() -> None:
    engine = await _make_engine()
    session_maker = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
    organization = Organization(id=uuid4(), name="Pipeline")
    member = OrganizationMember(
        id=uuid4(),
        organization_id=organization.id,
        user_id=uuid4(),
        role="owner",
    )
    app = _build_test_app(
        session_maker,
        OrganizationContext(organization=organization, member=member),
    )

    try:
        async with session_maker() as session:
            session.add(organization)
            await session.commit()

        async with AsyncClient(
            transport=ASGITransport(app=app),
            base_url="http://testserver",
        ) as client:
            response = await client.get("/api/v1/forgejo/metrics")

        assert response.status_code == 200
        data = response.json()
        assert data["open_issues"] == 0
        assert data["closed_issues"] == 0
        assert data["closed_in_selected_range"] == 0
        assert data["stale_open_issues"] == 0
        assert data["repositories_synced"] == 0
        assert data["repository_sync_error_count"] == 0
        assert data["last_sync_timestamps"] == {}
        assert data["sync_error_counts"] == {}
    finally:
        await engine.dispose()


@pytest.mark.asyncio
async def test_forgejo_metrics_board_and_repo_filters_exclude_pull_requests() -> None:
    engine = await _make_engine()
    session_maker = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
    organization = Organization(id=uuid4(), name="Pipeline")
    member = OrganizationMember(
        id=uuid4(),
        organization_id=organization.id,
        user_id=uuid4(),
        role="owner",
    )
    app = _build_test_app(
        session_maker,
        OrganizationContext(organization=organization, member=member),
    )
    connection = ForgejoConnection(
        id=uuid4(),
        organization_id=organization.id,
        name="Dream Forgejo",
        base_url="https://forgejo.example.local",
    )
    repo_a = ForgejoRepository(
        id=uuid4(),
        organization_id=organization.id,
        connection_id=connection.id,
        owner="openclaw",
        repo="pipeline-a",
        display_name="Pipeline A",
        last_sync_at=utcnow(),
    )
    repo_b = ForgejoRepository(
        id=uuid4(),
        organization_id=organization.id,
        connection_id=connection.id,
        owner="openclaw",
        repo="pipeline-b",
        display_name="Pipeline B",
        last_sync_error="Sync failed",
    )
    board = Board(
        id=uuid4(),
        organization_id=organization.id,
        name="Pipeline Board",
        slug="pipeline-board",
    )
    link = BoardRepositoryLink(
        id=uuid4(),
        organization_id=organization.id,
        board_id=board.id,
        repository_id=repo_a.id,
    )
    now = utcnow()

    open_issue = _issue(
        organization_id=organization.id,
        repository_id=repo_a.id,
        issue_number=1,
    )
    closed_issue = _issue(
        organization_id=organization.id,
        repository_id=repo_a.id,
        issue_number=2,
    )
    closed_issue.state = "closed"
    closed_issue.forgejo_closed_at = now - timedelta(days=3)
    closed_issue.forgejo_updated_at = now - timedelta(days=3)

    stale_issue = _issue(
        organization_id=organization.id,
        repository_id=repo_a.id,
        issue_number=3,
    )
    stale_issue.forgejo_updated_at = now - timedelta(days=20)

    pull_request = _issue(
        organization_id=organization.id,
        repository_id=repo_a.id,
        issue_number=4,
    )
    pull_request.is_pull_request = True
    pull_request.state = "closed"
    pull_request.forgejo_closed_at = now - timedelta(days=1)

    repo_b_issue = _issue(
        organization_id=organization.id,
        repository_id=repo_b.id,
        issue_number=5,
    )
    repo_b_issue.state = "closed"
    repo_b_issue.forgejo_closed_at = now - timedelta(days=2)

    try:
        async with session_maker() as session:
            session.add(organization)
            session.add(connection)
            session.add(repo_a)
            session.add(repo_b)
            session.add(board)
            session.add(link)
            session.add(open_issue)
            session.add(closed_issue)
            session.add(stale_issue)
            session.add(pull_request)
            session.add(repo_b_issue)
            await session.commit()

        async with AsyncClient(
            transport=ASGITransport(app=app),
            base_url="http://testserver",
        ) as client:
            board_metrics = await client.get(f"/api/v1/forgejo/metrics?board_id={board.id}")
            repo_metrics = await client.get(f"/api/v1/forgejo/metrics?repository_id={repo_a.id}")
            org_metrics = await client.get(
                f"/api/v1/forgejo/metrics?organization_id={organization.id}&closed_range_days=14"
            )

        assert board_metrics.status_code == 200
        board_data = board_metrics.json()
        assert board_data["open_issues"] == 2
        assert board_data["closed_issues"] == 1
        assert board_data["stale_open_issues"] == 1
        assert board_data["closed_in_selected_range"] == 1
        assert board_data["repository_sync_error_count"] == 0

        assert repo_metrics.status_code == 200
        repo_data = repo_metrics.json()
        assert repo_data["open_issues"] == 2
        assert repo_data["closed_issues"] == 1
        assert repo_data["closed_in_selected_range"] == 1
        assert repo_data["stale_open_issues"] == 1

        assert org_metrics.status_code == 200
        org_data = org_metrics.json()
        assert org_data["open_issues"] == 2
        assert org_data["closed_issues"] == 2
        assert org_data["selected_range_days"] == 14
        assert org_data["closed_in_selected_range"] == 2
        assert org_data["repository_sync_error_count"] == 1
    finally:
        await engine.dispose()
