"""
Tests for PrecisionExtractionHandler LLM error paths.

Covers previously untested exception handlers in:
- _extract_single_answer() - LLM exception fallback (lines 326-329)
- _extract_dimension() - LLM exception fallback (lines 468-471)
- _extract_score() - LLM exception → first-score fallback (lines 491-493)
- _extract_temporal() - LLM exception → first-year fallback (lines 519-521)
- _extract_number() - LLM exception → first-number fallback (lines 544-546)
- _extract_best_name() - no names found (line 567)
- _extract_dimension() - no measurement in LLM response (fallback scoring)
"""

import pytest
from unittest.mock import Mock


@pytest.fixture
def mock_llm_error():
    """Create a mock LLM that raises on invoke."""
    llm = Mock()
    llm.invoke.side_effect = RuntimeError("LLM connection failed")
    return llm


@pytest.fixture
def handler_with_error_llm(mock_llm_error):
    """PrecisionExtractionHandler whose LLM always raises."""
    from local_deep_research.citation_handlers.precision_extraction_handler import (
        PrecisionExtractionHandler,
    )

    return PrecisionExtractionHandler(llm=mock_llm_error)


@pytest.fixture
def handler(mock_llm):
    """PrecisionExtractionHandler with a working mock LLM."""
    from local_deep_research.citation_handlers.precision_extraction_handler import (
        PrecisionExtractionHandler,
    )

    return PrecisionExtractionHandler(llm=mock_llm)


class TestExtractSingleAnswerError:
    """Tests for _extract_single_answer exception handler (lines 326-329)."""

    def test_returns_content_on_llm_error(self, handler_with_error_llm):
        """When LLM raises, returns original content unchanged."""
        content = "The capital of France is Paris."
        result = handler_with_error_llm._extract_single_answer(
            content, "What is the capital of France?", ""
        )
        assert result == content

    def test_empty_answer_returns_content_unchanged(self, mock_llm):
        """An empty LLM answer returns the original content unchanged (no ". " prefix).

        A None response (or a think-only response that strips to "") normalizes to
        "" via ``get_llm_response_text``. The extractor must then fall back to the
        original content rather than prepending an empty answer (". content").
        """
        from local_deep_research.citation_handlers.precision_extraction_handler import (
            PrecisionExtractionHandler,
        )

        mock_llm.invoke.return_value = None
        handler = PrecisionExtractionHandler(llm=mock_llm)

        content = "Test content"
        result = handler._extract_single_answer(content, "test query?", "")
        assert result == "Test content"

    def test_successful_extraction_prepends_answer(self, mock_llm):
        """Successful extraction prepends the answer to content."""
        from local_deep_research.citation_handlers.precision_extraction_handler import (
            PrecisionExtractionHandler,
        )

        mock_llm.invoke.return_value = Mock(content="Paris")
        handler = PrecisionExtractionHandler(llm=mock_llm)

        content = "The capital of France is Paris, a major European city."
        result = handler._extract_single_answer(
            content, "What is the capital?", ""
        )
        assert result.startswith("Paris.")
        assert content in result

    def test_extraction_cleans_comma_separated(self, mock_llm):
        """Extraction takes only first item from comma-separated answer."""
        from local_deep_research.citation_handlers.precision_extraction_handler import (
            PrecisionExtractionHandler,
        )

        mock_llm.invoke.return_value = Mock(content="Paris, London, Berlin")
        handler = PrecisionExtractionHandler(llm=mock_llm)

        result = handler._extract_single_answer("test", "query?", "")
        assert result.startswith("Paris.")

    def test_extraction_cleans_and_separated(self, mock_llm):
        """Extraction takes only first item from 'and'-separated answer."""
        from local_deep_research.citation_handlers.precision_extraction_handler import (
            PrecisionExtractionHandler,
        )

        mock_llm.invoke.return_value = Mock(
            content="Paris and London and Berlin"
        )
        handler = PrecisionExtractionHandler(llm=mock_llm)

        result = handler._extract_single_answer("test", "query?", "")
        assert result.startswith("Paris.")

    def test_extraction_cleans_or_separated(self, mock_llm):
        """Extraction takes only first item from 'or'-separated answer."""
        from local_deep_research.citation_handlers.precision_extraction_handler import (
            PrecisionExtractionHandler,
        )

        mock_llm.invoke.return_value = Mock(content="Paris or Lyon")
        handler = PrecisionExtractionHandler(llm=mock_llm)

        result = handler._extract_single_answer("test", "query?", "")
        assert result.startswith("Paris.")


