import asyncio

import pytest

import app.dependencies as deps
from app.config import settings
from app.core import memory_extractor as me


class StubLLM:
    def __init__(self, raw):
        self.raw = raw
        self.calls = 0

    async def complete(self, text, history=None, session_id=None, language=None):
        self.calls += 1
        return self.raw


@pytest.fixture(autouse=True)
def _clear_turn_counts():
    me._turn_counts.clear()
    yield
    me._turn_counts.clear()


def _seed_conversation(store, session_id="conv", user_id="anonymous"):
    store.append_message(session_id, user_id, "user", "Ich heisse Anna und wohne in Kiel.")
    store.append_message(session_id, user_id, "assistant", "Schoen, Anna!")


def test_parse_facts_variants():
    assert me.parse_facts('["heisst Anna", "wohnt in Kiel"]') == ["heisst Anna", "wohnt in Kiel"]
    # umschlossen von Geschwafel/Markdown
    assert me.parse_facts('Hier:\n```json\n["x"]\n```') == ["x"]
    assert me.parse_facts("[]") == []
    assert me.parse_facts("kein json") == []
    assert me.parse_facts("") == []
    # Nicht-Strings werden ignoriert
    assert me.parse_facts('["ok", 5, null, "  "]') == ["ok"]


def test_extracts_and_stores_new_facts(monkeypatch):
    store = deps.get_store()
    _seed_conversation(store)
    monkeypatch.setattr(me, "_build_extractor_llm",
                        lambda cfg: StubLLM('["heisst Anna", "wohnt in Kiel"]'))

    added = asyncio.run(me.extract_and_store(store, "anonymous", "conv", settings))

    assert added == 2
    contents = [m.content for m in store.get_memories("anonymous")]
    assert contents == ["heisst Anna", "wohnt in Kiel"]


def test_dedup_skips_known(monkeypatch):
    store = deps.get_store()
    _seed_conversation(store)
    store.add_memory("anonymous", "heisst Anna")
    # LLM liefert einen bekannten (anders gross-/kleingeschrieben) + einen neuen Fakt.
    monkeypatch.setattr(me, "_build_extractor_llm",
                        lambda cfg: StubLLM('["Heisst Anna", "wohnt in Kiel"]'))

    added = asyncio.run(me.extract_and_store(store, "anonymous", "conv", settings))

    assert added == 1
    contents = [m.content for m in store.get_memories("anonymous")]
    assert contents == ["heisst Anna", "wohnt in Kiel"]


def test_malformed_output_no_crash(monkeypatch):
    store = deps.get_store()
    _seed_conversation(store)
    monkeypatch.setattr(me, "_build_extractor_llm",
                        lambda cfg: StubLLM("Tut mir leid, kein JSON hier."))

    added = asyncio.run(me.extract_and_store(store, "anonymous", "conv", settings))

    assert added == 0
    assert store.get_memories("anonymous") == []


def test_cap_respected(monkeypatch):
    store = deps.get_store()
    _seed_conversation(store)
    monkeypatch.setattr(settings, "memory_extraction_max", 1)
    monkeypatch.setattr(me, "_build_extractor_llm",
                        lambda cfg: StubLLM('["fakt a", "fakt b", "fakt c"]'))

    added = asyncio.run(me.extract_and_store(store, "anonymous", "conv", settings))

    assert added == 1
    assert len(store.get_memories("anonymous")) == 1


def test_empty_conversation_skips_llm(monkeypatch):
    store = deps.get_store()
    called = StubLLM("[]")
    monkeypatch.setattr(me, "_build_extractor_llm", lambda cfg: called)

    added = asyncio.run(me.extract_and_store(store, "anonymous", "leer", settings))

    assert added == 0
    assert called.calls == 0  # ohne Gespraech kein LLM-Aufruf


def test_schedule_only_every_n_turns(monkeypatch):
    store = deps.get_store()
    monkeypatch.setattr(settings, "memory_extraction_enabled", True)
    monkeypatch.setattr(settings, "memory_extraction_every_n_turns", 3)

    async def run():
        # Session "s1" hat keine Nachrichten -> der geplante Task endet sofort (kein LLM).
        results = [me.maybe_schedule_extraction(store, "anonymous", "s1") for _ in range(3)]
        for task in results:
            if task is not None:
                await task
        return results

    tasks = asyncio.run(run())
    # nur der 3. Aufruf plant einen Task
    assert tasks[0] is None and tasks[1] is None
    assert tasks[2] is not None


def test_schedule_disabled_is_noop(monkeypatch):
    store = deps.get_store()
    monkeypatch.setattr(settings, "memory_extraction_enabled", False)
    assert me.maybe_schedule_extraction(store, "anonymous", "s1") is None


def test_schedule_without_session_is_noop(monkeypatch):
    store = deps.get_store()
    monkeypatch.setattr(settings, "memory_extraction_enabled", True)
    assert me.maybe_schedule_extraction(store, "anonymous", None) is None
