"""
Tests for performance-first context pruning (Tier 1 + Tier 2).

Covers:
- Stale tool result summarization
- Assistant self-repetition collapse
- Combined prune_for_performance pipeline
- Edge cases (empty messages, no tool results, single-turn conversations)
"""

import pytest

from api.models import CompleteRequest
from api.services.context_management_service import (
    summarize_stale_tool_results,
    collapse_self_repetition,
    prune_for_performance,
    _is_read_only_tool_result,
    _count_assistant_turns_after,
    _paragraph_fingerprints,
    _summarize_tool_result,
)


def _msg(role, content, *, tool_call_id=None, tool_calls=None):
    return CompleteRequest.Message(
        role=role,
        content=content,
        tool_call_id=tool_call_id,
        tool_calls=tool_calls,
    )


def _tool_call(name, tool_call_id="tc_1", arguments="{}"):
    return {
        "id": tool_call_id,
        "function": {"name": name, "arguments": arguments},
    }


# ===========================================================================
# _summarize_tool_result
# ===========================================================================

class TestSummarizeToolResult:
    def test_short_content_unchanged(self):
        result = _summarize_tool_result("line1\nline2\nline3", max_chars=300)
        assert result == "line1\nline2\nline3"

    def test_long_content_summarized(self):
        lines = [f"line {i}: some content here" for i in range(50)]
        content = "\n".join(lines)
        result = _summarize_tool_result(content, max_chars=300)
        assert "[Summarized tool result:" in result
        assert "50 lines" in result
        assert "more lines truncated" in result

    def test_empty_content(self):
        assert _summarize_tool_result("") == "[Empty tool result]"
        assert _summarize_tool_result(None) == "[Empty tool result]"


# ===========================================================================
# _count_assistant_turns_after
# ===========================================================================

class TestCountAssistantTurnsAfter:
    def test_counts_correctly(self):
        msgs = [
            _msg("user", "q1"),
            _msg("assistant", "a1"),
            _msg("tool", "r1"),
            _msg("assistant", "a2"),
            _msg("user", "q2"),
            _msg("assistant", "a3"),
        ]
        assert _count_assistant_turns_after(msgs, 0) == 3
        assert _count_assistant_turns_after(msgs, 2) == 2
        assert _count_assistant_turns_after(msgs, 5) == 0


# ===========================================================================
# _is_read_only_tool_result
# ===========================================================================

class TestIsReadOnlyToolResult:
    def test_read_tool_detected(self):
        msgs = [
            _msg("assistant", "reading file", tool_calls=[_tool_call("read", "tc_1")]),
            _msg("tool", "file contents here", tool_call_id="tc_1"),
        ]
        assert _is_read_only_tool_result(msgs[1], msgs, 1) is True

    def test_grep_tool_detected(self):
        msgs = [
            _msg("assistant", "searching", tool_calls=[_tool_call("grep", "tc_2")]),
            _msg("tool", "match found", tool_call_id="tc_2"),
        ]
        assert _is_read_only_tool_result(msgs[1], msgs, 1) is True

    def test_write_tool_not_read_only(self):
        msgs = [
            _msg("assistant", "editing", tool_calls=[_tool_call("strreplace", "tc_3")]),
            _msg("tool", "edit applied", tool_call_id="tc_3"),
        ]
        assert _is_read_only_tool_result(msgs[1], msgs, 1) is False

    def test_no_preceding_assistant(self):
        msgs = [_msg("tool", "orphan result", tool_call_id="tc_4")]
        assert _is_read_only_tool_result(msgs[0], msgs, 0) is False


# ===========================================================================
# _paragraph_fingerprints
# ===========================================================================

class TestParagraphFingerprints:
    def test_ignores_short_paragraphs(self):
        fps = _paragraph_fingerprints("short\n\nalso short")
        assert len(fps) == 0

    def test_captures_long_paragraphs(self):
        text = (
            "This is a reasonably long paragraph that should be captured by the fingerprinting system.\n\n"
            "This is another reasonably long paragraph that is different from the first one above."
        )
        fps = _paragraph_fingerprints(text)
        assert len(fps) == 2

    def test_identical_paragraphs_same_fingerprint(self):
        para = "This is a reasonably long paragraph that should produce a consistent fingerprint value."
        fps1 = _paragraph_fingerprints(para)
        fps2 = _paragraph_fingerprints(para)
        assert fps1 == fps2

    def test_whitespace_normalization(self):
        p1 = "This is a reasonably long paragraph that should produce a consistent fingerprint value."
        p2 = "This  is  a  reasonably  long  paragraph  that  should  produce  a  consistent  fingerprint  value."
        assert _paragraph_fingerprints(p1) == _paragraph_fingerprints(p2)


