"""High-value edge case tests for citation_handlers module.

Covers gaps not addressed by existing tests:
- _create_documents index assignment and preservation logic
- _format_sources formatting correctness
- StandardCitationHandler prompt construction
- StandardCitationHandler fact-checking toggle
"""

from unittest.mock import MagicMock

from langchain_core.documents import Document

from local_deep_research.citation_handlers.base_citation_handler import (
    BaseCitationHandler,
)
from local_deep_research.citation_handlers.standard_citation_handler import (
    StandardCitationHandler,
)


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

    def analyze_initial(self, query, search_results):
        return {}

    def analyze_followup(self, question, search_results, prev, nr):
        return {}


class TestCreateDocumentsIndexLogic:
    """Test _create_documents index assignment edge cases."""

    def test_sequential_index_starts_at_one(self):
        """Without nr_of_links offset, indices start at 1."""
        handler = ConcreteCitationHandler(llm=MagicMock())
        results = [
            {"full_content": "content A", "link": "http://a.com", "title": "A"},
            {"full_content": "content B", "link": "http://b.com", "title": "B"},
        ]
        docs = handler._create_documents(results)
        assert docs[0].metadata["index"] == 1
        assert docs[1].metadata["index"] == 2

    def test_nr_of_links_offsets_indices(self):
        """nr_of_links shifts index values."""
        handler = ConcreteCitationHandler(llm=MagicMock())
        results = [
            {"full_content": "content", "link": "http://a.com", "title": "A"},
        ]
        docs = handler._create_documents(results, nr_of_links=10)
        assert docs[0].metadata["index"] == 11

    def test_preserves_existing_index(self):
        """If a result already has an 'index', it is preserved."""
        handler = ConcreteCitationHandler(llm=MagicMock())
        results = [
            {
                "full_content": "content",
                "link": "http://a.com",
                "title": "A",
                "index": "42",
            },
        ]
        docs = handler._create_documents(results)
        assert docs[0].metadata["index"] == 42

    def test_adds_index_to_original_dict(self):
        """_create_documents mutates the original dict to add index."""
        handler = ConcreteCitationHandler(llm=MagicMock())
        results = [
            {"full_content": "content", "link": "http://a.com", "title": "A"},
        ]
        handler._create_documents(results)
        assert "index" in results[0]

    def test_does_not_overwrite_existing_index_in_dict(self):
        """Existing index in dict is not overwritten."""
        handler = ConcreteCitationHandler(llm=MagicMock())
        results = [
            {
                "full_content": "content",
                "link": "http://a.com",
                "title": "A",
                "index": "99",
            },
        ]
        handler._create_documents(results)
        assert results[0]["index"] == "99"

    def test_prefers_full_content_over_snippet(self):
        """full_content is used when both full_content and snippet exist."""
        handler = ConcreteCitationHandler(llm=MagicMock())
        results = [
            {
                "full_content": "full text",
                "snippet": "short text",
                "link": "http://a.com",
                "title": "A",
            },
        ]
        docs = handler._create_documents(results)
        assert docs[0].page_content == "full text"

    def test_falls_back_to_snippet(self):
        """When full_content is missing, snippet is used."""
        handler = ConcreteCitationHandler(llm=MagicMock())
        results = [
            {"snippet": "short text", "link": "http://a.com", "title": "A"},
        ]
        docs = handler._create_documents(results)
        assert docs[0].page_content == "short text"

    def test_empty_list_input(self):
        """Empty list input returns empty list."""
        handler = ConcreteCitationHandler(llm=MagicMock())
        docs = handler._create_documents([])
        assert docs == []

    def test_string_input_returns_empty(self):
        """String input returns empty document list."""
        handler = ConcreteCitationHandler(llm=MagicMock())
        docs = handler._create_documents("some string")
        assert docs == []


