"""Unit tests for WorkingMemoryCompressor."""

from ouro.capabilities.compaction import CompressionStrategy, WorkingMemoryCompressor
from ouro.core.llm.base import LLMMessage


class TestCompressorBasics:
    """Test basic compressor functionality."""

    async def test_initialization(self, mock_llm):
        """Test compressor initialization."""
        compressor = WorkingMemoryCompressor(mock_llm)

        assert compressor.llm == mock_llm

    async def test_compress_empty_messages(self, mock_llm):
        """Test compressing empty message list."""
        compressor = WorkingMemoryCompressor(mock_llm)

        result = await compressor.compress([])

        assert len(result.messages) == 0

    async def test_compress_single_message(self, mock_llm):
        """Test compressing a single message."""
        compressor = WorkingMemoryCompressor(mock_llm)

        messages = [LLMMessage(role="user", content="Hello")]
        result = await compressor.compress(messages, strategy=CompressionStrategy.SLIDING_WINDOW)

        assert result is not None
        assert result.original_message_count == 1


class TestCompressionStrategies:
    """Test different compression strategies."""

    async def test_sliding_window_strategy(self, mock_llm, simple_messages):
        """Test sliding window compression strategy."""
        compressor = WorkingMemoryCompressor(mock_llm)

        result = await compressor.compress(
            simple_messages, strategy=CompressionStrategy.SLIDING_WINDOW, target_tokens=100
        )

        assert result is not None
        assert len(result.messages) > 0  # Should have summary message
        assert result.messages[0].role == "user"  # Summary is a user message
        assert result.original_message_count == len(simple_messages)
        assert result.metadata["strategy"] == "sliding_window"
        assert result.compressed_tokens < result.original_tokens

    async def test_deletion_strategy(self, mock_llm, simple_messages):
        """Test deletion compression strategy."""
        compressor = WorkingMemoryCompressor(mock_llm)

        result = await compressor.compress(simple_messages, strategy=CompressionStrategy.DELETION)

        assert result is not None
        assert len(result.messages) == 0  # Deletion removes all messages
        assert result.compressed_tokens == 0
        assert result.metadata["strategy"] == "deletion"

    async def test_selective_strategy_with_tools(
        self, set_memory_config, mock_llm, tool_use_messages
    ):
        """Test selective compression with tool messages."""
        set_memory_config(MEMORY_SHORT_TERM_MIN_SIZE=2)
        compressor = WorkingMemoryCompressor(mock_llm)

        result = await compressor.compress(
            tool_use_messages, strategy=CompressionStrategy.SELECTIVE, target_tokens=200
        )

        assert result is not None
        assert result.metadata["strategy"] == "selective"
        # Should have messages (summary + preserved)
        assert len(result.messages) > 0

    async def test_selective_strategy_preserves_system_messages(self, set_memory_config, mock_llm):
        """Test that selective strategy preserves system messages."""
        set_memory_config(MEMORY_PRESERVE_SYSTEM_PROMPTS=True)
        compressor = WorkingMemoryCompressor(mock_llm)

        messages = [
            LLMMessage(role="system", content="System prompt"),
            LLMMessage(role="user", content="User message"),
            LLMMessage(role="assistant", content="Assistant response"),
        ]

        result = await compressor.compress(
            messages, strategy=CompressionStrategy.SELECTIVE, target_tokens=100
        )

        # System message should be preserved in result.messages
        system_preserved = any(msg.role == "system" for msg in result.messages)
        assert system_preserved


