"""Integration smoke tests — MemoryClient write → retrieve round-trip.

Tests the MemoryClient interface end-to-end by mocking the pipeline layer.
SQLite cannot handle PostgreSQL-specific types (JSONB, pgvector), so we mock
the DB session and pipeline functions while validating the client orchestration.

This validates:
- MemoryClient.write() calls the write pipeline and returns WriteResult
- MemoryClient.retrieve() calls the read pipeline and returns RetrieveResult
- Result types have the correct structure for the adapter to consume
"""

from __future__ import annotations

import uuid
from dataclasses import dataclass, field
from unittest.mock import AsyncMock, MagicMock, patch

from conftest import FakeEmbeddingProvider, FakeLLMProvider, run_async

from arandu.client import MemoryClient, RetrieveResult, ScoredFact, WriteResult
from arandu.config import MemoryConfig
from arandu.write.pipeline import WritePipelineResult

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _make_mock_client() -> MemoryClient:
    """Create a MemoryClient with mocked DB engine."""
    llm = FakeLLMProvider()
    embeddings = FakeEmbeddingProvider()
    with (
        patch("arandu.client.create_engine") as mock_engine,
        patch("arandu.client.create_session_factory") as mock_factory,
    ):
        mock_engine.return_value = MagicMock()
        mock_session = AsyncMock()
        mock_session.__aenter__ = AsyncMock(return_value=mock_session)
        mock_session.__aexit__ = AsyncMock(return_value=False)
        mock_session.commit = AsyncMock()
        mock_factory.return_value = MagicMock(return_value=mock_session)
        client = MemoryClient(
            database_url="postgresql+psycopg://fake:fake@localhost/fake",
            llm=llm,
            embeddings=embeddings,
            config=MemoryConfig(),
        )
    return client


def _mock_write_result() -> WritePipelineResult:
    """Fake write pipeline result."""
    return WritePipelineResult(
        event_id=str(uuid.uuid4()),
        facts_added=[{"entity": "Alice", "attribute": "pet", "value": "dog"}],
        facts_updated=[],
        facts_unchanged=[],
        facts_deleted=[],
        entities_resolved=["person::alice"],
        duration_ms=42.0,
    )


@dataclass
class _FakeReadResult:
    """Mimics the read pipeline result object."""

    facts: list = field(default_factory=list)
    context: str = ""
    total_candidates: int = 0
    duration_ms: float = 0.0


@dataclass
class _FakeScoredFact:
    fact_id: str = ""
    entity_name: str = ""
    attribute_key: str = ""
    fact_text: str = ""
    score: float = 0.0
    scores: dict = field(default_factory=dict)


# ---------------------------------------------------------------------------
# Write smoke tests
# ---------------------------------------------------------------------------


def test_write_returns_write_result():
    """write() returns a WriteResult with expected structure."""
    client = _make_mock_client()
    mock_result = _mock_write_result()

    async def _run():
        with patch("arandu.write.pipeline.run_write_pipeline", new_callable=AsyncMock) as mock_write:
            mock_write.return_value = mock_result
            result = await client.write(user_id="user-1", message="I like coffee")
        return result

    result = run_async(_run())
    assert isinstance(result, WriteResult)
    assert isinstance(result.event_id, str)
    assert result.event_id != ""
    assert isinstance(result.facts_added, list)
    assert len(result.facts_added) == 1
    assert isinstance(result.facts_updated, list)
    assert isinstance(result.facts_unchanged, list)
    assert isinstance(result.facts_deleted, list)
    assert isinstance(result.entities_resolved, list)
    assert isinstance(result.duration_ms, float)


def test_write_passes_user_id_and_message():
    """write() forwards user_id, message, and source to the pipeline."""
    client = _make_mock_client()

    async def _run():
        with patch("arandu.write.pipeline.run_write_pipeline", new_callable=AsyncMock) as mock_write:
            mock_write.return_value = _mock_write_result()
            await client.write(user_id="u-42", message="hello", source="whatsapp")
            call_kwargs = mock_write.call_args.kwargs
            return call_kwargs

    kwargs = run_async(_run())
    assert kwargs["user_id"] == "u-42"
    assert kwargs["message"] == "hello"
    assert kwargs["source"] == "whatsapp"


