"""
Tests for the BaseCitationHandler abstract class.

Tests the concrete methods in the base class:
- get_setting: Gets settings from snapshot
- _get_output_instruction_prefix: Formats output instructions
- _create_documents: Converts search results to documents
- _format_sources: Formats sources with citation numbers
"""

from unittest.mock import MagicMock
from typing import Dict, Any, List, Union

from langchain_core.documents import Document

from local_deep_research.citation_handlers.base_citation_handler import (
    BaseCitationHandler,
)


class ConcreteCitationHandler(BaseCitationHandler):
    """Concrete implementation for testing abstract base class."""

    def analyze_initial(
        self, query: str, search_results: Union[str, List[Dict]]
    ) -> Dict[str, Any]:
        """Test implementation."""
        return {"query": query, "results": search_results}

    def analyze_followup(
        self,
        question: str,
        search_results: Union[str, List[Dict]],
        previous_knowledge: str,
        nr_of_links: int,
    ) -> Dict[str, Any]:
        """Test implementation."""
        return {
            "question": question,
            "results": search_results,
            "previous": previous_knowledge,
        }


class TestBaseCitationHandlerInit:
    """Tests for BaseCitationHandler initialization."""

    def test_init_with_llm_only(self):
        """Test initialization with only LLM."""
        mock_llm = MagicMock()
        handler = ConcreteCitationHandler(mock_llm)

        assert handler.llm is mock_llm
        assert handler.settings_snapshot == {}

    def test_init_with_settings_snapshot(self):
        """Test initialization with settings snapshot."""
        mock_llm = MagicMock()
        settings = {"general.output_language": "Spanish"}
        handler = ConcreteCitationHandler(mock_llm, settings_snapshot=settings)

        assert handler.llm is mock_llm
        assert handler.settings_snapshot == settings

    def test_init_with_none_settings_defaults_to_empty(self):
        """Test that None settings defaults to empty dict."""
        mock_llm = MagicMock()
        handler = ConcreteCitationHandler(mock_llm, settings_snapshot=None)

        assert handler.settings_snapshot == {}


class TestInvokeText:
    """Tests for the _invoke_text helper (normalizes LLM responses)."""

    @staticmethod
    def _handler(return_value):
        mock_llm = MagicMock()
        mock_llm.invoke.return_value = return_value
        return ConcreteCitationHandler(mock_llm)

    def test_returns_string_response_unchanged(self):
        """A raw string response is returned as-is."""
        handler = self._handler("plain answer [1].")
        assert handler._invoke_text("prompt") == "plain answer [1]."

    def test_extracts_content_from_message_object(self):
        """A message object's .content is extracted."""
        handler = self._handler(MagicMock(content="message answer [1]."))
        assert handler._invoke_text("prompt") == "message answer [1]."

    def test_strips_think_tags(self):
        """<think>...</think> reasoning blocks are removed."""
        handler = self._handler(
            MagicMock(content="<think>reasoning</think>final answer [1].")
        )
        result = handler._invoke_text("prompt")
        assert "<think>" not in result
        assert "reasoning" not in result
        assert result == "final answer [1]."

    def test_none_response_returns_empty_string(self):
        """A None response normalizes to "" instead of raising."""
        handler = self._handler(None)
        assert handler._invoke_text("prompt") == ""


class TestGetSetting:
    """Tests for the get_setting method."""

    def test_get_setting_returns_value_directly(self):
        """Test get_setting returns value when it's not a dict."""
        mock_llm = MagicMock()
        settings = {"language": "Spanish", "max_results": 10}
        handler = ConcreteCitationHandler(mock_llm, settings_snapshot=settings)

        assert handler.get_setting("language") == "Spanish"
        assert handler.get_setting("max_results") == 10

    def test_get_setting_extracts_value_from_dict(self):
        """Test get_setting extracts 'value' key from dict structure."""
        mock_llm = MagicMock()
        settings = {
            "general.output_language": {"value": "French", "type": "string"},
            "search.max_results": {"value": 20, "description": "Max results"},
        }
        handler = ConcreteCitationHandler(mock_llm, settings_snapshot=settings)

        assert handler.get_setting("general.output_language") == "French"
        assert handler.get_setting("search.max_results") == 20

    def test_get_setting_returns_dict_if_no_value_key(self):
        """Test get_setting returns entire dict if no 'value' key."""
        mock_llm = MagicMock()
        settings = {"config": {"nested": "data", "other": 123}}
        handler = ConcreteCitationHandler(mock_llm, settings_snapshot=settings)

        result = handler.get_setting("config")
        assert result == {"nested": "data", "other": 123}

    def test_get_setting_returns_default_for_missing_key(self):
        """Test get_setting returns default for missing keys."""
        mock_llm = MagicMock()
        handler = ConcreteCitationHandler(mock_llm, settings_snapshot={})

        assert handler.get_setting("missing_key") is None
        assert (
            handler.get_setting("missing_key", default="fallback") == "fallback"
        )
        assert handler.get_setting("missing_key", default=42) == 42

    def test_get_setting_with_empty_string_value(self):
        """Test get_setting handles empty string values."""
        mock_llm = MagicMock()
        settings = {"empty": "", "empty_dict": {"value": ""}}
        handler = ConcreteCitationHandler(mock_llm, settings_snapshot=settings)

        assert handler.get_setting("empty") == ""
        assert handler.get_setting("empty_dict") == ""


