"""Tests for the file-based document comparison endpoint and service method.

Covers:
- compare_files() service: text extraction, diff, error on empty text
- POST /documents/compare-files router: validation, file extensions, response schema
"""

from __future__ import annotations

import time
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
from httpx import ASGITransport, AsyncClient

pytestmark = pytest.mark.asyncio

TEST_ORG = "test-org"


def _make_service():
    """Create a RagService with all internal dependencies pre-mocked.

    Pre-seeds the per-org client cache for `TEST_ORG` so compare_files
    doesn't trigger the lazy-init / provider-catalog path.
    """
    from app.services.rag_service import RagService, _OrgClients

    service = RagService()
    service.initialized = True
    service._pool = MagicMock()
    service._pinned_dims = 1536

    embedding = AsyncMock()
    embedding.dimensions = 1536
    openai_client = AsyncMock()
    vision_client = MagicMock()
    search_service = AsyncMock()

    service._org_clients[TEST_ORG] = _OrgClients(
        llm_config={
            "model": "gpt-test",
            "embedding_model": "embed-test",
            "api_key": "k",
            "base_url": "http://test",
            "embedding_api_key": "k",
            "embedding_base_url": "http://test",
        },
        vision_config=None,
        embedding_service=embedding,
        openai_client=openai_client,
        vision_client=vision_client,
        search_service=search_service,
        last_check=time.monotonic(),
    )
    # Back-compat aliases for tests that grab mocks off the service.
    service._search_service = search_service
    service._openai_client = openai_client
    service._embedding_service = embedding
    service._vision_client = vision_client
    return service


class TestCompareFilesService:
    """compare_files() extracts text from bytes and diffs."""

    async def test_returns_diff_result(self):
        service = _make_service()

        async def mock_extract(content_bytes, filename, *, vision_client=None):
            text = content_bytes.decode("utf-8")
            return text, False

        with patch("tale_knowledge.extraction.extract_text", side_effect=mock_extract):
            result = await service.compare_files(
                TEST_ORG,
                b"Section 1\n\nOriginal clause.",
                "base.txt",
                b"Section 1\n\nModified clause.",
                "comparison.txt",
            )

        assert result["success"] is True
        assert result["base_document"]["title"] == "base.txt"
        assert result["comparison_document"]["title"] == "comparison.txt"
        assert "change_blocks" in result
        assert "stats" in result

    async def test_identical_files_no_changes(self):
        service = _make_service()
        content = b"Identical content.\n\nParagraph two."

        async def mock_extract(content_bytes, filename, *, vision_client=None):
            return content_bytes.decode("utf-8"), False

        with patch("tale_knowledge.extraction.extract_text", side_effect=mock_extract):
            result = await service.compare_files(
                TEST_ORG,
                content,
                "a.txt",
                content,
                "b.txt",
            )

        assert result["success"] is True
        assert result["stats"]["modified"] == 0
        assert result["stats"]["added"] == 0
        assert result["stats"]["deleted"] == 0

    async def test_raises_on_empty_base_text(self):
        service = _make_service()

        async def mock_extract(content_bytes, filename, *, vision_client=None):
            if filename == "empty.txt":
                return "", False
            return "some text", False

        with (
            patch("tale_knowledge.extraction.extract_text", side_effect=mock_extract),
            pytest.raises(ValueError, match="No text could be extracted from base file"),
        ):
            await service.compare_files(
                TEST_ORG,
                b"",
                "empty.txt",
                b"content",
                "comparison.txt",
            )

    async def test_raises_on_empty_comparison_text(self):
        service = _make_service()

        async def mock_extract(content_bytes, filename, *, vision_client=None):
            if filename == "empty.txt":
                return "", False
            return "some text", False

        with (
            patch("tale_knowledge.extraction.extract_text", side_effect=mock_extract),
            pytest.raises(ValueError, match="No text could be extracted from comparison file"),
        ):
            await service.compare_files(
                TEST_ORG,
                b"content",
                "base.txt",
                b"",
                "empty.txt",
            )

    async def test_parallel_extraction(self):
        """Both files should be extracted concurrently via asyncio.gather."""
        import asyncio

        service = _make_service()
        call_order: list[str] = []

        async def mock_extract(content_bytes, filename, *, vision_client=None):
            call_order.append(f"start:{filename}")
            await asyncio.sleep(0.01)
            call_order.append(f"end:{filename}")
            return content_bytes.decode("utf-8"), False

        with patch("tale_knowledge.extraction.extract_text", side_effect=mock_extract):
            result = await service.compare_files(
                TEST_ORG,
                b"Section 1\n\nParagraph A.",
                "base.txt",
                b"Section 1\n\nParagraph B.",
                "comp.txt",
            )

        assert result["success"] is True
        # Both extractions should start before either finishes (parallel)
        assert call_order[0].startswith("start:")
        assert call_order[1].startswith("start:")

    async def test_max_changes_forwarded(self):
        service = _make_service()

        async def mock_extract(content_bytes, filename, *, vision_client=None):
            return content_bytes.decode("utf-8"), False

        base = "\n\n".join(f"base {i}" for i in range(20))
        comp = "\n\n".join(f"comp {i}" for i in range(20))

        with patch("tale_knowledge.extraction.extract_text", side_effect=mock_extract):
            result = await service.compare_files(
                TEST_ORG,
                base.encode(),
                "base.txt",
                comp.encode(),
                "comp.txt",
                max_changes=3,
            )

        assert result["success"] is True
        total_items = sum(
            len([i for i in block["items"] if i["type"] != "context"]) for block in result["change_blocks"]
        )
        assert total_items <= 3


