"""Regression: auto memory extraction must survive a runtime vector-store
failure.

The vector index reports `.healthy` only at init time. If the embedding
backend dies later (OOM, model evicted, remote endpoint down), the per-fact
`find_similar` / `add` calls raise. Before the fix these exceptions escaped the
dedup loop, jumped past `memory_manager.save(...)`, and were swallowed by the
function's outer try/except — so EVERY validated fact from the session was
silently lost (the feature promises "Errors are logged, never raised", but it
also quietly dropped all the data).

After the fix a degraded vector store falls through to the text/fuzzy dedup
path (which the code already maintains "when vector index is unavailable") and
the facts still land in the JSON store.
"""

import asyncio
import tempfile

import src.llm_core
import src.event_bus
from src.memory import MemoryManager
from services.memory.memory_extractor import extract_and_store


class _FakeSession:
    """Minimal session: two-message history so extraction proceeds."""

    owner = "alice"
    session_id = "sess-1"

    def get_context_messages(self):
        return [
            {"role": "user", "content": "Hi, a few things about me."},
            {"role": "assistant", "content": "Noted."},
        ]


class _BrokenVectorStore:
    """Healthy at init, but every embedding-backed op raises at runtime."""

    healthy = True

    def find_similar(self, text, threshold=0.72):
        raise RuntimeError("embedding backend unavailable")

    def add(self, memory_id, text):
        raise RuntimeError("embedding backend unavailable")


def _run(coro):
    return asyncio.new_event_loop().run_until_complete(coro)


def test_extraction_persists_facts_when_vector_store_fails_at_runtime(monkeypatch):
    facts_json = (
        '[{"text": "Alice lives in Lisbon", "category": "fact"}, '
        '{"text": "Alice prefers tea over coffee", "category": "preference"}]'
    )

    async def _fake_llm(url, model, messages, **kwargs):
        return facts_json

    monkeypatch.setattr(src.llm_core, "llm_call_async", _fake_llm)
    # fire_event touches an async event loop / disk — neutralize it.
    monkeypatch.setattr(src.event_bus, "fire_event", lambda *a, **k: None)

    with tempfile.TemporaryDirectory() as data_dir:
        mgr = MemoryManager(data_dir)

        _run(extract_and_store(
            _FakeSession(),
            mgr,
            _BrokenVectorStore(),
            endpoint_url="http://x",
            model="m",
            headers=None,
        ))

        stored = mgr.load(owner="alice")
        texts = {e["text"] for e in stored}

    # The bug lost ALL of them (save() was never reached); both must survive.
    assert "Alice lives in Lisbon" in texts
    assert "Alice prefers tea over coffee" in texts


def test_healthy_vector_store_still_dedups_normally(monkeypatch):
    """Control: when find_similar reports a match, that fact is skipped — the
    try/except added around it must not swallow a legitimate dedup hit."""

    async def _fake_llm(url, model, messages, **kwargs):
        return '[{"text": "Alice lives in Lisbon", "category": "fact"}]'

    monkeypatch.setattr(src.llm_core, "llm_call_async", _fake_llm)
    monkeypatch.setattr(src.event_bus, "fire_event", lambda *a, **k: None)

    class _DedupVectorStore:
        healthy = True

        def find_similar(self, text, threshold=0.72):
            return "existing-id"  # claim it already exists

        def add(self, memory_id, text):  # pragma: no cover - should not run
            raise AssertionError("add should not be called for a deduped fact")

    with tempfile.TemporaryDirectory() as data_dir:
        mgr = MemoryManager(data_dir)
        _run(extract_and_store(
            _FakeSession(), mgr, _DedupVectorStore(),
            endpoint_url="http://x", model="m", headers=None,
        ))
        assert mgr.load(owner="alice") == []
