"""Tests for EpisodeDigestor — topic-based segmentation."""

import time
from unittest.mock import AsyncMock

import pytest

from neo_cortex.conversation_log import ConversationLog
from neo_cortex.digestor import EpisodeDigestor, ACTIVE_THRESHOLD_SECS
from neo_cortex.models import ConversationAppendRequest, ConversationEntry


def make_entry(
    id: int = 0,
    role: str = "user",
    content: str = "hello",
    session_id: str = "sess-001",
    timestamp: float | None = None,
) -> ConversationEntry:
    return ConversationEntry(
        id=id,
        timestamp=timestamp or time.time(),
        session_id=session_id,
        role=role,
        content=content,
    )


def _make_log(tmp_path, entries_data: list[dict]) -> ConversationLog:
    """Create a log with given entries."""
    db = str(tmp_path / "log.db")
    log = ConversationLog(db)
    for e in entries_data:
        log.append(ConversationAppendRequest(
            session_id=e.get("session_id", "sess-001"),
            role=e["role"],
            content=e["content"],
            timestamp=e.get("timestamp", time.time() - 120),
        ))
    return log


def _make_multi_topic_log(tmp_path) -> ConversationLog:
    """Create a log with 3 distinct topics, all old timestamps."""
    db = str(tmp_path / "log.db")
    log = ConversationLog(db)
    old = time.time() - 120

    # Topic 1: CORS fix
    log.append(ConversationAppendRequest(session_id="s1", role="user", content="Fix CORS on neo-reloaded", timestamp=old))
    log.append(ConversationAppendRequest(session_id="s1", role="assistant", content="Added app.UseCors() to pipeline", timestamp=old + 1))
    log.append(ConversationAppendRequest(session_id="s1", role="user", content="test it", timestamp=old + 2))
    log.append(ConversationAppendRequest(session_id="s1", role="assistant", content="132 tests pass", timestamp=old + 3))

    # Topic 2: systemd deploy
    log.append(ConversationAppendRequest(session_id="s1", role="user", content="Deploy to systemd", timestamp=old + 4))
    log.append(ConversationAppendRequest(session_id="s1", role="assistant", content="Created service file, enabled, started", timestamp=old + 5))

    # Topic 3: memory audit
    log.append(ConversationAppendRequest(session_id="s1", role="user", content="Run precision audit on cortex", timestamp=old + 6))
    log.append(ConversationAppendRequest(session_id="s1", role="assistant", content="30 queries: GOOD=8, PARTIAL=11, BAD=11", timestamp=old + 7))
    log.append(ConversationAppendRequest(session_id="s1", role="user", content="what was the weighted score?", timestamp=old + 8))
    log.append(ConversationAppendRequest(session_id="s1", role="assistant", content="Weighted: 45%", timestamp=old + 9))

    return log


class TestPrepareTurns:
    def test_pairs_user_assistant(self):
        """User+assistant entries are paired into one turn."""
        cortex = AsyncMock()
        d = EpisodeDigestor(log=None, cortex=cortex)
        entries = [
            make_entry(id=1, role="user", content="Fix CORS"),
            make_entry(id=2, role="assistant", content="Added UseCors()"),
            make_entry(id=3, role="user", content="test it"),
            make_entry(id=4, role="assistant", content="All green"),
        ]
        turns = d._prepare_turns(entries)
        assert len(turns) == 2
        assert "Fix CORS" in turns[0]["text"]
        assert "UseCors" in turns[0]["text"]
        assert turns[0]["entry_ids"] == [1, 2]

    def test_strips_thinking_tags(self):
        """<thinking> blocks removed from assistant text."""
        d = EpisodeDigestor(log=None, cortex=AsyncMock())
        entries = [
            make_entry(id=1, role="user", content="question"),
            make_entry(id=2, role="assistant", content="<thinking>internal</thinking>The answer."),
        ]
        turns = d._prepare_turns(entries)
        assert "internal" not in turns[0]["text"]
        assert "answer" in turns[0]["text"]

    def test_includes_tool_use_in_entry_ids(self):
        """tool_use entries are grouped with their user turn."""
        d = EpisodeDigestor(log=None, cortex=AsyncMock())
        entries = [
            make_entry(id=1, role="user", content="read the file"),
            make_entry(id=2, role="tool_use", content='{"tool": "Read"}'),
            make_entry(id=3, role="tool_result", content="file contents..."),
            make_entry(id=4, role="assistant", content="The file contains Foo"),
        ]
        turns = d._prepare_turns(entries)
        assert len(turns) == 1
        assert set(turns[0]["entry_ids"]) == {1, 2, 3, 4}

    def test_empty_entries(self):
        d = EpisodeDigestor(log=None, cortex=AsyncMock())
        assert d._prepare_turns([]) == []


