"""Tests for cortex/hippo/tools/grep.py."""

from __future__ import annotations

from dataclasses import dataclass, field
from datetime import datetime, timezone
from unittest.mock import AsyncMock, MagicMock

import pytest

from cortex.hippo.models import MessageRecord, SummaryRecord
from cortex.hippo.retrieval import GrepResult
from cortex.hippo.tools.conversation_scope import LcmConversationScope
from cortex.hippo.tools.grep import (
    MAX_RESULT_CHARS,
    execute_grep,
    truncate_snippet,
)


# ── Helpers ────────────────────────────────────────────────────────────────


def make_message(
    message_id: int = 1,
    content: str = "test message",
    role: str = "user",
    created_at: str = "2026-03-15T10:00:00Z",
) -> MessageRecord:
    return MessageRecord(
        message_id=message_id,
        conversation_id=1,
        owner_id="test",
        seq=message_id,
        role=role,
        content=content,
        token_count=10,
        created_at=created_at,
    )


def make_summary(
    summary_id: str = "sum_001",
    content: str = "test summary",
    kind: str = "leaf",
    created_at: str = "2026-03-15T10:00:00Z",
) -> SummaryRecord:
    return SummaryRecord(
        summary_id=summary_id,
        conversation_id=1,
        owner_id="test",
        kind=kind,
        depth=1,
        content=content,
        token_count=10,
        created_at=created_at,
    )


def make_retrieval(result: GrepResult) -> AsyncMock:
    retrieval = AsyncMock()
    retrieval.grep.return_value = result
    return retrieval


def active_scope(conversation_id: int = 1) -> LcmConversationScope:
    return LcmConversationScope(conversation_id=conversation_id, all_conversations=False)


def all_scope() -> LcmConversationScope:
    return LcmConversationScope(conversation_id=None, all_conversations=True)


# ── Tests: truncate_snippet ────────────────────────────────────────────────


class TestTruncateSnippet:
    def test_short_content_unchanged(self):
        assert truncate_snippet("hello world") == "hello world"

    def test_newlines_replaced(self):
        assert truncate_snippet("line1\nline2\nline3") == "line1 line2 line3"

    def test_long_content_truncated(self):
        long_text = "a" * 300
        result = truncate_snippet(long_text)
        assert len(result) == 200
        assert result.endswith("...")

    def test_exact_boundary(self):
        text = "a" * 200
        assert truncate_snippet(text) == text  # Exactly 200, no truncation

    def test_custom_max_len(self):
        text = "a" * 50
        result = truncate_snippet(text, max_len=20)
        assert len(result) == 20
        assert result.endswith("...")


# ── Tests: since/before filtering ──────────────────────────────────────────


class TestSinceBeforeFiltering:
    @pytest.mark.asyncio
    async def test_since_param_passed(self):
        retrieval = make_retrieval(GrepResult(total_matches=0))
        params = {
            "pattern": "test",
            "since": "2026-03-10T00:00:00Z",
        }
        await execute_grep(retrieval, params, active_scope())

        call_args = retrieval.grep.call_args[0][0]
        assert call_args.since is not None
        assert call_args.since.year == 2026
        assert call_args.since.month == 3
        assert call_args.since.day == 10

    @pytest.mark.asyncio
    async def test_before_param_passed(self):
        retrieval = make_retrieval(GrepResult(total_matches=0))
        params = {
            "pattern": "test",
            "before": "2026-03-20T00:00:00Z",
        }
        await execute_grep(retrieval, params, active_scope())

        call_args = retrieval.grep.call_args[0][0]
        assert call_args.before is not None

    @pytest.mark.asyncio
    async def test_since_before_both(self):
        retrieval = make_retrieval(GrepResult(total_matches=0))
        params = {
            "pattern": "test",
            "since": "2026-03-10T00:00:00Z",
            "before": "2026-03-20T00:00:00Z",
        }
        result = await execute_grep(retrieval, params, active_scope())
        # Should succeed, not error
        assert "content" in result
        text = result["content"][0]["text"]
        assert "Time filter" in text

    @pytest.mark.asyncio
    async def test_since_gte_before_error(self):
        retrieval = make_retrieval(GrepResult(total_matches=0))
        params = {
            "pattern": "test",
            "since": "2026-03-20T00:00:00Z",
            "before": "2026-03-10T00:00:00Z",
        }
        result = await execute_grep(retrieval, params, active_scope())
        assert "details" in result
        assert "error" in result["details"]
        assert "`since` must be earlier than `before`" in result["details"]["error"]

    @pytest.mark.asyncio
    async def test_since_equals_before_error(self):
        retrieval = make_retrieval(GrepResult(total_matches=0))
        params = {
            "pattern": "test",
            "since": "2026-03-15T00:00:00Z",
            "before": "2026-03-15T00:00:00Z",
        }
        result = await execute_grep(retrieval, params, active_scope())
        assert "error" in result["details"]

    @pytest.mark.asyncio
    async def test_invalid_since_returns_error(self):
        retrieval = make_retrieval(GrepResult(total_matches=0))
        params = {"pattern": "test", "since": "not-a-date"}
        result = await execute_grep(retrieval, params, active_scope())
        assert "error" in result["details"]