# ===========================================================================
# summarize_stale_tool_results
# ===========================================================================

class TestSummarizeStaleToolResults:
    def test_recent_tool_results_untouched(self):
        msgs = [
            _msg("user", "do something"),
            _msg("assistant", "reading", tool_calls=[_tool_call("read", "tc_1")]),
            _msg("tool", "big file contents " * 100, tool_call_id="tc_1"),
            _msg("assistant", "here's the answer"),
        ]
        result = summarize_stale_tool_results(msgs, turn_threshold=3)
        assert result[2].content == msgs[2].content

    def test_old_read_only_results_summarized(self):
        msgs = [
            _msg("user", "q1"),
            _msg("assistant", "reading", tool_calls=[_tool_call("read", "tc_1")]),
            _msg("tool", "big file contents " * 100, tool_call_id="tc_1"),
            _msg("assistant", "a1"),
            _msg("user", "q2"),
            _msg("assistant", "a2"),
            _msg("user", "q3"),
            _msg("assistant", "a3"),
            _msg("user", "q4"),
            _msg("assistant", "a4"),
        ]
        result = summarize_stale_tool_results(msgs, turn_threshold=3)
        assert "[Summarized tool result:" in result[2].content
        assert result[2].tool_call_id == "tc_1"

    def test_old_write_results_preserved(self):
        msgs = [
            _msg("user", "q1"),
            _msg("assistant", "editing", tool_calls=[_tool_call("strreplace", "tc_1")]),
            _msg("tool", "edit applied successfully " * 50, tool_call_id="tc_1"),
            _msg("assistant", "a1"),
            _msg("assistant", "a2"),
            _msg("assistant", "a3"),
            _msg("assistant", "a4"),
        ]
        result = summarize_stale_tool_results(msgs, turn_threshold=3)
        assert result[2].content == msgs[2].content

    def test_already_capsule_untouched(self):
        msgs = [
            _msg("user", "q1"),
            _msg("assistant", "reading", tool_calls=[_tool_call("read", "tc_1")]),
            _msg("tool", "[Capsule] already externalized", tool_call_id="tc_1"),
            _msg("assistant", "a1"),
            _msg("assistant", "a2"),
            _msg("assistant", "a3"),
            _msg("assistant", "a4"),
        ]
        result = summarize_stale_tool_results(msgs, turn_threshold=3)
        assert result[2].content == "[Capsule] already externalized"

    def test_threshold_zero_disables(self):
        msgs = [
            _msg("assistant", "reading", tool_calls=[_tool_call("read", "tc_1")]),
            _msg("tool", "content " * 100, tool_call_id="tc_1"),
            _msg("assistant", "a1"),
            _msg("assistant", "a2"),
            _msg("assistant", "a3"),
            _msg("assistant", "a4"),
        ]
        result = summarize_stale_tool_results(msgs, turn_threshold=0)
        assert result[1].content == msgs[1].content

    def test_empty_messages(self):
        assert summarize_stale_tool_results([], turn_threshold=3) == []


# ===========================================================================
# collapse_self_repetition
# ===========================================================================

