"""
Tests for the NewsAnalyzer class.

Tests cover:
- Empty result handling
- News item extraction and validation
- Category counting
- Impact summarization
- Snippet preparation
- LLM-based analysis methods
- Error handling
"""

from unittest.mock import Mock, patch


class TestNewsAnalyzerInit:
    """Tests for NewsAnalyzer initialization."""

    def test_init_with_llm_client(self):
        """Test initialization with custom LLM client."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        mock_llm = Mock()
        analyzer = NewsAnalyzer(llm_client=mock_llm)

        assert analyzer.llm_client is mock_llm

    def test_init_without_llm_client(self):
        """Test initialization uses default LLM from config."""
        with patch(
            "local_deep_research.news.core.news_analyzer.get_llm"
        ) as mock_get_llm:
            mock_llm = Mock()
            mock_get_llm.return_value = mock_llm

            from local_deep_research.news.core.news_analyzer import NewsAnalyzer

            analyzer = NewsAnalyzer()

            assert analyzer.llm_client is mock_llm


class TestNewsAnalyzer:
    """Tests for the NewsAnalyzer class."""

    def test_news_analyzer_empty_results(self):
        """Test with empty search results."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        analyzer = NewsAnalyzer(llm_client=Mock())

        result = analyzer.analyze_news([])

        assert result["items"] == []
        assert result["item_count"] == 0
        assert result["big_picture"] == ""
        assert result["watch_for"] == []
        assert result["patterns"] == ""
        assert result["topics"] == []
        assert result["categories"] == {}
        assert result["impact_summary"]["average"] == 0
        assert result["impact_summary"]["high_impact_count"] == 0

    def test_validate_news_item(self):
        """Test field validation for news items."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        analyzer = NewsAnalyzer(llm_client=Mock())

        # Valid item
        valid_item = {
            "headline": "Test headline",
            "summary": "Test summary",
            "category": "Tech",
        }
        assert analyzer._validate_news_item(valid_item) is True

        # Missing headline
        invalid_item1 = {"summary": "Test summary"}
        assert analyzer._validate_news_item(invalid_item1) is False

        # Missing summary
        invalid_item2 = {"headline": "Test headline"}
        assert analyzer._validate_news_item(invalid_item2) is False

        # Empty headline
        invalid_item3 = {"headline": "", "summary": "Test summary"}
        assert analyzer._validate_news_item(invalid_item3) is False

    def test_count_categories(self):
        """Test category grouping."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        analyzer = NewsAnalyzer(llm_client=Mock())

        news_items = [
            {"category": "Technology", "headline": "Tech news 1"},
            {"category": "Technology", "headline": "Tech news 2"},
            {"category": "Sports", "headline": "Sports news"},
            {"category": "Politics", "headline": "Political news"},
            {"headline": "No category news"},  # Missing category
        ]

        result = analyzer._count_categories(news_items)

        assert result["Technology"] == 2
        assert result["Sports"] == 1
        assert result["Politics"] == 1
        assert result["Other"] == 1  # Default for missing category

    def test_summarize_impact(self):
        """Test impact statistics."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        analyzer = NewsAnalyzer(llm_client=Mock())

        # Normal items with varying impact scores
        news_items = [
            {"impact_score": 9},
            {"impact_score": 8},
            {"impact_score": 5},
            {"impact_score": 3},
        ]

        result = analyzer._summarize_impact(news_items)

        assert result["average"] == 6.25
        assert result["high_impact_count"] == 2  # Scores >= 8
        assert result["max"] == 9
        assert result["min"] == 3

    def test_summarize_impact_empty(self):
        """Test impact statistics with empty list."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        analyzer = NewsAnalyzer(llm_client=Mock())

        result = analyzer._summarize_impact([])

        assert result["average"] == 0
        assert result["high_impact_count"] == 0

    def test_prepare_snippets(self):
        """Test snippet formatting for LLM."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        analyzer = NewsAnalyzer(llm_client=Mock())

        search_results = [
            {
                "title": "First Article",
                "url": "https://example.com/1",
                "snippet": "This is the first article snippet.",
            },
            {
                "title": "Second Article",
                "url": "https://example.com/2",
                "content": "This is the second article content.",
            },
        ]

        result = analyzer._prepare_snippets(search_results)

        assert "[1]" in result
        assert "[2]" in result
        assert "First Article" in result
        assert "https://example.com/1" in result
        assert "first article snippet" in result
        assert "Second Article" in result
        assert "second article content" in result

    def test_empty_analysis_structure(self):
        """Verify empty analysis structure has all required fields."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        analyzer = NewsAnalyzer(llm_client=Mock())

        result = analyzer._empty_analysis()

        # Check all required fields are present
        required_fields = [
            "items",
            "item_count",
            "big_picture",
            "watch_for",
            "patterns",
            "topics",
            "categories",
            "impact_summary",
            "timestamp",
        ]

        for field in required_fields:
            assert field in result, f"Missing field: {field}"

        # Check types
        assert isinstance(result["items"], list)
        assert isinstance(result["item_count"], int)
        assert isinstance(result["big_picture"], str)
        assert isinstance(result["watch_for"], list)
        assert isinstance(result["patterns"], str)
        assert isinstance(result["topics"], list)
        assert isinstance(result["categories"], dict)
        assert isinstance(result["impact_summary"], dict)
        assert isinstance(result["timestamp"], str)


