#!/usr/bin/env python3
"""
Module 10 — Telnyx KB Sync Integration Tests
=============================================
All external calls are fully mocked.  No real network traffic, Qdrant,
or Telnyx API calls are made.

Test summary (14 tests across 3 classes):

Class TestTelnyxUpload (4 tests — black-box):
  BB  test_upload_document           — successful upload → response with data dict
  BB  test_upload_error_handled      — HTTP 500 → error dict returned (no raise)
  BB  test_list_documents            — list → returns docs list from "data" key
  BB  test_delete_document           — HTTP 204 delete → returns True

Class TestSyncKbToAssistant (6 tests — black-box):
  BB  test_sync_creates_documents    — 10 chunks from 3 URLs → 3 docs uploaded
  BB  test_sync_max_chunks           — max_chunks=5 → top_k=5 in search call
  BB  test_sync_empty_platform       — 0 chunks returned → 0 docs, no errors
  BB  test_sync_error_in_upload      — 1 of 3 uploads fails → errors=1, synced=2
  BB  test_sync_stats_accurate       — stat keys and values match actual ops
  BB  test_customer_scoped           — customer_id forwarded to search_platform

Class TestDocumentFormatting (4 tests — white-box):
  WB  test_chunks_grouped_by_url     — same URL chunks → single group
  WB  test_document_title_from_first_chunk  — title = first chunk's title field
  WB  test_document_content_ordered  — content joined in chunk_index order
  WB  test_url_as_fallback_title     — missing title → URL used as title
"""

from __future__ import annotations

import asyncio
import sys
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

# ── Project root on sys.path ─────────────────────────────────────────────────
_PROJECT_ROOT = "/mnt/e/genesis-system"
if _PROJECT_ROOT not in sys.path:
    sys.path.insert(0, _PROJECT_ROOT)

from core.kb.telnyx_sync import (
    _build_document,
    _group_chunks_by_url,
    sync_kb_to_assistant,
    telnyx_delete_document,
    telnyx_list_documents,
    telnyx_upload_document,
)

# ─────────────────────────────────────────────────────────────────────────────
# Helpers
# ─────────────────────────────────────────────────────────────────────────────

def _run(coro):
    """Execute a coroutine synchronously (compatible with all pytest versions)."""
    return asyncio.get_event_loop().run_until_complete(coro)


def _make_chunks(
    n: int,
    platform: str = "telnyx",
    *,
    urls: list[str] | None = None,
) -> list[dict]:
    """
    Build n fake chunk dicts.

    If ``urls`` is provided, chunks are distributed round-robin across them.
    Otherwise every chunk gets its own unique URL.
    """
    chunks = []
    for i in range(n):
        url = urls[i % len(urls)] if urls else f"https://example.com/page-{i}"
        chunks.append(
            {
                "chunk_id": f"chunk-{i}",
                "platform": platform,
                "source_url": url,
                "title": f"Title for {url}",
                "text": f"Content chunk {i}",
                "heading_context": "",
                "chunk_index": i,
                "score": 0.95,
            }
        )
    return chunks


def _mock_http_response(status_code: int, json_body: Any) -> MagicMock:
    """Return a mock httpx.Response object."""
    resp = MagicMock()
    resp.status_code = status_code
    resp.json.return_value = json_body
    resp.text = str(json_body)
    return resp


# ─────────────────────────────────────────────────────────────────────────────
# Class 1 — TestTelnyxUpload (4 BB tests)
# ─────────────────────────────────────────────────────────────────────────────


