"""Tests for crawl source route delegation."""

from __future__ import annotations

import sys
from contextlib import asynccontextmanager
from datetime import UTC, datetime
from types import SimpleNamespace
from unittest.mock import AsyncMock, patch
from uuid import UUID

import pytest
from fastapi import HTTPException

from sibyl.api.event_types import WSEvent
from sibyl.api.routes import crawler as crawler_module
from sibyl.api.routes.crawler import create_source, get_health, list_sources
from sibyl.api.schemas import CrawlSourceCreate
from sibyl.crawler.service import SourceAlreadyExistsError
from sibyl_core.models import CrawlStatus, SourceType


def _make_source() -> SimpleNamespace:
    return SimpleNamespace(
        id=UUID("00000000-0000-0000-0000-000000000123"),
        name="Docs",
        url="https://docs.example.com",
        source_type=SourceType.WEBSITE,
        description="Reference docs",
        crawl_depth=3,
        crawl_status=CrawlStatus.PENDING,
        document_count=8,
        chunk_count=21,
        last_crawled_at=None,
        last_error=None,
        created_at=datetime(2026, 4, 14, 3, 30, tzinfo=UTC),
        include_patterns=["/reference/.*"],
        exclude_patterns=["/blog/.*"],
    )


class TestCrawlSourceRoutes:
    @pytest.mark.asyncio
    async def test_get_health_skips_relational_probe_in_fully_surreal_mode(
        self, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        check_relational_backend_health = AsyncMock(
            side_effect=AssertionError("relational probe should stay off")
        )

        monkeypatch.setattr(crawler_module.settings, "store", "surreal")
        monkeypatch.setattr(crawler_module.settings, "auth_store", "surreal")
        monkeypatch.setattr(
            crawler_module,
            "check_relational_backend_health",
            check_relational_backend_health,
        )
        monkeypatch.setitem(sys.modules, "crawl4ai", SimpleNamespace(AsyncWebCrawler=object))

        response = await get_health()

        check_relational_backend_health.assert_not_awaited()
        assert response.relational_backend_enabled is False
        assert response.relational_backend_healthy is True
        assert response.relational_backend_version is None
        assert response.vector_extension_version is None
        assert response.error is None
        assert response.crawl4ai_available is True

    @pytest.mark.asyncio
    async def test_create_source_delegates_to_service_and_broadcasts(self) -> None:
        org = SimpleNamespace(id=UUID("00000000-0000-0000-0000-000000000999"))
        request = CrawlSourceCreate(
            name="Docs",
            url="https://docs.example.com/",
            source_type="website",
            description="Reference docs",
            crawl_depth=3,
            include_patterns=["/reference/.*"],
            exclude_patterns=["/blog/.*"],
        )
        source = _make_source()
        broadcast = AsyncMock()

        @asynccontextmanager
        async def mock_content_session():
            yield None

        with (
            patch(
                "sibyl.api.routes.crawler.get_content_read_session",
                mock_content_session,
            ),
            patch(
                "sibyl.api.routes.crawler.create_crawl_source_record",
                AsyncMock(return_value=source),
            ) as create_record,
            patch("sibyl.api.routes.crawler.broadcast_event", broadcast),
        ):
            response = await create_source(request=request, org=org)

        create_record.assert_awaited_once_with(
            None,
            name="Docs",
            url="https://docs.example.com/",
            organization_id=org.id,
            source_type=SourceType.WEBSITE,
            description="Reference docs",
            crawl_depth=3,
            include_patterns=["/reference/.*"],
            exclude_patterns=["/blog/.*"],
        )
        broadcast.assert_awaited_once_with(
            WSEvent.ENTITY_CREATED,
            {"type": "crawl_source", "id": str(source.id)},
            org_id=str(org.id),
        )
        assert response.model_dump() == {
            "id": str(source.id),
            "name": "Docs",
            "url": "https://docs.example.com",
            "source_type": "website",
            "description": "Reference docs",
            "crawl_depth": 3,
            "crawl_status": "pending",
            "document_count": 8,
            "chunk_count": 21,
            "last_crawled_at": None,
            "last_error": None,
            "created_at": datetime(2026, 4, 14, 3, 30, tzinfo=UTC),
            "include_patterns": ["/reference/.*"],
            "exclude_patterns": ["/blog/.*"],
        }

    @pytest.mark.asyncio
    async def test_create_source_maps_duplicate_to_conflict(self) -> None:
        org = SimpleNamespace(id=UUID("00000000-0000-0000-0000-000000000999"))
        request = CrawlSourceCreate(name="Docs", url="https://docs.example.com/")

        @asynccontextmanager
        async def mock_content_session():
            yield None

        with (
            patch(
                "sibyl.api.routes.crawler.get_content_read_session",
                mock_content_session,
            ),
            patch(
                "sibyl.api.routes.crawler.create_crawl_source_record",
                AsyncMock(side_effect=SourceAlreadyExistsError("https://docs.example.com")),
            ),
            pytest.raises(HTTPException) as exc_info,
        ):
            await create_source(request=request, org=org)

        assert exc_info.value.status_code == 409
        assert exc_info.value.detail == "Source with URL https://docs.example.com/ already exists"

    @pytest.mark.asyncio
    async def test_list_sources_delegates_org_filter_and_maps_response(self) -> None:
        org = SimpleNamespace(id=UUID("00000000-0000-0000-0000-000000000999"))

        @asynccontextmanager
        async def mock_content_session():
            yield None

        with (
            patch(
                "sibyl.api.routes.crawler.get_content_read_session",
                mock_content_session,
            ),
            patch(
                "sibyl.api.routes.crawler.list_crawl_sources_for_org",
                AsyncMock(return_value=([_make_source()], 7)),
            ) as list_sources_for_org,
        ):
            response = await list_sources(status="pending", limit=25, org=org)

        list_sources_for_org.assert_awaited_once_with(
            None,
            organization_id=org.id,
            status=CrawlStatus.PENDING,
            limit=25,
        )
        assert response.total == 7
        assert len(response.sources) == 1
        assert response.sources[0].model_dump() == {
            "id": "00000000-0000-0000-0000-000000000123",
            "name": "Docs",
            "url": "https://docs.example.com",
            "source_type": "website",
            "description": "Reference docs",
            "crawl_depth": 3,
            "crawl_status": "pending",
            "document_count": 8,
            "chunk_count": 21,
            "last_crawled_at": None,
            "last_error": None,
            "created_at": datetime(2026, 4, 14, 3, 30, tzinfo=UTC),
            "include_patterns": ["/reference/.*"],
            "exclude_patterns": ["/blog/.*"],
        }
