"""Tests for tome service layer."""

import hashlib
from uuid import uuid4

import pytest

from sensei.database import storage
from sensei.tome.chunker import chunk_markdown
from sensei.tome.crawler import flatten_section_tree
from sensei.tome.service import tome_get, tome_search
from sensei.types import NoResults, SearchResult, Success, ToolError


def _hash(content: str) -> str:
    """Generate content hash for testing."""
    return hashlib.sha256(content.encode()).hexdigest()[:16]


@pytest.fixture
async def sample_docs(test_db):
    """Create sample documents for testing using generation-based API."""
    generation_id = uuid4()

    docs_data = [
        {
            "domain": "llmstext.org",
            "url": "https://llmstext.org/llms.txt",
            "path": "/llms.txt",
            "content": "# React Documentation Index\n\n- [Hooks](/hooks)\n- [Components](/components)",
        },
        {
            "domain": "llmstext.org",
            "url": "https://llmstext.org/hooks/useState",
            "path": "/hooks/useState",
            "content": "# useState\n\nuseState is a React Hook that lets you add state to functional components.",
        },
        {
            "domain": "llmstext.org",
            "url": "https://llmstext.org/hooks/useEffect",
            "path": "/hooks/useEffect",
            "content": "# useEffect\n\nuseEffect is a React Hook for side effects and synchronization.",
        },
        {
            "domain": "llmstext.org",
            "url": "https://llmstext.org/components/button",
            "path": "/components/button",
            "content": "# Button Component\n\nA reusable button component for React applications.",
        },
    ]

    for doc in docs_data:
        doc_id = await storage.insert_document(
            domain=doc["domain"],
            url=doc["url"],
            path=doc["path"],
            content_hash=_hash(doc["content"]),
            generation_id=generation_id,
        )
        section_tree = chunk_markdown(doc["content"])
        sections = flatten_section_tree(section_tree, doc_id)
        await storage.insert_sections(sections)

    # Activate the generation to make documents visible
    await storage.activate_generation("llmstext.org", generation_id)

    return docs_data


# =============================================================================
# tome_get tests
# =============================================================================


@pytest.mark.asyncio
async def test_tome_get_index_sentinel(sample_docs):
    """Test INDEX sentinel returns /llms.txt content."""
    result = await tome_get("llmstext.org", "INDEX")
    assert isinstance(result, Success)
    assert "React Documentation Index" in result.data


@pytest.mark.asyncio
async def test_tome_get_specific_path(sample_docs):
    """Test fetching a specific document path."""
    result = await tome_get("llmstext.org", "/hooks/useState")
    assert isinstance(result, Success)
    assert "useState" in result.data
    assert "React Hook" in result.data


@pytest.mark.asyncio
async def test_tome_get_path_without_slash(sample_docs):
    """Test that path without leading slash still works."""
    result = await tome_get("llmstext.org", "hooks/useState")
    assert isinstance(result, Success)
    assert "useState" in result.data


@pytest.mark.asyncio
async def test_tome_get_not_found(sample_docs):
    """Test that missing document returns NoResults."""
    result = await tome_get("llmstext.org", "/nonexistent")
    assert isinstance(result, NoResults)


@pytest.mark.asyncio
async def test_tome_get_domain_not_ingested(test_db):
    """Test that non-ingested domain returns NoResults."""
    result = await tome_get("unknown.com", "INDEX")
    assert isinstance(result, NoResults)


# =============================================================================
# tome_search tests
# =============================================================================


@pytest.mark.asyncio
async def test_tome_search_finds_matching_content(sample_docs):
    """Test basic FTS search finds matching documents."""
    result = await tome_search("llmstext.org", "Hook")
    assert isinstance(result, Success)
    # Should find useState and useEffect (both mention "Hook")
    assert len(result.data) >= 2
    assert all(isinstance(r, SearchResult) for r in result.data)