class TestTelnyxUpload:
    """Black-box tests for the three Telnyx HTTP wrapper functions."""

    def test_upload_document(self):
        """BB: Successful POST → returns response dict containing doc data."""
        api_resp = {"data": {"id": "doc-abc123", "title": "My Doc"}}
        mock_resp = _mock_http_response(201, api_resp)

        # Patch httpx.AsyncClient so no real HTTP is made
        mock_client = AsyncMock()
        mock_client.__aenter__ = AsyncMock(return_value=mock_client)
        mock_client.__aexit__ = AsyncMock(return_value=False)
        mock_client.post = AsyncMock(return_value=mock_resp)

        with patch("core.kb.telnyx_sync.httpx.AsyncClient", return_value=mock_client):
            result = _run(
                telnyx_upload_document(
                    assistant_id="asst-001",
                    title="My Doc",
                    content="Some content here.",
                )
            )

        assert "data" in result
        assert result["data"]["id"] == "doc-abc123"
        assert "error" not in result

    def test_upload_error_handled(self):
        """BB: HTTP 500 → returns error dict, does not raise."""
        mock_resp = _mock_http_response(500, {"detail": "Internal Server Error"})

        mock_client = AsyncMock()
        mock_client.__aenter__ = AsyncMock(return_value=mock_client)
        mock_client.__aexit__ = AsyncMock(return_value=False)
        mock_client.post = AsyncMock(return_value=mock_resp)

        with patch("core.kb.telnyx_sync.httpx.AsyncClient", return_value=mock_client):
            result = _run(
                telnyx_upload_document(
                    assistant_id="asst-001",
                    title="Fail Doc",
                    content="content",
                )
            )

        assert "error" in result
        assert result["status_code"] == 500

    def test_list_documents(self):
        """BB: GET list → returns docs list extracted from 'data' wrapper key."""
        docs = [
            {"id": "doc-1", "title": "Doc One"},
            {"id": "doc-2", "title": "Doc Two"},
        ]
        mock_resp = _mock_http_response(200, {"data": docs})

        mock_client = AsyncMock()
        mock_client.__aenter__ = AsyncMock(return_value=mock_client)
        mock_client.__aexit__ = AsyncMock(return_value=False)
        mock_client.get = AsyncMock(return_value=mock_resp)

        with patch("core.kb.telnyx_sync.httpx.AsyncClient", return_value=mock_client):
            result = _run(telnyx_list_documents("asst-001"))

        assert isinstance(result, list)
        assert len(result) == 2
        assert result[0]["id"] == "doc-1"

    def test_delete_document(self):
        """BB: HTTP 204 DELETE → returns True."""
        mock_resp = _mock_http_response(204, {})

        mock_client = AsyncMock()
        mock_client.__aenter__ = AsyncMock(return_value=mock_client)
        mock_client.__aexit__ = AsyncMock(return_value=False)
        mock_client.delete = AsyncMock(return_value=mock_resp)

        with patch("core.kb.telnyx_sync.httpx.AsyncClient", return_value=mock_client):
            result = _run(telnyx_delete_document("asst-001", "doc-xyz"))

        assert result is True


# ─────────────────────────────────────────────────────────────────────────────
# Class 2 — TestSyncKbToAssistant (6 BB tests)
# ─────────────────────────────────────────────────────────────────────────────


