#!/usr/bin/env python3
"""
Tests for Story 3.10: PostCallEnricher — Postgres Write + Redis Cleanup
AIVA RLM Nexus PRD v2 — Track A

Black-box tests (BB1-BB4): verify the full enrich() contract including
Postgres UPDATE and Redis DELETE from the outside.

White-box tests (WB1-WB4): verify parameterized SQL pattern, Redis DELETE
failure handling, memory_vector_id propagation, and getconn/putconn usage.

ALL external calls (Redis, Gemini, Qdrant, Postgres) are fully mocked.
Zero real I/O in this test suite.
"""
import asyncio
import json
import sys
from unittest.mock import AsyncMock, MagicMock, call, patch

import pytest

sys.path.insert(0, "/mnt/e/genesis-system")

from core.enrichers import PostCallEnricher, EnrichmentError
from core.enrichers.post_call_enricher import PostCallEnricher as DirectImport

# ---------------------------------------------------------------------------
# Shared constants
# ---------------------------------------------------------------------------

_SESSION_ID = "sess-3-10-test"
_FAKE_API_KEY = "fake-gemini-api-key-310"
_FAKE_VECTOR_ID = "a1b2c3d4-e5f6-7890-abcd-ef1234567890"

_VALID_ENRICHMENT = {
    "summary": "Customer called about a leaking pipe. Plumber booked for Thursday 2pm.",
    "entities": ["John Smith", "plumber"],
    "decisions_made": ["Book plumber for Thursday 2pm"],
    "action_items": [{"task": "Send booking confirmation", "owner": "Agent", "deadline": "EOD"}],
    "emotional_signal": "positive",
    "key_facts": ["Leaking pipe", "Thursday 2pm appointment"],
    "kinan_directives": ["Send SMS reminder"],
}

_REDIS_TRANSCRIPT_KEY = f"aiva:transcript:{_SESSION_ID}"


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

class FakePointStruct:
    """Stand-in for qdrant_client.models.PointStruct to avoid the import."""

    def __init__(self, id, vector, payload):
        self.id = id
        self.vector = vector
        self.payload = payload


def _make_redis(chunks=None) -> AsyncMock:
    """Return an async-mock Redis client with lrange and delete."""
    redis = AsyncMock()
    redis.lrange = AsyncMock(return_value=chunks if chunks is not None else [])
    redis.delete = AsyncMock(return_value=1)
    return redis


def _make_qdrant_mock(upsert_ok: bool = True) -> MagicMock:
    """Return a synchronous mock Qdrant client."""
    mock_qdrant = MagicMock()
    if upsert_ok:
        mock_qdrant.upsert.return_value = MagicMock()
    else:
        mock_qdrant.upsert.side_effect = Exception("Qdrant connection refused")
    return mock_qdrant


def _make_postgres_pool(execute_ok: bool = True) -> MagicMock:
    """
    Return a mock psycopg2-style connection pool.
    getconn() → mock connection → cursor() → mock cursor → execute() / commit().
    """
    pool = MagicMock()
    conn = MagicMock()
    cursor = MagicMock()

    # cursor() is used as context manager: with conn.cursor() as cur:
    cursor.__enter__ = MagicMock(return_value=cursor)
    cursor.__exit__ = MagicMock(return_value=False)
    conn.cursor.return_value = cursor

    if execute_ok:
        cursor.execute.return_value = None
        conn.commit.return_value = None
    else:
        cursor.execute.side_effect = Exception("Postgres connection refused")

    pool.getconn.return_value = conn
    pool.putconn.return_value = None
    return pool


def _make_enricher(
    chunks=None,
    qdrant_client=None,
    postgres_pool=None,
    api_key: str = _FAKE_API_KEY,
) -> PostCallEnricher:
    """Return a PostCallEnricher with all collaborators mocked."""
    redis = _make_redis(chunks=chunks)
    return PostCallEnricher(
        redis_client=redis,
        gemini_api_key=api_key,
        qdrant_client=qdrant_client,
        postgres_pool=postgres_pool,
    )


def _run(coro):
    """Run a coroutine synchronously in a new event loop."""
    loop = asyncio.new_event_loop()
    try:
        return loop.run_until_complete(coro)
    finally:
        loop.close()


