"""Tests for the background memory service (openjarvis.memory.service)."""

from __future__ import annotations

import threading
import time
from types import SimpleNamespace

from openjarvis.core.config import StorageConfig
from openjarvis.core.events import EventBus
from openjarvis.memory.service import (
    MemoryService,
    build_memory_service,
    publish_completed_exchange,
)
from openjarvis.memory.store import LocalFactStore


def _wait_until(predicate, timeout=2.0, interval=0.01):
    deadline = time.time() + timeout
    while time.time() < deadline:
        if predicate():
            return True
        time.sleep(interval)
    return predicate()


class FakeExtractor:
    """Extractor stub with controllable output, blocking and failures."""

    def __init__(self, facts=None, *, raises=None, gate=None):
        self._facts = facts or []
        self._raises = raises
        self._gate = gate  # optional threading.Event to block on
        self.calls = []

    def extract(self, user_text, assistant_text=""):
        self.calls.append((user_text, assistant_text))
        if self._gate is not None:
            self._gate.wait(timeout=2.0)
        if self._raises is not None:
            raise self._raises
        return list(self._facts)


def _service(tmp_path, extractor, **kwargs):
    store = LocalFactStore(tmp_path / "facts.jsonl")
    return MemoryService(store, extractor, **kwargs)


def test_start_stop_lifecycle(tmp_path):
    svc = _service(tmp_path, FakeExtractor())
    assert svc.is_running is False
    svc.start()
    assert svc.is_running is True
    svc.start()  # idempotent
    assert svc.is_running is True
    svc.stop()
    assert svc.is_running is False
    svc.stop()  # idempotent


def test_submit_extracts_and_stores(tmp_path):
    extractor = FakeExtractor(["User likes hiking"])
    svc = _service(tmp_path, extractor)
    svc.start()
    try:
        assert svc.submit("I love hiking", "Nice!") is True
        assert _wait_until(lambda: svc.fact_count() == 1)
        assert [f.text for f in svc.list_facts()] == ["User likes hiking"]
    finally:
        svc.stop()


def test_completed_exchange_event_extracts_and_stores(tmp_path):
    bus = EventBus(record_history=True)
    extractor = FakeExtractor(["User likes jazz"])
    store = LocalFactStore(tmp_path / "facts.jsonl")
    svc = MemoryService(store, extractor, event_bus=bus)
    svc.start()
    try:
        assert publish_completed_exchange(
            bus,
            "I like jazz",
            "Noted.",
            source="test",
        )
        assert _wait_until(lambda: svc.fact_count() == 1)
        assert extractor.calls == [("I like jazz", "Noted.")]
    finally:
        svc.stop()


def test_completed_exchange_event_unsubscribes_on_stop(tmp_path):
    bus = EventBus(record_history=True)
    extractor = FakeExtractor(["User likes jazz"])
    store = LocalFactStore(tmp_path / "facts.jsonl")
    svc = MemoryService(store, extractor, event_bus=bus)
    svc.start()
    svc.stop()

    publish_completed_exchange(bus, "I like jazz", "Noted.", source="test")

    assert extractor.calls == []


def test_submit_when_not_running_is_dropped(tmp_path):
    extractor = FakeExtractor(["x"])
    svc = _service(tmp_path, extractor)
    assert svc.submit("hi", "there") is False
    assert extractor.calls == []


def test_submit_empty_user_text_dropped(tmp_path):
    extractor = FakeExtractor(["x"])
    svc = _service(tmp_path, extractor)
    svc.start()
    try:
        assert svc.submit("   ", "y") is False
    finally:
        svc.stop()


def test_worker_survives_extractor_broken_pipe(tmp_path):
    """A BrokenPipeError in one job must not kill the worker."""
    extractor = FakeExtractor(raises=BrokenPipeError("client gone"))
    svc = _service(tmp_path, extractor)
    svc.start()
    try:
        svc.submit("first", "a")
        assert _wait_until(lambda: len(extractor.calls) == 1)
        # Service is still alive and accepting work.
        assert svc.is_running is True
        assert svc.submit("second", "b") is True
        assert _wait_until(lambda: len(extractor.calls) == 2)
    finally:
        svc.stop()


def test_worker_survives_generic_exception(tmp_path):
    extractor = FakeExtractor(raises=RuntimeError("boom"))
    svc = _service(tmp_path, extractor)
    svc.start()
    try:
        svc.submit("x", "y")
        assert _wait_until(lambda: len(extractor.calls) == 1)
        assert svc.is_running is True
    finally:
        svc.stop()


def test_submit_returns_false_when_queue_full(tmp_path):
    """Backpressure: a full queue drops work instead of blocking the caller."""
    gate = threading.Event()
    extractor = FakeExtractor(["fact"], gate=gate)
    svc = _service(tmp_path, extractor, max_queue=1)
    svc.start()
    try:
        # First submit is pulled by the worker and blocks on the gate.
        assert svc.submit("job1", "a") is True
        assert _wait_until(lambda: len(extractor.calls) == 1)
        # Fill the (size-1) queue, then the next submit must be dropped.
        assert svc.submit("job2", "b") is True
        dropped = svc.submit("job3", "c")
        assert dropped is False
    finally:
        gate.set()
        svc.stop()


def test_build_memory_service_disabled_returns_none(tmp_path):
    cfg = SimpleNamespace(memory=StorageConfig(enabled=False))
    assert build_memory_service(cfg, object(), "model") is None


def test_build_memory_service_no_engine_returns_none(tmp_path):
    cfg = SimpleNamespace(memory=StorageConfig(enabled=True))
    assert build_memory_service(cfg, None, "model") is None


def test_build_memory_service_no_model_returns_none(tmp_path):
    cfg = SimpleNamespace(memory=StorageConfig(enabled=True, extraction_model=""))
    assert build_memory_service(cfg, object(), "") is None


def test_build_memory_service_enabled(tmp_path):
    cfg = SimpleNamespace(
        memory=StorageConfig(
            enabled=True,
            extraction_model="qwen3:14b",
            facts_path=str(tmp_path / "facts.jsonl"),
            max_facts=10,
        )
    )
    svc = build_memory_service(cfg, object(), "fallback-model")
    assert isinstance(svc, MemoryService)


def test_build_memory_service_falls_back_to_default_model(tmp_path):
    cfg = SimpleNamespace(
        memory=StorageConfig(
            enabled=True,
            extraction_model="",
            facts_path=str(tmp_path / "facts.jsonl"),
        )
    )
    svc = build_memory_service(cfg, object(), "active-model")
    assert isinstance(svc, MemoryService)