class TestCompareFilesEndpoint:
    """POST /api/v1/documents/compare-files endpoint tests."""

    @staticmethod
    def _get_app():
        from app.auth import verify_auth_token
        from app.main import app

        # Override the auth dependency for the test client. The router
        # ships behind `Depends(verify_auth_token)` (round-2 v15 fix);
        # these tests assert business logic, not auth wiring.
        app.dependency_overrides[verify_auth_token] = lambda: None
        return app

    async def test_happy_path(self):
        mock_result = {
            "success": True,
            "base_document": {"file_id": "base.txt", "title": "base.txt"},
            "comparison_document": {"file_id": "comp.txt", "title": "comp.txt"},
            "change_blocks": [],
            "stats": {
                "total_paragraphs_base": 2,
                "total_paragraphs_comparison": 2,
                "unchanged": 2,
                "modified": 0,
                "added": 0,
                "deleted": 0,
                "high_divergence": False,
            },
            "truncated": False,
        }

        with patch("app.routers.documents.rag_service") as mock_svc:
            mock_svc.compare_files = AsyncMock(return_value=mock_result)

            transport = ASGITransport(app=self._get_app())
            async with AsyncClient(transport=transport, base_url="http://test") as client:
                response = await client.post(
                    "/api/v1/documents/compare-files",
                    headers={"X-Tale-Org": TEST_ORG},
                    files={
                        "base_file": ("base.txt", b"Hello\n\nWorld", "text/plain"),
                        "comparison_file": ("comp.txt", b"Hello\n\nEarth", "text/plain"),
                    },
                )

        assert response.status_code == 200
        data = response.json()
        assert data["success"] is True
        assert data["base_document"]["title"] == "base.txt"

    async def test_unsupported_extension(self):
        with patch("app.routers.documents.rag_service"):
            transport = ASGITransport(app=self._get_app())
            async with AsyncClient(transport=transport, base_url="http://test") as client:
                response = await client.post(
                    "/api/v1/documents/compare-files",
                    headers={"X-Tale-Org": TEST_ORG},
                    files={
                        "base_file": ("base.exe", b"binary", "application/octet-stream"),
                        "comparison_file": ("comp.txt", b"text", "text/plain"),
                    },
                )

        assert response.status_code == 400
        assert "Unsupported file type" in response.json()["message"]

    async def test_extraction_failure_returns_422(self):
        with patch("app.routers.documents.rag_service") as mock_svc:
            mock_svc.compare_files = AsyncMock(
                side_effect=ValueError("No text could be extracted from base file: empty.pdf"),
            )

            transport = ASGITransport(app=self._get_app())
            async with AsyncClient(transport=transport, base_url="http://test") as client:
                response = await client.post(
                    "/api/v1/documents/compare-files",
                    headers={"X-Tale-Org": TEST_ORG},
                    files={
                        "base_file": ("empty.pdf", b"fake-pdf", "application/pdf"),
                        "comparison_file": ("comp.pdf", b"fake-pdf", "application/pdf"),
                    },
                )

        assert response.status_code == 422
        assert "No text could be extracted" in response.json()["message"]