class TestExtractDimensionError:
    """Tests for _extract_dimension exception handler (lines 468-471)."""

    def test_returns_content_on_llm_error(self, handler_with_error_llm):
        """When LLM raises, returns original content unchanged."""
        content = "The Eiffel Tower is 324 meters tall."
        result = handler_with_error_llm._extract_dimension(
            content, "How tall is the Eiffel Tower?", ""
        )
        assert result == content

    def test_returns_content_on_no_measurement_match(self, mock_llm):
        """When LLM response has no valid measurement, falls back to regex."""
        from local_deep_research.citation_handlers.precision_extraction_handler import (
            PrecisionExtractionHandler,
        )

        mock_llm.invoke.return_value = Mock(content="I'm not sure about that")
        handler = PrecisionExtractionHandler(llm=mock_llm)

        content = "The height is 324 meters approximately."
        result = handler._extract_dimension(content, "How tall is it?", "")
        # Falls through to regex pattern matching
        assert "324 meters" in result

    def test_successful_dimension_extraction(self, mock_llm):
        """Successful extraction returns measurement prepended to content."""
        from local_deep_research.citation_handlers.precision_extraction_handler import (
            PrecisionExtractionHandler,
        )

        mock_llm.invoke.return_value = Mock(content="324 meters")
        handler = PrecisionExtractionHandler(llm=mock_llm)

        content = "The Eiffel Tower stands at 324 meters tall."
        result = handler._extract_dimension(
            content, "How tall is the Eiffel Tower?", ""
        )
        assert result.startswith("324 meters.")
        assert content in result

    def test_dimension_type_detection_height(self, handler_with_error_llm):
        """Height keywords in query detected correctly."""
        content = "The building is 100 meters tall and 50 meters wide."
        # LLM will fail, but regex fallback uses dimension scoring
        result = handler_with_error_llm._extract_dimension(
            content, "How tall is the building?", ""
        )
        # Content returned as-is since LLM fails
        assert result == content

    def test_dimension_type_detection_weight(self, handler_with_error_llm):
        """Weight keywords in query detected correctly."""
        content = "The elephant weighs 6000 kg and is 3 meters tall."
        result = handler_with_error_llm._extract_dimension(
            content, "How much does the elephant weigh?", ""
        )
        assert result == content


class TestExtractScoreError:
    """Tests for _extract_score exception handler (lines 491-493)."""

    def test_returns_first_score_on_llm_error(self, handler_with_error_llm):
        """When LLM raises, returns first score found in content."""
        content = "The final score was 3-2. The halftime score was 1-1."
        result = handler_with_error_llm._extract_score(
            content, "What was the final score?", ""
        )
        # Should fallback to first score pattern: "3-2"
        assert result.startswith("3-2.")
        assert content in result

    def test_returns_first_score_on_empty_answer(self, mock_llm):
        """An empty LLM answer (None) falls back to the first score, not '. content'."""
        from local_deep_research.citation_handlers.precision_extraction_handler import (
            PrecisionExtractionHandler,
        )

        mock_llm.invoke.return_value = None
        handler = PrecisionExtractionHandler(llm=mock_llm)
        content = "The final score was 3-2. The halftime score was 1-1."
        result = handler._extract_score(
            content, "What was the final score?", ""
        )
        assert result.startswith("3-2.")
        assert not result.startswith(". ")

    def test_returns_content_when_no_scores_found(self, handler_with_error_llm):
        """When no score pattern is found, returns content unchanged."""
        content = "It was an exciting game with many plays."
        result = handler_with_error_llm._extract_score(
            content, "What was the score?", ""
        )
        assert result == content

    def test_successful_score_extraction(self, mock_llm):
        """Successful LLM extraction returns answer prepended to content."""
        from local_deep_research.citation_handlers.precision_extraction_handler import (
            PrecisionExtractionHandler,
        )

        mock_llm.invoke.return_value = Mock(content="3-2 (Team A wins)")
        handler = PrecisionExtractionHandler(llm=mock_llm)

        content = "Final score was 3-2. Halftime 1-1."
        result = handler._extract_score(content, "What was the score?", "")
        assert result.startswith("3-2 (Team A wins).")

    def test_score_fallback_uses_first_pattern_match(
        self, handler_with_error_llm
    ):
        """LLM error fallback specifically uses scores[0], not the best match."""
        content = "Halftime 1-0. Final 4-2. Overtime 5-3."
        result = handler_with_error_llm._extract_score(
            content, "What was the overtime score?", ""
        )
        # Fallback always uses first match (1-0), not the contextually best one
        assert result.startswith("1-0.")

    def test_score_from_sources(self, handler_with_error_llm):
        """Score patterns in sources string also get picked up."""
        content = "An exciting match."
        sources = "Final: 7-3 in favor of the home team."
        result = handler_with_error_llm._extract_score(
            content, "What was the score?", sources
        )
        assert result.startswith("7-3.")