def _run_enrich_mocked(enricher, qdrant, enrichment=None):
    """
    Run enrich() with:
    - _enrich_with_gemini mocked to return enrichment (default: _VALID_ENRICHMENT)
    - qdrant_client.models.PointStruct replaced with FakePointStruct
    """
    if enrichment is None:
        enrichment = _VALID_ENRICHMENT

    with patch.object(enricher, "_enrich_with_gemini", new=AsyncMock(return_value=enrichment)):
        with patch("qdrant_client.models.PointStruct", FakePointStruct):
            return _run(enricher.enrich(_SESSION_ID))


# ---------------------------------------------------------------------------
# BB1: enrich() → Postgres execute called with UPDATE and all enriched fields
# ---------------------------------------------------------------------------


class TestBB1_Enrich_PostgresUpdateCalled:
    """
    BB1: Valid enrichment + working Qdrant + working Postgres pool →
    enrich() calls cursor.execute() with an UPDATE statement containing
    all enriched fields.
    """

    def test_postgres_execute_is_called(self):
        qdrant = _make_qdrant_mock(upsert_ok=True)
        pg_pool = _make_postgres_pool(execute_ok=True)
        enricher = _make_enricher(qdrant_client=qdrant, postgres_pool=pg_pool)
        _run_enrich_mocked(enricher, qdrant)
        conn = pg_pool.getconn.return_value
        cursor = conn.cursor.return_value.__enter__.return_value
        assert cursor.execute.called, "cursor.execute() must be called for Postgres UPDATE"

    def test_postgres_sql_contains_update(self):
        qdrant = _make_qdrant_mock(upsert_ok=True)
        pg_pool = _make_postgres_pool(execute_ok=True)
        enricher = _make_enricher(qdrant_client=qdrant, postgres_pool=pg_pool)
        _run_enrich_mocked(enricher, qdrant)
        conn = pg_pool.getconn.return_value
        cursor = conn.cursor.return_value.__enter__.return_value
        sql_used = cursor.execute.call_args[0][0]
        assert "UPDATE" in sql_used.upper(), f"SQL must contain UPDATE, got: {sql_used!r}"

    def test_postgres_sql_targets_royal_conversations(self):
        qdrant = _make_qdrant_mock(upsert_ok=True)
        pg_pool = _make_postgres_pool(execute_ok=True)
        enricher = _make_enricher(qdrant_client=qdrant, postgres_pool=pg_pool)
        _run_enrich_mocked(enricher, qdrant)
        conn = pg_pool.getconn.return_value
        cursor = conn.cursor.return_value.__enter__.return_value
        sql_used = cursor.execute.call_args[0][0]
        assert "royal_conversations" in sql_used, (
            f"SQL must reference royal_conversations, got: {sql_used!r}"
        )

    def test_postgres_params_contain_summary(self):
        qdrant = _make_qdrant_mock(upsert_ok=True)
        pg_pool = _make_postgres_pool(execute_ok=True)
        enricher = _make_enricher(qdrant_client=qdrant, postgres_pool=pg_pool)
        _run_enrich_mocked(enricher, qdrant)
        conn = pg_pool.getconn.return_value
        cursor = conn.cursor.return_value.__enter__.return_value
        params = cursor.execute.call_args[0][1]
        assert _VALID_ENRICHMENT["summary"] in params, "Summary must be in Postgres params"

    def test_postgres_params_contain_session_id(self):
        qdrant = _make_qdrant_mock(upsert_ok=True)
        pg_pool = _make_postgres_pool(execute_ok=True)
        enricher = _make_enricher(qdrant_client=qdrant, postgres_pool=pg_pool)
        _run_enrich_mocked(enricher, qdrant)
        conn = pg_pool.getconn.return_value
        cursor = conn.cursor.return_value.__enter__.return_value
        params = cursor.execute.call_args[0][1]
        assert _SESSION_ID in params, "session_id must be in Postgres params"

    def test_enrich_returns_vector_id_after_postgres_write(self):
        qdrant = _make_qdrant_mock(upsert_ok=True)
        pg_pool = _make_postgres_pool(execute_ok=True)
        enricher = _make_enricher(qdrant_client=qdrant, postgres_pool=pg_pool)
        result = _run_enrich_mocked(enricher, qdrant)
        assert isinstance(result, str) and len(result) > 0


# ---------------------------------------------------------------------------
# BB2: After successful enrich() → Redis DELETE called for transcript key
# ---------------------------------------------------------------------------