class TestExtractNewsItems:
    """Tests for extract_news_items method."""

    def test_extract_news_items_without_llm_client(self):
        """Test extraction returns empty list when no LLM client."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        # Create analyzer with a mock, then set llm_client to None
        analyzer = NewsAnalyzer(llm_client=Mock())
        analyzer.llm_client = None

        result = analyzer.extract_news_items([{"title": "Test"}])

        assert result == []

    def test_extract_news_items_success(self):
        """Test successful news item extraction."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        mock_llm = Mock()
        mock_response = Mock()
        mock_response.content = """
        [
            {"headline": "Breaking News", "summary": "Something happened", "category": "Tech"},
            {"headline": "Another Story", "summary": "More details here", "category": "Sports"}
        ]
        """
        mock_llm.invoke.return_value = mock_response

        analyzer = NewsAnalyzer(llm_client=mock_llm)

        result = analyzer.extract_news_items(
            [{"title": "Test", "snippet": "content"}]
        )

        assert len(result) == 2
        assert result[0]["headline"] == "Breaking News"
        assert result[1]["headline"] == "Another Story"
        assert all("id" in item for item in result)

    def test_extract_news_items_with_max_items(self):
        """Test extraction respects max_items parameter."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        mock_llm = Mock()
        mock_response = Mock()
        # Return more items than max
        mock_response.content = """
        [
            {"headline": "Item 1", "summary": "Summary 1"},
            {"headline": "Item 2", "summary": "Summary 2"},
            {"headline": "Item 3", "summary": "Summary 3"},
            {"headline": "Item 4", "summary": "Summary 4"},
            {"headline": "Item 5", "summary": "Summary 5"}
        ]
        """
        mock_llm.invoke.return_value = mock_response

        analyzer = NewsAnalyzer(llm_client=mock_llm)

        result = analyzer.extract_news_items(
            [{"title": "Test", "snippet": "content"}], max_items=3
        )

        assert len(result) == 3

    def test_extract_news_items_filters_invalid(self):
        """Test that invalid items are filtered out."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        mock_llm = Mock()
        mock_response = Mock()
        mock_response.content = """
        [
            {"headline": "Valid Item", "summary": "Has all fields"},
            {"headline": "Missing Summary"},
            {"summary": "Missing Headline"}
        ]
        """
        mock_llm.invoke.return_value = mock_response

        analyzer = NewsAnalyzer(llm_client=mock_llm)

        result = analyzer.extract_news_items(
            [{"title": "Test", "snippet": "content"}]
        )

        assert len(result) == 1
        assert result[0]["headline"] == "Valid Item"

    def test_extract_news_items_handles_llm_error(self):
        """Test that LLM errors are handled gracefully."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        mock_llm = Mock()
        mock_llm.invoke.side_effect = Exception("LLM Error")

        analyzer = NewsAnalyzer(llm_client=mock_llm)

        result = analyzer.extract_news_items(
            [{"title": "Test", "snippet": "content"}]
        )

        assert result == []

    def test_extract_news_items_handles_malformed_json(self):
        """Test that malformed JSON is handled gracefully."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        mock_llm = Mock()
        mock_response = Mock()
        mock_response.content = "This is not valid JSON at all"
        mock_llm.invoke.return_value = mock_response

        analyzer = NewsAnalyzer(llm_client=mock_llm)

        result = analyzer.extract_news_items(
            [{"title": "Test", "snippet": "content"}]
        )

        assert result == []

    def test_extract_news_items_handles_response_without_content(self):
        """Test handling response that returns string directly."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        mock_llm = Mock()
        # Response without .content attribute
        mock_response = '[{"headline": "Test", "summary": "Summary"}]'
        mock_llm.invoke.return_value = mock_response

        analyzer = NewsAnalyzer(llm_client=mock_llm)

        result = analyzer.extract_news_items(
            [{"title": "Test", "snippet": "content"}]
        )

        assert len(result) == 1


class TestGenerateBigPicture:
    """Tests for generate_big_picture method."""

    def test_generate_big_picture_without_llm_client(self):
        """Test big picture returns empty when no LLM client."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        # Create analyzer with a mock, then set llm_client to None
        analyzer = NewsAnalyzer(llm_client=Mock())
        analyzer.llm_client = None

        result = analyzer.generate_big_picture([{"headline": "Test"}])

        assert result == ""

    def test_generate_big_picture_without_news_items(self):
        """Test big picture returns empty when no news items."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        analyzer = NewsAnalyzer(llm_client=Mock())

        result = analyzer.generate_big_picture([])

        assert result == ""

    def test_generate_big_picture_success(self):
        """Test successful big picture generation."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        mock_llm = Mock()
        mock_response = Mock()
        mock_response.content = "  The economy is shifting towards AI.  "
        mock_llm.invoke.return_value = mock_response

        analyzer = NewsAnalyzer(llm_client=mock_llm)

        news_items = [
            {"headline": "AI Boom", "summary": "Companies investing in AI"},
            {"headline": "Tech Growth", "summary": "Sector growing rapidly"},
        ]

        result = analyzer.generate_big_picture(news_items)

        assert result == "The economy is shifting towards AI."
        mock_llm.invoke.assert_called_once()

    def test_generate_big_picture_handles_error(self):
        """Test that errors are handled gracefully."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        mock_llm = Mock()
        mock_llm.invoke.side_effect = Exception("LLM Error")

        analyzer = NewsAnalyzer(llm_client=mock_llm)

        result = analyzer.generate_big_picture(
            [{"headline": "Test", "summary": "S"}]
        )

        assert result == ""


class TestGenerateWatchFor:
    """Tests for generate_watch_for method."""

    def test_generate_watch_for_without_llm_client(self):
        """Test watch_for returns empty list when no LLM client."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        # Create analyzer with a mock, then set llm_client to None
        analyzer = NewsAnalyzer(llm_client=Mock())
        analyzer.llm_client = None

        result = analyzer.generate_watch_for([{"headline": "Test"}])

        assert result == []

    def test_generate_watch_for_without_news_items(self):
        """Test watch_for returns empty list when no news items."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        analyzer = NewsAnalyzer(llm_client=Mock())

        result = analyzer.generate_watch_for([])

        assert result == []

    def test_generate_watch_for_success(self):
        """Test successful watch_for generation."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        mock_llm = Mock()
        mock_response = Mock()
        mock_response.content = """
        - Market opening on Monday
        - Fed announcement expected
        - Earnings reports due
        """
        mock_llm.invoke.return_value = mock_response

        analyzer = NewsAnalyzer(llm_client=mock_llm)

        news_items = [
            {
                "headline": "Markets volatile",
                "summary": "Details",
                "is_developing": True,
            }
        ]

        result = analyzer.generate_watch_for(news_items)

        assert len(result) == 3
        assert "Market opening on Monday" in result

    def test_generate_watch_for_uses_developing_stories(self):
        """Test that developing stories are prioritized."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        mock_llm = Mock()
        mock_response = Mock()
        mock_response.content = "- Watch item 1"
        mock_llm.invoke.return_value = mock_response

        analyzer = NewsAnalyzer(llm_client=mock_llm)

        news_items = [
            {
                "headline": "Old News",
                "summary": "Details",
                "is_developing": False,
            },
            {
                "headline": "Breaking",
                "summary": "Developing",
                "is_developing": True,
            },
        ]

        analyzer.generate_watch_for(news_items)

        # Check that the prompt contains the developing story
        call_args = mock_llm.invoke.call_args[0][0]
        assert "Breaking" in call_args

    def test_generate_watch_for_limits_to_five(self):
        """Test that watch_for items are limited to 5."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        mock_llm = Mock()
        mock_response = Mock()
        mock_response.content = """
        - Item 1
        - Item 2
        - Item 3
        - Item 4
        - Item 5
        - Item 6
        - Item 7
        """
        mock_llm.invoke.return_value = mock_response

        analyzer = NewsAnalyzer(llm_client=mock_llm)

        result = analyzer.generate_watch_for(
            [{"headline": "Test", "summary": "S", "is_developing": True}]
        )

        assert len(result) <= 5

    def test_generate_watch_for_handles_error(self):
        """Test that errors are handled gracefully."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        mock_llm = Mock()
        mock_llm.invoke.side_effect = Exception("LLM Error")

        analyzer = NewsAnalyzer(llm_client=mock_llm)

        result = analyzer.generate_watch_for(
            [{"headline": "Test", "summary": "S", "is_developing": True}]
        )

        assert result == []


class TestGeneratePatterns:
    """Tests for generate_patterns method."""

    def test_generate_patterns_without_llm_client(self):
        """Test patterns returns empty when no LLM client."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        # Create analyzer with a mock, then set llm_client to None
        analyzer = NewsAnalyzer(llm_client=Mock())
        analyzer.llm_client = None

        result = analyzer.generate_patterns([{"headline": "Test"}])

        assert result == ""

    def test_generate_patterns_without_news_items(self):
        """Test patterns returns empty when no news items."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        analyzer = NewsAnalyzer(llm_client=Mock())

        result = analyzer.generate_patterns([])

        assert result == ""

    def test_generate_patterns_success(self):
        """Test successful pattern generation."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        mock_llm = Mock()
        mock_response = Mock()
        mock_response.content = "  Technology dominates today's headlines.  "
        mock_llm.invoke.return_value = mock_response

        analyzer = NewsAnalyzer(llm_client=mock_llm)

        news_items = [
            {"headline": "Tech News 1", "category": "Technology"},
            {"headline": "Tech News 2", "category": "Technology"},
            {"headline": "Sports Update", "category": "Sports"},
        ]

        result = analyzer.generate_patterns(news_items)

        assert result == "Technology dominates today's headlines."

    def test_generate_patterns_handles_error(self):
        """Test that errors are handled gracefully."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        mock_llm = Mock()
        mock_llm.invoke.side_effect = Exception("LLM Error")

        analyzer = NewsAnalyzer(llm_client=mock_llm)

        result = analyzer.generate_patterns(
            [{"headline": "Test", "category": "Tech"}]
        )

        assert result == ""


class TestExtractTopics:
    """Tests for extract_topics method."""

    def test_extract_topics_from_news_items(self):
        """Test topic extraction from news items."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        analyzer = NewsAnalyzer(llm_client=Mock())

        with patch(
            "local_deep_research.news.core.news_analyzer.generate_topics"
        ) as mock_gen:
            mock_gen.return_value = ["AI", "Machine Learning"]

            news_items = [
                {
                    "id": "item1",
                    "headline": "AI Revolution",
                    "summary": "AI is changing everything",
                    "category": "Technology",
                    "impact_score": 8,
                }
            ]

            result = analyzer.extract_topics(news_items)

        assert len(result) >= 1
        assert any(topic["name"] == "AI" for topic in result)

    def test_extract_topics_deduplicates(self):
        """Test that topics are deduplicated."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        analyzer = NewsAnalyzer(llm_client=Mock())

        with patch(
            "local_deep_research.news.core.news_analyzer.generate_topics"
        ) as mock_gen:
            # Same topic from multiple items
            mock_gen.return_value = ["AI"]

            news_items = [
                {
                    "id": "1",
                    "headline": "AI News 1",
                    "summary": "S1",
                    "impact_score": 5,
                },
                {
                    "id": "2",
                    "headline": "AI News 2",
                    "summary": "S2",
                    "impact_score": 7,
                },
            ]

            result = analyzer.extract_topics(news_items)

        # Should have only one "AI" topic
        ai_topics = [t for t in result if t["name"] == "AI"]
        assert len(ai_topics) == 1
        # Should have frequency count
        assert ai_topics[0]["frequency"] == 2

    def test_extract_topics_keeps_highest_impact(self):
        """Test that highest impact score is kept for duplicates."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        analyzer = NewsAnalyzer(llm_client=Mock())

        with patch(
            "local_deep_research.news.core.news_analyzer.generate_topics"
        ) as mock_gen:
            mock_gen.return_value = ["Climate"]

            news_items = [
                {
                    "id": "1",
                    "headline": "Climate 1",
                    "summary": "S",
                    "impact_score": 5,
                },
                {
                    "id": "2",
                    "headline": "Climate 2",
                    "summary": "S",
                    "impact_score": 9,
                },
            ]

            result = analyzer.extract_topics(news_items)

        climate_topic = next(t for t in result if t["name"] == "Climate")
        assert climate_topic["impact_score"] == 9

    def test_extract_topics_limits_to_ten(self):
        """Test that topics are limited to 10."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        analyzer = NewsAnalyzer(llm_client=Mock())

        with patch(
            "local_deep_research.news.core.news_analyzer.generate_topics"
        ) as mock_gen:
            # Return many topics
            mock_gen.side_effect = [
                [f"Topic{i}a", f"Topic{i}b", f"Topic{i}c"] for i in range(10)
            ]

            news_items = [
                {
                    "id": str(i),
                    "headline": f"H{i}",
                    "summary": "S",
                    "impact_score": 5,
                }
                for i in range(10)
            ]

            result = analyzer.extract_topics(news_items)

        assert len(result) <= 10

    def test_extract_topics_includes_query(self):
        """Test that topics include subscription query."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        analyzer = NewsAnalyzer(llm_client=Mock())

        with patch(
            "local_deep_research.news.core.news_analyzer.generate_topics"
        ) as mock_gen:
            mock_gen.return_value = ["Technology"]

            news_items = [
                {
                    "id": "1",
                    "headline": "Tech",
                    "summary": "S",
                    "impact_score": 5,
                }
            ]

            result = analyzer.extract_topics(news_items)

        assert "query" in result[0]
        assert "Technology" in result[0]["query"]