class TestToolPairDetection:
    """Test tool pair detection and preservation."""

    async def test_find_tool_pairs_basic(self, mock_llm, tool_use_messages):
        """Test basic tool pair detection."""
        compressor = WorkingMemoryCompressor(mock_llm)

        pairs, orphaned = compressor._find_tool_pairs(tool_use_messages)

        # Should find at least one pair
        assert len(pairs) > 0
        # Each pair should be [assistant_index, user_index]
        for pair in pairs:
            assert len(pair) == 2
            assert pair[0] < pair[1]  # Assistant comes before user
        # Should have no orphaned tool_use (all have results)
        assert len(orphaned) == 0

    async def test_find_tool_pairs_multiple(self, mock_llm):
        """Test finding multiple tool pairs."""
        compressor = WorkingMemoryCompressor(mock_llm)

        messages = []
        for i in range(3):
            messages.extend(
                [
                    LLMMessage(
                        role="assistant",
                        content=[
                            {
                                "type": "tool_use",
                                "id": f"tool_{i}",
                                "name": f"tool_{i}",
                                "input": {},
                            }
                        ],
                    ),
                    LLMMessage(
                        role="user",
                        content=[
                            {
                                "type": "tool_result",
                                "tool_use_id": f"tool_{i}",
                                "content": f"result_{i}",
                            }
                        ],
                    ),
                ]
            )

        pairs, orphaned = compressor._find_tool_pairs(messages)
        assert len(pairs) == 3
        assert len(orphaned) == 0

    async def test_find_tool_pairs_with_mismatches(self, mock_llm, mismatched_tool_messages):
        """Test tool pair detection with mismatched pairs."""
        compressor = WorkingMemoryCompressor(mock_llm)

        pairs, orphaned = compressor._find_tool_pairs(mismatched_tool_messages)

        # Should only find matched pairs (tool_2 has a result, tool_1 doesn't)
        assert len(pairs) == 1
        # Should have one orphaned tool_use (tool_1)
        assert len(orphaned) == 1

    async def test_tool_pairs_preserved_together(
        self, set_memory_config, mock_llm, tool_use_messages
    ):
        """Test that when a tool pair is found, both messages are preserved together."""
        set_memory_config(MEMORY_SHORT_TERM_MIN_SIZE=1)
        compressor = WorkingMemoryCompressor(mock_llm)

        preserved, to_compress = compressor._separate_messages(tool_use_messages)

        # Find tool_use and tool_result in preserved messages
        tool_use_indices = []
        tool_result_indices = []

        for i, msg in enumerate(tool_use_messages):
            if isinstance(msg.content, list):
                for block in msg.content:
                    if isinstance(block, dict):
                        if block.get("type") == "tool_use":
                            tool_use_indices.append(i)
                        elif block.get("type") == "tool_result":
                            tool_result_indices.append(i)

        # Check that if tool_use is preserved, tool_result is also preserved
        for tool_use_idx in tool_use_indices:
            if tool_use_messages[tool_use_idx] in preserved:
                # Find corresponding tool_result
                # This is a simplified check - in reality we'd match by ID
                assert len(tool_result_indices) > 0


class TestProtectedTools:
    """Test protected tool handling and todo context injection."""

    async def test_protected_tools_set_is_empty_by_default(self, mock_llm):
        """Test that PROTECTED_TOOLS is empty - todo state is now injected via context."""
        compressor = WorkingMemoryCompressor(mock_llm)
        # PROTECTED_TOOLS should be empty because todo state is now injected
        # via todo_context parameter instead of preserving tool messages
        assert len(compressor.PROTECTED_TOOLS) == 0

    async def test_todo_tool_messages_can_be_compressed(
        self, set_memory_config, mock_llm, protected_tool_messages
    ):
        """Test that todo tool messages can now be compressed (state preserved via injection)."""
        set_memory_config(MEMORY_SHORT_TERM_MIN_SIZE=0)  # Don't preserve anything by default
        compressor = WorkingMemoryCompressor(mock_llm)

        preserved, to_compress = compressor._separate_messages(protected_tool_messages)

        # Todo tool messages should now be compressible (not protected)
        # Only system messages should be preserved when MEMORY_SHORT_TERM_MIN_SIZE=0
        assert len(to_compress) > 0

    async def test_todo_context_injected_in_sliding_window(self, mock_llm, simple_messages):
        """Test that todo context is injected into sliding window compression."""
        compressor = WorkingMemoryCompressor(mock_llm)
        todo_context = "1. [pending] Fix bug\n2. [in_progress] Write tests"

        result = await compressor.compress(
            simple_messages,
            strategy="sliding_window",
            target_tokens=500,
            todo_context=todo_context,
        )

        # The summary should contain the todo context
        summary_content = result.messages[-1].content if result.messages else ""
        assert "[Current Tasks]" in summary_content
        assert "Fix bug" in summary_content

    async def test_todo_context_injected_in_selective(
        self, set_memory_config, mock_llm, tool_use_messages
    ):
        """Test that todo context is injected into selective compression."""
        set_memory_config(MEMORY_SHORT_TERM_MIN_SIZE=2)
        compressor = WorkingMemoryCompressor(mock_llm)
        todo_context = "1. [completed] Setup project"

        result = await compressor.compress(
            tool_use_messages,
            strategy="selective",
            target_tokens=500,
            todo_context=todo_context,
        )

        # Find the summary message and check for todo context
        summary_found = False
        for msg in result.messages:
            content = str(msg.content)
            if (
                "[Previous conversation summary]" in content
                and "[Current Tasks]" in content
                and "Setup project" in content
            ):
                summary_found = True
                break

        assert summary_found, "Todo context should be in the summary"

    async def test_no_todo_context_when_none(self, mock_llm, simple_messages):
        """Test that no todo section is added when todo_context is None."""
        compressor = WorkingMemoryCompressor(mock_llm)

        result = await compressor.compress(
            simple_messages,
            strategy="sliding_window",
            target_tokens=500,
            todo_context=None,
        )

        summary_content = result.messages[-1].content if result.messages else ""
        assert "[Current Tasks]" not in summary_content