class TestBB2_SuccessfulEnrich_RedisDeleteCalled:
    """
    BB2: When Postgres write succeeds, enrich() must call redis.delete() on
    aiva:transcript:{session_id} to clean up the transcript.
    """

    def test_redis_delete_called_on_success(self):
        qdrant = _make_qdrant_mock(upsert_ok=True)
        pg_pool = _make_postgres_pool(execute_ok=True)
        enricher = _make_enricher(qdrant_client=qdrant, postgres_pool=pg_pool)
        _run_enrich_mocked(enricher, qdrant)
        enricher._redis.delete.assert_called_once_with(_REDIS_TRANSCRIPT_KEY)

    def test_redis_delete_uses_correct_key_format(self):
        """Key must be aiva:transcript:{session_id}."""
        qdrant = _make_qdrant_mock(upsert_ok=True)
        pg_pool = _make_postgres_pool(execute_ok=True)
        enricher = _make_enricher(qdrant_client=qdrant, postgres_pool=pg_pool)
        _run_enrich_mocked(enricher, qdrant)
        deleted_key = enricher._redis.delete.call_args[0][0]
        assert deleted_key == f"aiva:transcript:{_SESSION_ID}"

    def test_redis_delete_called_after_postgres_commit(self):
        """Ordering: Postgres commit must happen before Redis delete."""
        call_order = []
        qdrant = _make_qdrant_mock(upsert_ok=True)
        pg_pool = _make_postgres_pool(execute_ok=True)
        conn = pg_pool.getconn.return_value

        original_commit = conn.commit
        def tracked_commit(*a, **kw):
            call_order.append("pg_commit")
            return original_commit(*a, **kw)
        conn.commit = tracked_commit

        enricher = _make_enricher(qdrant_client=qdrant, postgres_pool=pg_pool)
        original_delete = enricher._redis.delete

        async def tracked_delete(key):
            call_order.append("redis_delete")
            return await original_delete(key)

        enricher._redis.delete = tracked_delete

        _run_enrich_mocked(enricher, qdrant)

        pg_idx = call_order.index("pg_commit")
        redis_idx = call_order.index("redis_delete")
        assert pg_idx < redis_idx, (
            f"Postgres commit must happen before Redis delete. Order: {call_order}"
        )


# ---------------------------------------------------------------------------
# BB3: Postgres fails → Redis DELETE NOT called (transcript preserved)
# ---------------------------------------------------------------------------


class TestBB3_PostgresFails_RedisNotDeleted:
    """
    BB3: When Postgres write fails, enrich() must NOT call redis.delete().
    The transcript stays in Redis for retry.
    """

    def test_redis_delete_not_called_on_postgres_failure(self):
        qdrant = _make_qdrant_mock(upsert_ok=True)
        pg_pool = _make_postgres_pool(execute_ok=False)
        enricher = _make_enricher(qdrant_client=qdrant, postgres_pool=pg_pool)
        _run_enrich_mocked(enricher, qdrant)
        enricher._redis.delete.assert_not_called()

    def test_vector_id_still_returned_on_postgres_failure(self):
        """Postgres failure is non-fatal — enrich() still returns the Qdrant UUID."""
        qdrant = _make_qdrant_mock(upsert_ok=True)
        pg_pool = _make_postgres_pool(execute_ok=False)
        enricher = _make_enricher(qdrant_client=qdrant, postgres_pool=pg_pool)
        result = _run_enrich_mocked(enricher, qdrant)
        assert isinstance(result, str) and len(result) > 0, (
            "enrich() must still return a UUID even when Postgres fails"
        )

    def test_no_exception_raised_on_postgres_failure(self):
        qdrant = _make_qdrant_mock(upsert_ok=True)
        pg_pool = _make_postgres_pool(execute_ok=False)
        enricher = _make_enricher(qdrant_client=qdrant, postgres_pool=pg_pool)
        try:
            _run_enrich_mocked(enricher, qdrant)
        except Exception as exc:
            pytest.fail(f"enrich() raised unexpectedly on Postgres failure: {exc}")

    def test_postgres_failure_does_not_affect_qdrant_return(self):
        """Postgres failing after successful Qdrant write still returns Qdrant UUID."""
        qdrant = _make_qdrant_mock(upsert_ok=True)
        pg_pool = _make_postgres_pool(execute_ok=False)
        enricher = _make_enricher(qdrant_client=qdrant, postgres_pool=pg_pool)
        result = _run_enrich_mocked(enricher, qdrant)
        import uuid as _uuid
        _uuid.UUID(result)  # must be valid UUID — raises ValueError if not


