"""Unit tests for ChatService boundary conditions.

These tests verify behavior at limit boundaries and edge cases.
Based on constants in the source code.
"""

from unittest.mock import MagicMock, patch
from contextlib import contextmanager


class TestMessageBoundaries:
    """Tests for message content boundaries."""

    def test_message_with_empty_content_accepted(self):
        """Test that empty message content is accepted."""
        from src.local_deep_research.chat.service import ChatService

        mock_session_obj = MagicMock()
        mock_session_obj.id = "test-session"
        mock_session_obj.message_count = 0

        added_messages = []

        @contextmanager
        def mock_get_user_db_session(username, password=None):
            mock_session = MagicMock()

            def mock_add(msg):
                added_messages.append(msg)

            mock_session.add = mock_add
            mock_session.commit = MagicMock()
            mock_session.query.return_value.filter_by.return_value.with_for_update.return_value.first.return_value = mock_session_obj
            mock_session.query.return_value.filter_by.return_value.first.return_value = mock_session_obj
            yield mock_session

        with patch(
            "src.local_deep_research.chat.service.get_user_db_session",
            mock_get_user_db_session,
        ):
            service = ChatService(username="testuser")
            message_id = service.add_message(
                session_id="test-session",
                role="user",
                content="",
                message_type="query",
            )

        assert message_id is not None
        assert len(added_messages) == 1
        assert added_messages[0].content == ""

    def test_message_with_none_content_is_rejected(self):
        """chat_messages.content is NOT NULL.
        add_message must reject content=None at the application layer."""
        import pytest
        from src.local_deep_research.chat.service import ChatService

        service = ChatService(username="testuser")
        with pytest.raises(ValueError, match="content is required"):
            service.add_message(
                session_id="test-session",
                role="assistant",
                content=None,
                message_type="response",
                research_id="research-123",
            )


class TestContextBoundaries:
    """Tests for accumulated context boundaries."""

    def test_accumulated_context_entities_limited_to_50(self):
        """Test that key_entities are limited to 50 items."""
        from src.local_deep_research.chat.service import ChatService

        mock_session_obj = MagicMock()
        mock_session_obj.id = "test-session"
        mock_session_obj.accumulated_context = {
            "key_entities": [f"existing_{i}" for i in range(40)],
            "topics": [],
            "summary": "",
        }

        @contextmanager
        def mock_get_user_db_session(username, password=None):
            mock_session = MagicMock()
            mock_session.query.return_value.filter_by.return_value.with_for_update.return_value.first.return_value = mock_session_obj
            mock_session.query.return_value.filter_by.return_value.first.return_value = mock_session_obj
            mock_session.commit = MagicMock()
            yield mock_session

        with patch(
            "src.local_deep_research.chat.service.get_user_db_session",
            mock_get_user_db_session,
        ):
            service = ChatService(username="testuser")
            # Try to add 20 more entities (40 + 20 = 60 > 50)
            service.update_accumulated_context(
                "test-session",
                new_entities=[f"new_{i}" for i in range(20)],
            )

        # Should be capped at 50
        entities = mock_session_obj.accumulated_context["key_entities"]
        assert len(entities) <= 50

    def test_accumulated_context_topics_limited_to_20(self):
        """Test that topics are limited to 20 items."""
        from src.local_deep_research.chat.service import ChatService

        mock_session_obj = MagicMock()
        mock_session_obj.id = "test-session"
        mock_session_obj.accumulated_context = {
            "key_entities": [],
            "topics": [f"existing_topic_{i}" for i in range(15)],
            "summary": "",
        }

        @contextmanager
        def mock_get_user_db_session(username, password=None):
            mock_session = MagicMock()
            mock_session.query.return_value.filter_by.return_value.with_for_update.return_value.first.return_value = mock_session_obj
            mock_session.query.return_value.filter_by.return_value.first.return_value = mock_session_obj
            mock_session.commit = MagicMock()
            yield mock_session

        with patch(
            "src.local_deep_research.chat.service.get_user_db_session",
            mock_get_user_db_session,
        ):
            service = ChatService(username="testuser")
            # Try to add 10 more topics (15 + 10 = 25 > 20)
            service.update_accumulated_context(
                "test-session",
                new_topics=[f"new_topic_{i}" for i in range(10)],
            )

        # Should be capped at 20
        topics = mock_session_obj.accumulated_context["topics"]
        assert len(topics) <= 20

    def test_accumulated_summary_truncated_to_8000_chars(self):
        """Test that summary is truncated to 8000 characters."""
        from src.local_deep_research.chat.service import ChatService

        mock_session_obj = MagicMock()
        mock_session_obj.id = "test-session"
        mock_session_obj.accumulated_context = {
            "key_entities": [],
            "topics": [],
            "summary": "A" * 7000,  # Already 7000 chars
        }

        @contextmanager
        def mock_get_user_db_session(username, password=None):
            mock_session = MagicMock()
            mock_session.query.return_value.filter_by.return_value.with_for_update.return_value.first.return_value = mock_session_obj
            mock_session.query.return_value.filter_by.return_value.first.return_value = mock_session_obj
            mock_session.commit = MagicMock()
            yield mock_session

        with patch(
            "src.local_deep_research.chat.service.get_user_db_session",
            mock_get_user_db_session,
        ):
            service = ChatService(username="testuser")
            # Add 2000 more chars (7000 + 2000 + separator = ~9000+ > 8000)
            service.update_accumulated_context(
                "test-session",
                summary_addition="B" * 2000,
            )

        # Should be capped at 8000
        summary = mock_session_obj.accumulated_context["summary"]
        assert len(summary) <= 8000