class TestGetOutputInstructionPrefix:
    """Tests for _get_output_instruction_prefix method."""

    def test_returns_formatted_prefix_when_instructions_set(self):
        """Test returns formatted prefix with custom instructions."""
        mock_llm = MagicMock()
        settings = {"general.output_instructions": "Respond in Spanish"}
        handler = ConcreteCitationHandler(mock_llm, settings_snapshot=settings)

        result = handler._get_output_instruction_prefix()

        assert result == "User-Specified Output Style: Respond in Spanish\n\n"

    def test_returns_empty_string_when_no_instructions(self):
        """Test returns empty string when no instructions set."""
        mock_llm = MagicMock()
        handler = ConcreteCitationHandler(mock_llm, settings_snapshot={})

        result = handler._get_output_instruction_prefix()

        assert result == ""

    def test_returns_empty_string_for_whitespace_only_instructions(self):
        """Test returns empty string for whitespace-only instructions."""
        mock_llm = MagicMock()
        settings = {"general.output_instructions": "   \n\t  "}
        handler = ConcreteCitationHandler(mock_llm, settings_snapshot=settings)

        result = handler._get_output_instruction_prefix()

        assert result == ""

    def test_strips_whitespace_from_instructions(self):
        """Test strips leading/trailing whitespace from instructions."""
        mock_llm = MagicMock()
        settings = {"general.output_instructions": "  Be concise  \n"}
        handler = ConcreteCitationHandler(mock_llm, settings_snapshot=settings)

        result = handler._get_output_instruction_prefix()

        assert result == "User-Specified Output Style: Be concise\n\n"

    def test_handles_dict_value_format(self):
        """Test handles dict format with 'value' key."""
        mock_llm = MagicMock()
        settings = {
            "general.output_instructions": {
                "value": "Use bullet points",
                "type": "string",
            }
        }
        handler = ConcreteCitationHandler(mock_llm, settings_snapshot=settings)

        result = handler._get_output_instruction_prefix()

        assert result == "User-Specified Output Style: Use bullet points\n\n"