# ── Tests: conversation scope validation ───────────────────────────────────


class TestConversationScopeValidation:
    @pytest.mark.asyncio
    async def test_no_scope_returns_error(self):
        retrieval = make_retrieval(GrepResult(total_matches=0))
        scope = LcmConversationScope(conversation_id=None, all_conversations=False)
        result = await execute_grep(retrieval, {"pattern": "test"}, scope)
        assert "error" in result["details"]
        assert "No LCM conversation" in result["details"]["error"]

    @pytest.mark.asyncio
    async def test_all_conversations_scope_ok(self):
        retrieval = make_retrieval(GrepResult(total_matches=0))
        result = await execute_grep(retrieval, {"pattern": "test"}, all_scope())
        text = result["content"][0]["text"]
        assert "all conversations" in text


# ── Tests: markdown output format ──────────────────────────────────────────


class TestMarkdownOutput:
    @pytest.mark.asyncio
    async def test_header_format(self):
        retrieval = make_retrieval(GrepResult(total_matches=0))
        result = await execute_grep(
            retrieval, {"pattern": "hello"}, active_scope()
        )
        text = result["content"][0]["text"]
        assert "## LCM Grep Results" in text
        assert "**Pattern:** `hello`" in text
        assert "**Mode:** regex" in text
        assert "No matches found." in text

    @pytest.mark.asyncio
    async def test_messages_section(self):
        messages = [make_message(message_id=1, content="found it", role="user")]
        retrieval = make_retrieval(
            GrepResult(messages=messages, total_matches=1)
        )
        result = await execute_grep(
            retrieval, {"pattern": "found"}, active_scope()
        )
        text = result["content"][0]["text"]
        assert "### Messages" in text
        assert "[msg#1]" in text
        assert "(user," in text
        assert "found it" in text

    @pytest.mark.asyncio
    async def test_summaries_section(self):
        summaries = [make_summary(summary_id="sum_001", content="summary text")]
        retrieval = make_retrieval(
            GrepResult(summaries=summaries, total_matches=1)
        )
        result = await execute_grep(
            retrieval, {"pattern": "summary"}, active_scope()
        )
        text = result["content"][0]["text"]
        assert "### Summaries" in text
        assert "[sum_001]" in text
        assert "summary text" in text

    @pytest.mark.asyncio
    async def test_details_counts(self):
        messages = [make_message()]
        summaries = [make_summary()]
        retrieval = make_retrieval(
            GrepResult(messages=messages, summaries=summaries, total_matches=2)
        )
        result = await execute_grep(
            retrieval, {"pattern": "test"}, active_scope()
        )
        details = result["details"]
        assert details["messageCount"] == 1
        assert details["summaryCount"] == 1
        assert details["totalMatches"] == 2


# ── Tests: MAX_RESULT_CHARS truncation ─────────────────────────────────────


class TestMaxResultCharsTruncation:
    @pytest.mark.asyncio
    async def test_truncation_on_large_result(self):
        # Create many messages to exceed MAX_RESULT_CHARS
        long_content = "x" * 190  # Close to snippet limit
        messages = [
            make_message(message_id=i, content=long_content)
            for i in range(300)
        ]
        retrieval = make_retrieval(
            GrepResult(messages=messages, total_matches=300)
        )
        result = await execute_grep(
            retrieval, {"pattern": "test"}, active_scope()
        )
        text = result["content"][0]["text"]
        assert "*(truncated — more results available)*" in text
        # Total output should be bounded
        assert len(text) < MAX_RESULT_CHARS + 500  # Some slack for the truncation line