class TestMessageSeparation:
    """Test message separation logic."""

    async def test_separate_messages_basic(self, set_memory_config, mock_llm, simple_messages):
        """Test basic message separation - latest user query is always preserved
        (Step 2c invariant), and with MIN_SIZE=0 nothing else is."""
        set_memory_config(
            MEMORY_SHORT_TERM_MIN_SIZE=0
        )  # Don't preserve recent messages for this test
        compressor = WorkingMemoryCompressor(mock_llm)

        preserved, to_compress = compressor._separate_messages(simple_messages)

        # With MIN_SIZE=0, only the latest non-summary user message is preserved.
        latest_user_idx = compressor._find_latest_user_query(simple_messages)
        if latest_user_idx is not None:
            assert len(preserved) == 1
            assert preserved[0] is simple_messages[latest_user_idx]
            assert len(to_compress) == len(simple_messages) - 1
        else:
            assert len(preserved) == 0
            assert len(to_compress) == len(simple_messages)
        # Total should equal original
        assert len(preserved) + len(to_compress) == len(simple_messages)

    async def test_separate_preserves_system_messages(self, set_memory_config, mock_llm):
        """Test that system messages are preserved (alongside the latest user
        query, which Step 2c always keeps)."""
        set_memory_config(MEMORY_PRESERVE_SYSTEM_PROMPTS=True, MEMORY_SHORT_TERM_MIN_SIZE=0)
        compressor = WorkingMemoryCompressor(mock_llm)

        messages = [
            LLMMessage(role="system", content="System prompt"),
            LLMMessage(role="user", content="Hello"),
            LLMMessage(role="assistant", content="Hi there!"),
        ]

        preserved, to_compress = compressor._separate_messages(messages)

        # System message AND the latest user query are preserved.
        preserved_roles = sorted(m.role for m in preserved)
        assert preserved_roles == ["system", "user"]
        # The assistant reply is the only thing that gets compressed.
        assert len(to_compress) == 1
        assert to_compress[0].role == "assistant"

    async def test_tool_pair_preservation_rule(
        self, set_memory_config, mock_llm, tool_use_messages
    ):
        """Test that tool pairs are preserved together (critical rule)."""
        set_memory_config(MEMORY_SHORT_TERM_MIN_SIZE=1)
        compressor = WorkingMemoryCompressor(mock_llm)

        preserved, to_compress = compressor._separate_messages(tool_use_messages)

        # Collect tool_use IDs and tool_result IDs from preserved messages
        preserved_tool_use_ids = set()
        preserved_tool_result_ids = set()

        for msg in preserved:
            if isinstance(msg.content, list):
                for block in msg.content:
                    if isinstance(block, dict):
                        if block.get("type") == "tool_use":
                            preserved_tool_use_ids.add(block.get("id"))
                        elif block.get("type") == "tool_result":
                            preserved_tool_result_ids.add(block.get("tool_use_id"))

        # Collect from to_compress
        compressed_tool_use_ids = set()
        compressed_tool_result_ids = set()

        for msg in to_compress:
            if isinstance(msg.content, list):
                for block in msg.content:
                    if isinstance(block, dict):
                        if block.get("type") == "tool_use":
                            compressed_tool_use_ids.add(block.get("id"))
                        elif block.get("type") == "tool_result":
                            compressed_tool_result_ids.add(block.get("tool_use_id"))

        # CRITICAL: Tool pairs should not be split between preserved and compressed
        # If a tool_use is preserved, its result should be preserved
        for tool_id in preserved_tool_use_ids:
            assert (
                tool_id in preserved_tool_result_ids
            ), f"Tool use {tool_id} is preserved but its result is not"

        # If a tool_result is preserved, its use should be preserved
        for tool_id in preserved_tool_result_ids:
            assert (
                tool_id in preserved_tool_use_ids
            ), f"Tool result for {tool_id} is preserved but its use is not"

    async def test_multi_tool_call_pair_preservation(self, set_memory_config, mock_llm):
        """Test that ALL tool responses are preserved when an assistant message
        with multiple tool_calls is partially in the recent window.

        Regression test: an assistant message at index N has 5 tool_calls with
        responses at N+1..N+5.  If only N+3..N+5 fall inside the recent window,
        a single-pass pair check would skip [N, N+1] and [N, N+2] because N is
        not yet marked when those pairs are visited.  The fixed-point loop must
        pull N+1 and N+2 into the preserved set.
        """
        # Use MIN_SIZE=3 so only the last 3 messages are initially preserved.
        set_memory_config(MEMORY_SHORT_TERM_MIN_SIZE=3, MEMORY_PRESERVE_SYSTEM_PROMPTS=False)
        compressor = WorkingMemoryCompressor(mock_llm)

        # Build messages: user, assistant(5 tool_calls), 5 tool responses, assistant final
        messages = [
            LLMMessage(role="user", content="Do many things"),
            LLMMessage(
                role="assistant",
                content=None,
                tool_calls=[
                    {
                        "id": f"tc_{i}",
                        "type": "function",
                        "function": {"name": "tool", "arguments": "{}"},
                    }
                    for i in range(5)
                ],
            ),
        ]
        messages.extend(
            LLMMessage(role="tool", content=f"result {i}", tool_call_id=f"tc_{i}", name="tool")
            for i in range(5)
        )
        messages.append(LLMMessage(role="assistant", content="All done."))

        # Total 8 messages (indices 0-7).  Last 3 = indices 5, 6, 7.
        # Index 5 is tool response tc_3, index 6 is tool response tc_4,
        # index 7 is assistant final.
        # Pair [1,5] triggers preserving index 1, which must cascade to
        # also preserve indices 2,3,4 (tool responses tc_0, tc_1, tc_2).
        preserved, to_compress = compressor._separate_messages(messages)

        preserved_tool_call_ids: set[str] = set()
        preserved_tool_response_ids: set[str] = set()
        for msg in preserved:
            if msg.role == "assistant" and msg.tool_calls:
                for tc in msg.tool_calls:
                    tid = tc.get("id") if isinstance(tc, dict) else getattr(tc, "id", None)
                    if tid:
                        preserved_tool_call_ids.add(tid)
            if msg.role == "tool" and hasattr(msg, "tool_call_id") and msg.tool_call_id:
                preserved_tool_response_ids.add(msg.tool_call_id)

        # All 5 tool_call_ids must have their responses preserved
        for i in range(5):
            assert f"tc_{i}" in preserved_tool_call_ids, f"tc_{i} assistant not preserved"
            assert f"tc_{i}" in preserved_tool_response_ids, f"tc_{i} response not preserved"

    async def test_latest_user_query_preserved_outside_recent_window(
        self, set_memory_config, mock_llm
    ):
        """The user message that triggered the current run must be preserved
        even when the recent-N window has been pushed past it by many tool
        iterations within the same turn.

        Regression: a 5/10 reproduction showed that with MIN_SIZE=6, after
        ~3 tool calls in one turn the new user message fell out of the recent
        window, got compressed away, and the post-compaction LLM had no idea
        what the user asked — it kept hallucinating fresh tool calls.
        """
        set_memory_config(MEMORY_SHORT_TERM_MIN_SIZE=6, MEMORY_PRESERVE_SYSTEM_PROMPTS=False)
        compressor = WorkingMemoryCompressor(mock_llm)

        # Build: old chatter (will compress) + new user query + 4 tool pairs
        # (= 8 trailing messages). With MIN_SIZE=6, only the last 6 land in
        # the recent window, pushing the new user query OUT.
        messages: list[LLMMessage] = []
        for i in range(10):
            messages.append(LLMMessage(role="user", content=f"old turn {i}"))
            messages.append(LLMMessage(role="assistant", content=f"old reply {i}"))
        new_user_idx = len(messages)
        messages.append(LLMMessage(role="user", content="WHAT THE USER JUST ASKED"))
        for i in range(4):
            messages.append(
                LLMMessage(
                    role="assistant",
                    content=None,
                    tool_calls=[
                        {
                            "id": f"new_tc_{i}",
                            "type": "function",
                            "function": {"name": "tool", "arguments": "{}"},
                        }
                    ],
                )
            )
            messages.append(
                LLMMessage(
                    role="tool",
                    content=f"new result {i}",
                    tool_call_id=f"new_tc_{i}",
                    name="tool",
                )
            )

        preserved, to_compress = compressor._separate_messages(messages)

        # The latest user query must survive compression.
        preserved_user_contents = [
            m.content for m in preserved if m.role == "user" and isinstance(m.content, str)
        ]
        assert "WHAT THE USER JUST ASKED" in preserved_user_contents, (
            f"latest user query was compressed away. preserved user msgs: "
            f"{preserved_user_contents}"
        )
        # And it must NOT appear in the to_compress list.
        compressed_user_contents = [
            m.content for m in to_compress if m.role == "user" and isinstance(m.content, str)
        ]
        assert "WHAT THE USER JUST ASKED" not in compressed_user_contents

        # Sanity: the index we tracked is in preserved (catches off-by-one).
        assert messages[new_user_idx] in preserved

    async def test_summary_user_message_does_not_count_as_latest_query(
        self, set_memory_config, mock_llm
    ):
        """If the most recent user message is a prior compaction summary
        (``SUMMARY_PREFIX``), keep walking backward — that's a synthetic
        message, not a real user query."""
        set_memory_config(MEMORY_SHORT_TERM_MIN_SIZE=2, MEMORY_PRESERVE_SYSTEM_PROMPTS=False)
        compressor = WorkingMemoryCompressor(mock_llm)

        messages = [
            LLMMessage(role="user", content="real user question"),
            LLMMessage(role="assistant", content="working on it"),
            LLMMessage(
                role="user",
                content=f"{compressor.SUMMARY_PREFIX}prior summary text here",
            ),
            LLMMessage(role="assistant", content="ack"),
            LLMMessage(role="assistant", content="another reply"),
        ]

        idx = compressor._find_latest_user_query(messages)
        assert idx == 0, f"expected to skip the summary at [2] and pick [0], got {idx}"

    async def test_find_latest_user_query_returns_none_when_no_user(self, mock_llm):
        compressor = WorkingMemoryCompressor(mock_llm)
        messages = [
            LLMMessage(role="system", content="sys"),
            LLMMessage(role="assistant", content="hi"),
        ]
        assert compressor._find_latest_user_query(messages) is None