class TestSyncKbToAssistant:
    """Black-box tests for the end-to-end sync_kb_to_assistant function."""

    def _patch_context(self, chunks: list[dict], upload_side_effect=None):
        """
        Return a dict of patches suitable for use with `with patch(...) as ...`.

        All heavy dependencies are mocked:
        - embed_text → returns a fake 3072-dim vector
        - search_platform → returns ``chunks``
        - telnyx_upload_document → returns success by default (or side_effect)
        """
        fake_vector = [0.1] * 3072

        if upload_side_effect is None:
            upload_resp = AsyncMock(return_value={"data": {"id": "doc-ok"}})
        else:
            upload_resp = AsyncMock(side_effect=upload_side_effect)

        return {
            "embed": patch(
                "core.kb.telnyx_sync.embed_text",
                return_value=fake_vector,
            ),
            "search": patch(
                "core.kb.telnyx_sync.search_platform",
                return_value=chunks,
            ),
            "upload": patch(
                "core.kb.telnyx_sync.telnyx_upload_document",
                new=upload_resp,
            ),
        }

    # ── BB 1 ─────────────────────────────────────────────────────────────────

    def test_sync_creates_documents(self):
        """BB: 10 chunks across 3 URLs → 3 documents uploaded, 3 synced."""
        three_urls = [
            "https://ex.com/page-a",
            "https://ex.com/page-b",
            "https://ex.com/page-c",
        ]
        chunks = _make_chunks(10, platform="telnyx", urls=three_urls)
        patches = self._patch_context(chunks)

        with patches["embed"], patches["search"], patches["upload"] as mock_upload:
            result = _run(
                sync_kb_to_assistant(
                    platform="telnyx",
                    assistant_id="asst-001",
                )
            )

        assert result["documents_synced"] == 3
        assert result["chunks_total"] == 10
        assert result["errors"] == 0
        assert mock_upload.call_count == 3

    # ── BB 2 ─────────────────────────────────────────────────────────────────

    def test_sync_max_chunks(self):
        """BB: max_chunks=5 → search_platform called with top_k=5."""
        chunks = _make_chunks(3, platform="telnyx")
        patches = self._patch_context(chunks)

        with patches["embed"], patches["search"] as mock_search, patches["upload"]:
            _run(
                sync_kb_to_assistant(
                    platform="telnyx",
                    assistant_id="asst-001",
                    max_chunks=5,
                )
            )

        _call_kwargs = mock_search.call_args[1] if mock_search.call_args[1] else {}
        _call_args = mock_search.call_args[0] if mock_search.call_args[0] else ()
        # top_k may be positional or keyword
        all_args = list(_call_args) + list(_call_kwargs.values())
        assert 5 in all_args or _call_kwargs.get("top_k") == 5

    # ── BB 3 ─────────────────────────────────────────────────────────────────

    def test_sync_empty_platform(self):
        """BB: 0 chunks returned → 0 documents synced, 0 errors."""
        patches = self._patch_context([])

        with patches["embed"], patches["search"], patches["upload"] as mock_upload:
            result = _run(
                sync_kb_to_assistant(
                    platform="telnyx",
                    assistant_id="asst-001",
                )
            )

        assert result["documents_synced"] == 0
        assert result["chunks_total"] == 0
        assert result["errors"] == 0
        mock_upload.assert_not_called()

    # ── BB 4 ─────────────────────────────────────────────────────────────────

    def test_sync_error_in_upload(self):
        """BB: 1 of 3 uploads fails → errors=1, documents_synced=2."""
        three_urls = [
            "https://ex.com/page-a",
            "https://ex.com/page-b",
            "https://ex.com/page-c",
        ]
        chunks = _make_chunks(9, platform="telnyx", urls=three_urls)

        # Fail on the second call (page-b), succeed otherwise
        call_counter = {"n": 0}

        async def upload_with_failure(*args, **kwargs):
            call_counter["n"] += 1
            if call_counter["n"] == 2:
                return {"error": "Upload rejected", "status_code": 422}
            return {"data": {"id": f"doc-{call_counter['n']}"}}

        patches = {
            "embed": patch("core.kb.telnyx_sync.embed_text", return_value=[0.1] * 3072),
            "search": patch("core.kb.telnyx_sync.search_platform", return_value=chunks),
            "upload": patch(
                "core.kb.telnyx_sync.telnyx_upload_document",
                new=AsyncMock(side_effect=upload_with_failure),
            ),
        }

        with patches["embed"], patches["search"], patches["upload"]:
            result = _run(
                sync_kb_to_assistant(
                    platform="telnyx",
                    assistant_id="asst-001",
                )
            )

        assert result["documents_synced"] == 2
        assert result["errors"] == 1
        assert len(result["error_details"]) == 1

    # ── BB 5 ─────────────────────────────────────────────────────────────────

    def test_sync_stats_accurate(self):
        """BB: Returned stats dict contains all required keys with correct types."""
        chunks = _make_chunks(4, platform="telnyx", urls=["https://ex.com/p"])
        patches = self._patch_context(chunks)

        with patches["embed"], patches["search"], patches["upload"]:
            result = _run(
                sync_kb_to_assistant(
                    platform="telnyx",
                    assistant_id="asst-test",
                )
            )

        required_keys = {
            "platform",
            "assistant_id",
            "documents_synced",
            "chunks_total",
            "errors",
            "error_details",
        }
        assert required_keys.issubset(result.keys()), f"Missing keys: {required_keys - result.keys()}"
        assert result["platform"] == "telnyx"
        assert result["assistant_id"] == "asst-test"
        assert isinstance(result["documents_synced"], int)
        assert isinstance(result["chunks_total"], int)
        assert isinstance(result["errors"], int)
        assert isinstance(result["error_details"], list)

    # ── BB 6 ─────────────────────────────────────────────────────────────────

    def test_customer_scoped(self):
        """BB: customer_id is passed through to search_platform."""
        chunks = _make_chunks(2, platform="telnyx")
        patches = self._patch_context(chunks)

        with patches["embed"], patches["search"] as mock_search, patches["upload"]:
            _run(
                sync_kb_to_assistant(
                    platform="telnyx",
                    assistant_id="asst-001",
                    customer_id="cust-xyz",
                )
            )

        # customer_id must appear in the search_platform call
        call_kwargs = mock_search.call_args[1] if mock_search.call_args else {}
        call_args = list(mock_search.call_args[0]) if mock_search.call_args else []
        assert "cust-xyz" in call_args or call_kwargs.get("customer_id") == "cust-xyz"


