"""
test_conversation_summarizer.py

Tests for ConversationSummarizer using a mock LLM (no real API calls).
Run with: pytest test_conversation_summarizer.py -v
"""

import json
import pytest
from .conversation_summarizer import (
    ConversationSummary,
    ConversationSummarizer,
    LLMClient,
    Message,
    Role,
)


# ---------------------------------------------------------------------------
# Fixtures / helpers
# ---------------------------------------------------------------------------

def _summary_json(**overrides) -> str:
    base = {
        "overview":      "The user wants to migrate their monolith to microservices.",
        "user_intent":   "Extract the auth service first using a strangler-fig pattern.",
        "key_decisions": ["Use strangler-fig over big-bang", "Auth service extracted first"],
        "action_items":  ["Get Workday OAuth2 creds from IT", "Draft Helm chart"],
    }
    base.update(overrides)
    return json.dumps(base)


class MockLLMClient(LLMClient):
    def __init__(self, response: str = ""):
        self.response    = response
        self.call_count  = 0
        self.last_messages: list[dict] = []

    def complete(self, messages):
        self.call_count += 1
        self.last_messages = messages
        return self.response


def msgs(n: int) -> list[Message]:
    return [
        Message(Role.USER if i % 2 == 0 else Role.ASSISTANT, f"Turn {i}")
        for i in range(n)
    ]


# ---------------------------------------------------------------------------
# ConversationSummary
# ---------------------------------------------------------------------------

class TestConversationSummary:
    def test_context_block_contains_all_fields(self):
        s = ConversationSummary(
            overview="Short overview.",
            user_intent="Build something useful.",
            key_decisions=["Use FastAPI", "Deploy on AKS"],
            action_items=["Get OAuth creds", "Write tests"],
        )
        block = s.to_context_block()
        assert "CONVERSATION SUMMARY" in block
        assert "Short overview."      in block
        assert "Build something"      in block
        assert "Use FastAPI"          in block
        assert "Get OAuth creds"      in block

    def test_empty_lists_omitted_from_block(self):
        s = ConversationSummary(overview="x", user_intent="y")
        block = s.to_context_block()
        assert "Key decisions" not in block
        assert "Action items"  not in block

    def test_multiple_decisions_joined_by_pipe(self):
        s = ConversationSummary(
            overview="x", user_intent="y",
            key_decisions=["A", "B", "C"],
        )
        assert "A | B | C" in s.to_context_block()


# ---------------------------------------------------------------------------
# ConversationSummarizer — buffer behaviour
# ---------------------------------------------------------------------------

class TestBufferBehaviour:
    def test_no_compression_below_threshold(self):
        client = MockLLMClient(_summary_json())
        s = ConversationSummarizer(client, auto_summarize_after=10)
        s.add_messages(msgs(9))
        assert s.current_summary is None
        assert client.call_count == 0

    def test_compression_fires_at_threshold(self):
        client = MockLLMClient(_summary_json())
        s = ConversationSummarizer(client, auto_summarize_after=5, keep_recent_turns=2)
        s.add_messages(msgs(5))
        assert s.current_summary is not None
        assert client.call_count == 1

    def test_buffer_shrinks_to_keep_recent_after_compression(self):
        client = MockLLMClient(_summary_json())
        s = ConversationSummarizer(client, auto_summarize_after=6, keep_recent_turns=2)
        s.add_messages(msgs(6))
        assert s.buffered_count <= 2

    def test_incremental_update_on_second_compression(self):
        client = MockLLMClient(_summary_json())
        s = ConversationSummarizer(client, auto_summarize_after=4, keep_recent_turns=1)
        s.add_messages(msgs(4))   # first compression  (full)
        s.add_messages(msgs(4))   # second compression (incremental)
        assert client.call_count == 2

    def test_incremental_prompt_references_existing_summary(self):
        client = MockLLMClient(_summary_json())
        s = ConversationSummarizer(client, auto_summarize_after=4, keep_recent_turns=1)
        s.add_messages(msgs(4))
        s.add_messages(msgs(4))
        # The incremental call should include the old summary block in its payload
        user_msg = client.last_messages[1]["content"]
        assert "CONVERSATION SUMMARY" in user_msg
        assert "New turns"            in user_msg

    def test_force_compress_works_below_threshold(self):
        client = MockLLMClient(_summary_json())
        s = ConversationSummarizer(client, auto_summarize_after=100)
        s.add_messages(msgs(5))
        result = s.force_compress()
        assert isinstance(result, ConversationSummary)
        assert client.call_count == 1


# ---------------------------------------------------------------------------
# ConversationSummarizer — get_context_messages
# ---------------------------------------------------------------------------

class TestContextMessages:
    def test_returns_summary_block_after_compression(self):
        client = MockLLMClient(_summary_json())
        s = ConversationSummarizer(client, auto_summarize_after=4, keep_recent_turns=2)
        s.add_messages(msgs(4))
        ctx = s.get_context_messages()
        assert ctx[0]["role"]    == "system"
        assert "CONVERSATION SUMMARY" in ctx[0]["content"]

    def test_no_system_block_before_first_compression(self):
        client = MockLLMClient(_summary_json())
        s = ConversationSummarizer(client, auto_summarize_after=20)
        s.add_messages(msgs(3))
        ctx = s.get_context_messages()
        assert all(m["role"] != "system" for m in ctx)

    def test_recent_turns_appended_after_summary(self):
        client = MockLLMClient(_summary_json())
        s = ConversationSummarizer(client, auto_summarize_after=4, keep_recent_turns=2)
        s.add_messages(msgs(4))
        ctx = s.get_context_messages()
        # system block + 2 recent turns
        assert len(ctx) == 3

    def test_empty_buffer_returns_only_summary(self):
        client = MockLLMClient(_summary_json())
        s = ConversationSummarizer(client, auto_summarize_after=4, keep_recent_turns=0)
        s.add_messages(msgs(4))
        ctx = s.get_context_messages()
        assert len(ctx) == 1
        assert ctx[0]["role"] == "system"


# ---------------------------------------------------------------------------
# JSON parsing edge cases
# ---------------------------------------------------------------------------

class TestParsing:
    def test_strips_markdown_fences(self):
        raw = "```json\n" + _summary_json() + "\n```"
        client = MockLLMClient(raw)
        s = ConversationSummarizer(client, auto_summarize_after=2, keep_recent_turns=0)
        s.add_messages(msgs(2))
        assert s.current_summary is not None
        assert s.current_summary.overview != ""

    def test_raises_on_invalid_json(self):
        client = MockLLMClient("not json at all")
        s = ConversationSummarizer(client, auto_summarize_after=2, keep_recent_turns=0)
        with pytest.raises(ValueError, match="non-JSON"):
            s.add_messages(msgs(2))

    def test_missing_optional_fields_default_to_empty_list(self):
        sparse = json.dumps({"overview": "x", "user_intent": "y"})
        client = MockLLMClient(sparse)
        s = ConversationSummarizer(client, auto_summarize_after=2, keep_recent_turns=0)
        s.add_messages(msgs(2))
        assert s.current_summary.key_decisions == []
        assert s.current_summary.action_items  == []
