"""
Regression tests for issue #4615.

Several call sites consumed ``response.content`` directly and called string
methods (``.strip()``, ``.split()``, ``len()``) on it. When a provider returns
content in LIST form (Anthropic content blocks from extended-thinking/tool-use),
this raised AttributeError/TypeError. The fix routes those reads through
``get_llm_response_text``, which coerces non-string content to a string.

These tests build fake responses whose ``.content`` is a LIST (simulating
Anthropic content blocks) and confirm the corrected sites no longer raise and
return the expected string.
"""

from unittest.mock import Mock


def _list_content_response(text):
    """Build a fake LLM response with Anthropic-style list content blocks."""
    response = Mock()
    response.content = [{"type": "text", "text": text}]
    return response


class TestGetLLMResponseTextBlocks:
    """Root-cause regression for #4615: get_llm_response_text must EXTRACT the
    text from list-type content blocks, not stringify the list to its repr.

    These assert *clean* extraction (exact equality), so they fail against the
    old ``str(raw)`` behavior — which returned ``"[{'type': 'text', ...}]"``.
    """

    def _msg(self, content):
        response = Mock()
        response.content = content
        return response

    def test_single_text_block_extracted_clean(self):
        from local_deep_research.utilities.json_utils import (
            get_llm_response_text,
        )

        out = get_llm_response_text(
            self._msg([{"type": "text", "text": "Paris"}])
        )
        assert out == "Paris"
        assert "'type'" not in out and "[{" not in out  # no repr leakage

    def test_newlines_preserved_so_split_works(self):
        from local_deep_research.utilities.json_utils import (
            get_llm_response_text,
        )

        out = get_llm_response_text(
            self._msg([{"type": "text", "text": "A\nB\nC"}])
        )
        # The #4615 corruption turned "\n" into literal "\\n", collapsing splits.
        assert out.split("\n") == ["A", "B", "C"]

    def test_multiple_text_blocks_joined(self):
        from local_deep_research.utilities.json_utils import (
            get_llm_response_text,
        )

        out = get_llm_response_text(
            self._msg(
                [
                    {"type": "text", "text": "Hello "},
                    {"type": "text", "text": "world"},
                ]
            )
        )
        assert out == "Hello world"

    def test_tool_use_block_skipped(self):
        from local_deep_research.utilities.json_utils import (
            get_llm_response_text,
        )

        out = get_llm_response_text(
            self._msg(
                [
                    {"type": "text", "text": "answer"},
                    {"type": "tool_use", "name": "search", "input": {"q": "x"}},
                ]
            )
        )
        assert out == "answer"

    def test_empty_list_returns_empty_string(self):
        from local_deep_research.utilities.json_utils import (
            get_llm_response_text,
        )

        assert get_llm_response_text(self._msg([])) == ""

    def test_plain_string_content_unchanged(self):
        from local_deep_research.utilities.json_utils import (
            get_llm_response_text,
        )

        # String content must pass straight through (block coercion only
        # applies to list content); no leading/trailing space to avoid the
        # helper's existing strip behavior masking the check.
        assert get_llm_response_text(self._msg("hello world")) == "hello world"


class TestFollowUpContextManagerListContent:
    """followup_context_manager.py sites (issue #4615)."""

    def _handler(self, text):
        from local_deep_research.advanced_search_system.knowledge.followup_context_manager import (
            FollowUpContextHandler,
        )

        model = Mock()
        model.invoke.return_value = _list_content_response(text)
        return FollowUpContextHandler(model)

    def test_extract_entities_does_not_raise_on_list_content(self):
        handler = self._handler("Paris")
        research_data = {
            "formatted_findings": "Some prior findings about Paris."
        }

        entities = handler._extract_entities(research_data)

        assert isinstance(entities, list)
        # Coerced content preserves the underlying text — cleanly, not as the
        # list's repr (the #4615 data-corruption regression).
        assert "Paris" in entities
        assert not any("'type'" in e or "[{" in e for e in entities)

    def test_generate_summary_does_not_raise_on_list_content(self):
        handler = self._handler("A concise summary.")

        summary = handler._generate_summary(
            findings="x" * 5000,  # long enough to trigger an LLM call
            query="follow-up question",
        )

        assert isinstance(summary, str)
        assert "A concise summary." in summary
        assert "'type'" not in summary and "[{" not in summary

    def test_identify_gaps_does_not_raise_on_list_content(self):
        handler = self._handler("Missing detail about funding.")
        research_data = {"formatted_findings": "Some prior findings."}

        gaps = handler.identify_gaps(research_data, "follow-up question")

        assert isinstance(gaps, list)
        assert any("funding" in gap for gap in gaps)
        assert not any("'type'" in g or "[{" in g for g in gaps)


class TestDecompositionQuestionListContent:
    """decomposition_question.py sites (issue #4615)."""

    def test_generate_questions_does_not_raise_on_list_content(self):
        from local_deep_research.advanced_search_system.questions.decomposition_question import (
            DecompositionQuestionGenerator,
        )

        model = Mock()
        model.invoke.return_value = _list_content_response(
            "What is X?\nHow does X work?\nWhy is X important?"
        )
        generator = DecompositionQuestionGenerator(model)

        questions = generator.generate_questions("X technology", context="")

        assert isinstance(questions, list)
        # Non-tautological: the pre-fix path crashes and falls back to generic
        # default questions, so the LLM's actual questions would be absent and
        # the repr ("'type'") would leak. Both checks fail on the old behavior.
        assert any("How does X work?" in q for q in questions)
        assert not any("'type'" in q or "[{" in q for q in questions)


class TestBaseExplorerListContent:
    """base_explorer.py sites (issue #4615)."""

    def _explorer(self, text):
        from local_deep_research.advanced_search_system.candidate_exploration.base_explorer import (
            BaseCandidateExplorer,
        )

        class _TestExplorer(BaseCandidateExplorer):
            def explore(
                self, initial_query, constraints=None, entity_type=None
            ):
                pass

            def generate_exploration_queries(
                self, base_query, found_candidates, constraints=None
            ):
                return []

        model = Mock()
        model.invoke.return_value = _list_content_response(text)
        return _TestExplorer(
            model=model,
            search_engine=Mock(),
            max_candidates=50,
            max_search_time=60.0,
        )

    def test_generate_answer_candidates_does_not_raise_on_list_content(self):
        explorer = self._explorer("Alice Smith")

        answers = explorer._generate_answer_candidates(
            "Who discovered X?", "search content"
        )

        assert isinstance(answers, list)
        assert "Alice Smith" in answers
        assert not any("'type'" in a or "[{" in a for a in answers)

    def test_extract_entity_names_does_not_raise_on_list_content(self):
        explorer = self._explorer("Marie Curie")

        names = explorer._extract_entity_names("Some text with a name.")

        assert isinstance(names, list)
        assert "Marie Curie" in names
        assert not any("'type'" in n or "[{" in n for n in names)