class TestSessionBoundaries:
    """Tests for session creation boundaries."""

    def test_session_title_at_max_100_chars(self):
        """Test that title from query is limited to 100 chars + ellipsis."""
        from src.local_deep_research.chat.service import ChatService

        added_sessions = []

        @contextmanager
        def mock_get_user_db_session(username, password=None):
            mock_session = MagicMock()

            def mock_add(session):
                added_sessions.append(session)

            mock_session.add = mock_add
            mock_session.commit = MagicMock()
            yield mock_session

        with patch(
            "src.local_deep_research.chat.service.get_user_db_session",
            mock_get_user_db_session,
        ):
            service = ChatService(username="testuser")
            # Create query longer than 100 chars
            long_query = "A" * 150
            service.create_session(initial_query=long_query)

        assert len(added_sessions) == 1
        title = added_sessions[0].title
        # _fallback_title truncates over-long queries to a fixed 100-char
        # display budget: query[:97].strip() + "...". The ellipsis lives
        # *inside* the 100-char cap, not on top of it — keeps sidebar
        # widths predictable.
        assert len(title) == 100
        assert title.endswith("...")

    def test_session_title_exactly_100_chars_no_ellipsis(self):
        """Test that 100-char query doesn't get ellipsis."""
        from src.local_deep_research.chat.service import ChatService

        added_sessions = []

        @contextmanager
        def mock_get_user_db_session(username, password=None):
            mock_session = MagicMock()

            def mock_add(session):
                added_sessions.append(session)

            mock_session.add = mock_add
            mock_session.commit = MagicMock()
            yield mock_session

        with patch(
            "src.local_deep_research.chat.service.get_user_db_session",
            mock_get_user_db_session,
        ):
            service = ChatService(username="testuser")
            # Create query exactly 100 chars
            exact_query = "A" * 100
            service.create_session(initial_query=exact_query)

        title = added_sessions[0].title
        # Title should be exactly the query (no ellipsis)
        assert len(title) == 100
        assert not title.endswith("...")


class TestContextManagerBoundaries:
    """Tests for ChatContextManager boundaries."""

    def test_findings_limited_to_5(self):
        """Test that findings are limited to MAX_FINDINGS_TO_INCLUDE (5)."""
        from src.local_deep_research.chat.context import ChatContextManager

        # Create 10 assistant messages with research_id
        messages = [
            {
                "id": f"msg-{i}",
                "role": "assistant",
                "content": f"Finding {i}",
                "message_type": "response",
                "research_id": f"research-{i}",
            }
            for i in range(10)
        ]

        manager = ChatContextManager(
            session_id="test-session",
            messages=messages,
            accumulated_context={},
        )

        findings = manager._extract_findings_from_history()

        # Findings is a combined string, but it should only have 5 messages worth
        # Count the number of "Finding" occurrences
        finding_count = findings.count("Finding")
        assert finding_count <= 5

    def test_create_summary_truncates_long_paragraphs_to_300(self):
        """Test that _create_summary truncates paragraphs to 300 chars."""
        from src.local_deep_research.chat.context import ChatContextManager

        long_paragraph = "Z" * 500

        manager = ChatContextManager(
            session_id="test-session",
            messages=[],
            accumulated_context={},
        )

        summary = manager._create_summary(long_paragraph)

        # Should be truncated to 300 + "..."
        assert len(summary) == 303
        assert summary.endswith("...")


class TestEdgeCases:
    """Tests for various edge cases."""

    def test_entity_deduplication(self):
        """Test that duplicate entities are deduplicated."""
        from src.local_deep_research.chat.service import ChatService

        mock_session_obj = MagicMock()
        mock_session_obj.id = "test-session"
        mock_session_obj.accumulated_context = {
            "key_entities": ["quantum", "computing"],
            "topics": [],
            "summary": "",
        }

        @contextmanager
        def mock_get_user_db_session(username, password=None):
            mock_session = MagicMock()
            mock_session.query.return_value.filter_by.return_value.with_for_update.return_value.first.return_value = mock_session_obj
            mock_session.query.return_value.filter_by.return_value.first.return_value = mock_session_obj
            mock_session.commit = MagicMock()
            yield mock_session

        with patch(
            "src.local_deep_research.chat.service.get_user_db_session",
            mock_get_user_db_session,
        ):
            service = ChatService(username="testuser")
            # Add duplicate entity
            service.update_accumulated_context(
                "test-session",
                new_entities=["quantum", "superposition"],
            )

        entities = mock_session_obj.accumulated_context["key_entities"]
        # quantum should appear only once
        assert entities.count("quantum") == 1
        # All three unique entities should be present
        assert set(entities) == {"quantum", "computing", "superposition"}