def test_write_empty_message_does_not_crash():
    """Writing an empty message should not raise."""
    client = _make_mock_client()

    async def _run():
        with patch("arandu.write.pipeline.run_write_pipeline", new_callable=AsyncMock) as mock_write:
            mock_write.return_value = WritePipelineResult()
            result = await client.write(user_id="user-1", message="")
        return result

    result = run_async(_run())
    assert isinstance(result, WriteResult)


# ---------------------------------------------------------------------------
# Retrieve smoke tests
# ---------------------------------------------------------------------------


def test_retrieve_returns_retrieve_result():
    """retrieve() returns a RetrieveResult with expected structure."""
    client = _make_mock_client()
    fake_facts = [
        _FakeScoredFact(
            fact_id="f-1",
            entity_name="Alice",
            attribute_key="pet",
            fact_text="dog",
            score=0.9,
        )
    ]
    fake_result = _FakeReadResult(
        facts=fake_facts,
        context="Alice has a dog.",
        total_candidates=10,
        duration_ms=33.0,
    )

    async def _run():
        with patch("arandu.read.pipeline.run_read_pipeline", new_callable=AsyncMock) as mock_read:
            mock_read.return_value = fake_result
            result = await client.retrieve(user_id="user-1", query="what pets?")
        return result

    result = run_async(_run())
    assert isinstance(result, RetrieveResult)
    assert isinstance(result.facts, list)
    assert len(result.facts) == 1
    assert isinstance(result.facts[0], ScoredFact)
    assert result.facts[0].fact_id == "f-1"
    assert result.facts[0].entity_name == "Alice"
    assert result.facts[0].fact_text == "dog"
    assert result.facts[0].score == 0.9
    assert result.context == "Alice has a dog."
    assert result.total_candidates == 10
    assert result.duration_ms == 33.0


def test_retrieve_passes_user_id_and_query():
    """retrieve() forwards user_id and query to the pipeline."""
    client = _make_mock_client()

    async def _run():
        with patch("arandu.read.pipeline.run_read_pipeline", new_callable=AsyncMock) as mock_read:
            mock_read.return_value = _FakeReadResult()
            await client.retrieve(user_id="u-99", query="where do I live?")
            call_kwargs = mock_read.call_args.kwargs
            return call_kwargs

    kwargs = run_async(_run())
    assert kwargs["user_id"] == "u-99"
    assert kwargs["query"] == "where do I live?"


def test_retrieve_empty_query_does_not_crash():
    """Retrieving with empty query should not raise."""
    client = _make_mock_client()

    async def _run():
        with patch("arandu.read.pipeline.run_read_pipeline", new_callable=AsyncMock) as mock_read:
            mock_read.return_value = _FakeReadResult()
            result = await client.retrieve(user_id="user-1", query="")
        return result

    result = run_async(_run())
    assert isinstance(result, RetrieveResult)


def test_retrieve_empty_results():
    """Retrieve with no matching facts returns empty lists."""
    client = _make_mock_client()

    async def _run():
        with patch("arandu.read.pipeline.run_read_pipeline", new_callable=AsyncMock) as mock_read:
            mock_read.return_value = _FakeReadResult()
            result = await client.retrieve(user_id="user-1", query="anything")
        return result

    result = run_async(_run())
    assert result.facts == []
    assert result.context == ""
    assert result.total_candidates == 0


# ---------------------------------------------------------------------------
# Client lifecycle tests
# ---------------------------------------------------------------------------


def test_client_config_accessible():
    """MemoryClient.config is accessible after construction."""
    client = _make_mock_client()
    assert isinstance(client.config, MemoryConfig)


def test_client_close_disposes_engine():
    """close() calls engine.dispose()."""
    client = _make_mock_client()

    async def _run():
        client._engine = AsyncMock()
        await client.close()
        client._engine.dispose.assert_called_once()

    run_async(_run())