class TestExtractTemporalError:
    """Tests for _extract_temporal exception handler (lines 519-521)."""

    def test_returns_first_year_on_llm_error(self, handler_with_error_llm):
        """When LLM raises, returns first year found in content."""
        content = "Founded in 1998. Went public in 2004. Acquired in 2015."
        result = handler_with_error_llm._extract_temporal(
            content, "When was the company founded?", ""
        )
        # Fallback returns first year found: "1998"
        assert result.startswith("1998.")
        assert content in result

    def test_returns_first_year_on_empty_answer(self, mock_llm):
        """An empty LLM answer (None) falls back to the first year, not '. content'."""
        from local_deep_research.citation_handlers.precision_extraction_handler import (
            PrecisionExtractionHandler,
        )

        mock_llm.invoke.return_value = None
        handler = PrecisionExtractionHandler(llm=mock_llm)
        content = "Founded in 1998. Went public in 2004. Acquired in 2015."
        result = handler._extract_temporal(
            content, "When was the company founded?", ""
        )
        assert result.startswith("1998.")
        assert not result.startswith(". ")

    def test_returns_content_when_no_years_found(self, handler_with_error_llm):
        """When no year pattern is found, returns content unchanged."""
        content = "The event happened many years ago."
        result = handler_with_error_llm._extract_temporal(
            content, "When did it happen?", ""
        )
        assert result == content

    def test_successful_temporal_extraction_with_year(self, mock_llm):
        """Successful extraction with year in LLM response."""
        from local_deep_research.citation_handlers.precision_extraction_handler import (
            PrecisionExtractionHandler,
        )

        mock_llm.invoke.return_value = Mock(content="The answer is 2004")
        handler = PrecisionExtractionHandler(llm=mock_llm)

        content = "Founded in 1998. Went public in 2004."
        result = handler._extract_temporal(
            content, "When did the company go public?", ""
        )
        assert result.startswith("2004.")

    def test_successful_temporal_extraction_without_year_in_response(
        self, mock_llm
    ):
        """When LLM response has no year pattern, raw answer is used."""
        from local_deep_research.citation_handlers.precision_extraction_handler import (
            PrecisionExtractionHandler,
        )

        mock_llm.invoke.return_value = Mock(content="In the late nineties")
        handler = PrecisionExtractionHandler(llm=mock_llm)

        content = "Founded in 1998."
        result = handler._extract_temporal(content, "When was it founded?", "")
        assert result.startswith("In the late nineties.")

    def test_temporal_fallback_bias_toward_first_year(
        self, handler_with_error_llm
    ):
        """LLM error fallback returns first year even if question asks for latest."""
        content = "Founded in 1998, restructured in 2010, acquired in 2020."
        result = handler_with_error_llm._extract_temporal(
            content, "When was the company acquired?", ""
        )
        # Bug-like behavior: always returns first year (1998) even for "acquired"
        assert result.startswith("1998.")

    def test_temporal_from_sources(self, handler_with_error_llm):
        """Year patterns in sources string also get picked up."""
        content = "The company has a long history."
        sources = "Founded in 2005 by entrepreneurs."
        result = handler_with_error_llm._extract_temporal(
            content, "When was it founded?", sources
        )
        assert result.startswith("2005.")