class TestCreateDocuments:
    """Tests for _create_documents method."""

    def test_converts_search_results_to_documents(self):
        """Test basic conversion of search results to documents."""
        mock_llm = MagicMock()
        handler = ConcreteCitationHandler(mock_llm)

        search_results = [
            {
                "link": "https://example.com/1",
                "title": "First Result",
                "snippet": "This is the first snippet",
            },
            {
                "link": "https://example.com/2",
                "title": "Second Result",
                "full_content": "Full content of second result",
            },
        ]

        docs = handler._create_documents(search_results)

        assert len(docs) == 2
        assert isinstance(docs[0], Document)
        assert docs[0].page_content == "This is the first snippet"
        assert docs[0].metadata["source"] == "https://example.com/1"
        assert docs[0].metadata["title"] == "First Result"
        assert docs[0].metadata["index"] == 1

        # Second doc should use full_content over snippet
        assert docs[1].page_content == "Full content of second result"
        assert docs[1].metadata["index"] == 2

    def test_returns_empty_list_for_string_input(self):
        """Test returns empty list when search_results is a string."""
        mock_llm = MagicMock()
        handler = ConcreteCitationHandler(mock_llm)

        docs = handler._create_documents("Some error message")

        assert docs == []

    def test_uses_nr_of_links_offset(self):
        """Test nr_of_links parameter offsets index correctly."""
        mock_llm = MagicMock()
        handler = ConcreteCitationHandler(mock_llm)

        search_results = [
            {
                "link": "https://example.com/1",
                "title": "Result",
                "snippet": "Content",
            }
        ]

        docs = handler._create_documents(search_results, nr_of_links=5)

        assert docs[0].metadata["index"] == 6  # 0 + 5 + 1

    def test_preserves_existing_index(self):
        """Test preserves index if already set in search result."""
        mock_llm = MagicMock()
        handler = ConcreteCitationHandler(mock_llm)

        search_results = [
            {
                "link": "https://example.com",
                "title": "Result",
                "snippet": "Content",
                "index": "42",
            }
        ]

        docs = handler._create_documents(search_results)

        assert docs[0].metadata["index"] == 42
        # Original dict should also preserve the index
        assert search_results[0]["index"] == "42"

    def test_adds_index_to_original_results(self):
        """Test adds index to original search result dicts."""
        mock_llm = MagicMock()
        handler = ConcreteCitationHandler(mock_llm)

        search_results = [
            {
                "link": "https://example.com/1",
                "title": "First",
                "snippet": "Content 1",
            },
            {
                "link": "https://example.com/2",
                "title": "Second",
                "snippet": "Content 2",
            },
        ]

        handler._create_documents(search_results)

        assert search_results[0]["index"] == "1"
        assert search_results[1]["index"] == "2"

    def test_handles_missing_link_and_title(self):
        """Test handles results with missing link and title."""
        mock_llm = MagicMock()
        handler = ConcreteCitationHandler(mock_llm)

        search_results = [{"snippet": "Just a snippet"}]

        docs = handler._create_documents(search_results)

        assert len(docs) == 1
        assert docs[0].metadata["source"] == "source_1"
        assert docs[0].metadata["title"] == "Source 1"

    def test_prefers_full_content_over_snippet(self):
        """Test full_content is preferred over snippet."""
        mock_llm = MagicMock()
        handler = ConcreteCitationHandler(mock_llm)

        search_results = [
            {
                "link": "https://example.com",
                "title": "Result",
                "snippet": "Short snippet",
                "full_content": "This is the full content which is much longer",
            }
        ]

        docs = handler._create_documents(search_results)

        assert (
            docs[0].page_content
            == "This is the full content which is much longer"
        )

    def test_handles_empty_list(self):
        """Test handles empty search results list."""
        mock_llm = MagicMock()
        handler = ConcreteCitationHandler(mock_llm)

        docs = handler._create_documents([])

        assert docs == []


class TestFormatSources:
    """Tests for _format_sources method."""

    def test_formats_single_document(self):
        """Test formatting a single document."""
        mock_llm = MagicMock()
        handler = ConcreteCitationHandler(mock_llm)

        docs = [
            Document(
                page_content="This is the content",
                metadata={"index": 1, "source": "https://example.com"},
            )
        ]

        result = handler._format_sources(docs)

        assert result == "[1] This is the content"

    def test_formats_multiple_documents(self):
        """Test formatting multiple documents."""
        mock_llm = MagicMock()
        handler = ConcreteCitationHandler(mock_llm)

        docs = [
            Document(page_content="First content", metadata={"index": 1}),
            Document(page_content="Second content", metadata={"index": 2}),
            Document(page_content="Third content", metadata={"index": 3}),
        ]

        result = handler._format_sources(docs)

        expected = (
            "[1] First content\n\n[2] Second content\n\n[3] Third content"
        )
        assert result == expected

    def test_handles_empty_document_list(self):
        """Test handles empty document list."""
        mock_llm = MagicMock()
        handler = ConcreteCitationHandler(mock_llm)

        result = handler._format_sources([])

        assert result == ""

    def test_preserves_document_content(self):
        """Test preserves document content including newlines."""
        mock_llm = MagicMock()
        handler = ConcreteCitationHandler(mock_llm)

        docs = [
            Document(
                page_content="Line 1\nLine 2\nLine 3",
                metadata={"index": 5},
            )
        ]

        result = handler._format_sources(docs)

        assert result == "[5] Line 1\nLine 2\nLine 3"


class TestAbstractMethods:
    """Tests that abstract methods are properly defined."""

    def test_analyze_initial_must_be_implemented(self):
        """Test that analyze_initial is abstract and must be implemented."""
        mock_llm = MagicMock()
        handler = ConcreteCitationHandler(mock_llm)

        # Our concrete implementation should work
        result = handler.analyze_initial("test query", [])
        assert result == {"query": "test query", "results": []}

    def test_analyze_followup_must_be_implemented(self):
        """Test that analyze_followup is abstract and must be implemented."""
        mock_llm = MagicMock()
        handler = ConcreteCitationHandler(mock_llm)

        result = handler.analyze_followup("question", [], "previous", 0)
        assert "question" in result
        assert "previous" in result