# ---------------------------------------------------------------------------
# BB4: enriched dict with all 7 fields → all passed to Postgres query
# ---------------------------------------------------------------------------


class TestBB4_AllSevenFields_PassedToPostgres:
    """
    BB4: All 7 enrichment fields plus memory_vector_id must be passed as
    parameters to the Postgres cursor.execute() call.
    """

    def _get_execute_params(self) -> tuple:
        qdrant = _make_qdrant_mock(upsert_ok=True)
        pg_pool = _make_postgres_pool(execute_ok=True)
        enricher = _make_enricher(qdrant_client=qdrant, postgres_pool=pg_pool)
        _run_enrich_mocked(enricher, qdrant)
        conn = pg_pool.getconn.return_value
        cursor = conn.cursor.return_value.__enter__.return_value
        return cursor.execute.call_args[0][1]

    def test_summary_in_params(self):
        params = self._get_execute_params()
        assert _VALID_ENRICHMENT["summary"] in params

    def test_emotional_signal_in_params(self):
        params = self._get_execute_params()
        assert _VALID_ENRICHMENT["emotional_signal"] in params

    def test_entities_json_in_params(self):
        params = self._get_execute_params()
        expected_json = json.dumps(_VALID_ENRICHMENT["entities"])
        assert expected_json in params

    def test_decisions_made_json_in_params(self):
        params = self._get_execute_params()
        expected_json = json.dumps(_VALID_ENRICHMENT["decisions_made"])
        assert expected_json in params

    def test_action_items_json_in_params(self):
        params = self._get_execute_params()
        expected_json = json.dumps(_VALID_ENRICHMENT["action_items"])
        assert expected_json in params

    def test_key_facts_json_in_params(self):
        params = self._get_execute_params()
        expected_json = json.dumps(_VALID_ENRICHMENT["key_facts"])
        assert expected_json in params

    def test_kinan_directives_json_in_params(self):
        params = self._get_execute_params()
        expected_json = json.dumps(_VALID_ENRICHMENT["kinan_directives"])
        assert expected_json in params

    def test_session_id_is_last_param(self):
        """WHERE session_id = %s — session_id must be the final parameter."""
        params = self._get_execute_params()
        assert params[-1] == _SESSION_ID, (
            f"session_id must be the last param for WHERE clause, got: {params[-1]!r}"
        )


# ---------------------------------------------------------------------------
# WB1: Parameterized SQL uses %s placeholders (no f-strings in SQL)
# ---------------------------------------------------------------------------