class TestAnalyzeNews:
    """Tests for the main analyze_news method."""

    def test_analyze_news_full_flow(self):
        """Test complete news analysis flow."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        mock_llm = Mock()

        # Mock response for extract_news_items
        extract_response = Mock()
        extract_response.content = '[{"headline": "Test", "summary": "Summary", "category": "Tech", "impact_score": 8}]'

        # Mock response for other methods
        text_response = Mock()
        text_response.content = "Analysis text"

        mock_llm.invoke.return_value = extract_response

        analyzer = NewsAnalyzer(llm_client=mock_llm)

        # Patch the individual methods to simplify testing
        with patch.object(
            analyzer,
            "extract_news_items",
            return_value=[
                {
                    "id": "1",
                    "headline": "Test",
                    "summary": "S",
                    "category": "Tech",
                    "impact_score": 8,
                }
            ],
        ):
            with patch.object(
                analyzer, "generate_big_picture", return_value="Big picture"
            ):
                with patch.object(
                    analyzer, "generate_watch_for", return_value=["Watch item"]
                ):
                    with patch.object(
                        analyzer, "generate_patterns", return_value="Pattern"
                    ):
                        with patch.object(
                            analyzer,
                            "extract_topics",
                            return_value=[{"name": "Topic"}],
                        ):
                            result = analyzer.analyze_news(
                                [{"title": "Raw result"}]
                            )

        assert result["item_count"] == 1
        assert result["big_picture"] == "Big picture"
        assert result["watch_for"] == ["Watch item"]
        assert result["patterns"] == "Pattern"
        assert len(result["topics"]) == 1
        assert "timestamp" in result

    def test_analyze_news_returns_empty_on_error(self):
        """Test that analyze_news returns empty analysis on error."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        analyzer = NewsAnalyzer(llm_client=Mock())

        with patch.object(
            analyzer, "extract_news_items", side_effect=Exception("Error")
        ):
            result = analyzer.analyze_news([{"title": "Test"}])

        assert result["items"] == []
        assert result["item_count"] == 0

    def test_analyze_news_skips_components_when_no_items(self):
        """Test that analysis skips components when no items extracted."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        analyzer = NewsAnalyzer(llm_client=Mock())

        with patch.object(analyzer, "extract_news_items", return_value=[]):
            result = analyzer.analyze_news([{"title": "Test"}])

        # Should return basic structure without calling other methods
        assert result["items"] == []
        assert "big_picture" not in result or result.get("big_picture") == ""


class TestValidateNewsItem:
    """Additional tests for _validate_news_item."""

    def test_validate_with_all_fields(self):
        """Test validation with all optional fields."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        analyzer = NewsAnalyzer(llm_client=Mock())

        item = {
            "headline": "Test",
            "summary": "Summary",
            "category": "Tech",
            "impact_score": 8,
            "source_url": "https://example.com",
            "entities": ["Apple", "Google"],
            "is_developing": True,
            "time_ago": "2 hours ago",
        }

        assert analyzer._validate_news_item(item) is True

    def test_validate_with_empty_summary(self):
        """Test validation fails with empty summary."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        analyzer = NewsAnalyzer(llm_client=Mock())

        item = {"headline": "Test", "summary": ""}

        assert analyzer._validate_news_item(item) is False


class TestPrepareSnippets:
    """Additional tests for _prepare_snippets."""

    def test_prepare_snippets_truncates_long_content(self):
        """Test that long content is truncated."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        analyzer = NewsAnalyzer(llm_client=Mock())

        long_content = "A" * 500
        search_results = [{"title": "Test", "snippet": long_content}]

        result = analyzer._prepare_snippets(search_results)

        # Should truncate at 200 chars + "..."
        assert "..." in result

    def test_prepare_snippets_handles_missing_fields(self):
        """Test handling of results with missing fields."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        analyzer = NewsAnalyzer(llm_client=Mock())

        search_results = [{"title": "Only Title"}]

        result = analyzer._prepare_snippets(search_results)

        assert "[1]" in result
        assert "Only Title" in result

    def test_prepare_snippets_prefers_snippet_over_content(self):
        """Test that snippet field is preferred over content."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        analyzer = NewsAnalyzer(llm_client=Mock())

        search_results = [
            {
                "title": "Test",
                "snippet": "This is the snippet",
                "content": "This is the content",
            }
        ]

        result = analyzer._prepare_snippets(search_results)

        assert "snippet" in result.lower()