class TestTopicSegmentation:
    @pytest.mark.asyncio
    async def test_segments_multiple_topics(self, tmp_path):
        """Classifier segments turns into topic clusters."""
        log = _make_multi_topic_log(tmp_path)
        cortex = AsyncMock()
        cortex.ingest_episode = AsyncMock(return_value="ep_123")
        classifier = AsyncMock()
        classifier.segment_topics = AsyncMock(return_value=[
            {"start": 0, "end": 1, "topic": "CORS fix"},
            {"start": 2, "end": 2, "topic": "systemd deploy"},
            {"start": 3, "end": 4, "topic": "precision audit"},
        ])

        d = EpisodeDigestor(log=log, cortex=cortex, classifier=classifier)
        await d.tick()

        assert classifier.segment_topics.call_count == 1
        assert cortex.ingest_episode.call_count == 3
        assert log.get_undigested() == []

    @pytest.mark.asyncio
    async def test_skips_last_segment_if_active(self, tmp_path):
        """Last segment skipped if last turn is recent."""
        db = str(tmp_path / "log.db")
        log = ConversationLog(db)
        now = time.time()

        # Old topic
        log.append(ConversationAppendRequest(session_id="s1", role="user", content="old topic", timestamp=now - 120))
        log.append(ConversationAppendRequest(session_id="s1", role="assistant", content="old answer", timestamp=now - 119))
        # Active topic (recent)
        log.append(ConversationAppendRequest(session_id="s1", role="user", content="new topic", timestamp=now - 5))
        log.append(ConversationAppendRequest(session_id="s1", role="assistant", content="new answer", timestamp=now - 4))

        cortex = AsyncMock()
        cortex.ingest_episode = AsyncMock(return_value="ep_old")
        classifier = AsyncMock()
        classifier.segment_topics = AsyncMock(return_value=[
            {"start": 0, "end": 0, "topic": "old stuff"},
            {"start": 1, "end": 1, "topic": "new stuff"},
        ])

        d = EpisodeDigestor(log=log, cortex=cortex, classifier=classifier)
        await d.tick()

        # Only old segment ingested
        assert cortex.ingest_episode.call_count == 1
        # Active segment left undigested
        remaining = log.get_undigested()
        assert len(remaining) == 2  # the 2 active entries


class TestTickBehavior:
    @pytest.mark.asyncio
    async def test_noop_when_empty(self, tmp_path):
        """tick() does nothing with no undigested entries."""
        db = str(tmp_path / "log.db")
        log = ConversationLog(db)
        cortex = AsyncMock()
        d = EpisodeDigestor(log=log, cortex=cortex)
        await d.tick()
        assert cortex.ingest_episode.call_count == 0

    @pytest.mark.asyncio
    async def test_no_classifier_single_segment(self, tmp_path):
        """Without classifier, all turns become 1 segment."""
        log = _make_multi_topic_log(tmp_path)
        cortex = AsyncMock()
        cortex.ingest_episode = AsyncMock(return_value="ep_all")
        d = EpisodeDigestor(log=log, cortex=cortex, classifier=None)
        await d.tick()

        assert cortex.ingest_episode.call_count == 1
        assert log.get_undigested() == []

    @pytest.mark.asyncio
    async def test_double_tick_no_reprocess(self, tmp_path):
        """Second tick doesn't re-process already digested entries."""
        log = _make_multi_topic_log(tmp_path)
        cortex = AsyncMock()
        cortex.ingest_episode = AsyncMock(return_value="ep_x")
        classifier = AsyncMock()
        classifier.segment_topics = AsyncMock(return_value=[
            {"start": 0, "end": 4, "topic": "everything"},
        ])
        d = EpisodeDigestor(log=log, cortex=cortex, classifier=classifier)
        await d.tick()
        await d.tick()
        # Only 1 ingest, not 2
        assert cortex.ingest_episode.call_count == 1

    @pytest.mark.asyncio
    async def test_no_log(self):
        """tick() does nothing with no log."""
        d = EpisodeDigestor(log=None, cortex=AsyncMock())
        await d.tick()  # no crash

    @pytest.mark.asyncio
    async def test_classifier_fallback_on_error(self, tmp_path):
        """If classifier fails, treat all as 1 segment."""
        log = _make_multi_topic_log(tmp_path)
        cortex = AsyncMock()
        cortex.ingest_episode = AsyncMock(return_value="ep_x")
        classifier = AsyncMock()
        classifier.segment_topics = AsyncMock(side_effect=Exception("Groq down"))

        d = EpisodeDigestor(log=log, cortex=cortex, classifier=classifier)
        await d.tick()

        # Fallback: 1 segment for everything
        assert cortex.ingest_episode.call_count == 1
        assert log.get_undigested() == []