class TestWB1_ParameterizedSQL_NoFStrings:
    """
    WB1: The SQL passed to cursor.execute() must use %s placeholders.
    No f-string or string concatenation should appear in the SQL string itself.
    """

    def test_sql_contains_percent_s_placeholders(self):
        qdrant = _make_qdrant_mock(upsert_ok=True)
        pg_pool = _make_postgres_pool(execute_ok=True)
        enricher = _make_enricher(qdrant_client=qdrant, postgres_pool=pg_pool)
        _run_enrich_mocked(enricher, qdrant)
        conn = pg_pool.getconn.return_value
        cursor = conn.cursor.return_value.__enter__.return_value
        sql = cursor.execute.call_args[0][0]
        assert "%s" in sql, f"SQL must contain %s placeholders, got: {sql!r}"

    def test_sql_does_not_contain_literal_session_id_value(self):
        """If session_id value appears literally in SQL, it's string-formatted — forbidden."""
        qdrant = _make_qdrant_mock(upsert_ok=True)
        pg_pool = _make_postgres_pool(execute_ok=True)
        enricher = _make_enricher(qdrant_client=qdrant, postgres_pool=pg_pool)
        _run_enrich_mocked(enricher, qdrant)
        conn = pg_pool.getconn.return_value
        cursor = conn.cursor.return_value.__enter__.return_value
        sql = cursor.execute.call_args[0][0]
        assert _SESSION_ID not in sql, (
            f"session_id value must NOT appear in SQL string (use %s param), got: {sql!r}"
        )

    def test_sql_placeholders_and_params_count_match(self):
        """Number of %s in SQL must equal number of params."""
        qdrant = _make_qdrant_mock(upsert_ok=True)
        pg_pool = _make_postgres_pool(execute_ok=True)
        enricher = _make_enricher(qdrant_client=qdrant, postgres_pool=pg_pool)
        _run_enrich_mocked(enricher, qdrant)
        conn = pg_pool.getconn.return_value
        cursor = conn.cursor.return_value.__enter__.return_value
        sql = cursor.execute.call_args[0][0]
        params = cursor.execute.call_args[0][1]
        placeholder_count = sql.count("%s")
        assert placeholder_count == len(params), (
            f"Placeholder count ({placeholder_count}) must equal param count ({len(params)})"
        )

    def test_no_fstring_sql_in_source(self):
        """AST check: _persist_to_postgres must not have f-strings in SQL assignment."""
        import ast
        import inspect
        import pathlib

        source = pathlib.Path(
            "/mnt/e/genesis-system/core/enrichers/post_call_enricher.py"
        ).read_text(encoding="utf-8")

        tree = ast.parse(source)

        # Find the _persist_to_postgres method body and check for JoinedStr (f-string)
        # in variable names that look like SQL (_SQL)
        for node in ast.walk(tree):
            if isinstance(node, ast.FunctionDef) and node.name == "_persist_to_postgres":
                for child in ast.walk(node):
                    if isinstance(child, ast.Assign):
                        for target in child.targets:
                            if isinstance(target, ast.Name) and target.id == "_SQL":
                                # The value of _SQL must NOT be a JoinedStr (f-string)
                                assert not isinstance(child.value, ast.JoinedStr), (
                                    "_SQL must not be an f-string"
                                )


# ---------------------------------------------------------------------------
# WB2: Redis DELETE failure → logged warning, no crash
# ---------------------------------------------------------------------------


class TestWB2_RedisDeleteFailure_NoCrash:
    """
    WB2: If redis.delete() raises an exception, enrich() must not propagate it.
    The failure must be swallowed (logged as warning), and enrich() still
    returns the vector_id.
    """

    def test_redis_delete_failure_does_not_raise(self):
        qdrant = _make_qdrant_mock(upsert_ok=True)
        pg_pool = _make_postgres_pool(execute_ok=True)
        redis = _make_redis()
        redis.delete = AsyncMock(side_effect=ConnectionError("Redis timeout"))

        enricher = PostCallEnricher(
            redis_client=redis,
            gemini_api_key=_FAKE_API_KEY,
            qdrant_client=qdrant,
            postgres_pool=pg_pool,
        )
        try:
            with patch.object(enricher, "_enrich_with_gemini", new=AsyncMock(return_value=_VALID_ENRICHMENT)):
                with patch("qdrant_client.models.PointStruct", FakePointStruct):
                    _run(enricher.enrich(_SESSION_ID))
        except Exception as exc:
            pytest.fail(f"enrich() raised on Redis delete failure: {exc}")

    def test_vector_id_returned_despite_redis_delete_failure(self):
        qdrant = _make_qdrant_mock(upsert_ok=True)
        pg_pool = _make_postgres_pool(execute_ok=True)
        redis = _make_redis()
        redis.delete = AsyncMock(side_effect=ConnectionError("Redis timeout"))

        enricher = PostCallEnricher(
            redis_client=redis,
            gemini_api_key=_FAKE_API_KEY,
            qdrant_client=qdrant,
            postgres_pool=pg_pool,
        )
        with patch.object(enricher, "_enrich_with_gemini", new=AsyncMock(return_value=_VALID_ENRICHMENT)):
            with patch("qdrant_client.models.PointStruct", FakePointStruct):
                result = _run(enricher.enrich(_SESSION_ID))
        assert isinstance(result, str) and len(result) > 0


# ---------------------------------------------------------------------------
# WB3: memory_vector_id in Postgres params matches vector_id arg
# ---------------------------------------------------------------------------