class TestCollapseSelfRepetition:
    def _long_para(self, seed: str) -> str:
        return f"This is a detailed analysis paragraph about {seed} that is long enough to be fingerprinted by the system."

    def test_no_repetition_unchanged(self):
        msgs = [
            _msg("user", "q1"),
            _msg("assistant", self._long_para("topic A")),
            _msg("user", "q2"),
            _msg("assistant", self._long_para("topic B")),
        ]
        result = collapse_self_repetition(msgs, overlap_threshold=0.6)
        assert len(result) == 4
        assert result[1].content == msgs[1].content
        assert result[3].content == msgs[3].content

    def test_repeated_planning_collapsed(self):
        shared_plan = (
            "REASONING SUMMARY\n\n"
            + self._long_para("the dashboard performance bottleneck in forecast_service") + "\n\n"
            + self._long_para("the frontend making eight parallel API calls to separate endpoints") + "\n\n"
            + self._long_para("the missing composite indexes on subscription_id and created_at")
        )
        msgs = [
            _msg("user", "fix the dashboard"),
            _msg("assistant", shared_plan + "\n\nLet me start implementing."),
            _msg("user", "continue"),
            _msg("assistant", shared_plan + "\n\nNow implementing step 1."),
            _msg("user", "continue"),
            _msg("assistant", shared_plan + "\n\nImplementing step 1 now for real."),
        ]
        result = collapse_self_repetition(msgs, overlap_threshold=0.6)
        assert "[Previous assistant response collapsed" in result[1].content
        assert "[Previous assistant response collapsed" in result[3].content
        assert "REASONING SUMMARY" in result[5].content

    def test_user_messages_never_collapsed(self):
        msgs = [
            _msg("user", self._long_para("same question") * 3),
            _msg("assistant", "answer 1"),
            _msg("user", self._long_para("same question") * 3),
            _msg("assistant", "answer 2"),
        ]
        result = collapse_self_repetition(msgs, overlap_threshold=0.6)
        assert result[0].content == msgs[0].content
        assert result[2].content == msgs[2].content

    def test_threshold_boundary(self):
        para = self._long_para("shared content across messages")
        unique_a = self._long_para("unique content only in first message")
        unique_b = self._long_para("unique content only in second message")
        msgs = [
            _msg("assistant", f"{para}\n\n{unique_a}"),
            _msg("assistant", f"{para}\n\n{unique_b}"),
        ]
        result_strict = collapse_self_repetition(msgs, overlap_threshold=0.9)
        assert result_strict[0].content == msgs[0].content

        result_loose = collapse_self_repetition(msgs, overlap_threshold=0.3)
        assert "[Previous assistant response collapsed" in result_loose[0].content

    def test_empty_messages(self):
        assert collapse_self_repetition([], overlap_threshold=0.6) == []


# ===========================================================================
# prune_for_performance (combined pipeline)
# ===========================================================================

class TestPruneForPerformance:
    def _long_para(self, seed: str) -> str:
        return f"This is a detailed analysis paragraph about {seed} that is long enough to be fingerprinted by the system."

    def test_both_pruning_types_applied(self):
        shared_plan = (
            self._long_para("the dashboard bottleneck") + "\n\n"
            + self._long_para("the forecast query issue") + "\n\n"
            + self._long_para("the missing database indexes")
        )
        msgs = [
            _msg("user", "analyze"),
            _msg("assistant", "reading", tool_calls=[_tool_call("grep", "tc_1")]),
            _msg("tool", "grep results " * 200, tool_call_id="tc_1"),
            _msg("assistant", shared_plan),
            _msg("user", "fix it"),
            _msg("assistant", shared_plan + "\n\nLet me implement."),
            _msg("user", "continue"),
            _msg("assistant", shared_plan + "\n\nImplementing now."),
        ]
        result = prune_for_performance(
            msgs,
            stale_turn_threshold=3,
            stale_summary_max_chars=300,
            self_repeat_overlap=0.6,
        )
        assert "[Summarized tool result:" in result[2].content
        assert "[Previous assistant response collapsed" in result[3].content
        assert result[7].content == msgs[7].content

    def test_short_conversation_unchanged(self):
        msgs = [
            _msg("user", "hello"),
            _msg("assistant", "hi there"),
        ]
        result = prune_for_performance(msgs)
        assert len(result) == 2
        assert result[0].content == "hello"
        assert result[1].content == "hi there"

    def test_preserves_message_count(self):
        msgs = [
            _msg("user", "q"),
            _msg("assistant", "a", tool_calls=[_tool_call("read", "tc_1")]),
            _msg("tool", "content " * 100, tool_call_id="tc_1"),
            _msg("assistant", "done"),
        ]
        result = prune_for_performance(msgs, stale_turn_threshold=0)
        assert len(result) == len(msgs)

    def test_tool_call_ids_preserved(self):
        msgs = [
            _msg("assistant", "reading", tool_calls=[_tool_call("read", "tc_42")]),
            _msg("tool", "content " * 200, tool_call_id="tc_42"),
            _msg("assistant", "a1"),
            _msg("assistant", "a2"),
            _msg("assistant", "a3"),
            _msg("assistant", "a4"),
        ]
        result = prune_for_performance(msgs, stale_turn_threshold=3)
        assert result[1].tool_call_id == "tc_42"