class TestFormatSources:
    """Test _format_sources formatting."""

    def test_formats_with_bracket_numbers(self):
        """Sources are formatted as [N] content."""
        handler = ConcreteCitationHandler(llm=MagicMock())
        docs = [
            Document(
                page_content="Content A",
                metadata={"source": "http://a.com", "title": "A", "index": 1},
            ),
            Document(
                page_content="Content B",
                metadata={"source": "http://b.com", "title": "B", "index": 2},
            ),
        ]
        result = handler._format_sources(docs)
        assert "[1] Content A" in result
        assert "[2] Content B" in result

    def test_sources_separated_by_double_newlines(self):
        """Sources are separated by double newlines."""
        handler = ConcreteCitationHandler(llm=MagicMock())
        docs = [
            Document(
                page_content="A",
                metadata={"source": "", "title": "", "index": 1},
            ),
            Document(
                page_content="B",
                metadata={"source": "", "title": "", "index": 2},
            ),
        ]
        result = handler._format_sources(docs)
        assert "\n\n" in result

    def test_empty_docs_returns_empty(self):
        """Empty document list returns empty string."""
        handler = ConcreteCitationHandler(llm=MagicMock())
        result = handler._format_sources([])
        assert result == ""


class TestStandardCitationHandlerAnalyzeInitial:
    """Test StandardCitationHandler.analyze_initial."""

    def test_returns_content_and_documents(self):
        """analyze_initial returns dict with 'content' and 'documents' keys."""
        mock_llm = MagicMock()
        mock_llm.invoke.return_value = MagicMock(content="Analysis text")
        handler = StandardCitationHandler(llm=mock_llm)

        results = [
            {
                "full_content": "Source content",
                "link": "http://a.com",
                "title": "A",
            },
        ]
        result = handler.analyze_initial("test query", results)

        assert "content" in result
        assert "documents" in result
        assert len(result["documents"]) == 1

    def test_string_search_results_returns_empty_documents(self):
        """When search_results is a string, documents list is empty."""
        mock_llm = MagicMock()
        mock_llm.invoke.return_value = "text response"
        handler = StandardCitationHandler(llm=mock_llm)

        result = handler.analyze_initial("test query", "string results")
        assert result["documents"] == []

    def test_prompt_includes_query(self):
        """The LLM prompt includes the research query."""
        mock_llm = MagicMock()
        mock_llm.invoke.return_value = MagicMock(content="response")
        handler = StandardCitationHandler(llm=mock_llm)

        results = [
            {"full_content": "content", "link": "http://a.com", "title": "A"},
        ]
        handler.analyze_initial("what is quantum computing", results)
        prompt = mock_llm.invoke.call_args[0][0]
        assert "what is quantum computing" in prompt


class TestStandardCitationHandlerFactChecking:
    """Test fact-checking toggle in analyze_followup."""

    def test_fact_checking_disabled_by_default(self):
        """Fact checking is off by default; LLM is invoked only once for analysis."""
        mock_llm = MagicMock()
        mock_llm.invoke.return_value = MagicMock(content="response")
        handler = StandardCitationHandler(llm=mock_llm)

        results = [
            {"full_content": "content", "link": "http://a.com", "title": "A"},
        ]
        handler.analyze_followup("question", results, "previous knowledge", 0)

        # Should be called once: just the main analysis (no fact-check pass)
        assert mock_llm.invoke.call_count == 1

    def test_fact_checking_disabled_skips_extra_call(self):
        """When fact_checking disabled, LLM is called only once for main analysis."""
        mock_llm = MagicMock()
        mock_llm.invoke.return_value = MagicMock(content="response")
        handler = StandardCitationHandler(
            llm=mock_llm,
            settings_snapshot={"general.enable_fact_checking": False},
        )

        results = [
            {"full_content": "content", "link": "http://a.com", "title": "A"},
        ]
        handler.analyze_followup("question", results, "previous knowledge", 0)

        # Should be called once: just the main analysis
        assert mock_llm.invoke.call_count == 1

    def test_output_instructions_included_in_followup(self):
        """Custom output instructions are included in the followup prompt."""
        mock_llm = MagicMock()
        mock_llm.invoke.return_value = MagicMock(content="response")
        handler = StandardCitationHandler(
            llm=mock_llm,
            settings_snapshot={
                "general.output_instructions": "Respond in French",
                "general.enable_fact_checking": False,
            },
        )

        handler.analyze_followup("question", [], "prev", 0)
        prompt = mock_llm.invoke.call_args[0][0]
        assert "Respond in French" in prompt