class TestWB3_MemoryVectorId_MatchesQdrantUUID:
    """
    WB3: The memory_vector_id written to Postgres must be the same UUID
    that was returned by enrich() (which was upserted to Qdrant).
    """

    def test_memory_vector_id_in_params_matches_returned_uuid(self):
        qdrant = _make_qdrant_mock(upsert_ok=True)
        pg_pool = _make_postgres_pool(execute_ok=True)
        enricher = _make_enricher(qdrant_client=qdrant, postgres_pool=pg_pool)
        returned_uuid = _run_enrich_mocked(enricher, qdrant)

        conn = pg_pool.getconn.return_value
        cursor = conn.cursor.return_value.__enter__.return_value
        params = cursor.execute.call_args[0][1]

        # memory_vector_id is the 8th param (index 7), session_id is last (index 8)
        # Params order: summary, entities, decisions, actions, emotional, facts, directives, vector_id, session_id
        assert returned_uuid in params, (
            f"returned UUID {returned_uuid!r} must appear in Postgres params"
        )

    def test_vector_id_position_is_second_to_last(self):
        """vector_id is the 8th param; session_id is the 9th (WHERE clause)."""
        qdrant = _make_qdrant_mock(upsert_ok=True)
        pg_pool = _make_postgres_pool(execute_ok=True)
        enricher = _make_enricher(qdrant_client=qdrant, postgres_pool=pg_pool)
        returned_uuid = _run_enrich_mocked(enricher, qdrant)

        conn = pg_pool.getconn.return_value
        cursor = conn.cursor.return_value.__enter__.return_value
        params = cursor.execute.call_args[0][1]

        # Index -2 is memory_vector_id (second to last before session_id)
        assert params[-2] == returned_uuid, (
            f"memory_vector_id must be params[-2]; got params[-2]={params[-2]!r}, "
            f"returned_uuid={returned_uuid!r}"
        )

    def test_direct_persist_call_passes_vector_id_through(self):
        """Direct _persist_to_postgres call: vector_id arg → appears in params."""
        pg_pool = _make_postgres_pool(execute_ok=True)
        redis = _make_redis()
        enricher = PostCallEnricher(
            redis_client=redis,
            gemini_api_key=_FAKE_API_KEY,
            postgres_pool=pg_pool,
        )
        test_vector_id = "deadbeef-cafe-4321-beef-123456789abc"
        _run(enricher._persist_to_postgres(_SESSION_ID, _VALID_ENRICHMENT, test_vector_id))

        conn = pg_pool.getconn.return_value
        cursor = conn.cursor.return_value.__enter__.return_value
        params = cursor.execute.call_args[0][1]
        assert test_vector_id in params, (
            f"vector_id {test_vector_id!r} must appear in params, got: {params}"
        )


# ---------------------------------------------------------------------------
# WB4: getconn/putconn pattern used
# ---------------------------------------------------------------------------


class TestWB4_GetconnPutconn_Pattern:
    """
    WB4: _persist_to_postgres must call pool.getconn() to acquire a connection
    and pool.putconn(conn) to release it — even on failure.
    """

    def test_getconn_called(self):
        pg_pool = _make_postgres_pool(execute_ok=True)
        redis = _make_redis()
        enricher = PostCallEnricher(
            redis_client=redis,
            gemini_api_key=_FAKE_API_KEY,
            postgres_pool=pg_pool,
        )
        _run(enricher._persist_to_postgres(_SESSION_ID, _VALID_ENRICHMENT, _FAKE_VECTOR_ID))
        pg_pool.getconn.assert_called_once()

    def test_putconn_called_on_success(self):
        pg_pool = _make_postgres_pool(execute_ok=True)
        redis = _make_redis()
        enricher = PostCallEnricher(
            redis_client=redis,
            gemini_api_key=_FAKE_API_KEY,
            postgres_pool=pg_pool,
        )
        _run(enricher._persist_to_postgres(_SESSION_ID, _VALID_ENRICHMENT, _FAKE_VECTOR_ID))
        pg_pool.putconn.assert_called_once()

    def test_putconn_called_even_on_postgres_failure(self):
        """putconn must be called in finally: even when execute() raises."""
        pg_pool = _make_postgres_pool(execute_ok=False)
        redis = _make_redis()
        enricher = PostCallEnricher(
            redis_client=redis,
            gemini_api_key=_FAKE_API_KEY,
            postgres_pool=pg_pool,
        )
        # Must not raise, and putconn must still be called
        result = _run(enricher._persist_to_postgres(_SESSION_ID, _VALID_ENRICHMENT, _FAKE_VECTOR_ID))
        assert result is False, "Must return False on Postgres failure"
        pg_pool.putconn.assert_called_once()

    def test_putconn_receives_the_connection_from_getconn(self):
        """putconn(conn) must receive the same conn object that getconn() returned."""
        pg_pool = _make_postgres_pool(execute_ok=True)
        acquired_conn = pg_pool.getconn.return_value
        redis = _make_redis()
        enricher = PostCallEnricher(
            redis_client=redis,
            gemini_api_key=_FAKE_API_KEY,
            postgres_pool=pg_pool,
        )
        _run(enricher._persist_to_postgres(_SESSION_ID, _VALID_ENRICHMENT, _FAKE_VECTOR_ID))
        putconn_arg = pg_pool.putconn.call_args[0][0]
        assert putconn_arg is acquired_conn, (
            "putconn() must receive the same connection object returned by getconn()"
        )

    def test_no_pool_injected_returns_false(self):
        """_persist_to_postgres returns False when postgres_pool is None."""
        redis = _make_redis()
        enricher = PostCallEnricher(
            redis_client=redis,
            gemini_api_key=_FAKE_API_KEY,
            postgres_pool=None,
        )
        result = _run(enricher._persist_to_postgres(_SESSION_ID, _VALID_ENRICHMENT, _FAKE_VECTOR_ID))
        assert result is False


