"""Tests for Classifier — mocked LLM client."""

import json

import pytest

from neo_cortex.classifier import Classifier, _strip_code_fences
from neo_cortex.llm import LLMClient
from neo_cortex.models import Activity


class MockLLM(LLMClient):
    """Test LLM that returns pre-configured responses."""

    def __init__(self, responses: list[str] | None = None):
        self._responses = list(responses) if responses else []
        self._call_count = 0

    @property
    def name(self) -> str:
        return "mock/test"

    async def call(self, prompt: str, text: str, max_tokens: int = 250, temperature: float = 0.1) -> str:
        if self._responses:
            resp = self._responses.pop(0)
            self._call_count += 1
            return resp
        raise RuntimeError("No more mock responses")

    async def close(self) -> None:
        pass


class FailingLLM(LLMClient):
    """Test LLM that always raises."""

    @property
    def name(self) -> str:
        return "mock/failing"

    async def call(self, prompt: str, text: str, max_tokens: int = 250, temperature: float = 0.1) -> str:
        raise RuntimeError("LLM is down")

    async def close(self) -> None:
        pass


class TestStripCodeFences:
    def test_strips_json_fences(self):
        assert _strip_code_fences('```json\n{"a": 1}\n```') == '{"a": 1}'

    def test_strips_bare_fences(self):
        assert _strip_code_fences('```\n{"a": 1}\n```') == '{"a": 1}'

    def test_no_fences_unchanged(self):
        assert _strip_code_fences('{"a": 1}') == '{"a": 1}'


class TestClassify:
    @pytest.mark.asyncio
    async def test_classify_returns_result(self):
        llm = MockLLM(['{"project": "neo-cortex", "topic": "memory store", "activity": "feature"}'])
        c = Classifier(llm)
        result = await c.classify("How do I add a store?", "Use SQLite.")
        assert result.project == "neo-cortex"
        assert result.topic == "memory store"
        assert result.activity == Activity.FEATURE

    @pytest.mark.asyncio
    async def test_classify_with_code_fences(self):
        llm = MockLLM(['```json\n{"project": "general", "topic": "test", "activity": "debug"}\n```'])
        c = Classifier(llm)
        result = await c.classify("What?", "Debug it.")
        assert result.project == "general"
        assert result.activity == Activity.DEBUG

    @pytest.mark.asyncio
    async def test_classify_unknown_activity_falls_back(self):
        llm = MockLLM(['{"project": "x", "topic": "y", "activity": "unknown_thing"}'])
        c = Classifier(llm)
        result = await c.classify("Q", "A")
        assert result.activity == Activity.DISCUSSION

    @pytest.mark.asyncio
    async def test_classify_api_error_returns_default(self):
        c = Classifier(FailingLLM())
        result = await c.classify("Q", "A")
        assert result.project == "general"
        assert result.activity == Activity.DISCUSSION


class TestNoise:
    @pytest.mark.asyncio
    async def test_noise_true(self):
        llm = MockLLM(['{"noise": true, "why": "greeting"}'])
        c = Classifier(llm)
        assert await c.is_noise("ciao") is True

    @pytest.mark.asyncio
    async def test_noise_false(self):
        llm = MockLLM(['{"noise": false, "why": "real question"}'])
        c = Classifier(llm)
        assert await c.is_noise("How do I fix this bug?") is False

    @pytest.mark.asyncio
    async def test_noise_api_error_defaults_false(self):
        c = Classifier(FailingLLM())
        assert await c.is_noise("test") is False


class TestAnalyzeQuery:
    @pytest.mark.asyncio
    async def test_analyze_returns_analysis(self):
        llm = MockLLM([json.dumps({
            "project": "neo-reloaded",
            "topic": "engine",
            "activity": "bugfix",
            "time_hint": "recent",
            "refined_query": "neo-reloaded engine bugfix",
        })])
        c = Classifier(llm)
        result = await c.analyze_query("recent bugs in engine")
        assert result.project == "neo-reloaded"
        assert result.time_hint == "recent"
        assert result.refined_query == "neo-reloaded engine bugfix"

    @pytest.mark.asyncio
    async def test_analyze_api_error_returns_passthrough(self):
        c = Classifier(FailingLLM())
        result = await c.analyze_query("my query")
        assert result.refined_query == "my query"
        assert result.project is None


class TestClassifyStructured:
    @pytest.mark.asyncio
    async def test_classify_returns_title_and_summary(self):
        llm = MockLLM([json.dumps({
            "project": "neo-cortex",
            "topic": "sqlite",
            "activity": "feature",
            "title": "Add SQLite memory index",
            "summary": "Created MemoryIndex with FTS5 for keyword search",
            "facts": ["SQLite FTS5 for search", "INSERT OR REPLACE for upsert"],
            "concepts": ["full-text-search", "sqlite"],
            "files_touched": ["src/neo_cortex/memory_index.py"],
        })])
        c = Classifier(llm)
        result = await c.classify("Add SQLite index", "Created MemoryIndex...")
        assert result.title == "Add SQLite memory index"
        assert result.summary is not None
        assert "SQLite FTS5 for search" in result.facts
        assert "full-text-search" in result.concepts
        assert "src/neo_cortex/memory_index.py" in result.files_touched

    @pytest.mark.asyncio
    async def test_classify_missing_structured_fields_graceful(self):
        llm = MockLLM(['{"project": "general", "topic": "test", "activity": "discussion"}'])
        c = Classifier(llm)
        result = await c.classify("Q", "A")
        assert result.project == "general"
        assert result.title is None
        assert result.facts == []
        assert result.concepts == []

    @pytest.mark.asyncio
    async def test_classify_partial_structured_fields(self):
        llm = MockLLM([json.dumps({
            "project": "neo-reloaded",
            "topic": "bugfix",
            "activity": "bugfix",
            "title": "Fix crash",
            "facts": ["HasExited throws after dispose"],
        })])
        c = Classifier(llm)
        result = await c.classify("Fix crash?", "Done.")
        assert result.title == "Fix crash"
        assert len(result.facts) == 1
        assert result.concepts == []
        assert result.files_touched == []

    @pytest.mark.asyncio
    async def test_classify_api_error_returns_empty_structured(self):
        c = Classifier(FailingLLM())
        result = await c.classify("Q", "A")
        assert result.title is None
        assert result.facts == []


class TestAggregate:
    @pytest.mark.asyncio
    async def test_aggregate_empty_returns_default(self):
        c = Classifier(MockLLM())
        result = await c.aggregate_to_mbel([])
        assert "empty" in result

    @pytest.mark.asyncio
    async def test_aggregate_returns_mbel(self):
        from neo_cortex.models import MemoryRecord, RecalledMemory
        mem = RecalledMemory(
            record=MemoryRecord(
                id="test1", session_id="s1", question="Q1",
                answer_preview="A1", document="Q: Q1\nA: A1",
            ),
            similarity=0.9,
        )
        llm = MockLLM(["@recall::Q1{bugfix}"])
        c = Classifier(llm)
        result = await c.aggregate_to_mbel([mem])
        assert "@recall" in result


class TestBackwardCompat:
    """GroqClassifier alias still works."""

    def test_alias_exists(self):
        from neo_cortex.classifier import GroqClassifier
        assert GroqClassifier is Classifier