class TestSummarizeImpact:
    """Additional tests for _summarize_impact."""

    def test_summarize_impact_with_default_scores(self):
        """Test impact summary when items have no impact_score."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        analyzer = NewsAnalyzer(llm_client=Mock())

        news_items = [{}, {}, {}]  # No impact_score

        result = analyzer._summarize_impact(news_items)

        # Should default to 5 for missing scores
        assert result["average"] == 5
        assert result["max"] == 5
        assert result["min"] == 5

    def test_summarize_impact_high_impact_threshold(self):
        """Test that high impact threshold is 8."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        analyzer = NewsAnalyzer(llm_client=Mock())

        news_items = [
            {"impact_score": 7},
            {"impact_score": 8},
            {"impact_score": 9},
            {"impact_score": 10},
        ]

        result = analyzer._summarize_impact(news_items)

        assert result["high_impact_count"] == 3  # 8, 9, 10


class TestNewsAnalyzerEdgeCases:
    """Additional edge case tests for NewsAnalyzer."""

    def test_analyze_news_with_exception(self):
        """Test analyze_news handles exceptions gracefully."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        mock_llm = Mock()
        mock_llm.invoke.side_effect = Exception("LLM unavailable")

        analyzer = NewsAnalyzer(llm_client=mock_llm)

        # Should return empty analysis on error
        result = analyzer.analyze_news([{"title": "Test"}])

        assert result["items"] == []
        assert result["item_count"] == 0

    def test_extract_news_items_empty_search_results(self):
        """Test extract_news_items with empty search results."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        mock_llm = Mock()
        mock_response = Mock()
        mock_response.content = "[]"
        mock_llm.invoke.return_value = mock_response

        analyzer = NewsAnalyzer(llm_client=mock_llm)

        result = analyzer.extract_news_items([])

        # Should still call LLM even with empty results
        assert result == []

    def test_extract_news_items_json_array_with_extra_text(self):
        """Test extract_news_items handles JSON with surrounding text."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        mock_llm = Mock()
        mock_response = Mock()
        mock_response.content = """
        Here are the news items:
        [{"headline": "Test", "summary": "Summary"}]
        That's all the news.
        """
        mock_llm.invoke.return_value = mock_response

        analyzer = NewsAnalyzer(llm_client=mock_llm)

        result = analyzer.extract_news_items([{"title": "Raw"}])

        assert len(result) == 1
        assert result[0]["headline"] == "Test"

    def test_generate_watch_for_fallback_to_all_stories(self):
        """Test watch_for uses all stories when no developing stories."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        mock_llm = Mock()
        mock_response = Mock()
        mock_response.content = "- Watch item"
        mock_llm.invoke.return_value = mock_response

        analyzer = NewsAnalyzer(llm_client=mock_llm)

        # All stories are NOT developing
        news_items = [
            {"headline": "Story 1", "summary": "S", "is_developing": False},
            {"headline": "Story 2", "summary": "S", "is_developing": False},
        ]

        result = analyzer.generate_watch_for(news_items)

        assert len(result) >= 0  # Should use fallback

    def test_generate_watch_for_strips_various_bullet_markers(self):
        """Test watch_for strips various bullet markers correctly."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        mock_llm = Mock()
        mock_response = Mock()
        mock_response.content = """
        - Item with dash
        • Item with bullet
        * Item with asterisk
        """
        mock_llm.invoke.return_value = mock_response

        analyzer = NewsAnalyzer(llm_client=mock_llm)

        result = analyzer.generate_watch_for(
            [{"headline": "Test", "summary": "S", "is_developing": True}]
        )

        # All bullet markers should be stripped
        for item in result:
            assert not item.startswith("-")
            assert not item.startswith("•")
            assert not item.startswith("*")

    def test_generate_patterns_groups_by_category(self):
        """Test patterns method groups items by category."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        mock_llm = Mock()
        mock_response = Mock()
        mock_response.content = "Tech dominates"
        mock_llm.invoke.return_value = mock_response

        analyzer = NewsAnalyzer(llm_client=mock_llm)

        news_items = [
            {"headline": "Tech 1", "category": "Technology"},
            {"headline": "Tech 2", "category": "Technology"},
            {"headline": "Sport 1", "category": "Sports"},
        ]

        result = analyzer.generate_patterns(news_items)

        assert result == "Tech dominates"

    def test_extract_topics_empty_news_items(self):
        """Test extract_topics with empty news items list."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        analyzer = NewsAnalyzer(llm_client=Mock())

        result = analyzer.extract_topics([])

        assert result == []

    def test_extract_topics_sorted_by_frequency(self):
        """Test topics are sorted by frequency."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        analyzer = NewsAnalyzer(llm_client=Mock())

        with patch(
            "local_deep_research.news.core.news_analyzer.generate_topics"
        ) as mock_gen:
            # Return same topic multiple times for first two items
            mock_gen.side_effect = [
                ["AI"],  # First item
                ["AI"],  # Second item - same topic
                ["Blockchain"],  # Third item - different topic
            ]

            news_items = [
                {
                    "id": "1",
                    "headline": "H1",
                    "summary": "S",
                    "impact_score": 5,
                },
                {
                    "id": "2",
                    "headline": "H2",
                    "summary": "S",
                    "impact_score": 5,
                },
                {
                    "id": "3",
                    "headline": "H3",
                    "summary": "S",
                    "impact_score": 5,
                },
            ]

            result = analyzer.extract_topics(news_items)

        # AI should be first (frequency 2) then Blockchain (frequency 1)
        assert result[0]["name"] == "AI"
        assert result[0]["frequency"] == 2
        if len(result) > 1:
            assert result[1]["name"] == "Blockchain"
            assert result[1]["frequency"] == 1

    def test_prepare_snippets_empty_results(self):
        """Test _prepare_snippets with empty search results."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        analyzer = NewsAnalyzer(llm_client=Mock())

        result = analyzer._prepare_snippets([])

        assert result == ""

    def test_prepare_snippets_result_without_title(self):
        """Test _prepare_snippets handles results without title."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        analyzer = NewsAnalyzer(llm_client=Mock())

        search_results = [
            {"url": "https://example.com", "snippet": "Some text"}
        ]

        result = analyzer._prepare_snippets(search_results)

        assert "[1]" in result
        assert "https://example.com" in result

    def test_validate_news_item_with_whitespace_only(self):
        """Test validation with whitespace-only fields."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        analyzer = NewsAnalyzer(llm_client=Mock())

        # Whitespace-only headline is truthy (non-empty string), so it passes
        # The validation only checks truthiness, not content quality
        item = {"headline": "   ", "summary": "Valid summary"}
        assert analyzer._validate_news_item(item) is True

    def test_count_categories_multiple_occurrences(self):
        """Test _count_categories counts multiple occurrences."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        analyzer = NewsAnalyzer(llm_client=Mock())

        news_items = [
            {"category": "Tech"},
            {"category": "Tech"},
            {"category": "Tech"},
            {"category": "Sports"},
        ]

        result = analyzer._count_categories(news_items)

        assert result["Tech"] == 3
        assert result["Sports"] == 1

    def test_summarize_impact_single_item(self):
        """Test _summarize_impact with single item."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        analyzer = NewsAnalyzer(llm_client=Mock())

        news_items = [{"impact_score": 7}]

        result = analyzer._summarize_impact(news_items)

        assert result["average"] == 7
        assert result["max"] == 7
        assert result["min"] == 7
        assert result["high_impact_count"] == 0

    def test_empty_analysis_has_timestamp(self):
        """Test _empty_analysis includes valid timestamp."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer
        from datetime import datetime

        analyzer = NewsAnalyzer(llm_client=Mock())

        result = analyzer._empty_analysis()

        # Timestamp should be parseable ISO format
        timestamp = result["timestamp"]
        # Should not raise
        datetime.fromisoformat(timestamp.replace("Z", "+00:00"))