class TestExtractNumberError:
    """Tests for _extract_number exception handler (lines 544-546)."""

    def test_returns_first_number_on_llm_error(self, handler_with_error_llm):
        """When LLM raises, returns first number found in content."""
        content = "The population is 8.3 million. The area is 302 sq km."
        result = handler_with_error_llm._extract_number(
            content, "What is the population?", ""
        )
        # Fallback returns first number pattern: "8.3"
        assert result.startswith("8.3.")
        assert content in result

    def test_returns_first_number_on_empty_answer(self, mock_llm):
        """An empty LLM answer (None) falls back to the first number, not '. content'."""
        from local_deep_research.citation_handlers.precision_extraction_handler import (
            PrecisionExtractionHandler,
        )

        mock_llm.invoke.return_value = None
        handler = PrecisionExtractionHandler(llm=mock_llm)
        content = "The population is 8.3 million. The area is 302 sq km."
        result = handler._extract_number(content, "What is the population?", "")
        assert result.startswith("8.3.")
        assert not result.startswith(". ")

    def test_returns_content_when_no_numbers_found(
        self, handler_with_error_llm
    ):
        """When no number pattern is found, returns content unchanged."""
        content = "Many people live in the city."
        result = handler_with_error_llm._extract_number(
            content, "How many people?", ""
        )
        assert result == content

    def test_successful_number_extraction(self, mock_llm):
        """Successful LLM extraction returns answer prepended to content."""
        from local_deep_research.citation_handlers.precision_extraction_handler import (
            PrecisionExtractionHandler,
        )

        mock_llm.invoke.return_value = Mock(content="8.3 million")
        handler = PrecisionExtractionHandler(llm=mock_llm)

        content = "The population is 8.3 million."
        result = handler._extract_number(content, "What is the population?", "")
        assert result.startswith("8.3 million.")

    def test_number_fallback_uses_first_match(self, handler_with_error_llm):
        """LLM error fallback uses first number, not the contextually best."""
        content = "It has 4 floors, 200 rooms, and costs 50 million dollars."
        result = handler_with_error_llm._extract_number(
            content, "How many rooms?", ""
        )
        # Fallback always returns first match: "4"
        assert result.startswith("4.")

    def test_number_from_sources(self, handler_with_error_llm):
        """Number patterns in sources string also get picked up."""
        content = "A large building."
        sources = "The building has 42 floors."
        result = handler_with_error_llm._extract_number(
            content, "How many floors?", sources
        )
        assert result.startswith("42.")


class TestExtractBestNameEdgeCases:
    """Tests for _extract_best_name edge cases."""

    def test_returns_content_when_no_names_found(self, mock_llm):
        """When no name pattern matches, returns content unchanged."""
        from local_deep_research.citation_handlers.precision_extraction_handler import (
            PrecisionExtractionHandler,
        )

        handler = PrecisionExtractionHandler(llm=mock_llm)

        content = "the quick brown fox jumps over the lazy dog"
        result = handler._extract_best_name(content, "who did it?", "")
        # No capitalized multi-word names → returns original
        assert result == content

    def test_extracts_most_frequent_name(self, mock_llm):
        """Most frequently occurring name is selected."""
        from local_deep_research.citation_handlers.precision_extraction_handler import (
            PrecisionExtractionHandler,
        )

        mock_llm.invoke.return_value = Mock(content="John Smith")
        handler = PrecisionExtractionHandler(llm=mock_llm)

        content = (
            "John Smith won the award. "
            "John Smith is a professor. "
            "Mary Johnson also attended."
        )
        result = handler._extract_best_name(content, "Who won the award?", "")
        assert "John Smith" in result


class TestDimensionScoringFallback:
    """Tests for dimension regex fallback scoring logic (lines 392-461)."""

    def test_dimension_with_keyword_context_scores_higher(self, mock_llm):
        """Dimensions near query keywords score higher."""
        from local_deep_research.citation_handlers.precision_extraction_handler import (
            PrecisionExtractionHandler,
        )

        # LLM returns non-measurement text → triggers fallback
        mock_llm.invoke.return_value = Mock(content="unclear measurement")
        handler = PrecisionExtractionHandler(llm=mock_llm)

        content = (
            "The width is 50 meters. The height of the tower is 324 meters."
        )
        result = handler._extract_dimension(
            content, "How tall is the tower?", ""
        )
        # "height" keyword near "324 meters" should score it higher
        assert "324 meters" in result

    def test_dimension_fallback_returns_first_when_no_context(self, mock_llm):
        """Without context clues, first dimension found is returned."""
        from local_deep_research.citation_handlers.precision_extraction_handler import (
            PrecisionExtractionHandler,
        )

        mock_llm.invoke.return_value = Mock(content="not a measurement")
        handler = PrecisionExtractionHandler(llm=mock_llm)

        content = "Measurements: 10 meters, 20 km, 5 feet."
        result = handler._extract_dimension(
            content, "What is the measurement?", ""
        )
        # Should return some dimension from the content
        assert "meters" in result or "km" in result or "feet" in result

    def test_dimension_no_patterns_returns_content(self, mock_llm):
        """When content has no dimension patterns, returns content unchanged."""
        from local_deep_research.citation_handlers.precision_extraction_handler import (
            PrecisionExtractionHandler,
        )

        mock_llm.invoke.return_value = Mock(content="not a measurement")
        handler = PrecisionExtractionHandler(llm=mock_llm)

        content = "There are no measurements here."
        result = handler._extract_dimension(content, "How tall is it?", "")
        # No regex match either → content returned unchanged
        # (LLM response has no number+unit, regex finds nothing)
        assert result == content