# ─────────────────────────────────────────────────────────────────────────────
# Class 3 — TestDocumentFormatting (4 WB tests)
# ─────────────────────────────────────────────────────────────────────────────


class TestDocumentFormatting:
    """White-box tests for _group_chunks_by_url and _build_document internals."""

    def test_chunks_grouped_by_url(self):
        """WB: Two chunks sharing a URL end up in one group; different URL = separate group."""
        chunks = [
            {"source_url": "https://ex.com/a", "chunk_index": 0, "title": "A", "text": "text-a0"},
            {"source_url": "https://ex.com/a", "chunk_index": 1, "title": "A", "text": "text-a1"},
            {"source_url": "https://ex.com/b", "chunk_index": 0, "title": "B", "text": "text-b0"},
        ]
        groups = _group_chunks_by_url(chunks)

        assert len(groups) == 2
        assert len(groups["https://ex.com/a"]) == 2
        assert len(groups["https://ex.com/b"]) == 1

    def test_document_title_from_first_chunk(self):
        """WB: _build_document uses the first chunk's 'title' as the document title."""
        ordered = [
            {"title": "My Title", "text": "First chunk text", "chunk_index": 0},
            {"title": "Ignored Title", "text": "Second chunk text", "chunk_index": 1},
        ]
        title, _ = _build_document("https://ex.com/page", ordered)

        assert title == "My Title"

    def test_document_content_ordered(self):
        """WB: Chunks are joined in chunk_index order in the document content."""
        # Provide deliberately out-of-order list to confirm sorting happened
        ordered = [
            {"title": "T", "text": "Second", "chunk_index": 1},
            {"title": "T", "text": "First", "chunk_index": 0},
        ]
        # _group_chunks_by_url sorts by chunk_index
        groups = _group_chunks_by_url(
            [
                {"source_url": "https://ex.com/p", "chunk_index": 1, "title": "T", "text": "Second"},
                {"source_url": "https://ex.com/p", "chunk_index": 0, "title": "T", "text": "First"},
            ]
        )
        _, content = _build_document("https://ex.com/p", groups["https://ex.com/p"])

        assert content.index("First") < content.index("Second")

    def test_url_as_fallback_title(self):
        """WB: When the first chunk has an empty title, the URL is used as the document title."""
        ordered = [
            {"title": "", "text": "Some content", "chunk_index": 0},
        ]
        title, _ = _build_document("https://ex.com/fallback", ordered)

        assert title == "https://ex.com/fallback"


# ─────────────────────────────────────────────────────────────────────────────
# Entry point
# ─────────────────────────────────────────────────────────────────────────────

if __name__ == "__main__":
    import subprocess
    import sys

    result = subprocess.run(
        [
            sys.executable,
            "-m",
            "pytest",
            __file__,
            "-v",
            "--tb=short",
        ],
        cwd=_PROJECT_ROOT,
    )
    sys.exit(result.returncode)