class TestTokenEstimation:
    """Test token estimation logic."""

    async def test_estimate_tokens_simple_text(self, mock_llm):
        """Test token estimation for simple text messages."""
        compressor = WorkingMemoryCompressor(mock_llm)

        messages = [LLMMessage(role="user", content="Hello world")]
        tokens = compressor._estimate_tokens(messages)

        assert tokens > 0
        assert tokens < 100  # Simple message shouldn't be huge

    async def test_estimate_tokens_long_text(self, mock_llm):
        """Test token estimation for long text."""
        compressor = WorkingMemoryCompressor(mock_llm)

        long_content = "This is a long message. " * 100
        messages = [LLMMessage(role="user", content=long_content)]
        tokens = compressor._estimate_tokens(messages)

        # litellm/tiktoken: roughly 4 chars per token for English, plus message overhead
        expected_range = (len(long_content) // 6, len(long_content) // 2)
        assert expected_range[0] < tokens < expected_range[1]

    async def test_estimate_tokens_with_tool_content(self, mock_llm, tool_use_messages):
        """Test token estimation with tool content."""
        compressor = WorkingMemoryCompressor(mock_llm)

        tokens = compressor._estimate_tokens(tool_use_messages)

        # Tool messages have overhead, should be more than just text
        assert tokens > 0

    async def test_extract_text_content_from_dict(self, mock_llm):
        """Test extracting text content from dict-based content."""
        compressor = WorkingMemoryCompressor(mock_llm)

        msg = LLMMessage(
            role="assistant",
            content=[
                {"type": "text", "text": "Hello"},
                {"type": "tool_use", "id": "t1", "name": "tool", "input": {}},
            ],
        )

        text = compressor._extract_text_content(msg)
        assert "Hello" in text


class TestCompressionMetrics:
    """Test compression metrics calculation."""

    async def test_compression_ratio_calculation(self, mock_llm, simple_messages):
        """Test that compression ratio is calculated correctly."""
        compressor = WorkingMemoryCompressor(mock_llm)

        result = await compressor.compress(
            simple_messages, strategy=CompressionStrategy.SLIDING_WINDOW, target_tokens=50
        )

        assert result.compression_ratio > 0
        assert result.compression_ratio <= 1.0
        # Compressed should be smaller than original
        assert result.compressed_tokens <= result.original_tokens

    async def test_token_savings_calculation(self, mock_llm, simple_messages):
        """Test token savings calculation."""
        compressor = WorkingMemoryCompressor(mock_llm)

        result = await compressor.compress(
            simple_messages, strategy=CompressionStrategy.SLIDING_WINDOW
        )

        savings = result.token_savings
        assert savings >= 0
        assert savings == result.original_tokens - result.compressed_tokens

    async def test_savings_percentage_calculation(self, mock_llm, simple_messages):
        """Test savings percentage calculation."""
        compressor = WorkingMemoryCompressor(mock_llm)

        result = await compressor.compress(
            simple_messages, strategy=CompressionStrategy.SLIDING_WINDOW
        )

        percentage = result.savings_percentage
        assert 0 <= percentage <= 100


class TestCompressionErrors:
    """Test error handling in compression."""

    async def test_compression_with_llm_error(self, mock_llm, simple_messages):
        """Test compression behavior when LLM call fails."""
        compressor = WorkingMemoryCompressor(mock_llm)

        # Make LLM raise an error
        async def error_call(*args, **kwargs):
            raise Exception("LLM error")

        mock_llm.call_async = error_call

        # Should handle error gracefully
        result = await compressor.compress(
            simple_messages, strategy=CompressionStrategy.SLIDING_WINDOW
        )

        assert result is not None
        # Should fallback to preserving key messages
        assert len(result.messages) > 0
        assert "error" in result.metadata

    async def test_unknown_strategy_fallback(self, mock_llm, simple_messages):
        """Test fallback to default strategy for unknown strategy."""
        compressor = WorkingMemoryCompressor(mock_llm)

        # Use invalid strategy name
        result = await compressor.compress(simple_messages, strategy="invalid_strategy")

        # Should fallback to sliding window
        assert result is not None


class TestBuildCompactionPrompt:
    """Test cache-safe compaction prompt generation."""

    async def test_sliding_window_prompt(self, mock_llm, simple_messages):
        """Test compaction prompt for sliding window strategy."""
        compressor = WorkingMemoryCompressor(mock_llm)

        prompt = compressor.build_compaction_prompt(
            simple_messages, CompressionStrategy.SLIDING_WINDOW, target_tokens=200
        )

        assert "Summarize the conversation above" in prompt
        assert "200 tokens" in prompt
        # Should NOT contain the messages themselves (they're in the LLM context)
        assert "Hello" not in prompt
        assert "Hi there" not in prompt

    async def test_selective_prompt_includes_preserved_count(
        self, set_memory_config, mock_llm, tool_use_messages
    ):
        """Test compaction prompt for selective strategy includes preserved message count."""
        set_memory_config(MEMORY_SHORT_TERM_MIN_SIZE=2)
        compressor = WorkingMemoryCompressor(mock_llm)

        prompt = compressor.build_compaction_prompt(
            tool_use_messages, CompressionStrategy.SELECTIVE, target_tokens=200
        )

        assert "Summarize the conversation above" in prompt
        assert "kept verbatim" in prompt

    async def test_prompt_with_todo_context(self, mock_llm, simple_messages):
        """Test compaction prompt includes todo context instruction."""
        compressor = WorkingMemoryCompressor(mock_llm)
        todo_context = "1. [pending] Fix bug\n2. [completed] Write docs"

        prompt = compressor.build_compaction_prompt(
            simple_messages,
            CompressionStrategy.SLIDING_WINDOW,
            target_tokens=200,
            todo_context=todo_context,
        )

        assert "[Current Tasks]" in prompt
        assert "Fix bug" in prompt

    async def test_prompt_without_todo_context(self, mock_llm, simple_messages):
        """Test compaction prompt without todo context."""
        compressor = WorkingMemoryCompressor(mock_llm)

        prompt = compressor.build_compaction_prompt(
            simple_messages,
            CompressionStrategy.SLIDING_WINDOW,
            target_tokens=200,
            todo_context=None,
        )

        assert "[Current Tasks]" not in prompt