# ---------------------------------------------------------------------------
# Regression: postgres_pool param is optional in __init__
# ---------------------------------------------------------------------------


class TestPostgresPoolInitOptional:
    """Verify that postgres_pool defaults to None and is not a breaking change."""

    def test_init_without_postgres_pool_does_not_raise(self):
        redis = AsyncMock()
        enricher = PostCallEnricher(redis_client=redis, gemini_api_key="test")
        assert enricher is not None

    def test_postgres_pool_defaults_to_none(self):
        redis = AsyncMock()
        enricher = PostCallEnricher(redis_client=redis, gemini_api_key="test")
        assert enricher._postgres_pool is None

    def test_postgres_pool_stored_when_provided(self):
        redis = AsyncMock()
        mock_pool = MagicMock()
        enricher = PostCallEnricher(
            redis_client=redis, gemini_api_key="test", postgres_pool=mock_pool
        )
        assert enricher._postgres_pool is mock_pool

    def test_existing_init_signature_still_works(self):
        """Story 3.08/3.09 construction signature must not be broken."""
        redis = AsyncMock()
        qdrant = MagicMock()
        enricher = PostCallEnricher(
            redis_client=redis,
            gemini_api_key="existing-key",
            qdrant_client=qdrant,
        )
        assert enricher._postgres_pool is None
        assert enricher._qdrant is qdrant


# ---------------------------------------------------------------------------
# Regression: Package exports still intact
# ---------------------------------------------------------------------------


class TestPackageExport:
    """Verify that both classes are still importable from the package root."""

    def test_post_call_enricher_importable_from_package(self):
        from core.enrichers import PostCallEnricher as PCE
        assert PCE is not None

    def test_enrichment_error_importable_from_package(self):
        from core.enrichers import EnrichmentError as EE
        assert EE is not None

    def test_direct_module_import_works(self):
        from core.enrichers.post_call_enricher import PostCallEnricher, EnrichmentError
        assert PostCallEnricher is not None
        assert EnrichmentError is not None


# ---------------------------------------------------------------------------
# No SQLite
# ---------------------------------------------------------------------------


class TestNoSQLite:
    """Verify the implementation never imports sqlite3."""

    def test_no_sqlite_import(self):
        import ast
        import pathlib

        impl_path = pathlib.Path("/mnt/e/genesis-system/core/enrichers/post_call_enricher.py")
        source = impl_path.read_text(encoding="utf-8")
        tree = ast.parse(source)

        for node in ast.walk(tree):
            if isinstance(node, ast.Import):
                for alias in node.names:
                    assert alias.name != "sqlite3", "sqlite3 import found in implementation!"
            elif isinstance(node, ast.ImportFrom):
                assert node.module != "sqlite3", "from sqlite3 import found in implementation!"


# ---------------------------------------------------------------------------
# Run summary
# ---------------------------------------------------------------------------

if __name__ == "__main__":
    result = pytest.main([__file__, "-v", "--tb=short"])
    sys.exit(result)