class TestNewsAnalyzerLLMResponses:
    """Tests for various LLM response formats."""

    def test_extract_news_items_llm_returns_dict(self):
        """Test handling when LLM returns dict instead of list."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        mock_llm = Mock()
        mock_response = Mock()
        mock_response.content = '{"error": "Invalid format"}'
        mock_llm.invoke.return_value = mock_response

        analyzer = NewsAnalyzer(llm_client=mock_llm)

        result = analyzer.extract_news_items([{"title": "Test"}])

        # Should return empty list on parse failure
        assert result == []

    def test_extract_news_items_llm_returns_nested_json(self):
        """Test handling when LLM returns nested JSON."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        mock_llm = Mock()
        mock_response = Mock()
        mock_response.content = """
        {
            "news": [
                {"headline": "Test", "summary": "S"}
            ]
        }
        """
        mock_llm.invoke.return_value = mock_response

        analyzer = NewsAnalyzer(llm_client=mock_llm)

        result = analyzer.extract_news_items([{"title": "Test"}])

        # Should extract the inner array
        assert len(result) == 1

    def test_generate_big_picture_llm_with_newlines(self):
        """Test big picture strips newlines correctly."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        mock_llm = Mock()
        mock_response = Mock()
        mock_response.content = "\n\n  The economy is changing.  \n\n"
        mock_llm.invoke.return_value = mock_response

        analyzer = NewsAnalyzer(llm_client=mock_llm)

        result = analyzer.generate_big_picture(
            [{"headline": "Test", "summary": "S"}]
        )

        assert result == "The economy is changing."

    def test_generate_watch_for_empty_response(self):
        """Test watch_for with empty LLM response."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        mock_llm = Mock()
        mock_response = Mock()
        mock_response.content = ""
        mock_llm.invoke.return_value = mock_response

        analyzer = NewsAnalyzer(llm_client=mock_llm)

        result = analyzer.generate_watch_for(
            [{"headline": "Test", "summary": "S", "is_developing": True}]
        )

        assert result == []

    def test_generate_patterns_empty_response(self):
        """Test patterns with empty LLM response."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        mock_llm = Mock()
        mock_response = Mock()
        mock_response.content = "  "  # Whitespace only
        mock_llm.invoke.return_value = mock_response

        analyzer = NewsAnalyzer(llm_client=mock_llm)

        result = analyzer.generate_patterns(
            [{"headline": "Test", "category": "Tech"}]
        )

        assert result == ""


class TestNewsAnalyzerSnippetFormats:
    """Tests for various search result snippet formats."""

    def test_prepare_snippets_very_long_snippet(self):
        """Test _prepare_snippets truncates very long snippets."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        analyzer = NewsAnalyzer(llm_client=Mock())

        long_text = "A" * 500
        search_results = [{"title": "Test", "snippet": long_text}]

        result = analyzer._prepare_snippets(search_results)

        # Should truncate at 200 chars
        assert "A" * 200 in result
        assert "A" * 300 not in result

    def test_prepare_snippets_uses_content_when_no_snippet(self):
        """Test _prepare_snippets uses content field as fallback."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        analyzer = NewsAnalyzer(llm_client=Mock())

        search_results = [{"title": "Test", "content": "Content text here"}]

        result = analyzer._prepare_snippets(search_results)

        assert "Content text here" in result

    def test_prepare_snippets_prefers_snippet_over_content(self):
        """Test _prepare_snippets prefers snippet when both present."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        analyzer = NewsAnalyzer(llm_client=Mock())

        search_results = [
            {
                "title": "Test",
                "snippet": "Snippet text",
                "content": "Content text",
            }
        ]

        result = analyzer._prepare_snippets(search_results)

        # Snippet should be used, not content
        assert "Snippet text" in result
        # Content should NOT appear (elif branch)
        assert "Content" not in result