# ---------------------------------------------------------------------------
# Write → Retrieve round-trip (mocked pipeline)
# ---------------------------------------------------------------------------


def test_write_then_retrieve_round_trip():
    """Full round-trip: write facts, then retrieve them."""
    client = _make_mock_client()
    write_result_dict = _mock_write_result()
    read_facts = [
        _FakeScoredFact(
            fact_id="f-1",
            entity_name="Bob",
            attribute_key="location",
            fact_text="São Paulo",
            score=0.95,
        )
    ]
    read_result = _FakeReadResult(
        facts=read_facts,
        context="Bob lives in São Paulo.",
        total_candidates=5,
        duration_ms=20.0,
    )

    async def _run():
        with (
            patch("arandu.write.pipeline.run_write_pipeline", new_callable=AsyncMock) as mock_write,
            patch("arandu.read.pipeline.run_read_pipeline", new_callable=AsyncMock) as mock_read,
        ):
            mock_write.return_value = write_result_dict
            mock_read.return_value = read_result
            wr = await client.write(user_id="user-1", message="Bob lives in São Paulo")
            rr = await client.retrieve(user_id="user-1", query="where does Bob live?")
        return wr, rr

    wr, rr = run_async(_run())

    # Write result
    assert isinstance(wr, WriteResult)
    assert wr.event_id != ""
    assert len(wr.facts_added) > 0

    # Retrieve result
    assert isinstance(rr, RetrieveResult)
    assert len(rr.facts) == 1
    assert rr.facts[0].fact_text == "São Paulo"
    assert rr.context == "Bob lives in São Paulo."


# ---------------------------------------------------------------------------
# user_id flexibility tests
# ---------------------------------------------------------------------------


def test_user_id_accepts_any_string():
    """write() accepts any string as user_id (UUID, email, alphanumeric)."""
    client = _make_mock_client()

    async def _run():
        with patch("arandu.write.pipeline.run_write_pipeline", new_callable=AsyncMock) as mock_write:
            mock_write.return_value = _mock_write_result()
            await client.write(user_id="inspector-user", message="hi")
            return mock_write.call_args.kwargs["user_id"]

    uid = run_async(_run())
    assert uid == "inspector-user"


def test_user_id_accepts_uuid_string():
    """write() accepts UUID strings as user_id."""
    client = _make_mock_client()
    test_uuid = "550e8400-e29b-41d4-a716-446655440000"

    async def _run():
        with patch("arandu.write.pipeline.run_write_pipeline", new_callable=AsyncMock) as mock_write:
            mock_write.return_value = _mock_write_result()
            await client.write(user_id=test_uuid, message="hi")
            return mock_write.call_args.kwargs["user_id"]

    uid = run_async(_run())
    assert uid == test_uuid


# ---------------------------------------------------------------------------
# WriteResult success/error tests
# ---------------------------------------------------------------------------


def test_write_result_success_on_normal_pipeline():
    """WriteResult.success is True when pipeline completes normally."""
    client = _make_mock_client()

    async def _run():
        with patch("arandu.write.pipeline.run_write_pipeline", new_callable=AsyncMock) as mock_write:
            mock_write.return_value = _mock_write_result()
            return await client.write(user_id="user-1", message="hi")

    result = run_async(_run())
    assert result.success is True
    assert result.error is None


def test_write_result_failure_propagated():
    """WriteResult.success is False when pipeline fails internally."""
    client = _make_mock_client()
    failed_result = WritePipelineResult(
        event_id=str(uuid.uuid4()),
        success=False,
        error="RuntimeError: LLM call failed",
    )

    async def _run():
        with patch("arandu.write.pipeline.run_write_pipeline", new_callable=AsyncMock) as mock_write:
            mock_write.return_value = failed_result
            return await client.write(user_id="user-1", message="hi")

    result = run_async(_run())
    assert result.success is False
    assert result.error == "RuntimeError: LLM call failed"