@pytest.mark.asyncio
async def test_tome_search_with_path_filter(sample_docs):
    """Test search respects path prefix filter."""
    result = await tome_search("llmstext.org", "React", paths=["/hooks"])
    assert isinstance(result, Success)
    # Should only find docs under /hooks
    assert all("/hooks" in r.path for r in result.data)


@pytest.mark.asyncio
async def test_tome_search_empty_paths_searches_all(sample_docs):
    """Test empty paths list searches all documents."""
    result = await tome_search("llmstext.org", "React", paths=[])
    assert isinstance(result, Success)
    # Should find multiple docs mentioning React
    assert len(result.data) >= 1


@pytest.mark.asyncio
async def test_tome_search_no_results(sample_docs):
    """Test search with no matches returns NoResults."""
    result = await tome_search("llmstext.org", "nonexistentterm12345")
    assert isinstance(result, NoResults)


@pytest.mark.asyncio
async def test_tome_search_empty_query_raises(sample_docs):
    """Test empty query raises ToolError."""
    with pytest.raises(ToolError):
        await tome_search("llmstext.org", "")


@pytest.mark.asyncio
async def test_tome_search_whitespace_query_raises(sample_docs):
    """Test whitespace-only query raises ToolError."""
    with pytest.raises(ToolError):
        await tome_search("llmstext.org", "   ")


@pytest.mark.asyncio
async def test_tome_search_returns_snippets(sample_docs):
    """Test search results include snippets."""
    result = await tome_search("llmstext.org", "useState")
    assert isinstance(result, Success)
    assert len(result.data) >= 1
    # Snippets should contain some content
    assert all(r.snippet for r in result.data)


@pytest.mark.asyncio
async def test_tome_search_respects_limit(sample_docs):
    """Test search respects limit parameter."""
    result = await tome_search("llmstext.org", "React", limit=1)
    assert isinstance(result, Success)
    assert len(result.data) == 1


# =============================================================================
# Integration tests with real domain
# =============================================================================


@pytest.mark.asyncio
@pytest.mark.integration
async def test_ingest_real_domain_llmstxt_org(test_db):
    """Integration test: ingest llmstxt.org and verify search works.

    This test crawls the real llmstxt.org site to verify the full
    ingest -> get -> search flow works end-to-end.
    """
    from sensei.tome.crawler import ingest_domain

    # Ingest llmstxt.org (small site, fast to crawl)
    result = await ingest_domain("llmstxt.org", max_depth=1)

    assert isinstance(result, Success)
    ingest_result = result.data
    assert ingest_result.domain == "llmstxt.org"
    # Should have at least the llms.txt and some linked docs
    assert ingest_result.documents_added >= 1, f"Expected at least 1 document, got {ingest_result.documents_added}"

    # Test tome_get with INDEX sentinel
    get_result = await tome_get("llmstxt.org", "INDEX")
    assert isinstance(get_result, Success), f"Expected Success, got {get_result}"
    assert "llms.txt" in get_result.data.lower()

    # Test tome_search
    search_result = await tome_search("llmstxt.org", "llm")
    assert isinstance(search_result, Success), f"Expected Success, got {search_result}"
    assert len(search_result.data) >= 1


@pytest.mark.asyncio
@pytest.mark.integration
async def test_ingest_crawlee_dev_python(test_db):
    """Integration test: ingest Crawlee's Python docs and verify search.

    This tests a different domain to avoid any caching/state issues between tests.
    """
    from sensei.tome.crawler import ingest_domain

    # Ingest crawlee.dev/python (small site, has llms.txt)
    result = await ingest_domain("crawlee.dev/python", max_depth=0)

    assert isinstance(result, Success)
    ingest_result = result.data
    # Should have ingested at least the llms.txt
    assert ingest_result.documents_added >= 1, f"Expected at least 1 document, got {ingest_result.documents_added}"

    # Test tome_get with INDEX sentinel
    # May be NoResults if domain normalization differs - that's OK for this test
    # The key test is that the crawl worked
    await tome_get("crawlee.dev", "INDEX")