class TestNewsAnalyzerValidation:
    """Tests for news item validation edge cases."""

    def test_validate_news_item_null_values(self):
        """Test validation with null/None values."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        analyzer = NewsAnalyzer(llm_client=Mock())

        item = {"headline": None, "summary": "Valid"}
        assert analyzer._validate_news_item(item) is False

    def test_validate_news_item_numeric_values(self):
        """Test validation with numeric headline (edge case)."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        analyzer = NewsAnalyzer(llm_client=Mock())

        # Numeric values are truthy, so they pass validation
        item = {"headline": 12345, "summary": "Valid"}
        assert analyzer._validate_news_item(item) is True

    def test_validate_news_item_list_values(self):
        """Test validation with list as headline (edge case)."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        analyzer = NewsAnalyzer(llm_client=Mock())

        # Non-empty list is truthy
        item = {"headline": ["title"], "summary": "Valid"}
        assert analyzer._validate_news_item(item) is True

        # Empty list is falsy
        item2 = {"headline": [], "summary": "Valid"}
        assert analyzer._validate_news_item(item2) is False


class TestNewsAnalyzerCategoryHandling:
    """Tests for category handling edge cases."""

    def test_count_categories_empty_category(self):
        """Test _count_categories handles empty string category."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        analyzer = NewsAnalyzer(llm_client=Mock())

        news_items = [{"category": ""}]

        result = analyzer._count_categories(news_items)

        # Empty string is used as the key (not converted to "Other")
        # The code uses item.get("category", "Other"), but empty string is truthy
        assert result[""] == 1 or result.get("Other") == 1

    def test_count_categories_none_category(self):
        """Test _count_categories handles None category."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        analyzer = NewsAnalyzer(llm_client=Mock())

        news_items = [{"category": None}]

        result = analyzer._count_categories(news_items)

        # The code uses: cat = item.get("category", "Other")
        # When category is None, .get() returns None (not the default)
        # So None is used as the key
        assert None in result


class TestNewsAnalyzerImpactScores:
    """Tests for impact score handling."""

    def test_summarize_impact_all_high_impact(self):
        """Test _summarize_impact when all items are high impact."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        analyzer = NewsAnalyzer(llm_client=Mock())

        news_items = [
            {"impact_score": 8},
            {"impact_score": 9},
            {"impact_score": 10},
        ]

        result = analyzer._summarize_impact(news_items)

        assert result["high_impact_count"] == 3
        assert result["average"] == 9.0

    def test_summarize_impact_all_low_impact(self):
        """Test _summarize_impact when all items are low impact."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        analyzer = NewsAnalyzer(llm_client=Mock())

        news_items = [
            {"impact_score": 1},
            {"impact_score": 2},
            {"impact_score": 3},
        ]

        result = analyzer._summarize_impact(news_items)

        assert result["high_impact_count"] == 0
        assert result["average"] == 2.0

    def test_summarize_impact_at_boundary(self):
        """Test _summarize_impact with score at boundary (8)."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        analyzer = NewsAnalyzer(llm_client=Mock())

        news_items = [
            {"impact_score": 7},  # Not high
            {"impact_score": 8},  # High (>= 8)
        ]

        result = analyzer._summarize_impact(news_items)

        assert result["high_impact_count"] == 1


class TestAnalyzeNewsFlow:
    """Tests for the complete analyze_news flow."""

    def test_analyze_news_skips_components_for_empty_items(self):
        """Test that analysis skips components when no items extracted."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        mock_llm = Mock()
        mock_response = Mock()
        mock_response.content = "[]"  # No items
        mock_llm.invoke.return_value = mock_response

        analyzer = NewsAnalyzer(llm_client=mock_llm)

        with patch.object(analyzer, "generate_big_picture") as mock_big_picture:
            result = analyzer.analyze_news([{"title": "Test"}])

            # big_picture should not be called for empty items
            mock_big_picture.assert_not_called()
            assert result["item_count"] == 0

    def test_analyze_news_includes_timestamp(self):
        """Test that analysis includes timestamp."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        mock_llm = Mock()
        mock_response = Mock()
        mock_response.content = "[]"
        mock_llm.invoke.return_value = mock_response

        analyzer = NewsAnalyzer(llm_client=mock_llm)

        result = analyzer.analyze_news([{"title": "Test"}])

        assert "timestamp" in result
        # Timestamp should be ISO format
        assert "T" in result["timestamp"]

    def test_analyze_news_counts_search_results(self):
        """Test that analysis counts original search results."""
        from local_deep_research.news.core.news_analyzer import NewsAnalyzer

        mock_llm = Mock()
        mock_response = Mock()
        mock_response.content = '[{"headline": "Test", "summary": "S"}]'
        mock_llm.invoke.return_value = mock_response

        analyzer = NewsAnalyzer(llm_client=mock_llm)

        search_results = [
            {"title": "Result 1"},
            {"title": "Result 2"},
            {"title": "Result 3"},
        ]

        with patch.object(analyzer, "generate_big_picture", return_value=""):
            with patch.object(analyzer, "generate_watch_for", return_value=[]):
                with patch.object(
                    analyzer, "generate_patterns", return_value=""
                ):
                    with patch.object(
                        analyzer, "extract_topics", return_value=[]
                    ):
                        result = analyzer.analyze_news(search_results)

        assert result["search_result_count"] == 3
